root/trunk/lib/protocols/httpclient.rb

Revision 786, 8.2 kB (checked in by francis, 9 months ago)

supported optional version string in HTTP client.

  • Property svn:keywords set to Id
Line 
1 # $Id$
2 #
3 # Author:: Francis Cianfrocca (gmail: blackhedd)
4 # Homepage::  http://rubyeventmachine.com
5 # Date:: 16 July 2006
6 #
7 # See EventMachine and EventMachine::Connection for documentation and
8 # usage examples.
9 #
10 #----------------------------------------------------------------------------
11 #
12 # Copyright (C) 2006-07 by Francis Cianfrocca. All Rights Reserved.
13 # Gmail: blackhedd
14 #
15 # This program is free software; you can redistribute it and/or modify
16 # it under the terms of either: 1) the GNU General Public License
17 # as published by the Free Software Foundation; either version 2 of the
18 # License, or (at your option) any later version; or 2) Ruby's License.
19 #
20 # See the file COPYING for complete licensing information.
21 #
22 #---------------------------------------------------------------------------
23 #
24 #
25
26
27
28 module EventMachine
29 module Protocols
30
31 class HttpClient < Connection
32   include EventMachine::Deferrable
33
34
35     MaxPostContentLength = 20 * 1024 * 1024
36
37   # USAGE SAMPLE:
38   #
39   # EventMachine.run {
40   #   http = EventMachine::Protocols::HttpClient.request(
41   #     :host => server,
42   #     :port => 80,
43   #     :request => "/index.html",
44   #     :query_string => "parm1=value1&parm2=value2"
45   #   )
46   #   http.callback {|response|
47   #     puts response[:status]
48   #     puts response[:headers]
49   #     puts response[:content]
50   #   }
51   # }
52   #
53
54   # TODO:
55   # Add streaming so we can support enormous POSTs. Current max is 20meg.
56   # Timeout for connections that run too long or hang somewhere in the middle.
57   # Persistent connections (HTTP/1.1), may need a associated delegate object.
58   # DNS: Some way to cache DNS lookups for hostnames we connect to. Ruby's
59   # DNS lookups are unbelievably slow.
60   # HEAD requests.
61   # Chunked transfer encoding.
62   # Convenience methods for requests. get, post, url, etc.
63   # SSL.
64   # Handle status codes like 304, 100, etc.
65   # Refactor this code so that protocol errors all get handled one way (an exception?),
66   # instead of sprinkling set_deferred_status :failed calls everywhere.
67
68   def self.request( args = {} )
69     args[:port] ||= 80
70     EventMachine.connect( args[:host], args[:port], self ) {|c|
71       # According to the docs, we will get here AFTER post_init is called.
72       c.instance_eval {@args = args}
73     }
74   end
75
76   def post_init
77     @start_time = Time.now
78     @data = ""
79     @read_state = :base
80   end
81
82   # We send the request when we get a connection.
83   # AND, we set an instance variable to indicate we passed through here.
84   # That allows #unbind to know whether there was a successful connection.
85   # NB: This naive technique won't work when we have to support multiple
86   # requests on a single connection.
87   def connection_completed
88     @connected = true
89     send_request @args
90   end
91
92   def send_request args
93     args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
94     args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
95
96     verb = args[:verb].to_s.upcase
97     unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
98       set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
99       return # NOTE THE EARLY RETURN, we're not sending any data.
100     end
101
102     request = args[:request] || "/"
103     unless request[0,1] == "/"
104       request = "/" + request
105     end
106
107     qs = args[:query_string] || ""
108     if qs.length > 0 and qs[0,1] != '?'
109       qs = "?" + qs
110     end
111
112     version = args[:version] || "1.1"
113
114     # Allow an override for the host header if it's not the connect-string.
115     host = args[:host_header] || args[:host] || "_"
116     # For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
117     port = args[:port]
118
119     # POST items.
120     postcontenttype = args[:contenttype] || "application/octet-stream"
121     postcontent = args[:content] || ""
122     raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength
123
124     # ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
125     # TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
126     req = [
127       "#{verb} #{request}#{qs} HTTP/#{version}",
128       "Host: #{host}:#{port}",
129       "User-agent: Ruby EventMachine",
130     ]
131
132     if verb == "POST" || verb == "PUT"
133       req << "Content-type: #{postcontenttype}"
134       req << "Content-length: #{postcontent.length}"
135     end
136
137     # TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
138     # Eventually we will want to deal intelligently with arrays and hashes.
139     if args[:cookie]
140       req << "Cookie: #{args[:cookie]}"
141     end
142
143     # Basic-auth stanza contributed by Mike Murphy.
144     if args[:basic_auth]
145       basic_auth_string = ["#{args[:basic_auth][:username]}:#{args[:basic_auth][:password]}"].pack('m').strip
146       req << "Authorization: Basic #{basic_auth_string}"
147     end
148
149     req << ""
150     reqstring = req.map {|l| "#{l}\r\n"}.join
151     send_data reqstring
152
153     if verb == "POST" || verb == "PUT"
154       send_data postcontent
155     end
156   end
157
158
159   def receive_data data
160     while data and data.length > 0
161       case @read_state
162       when :base
163         # Perform any per-request initialization here and don't consume any data.
164         @data = ""
165         @headers = []
166         @content_length = nil # not zero
167         @content = ""
168         @status = nil
169         @read_state = :header
170         @connection_close = nil
171       when :header
172         ary = data.split( /\r?\n/m, 2 )
173         if ary.length == 2
174           data = ary.last
175           if ary.first == ""
176               if (@content_length and @content_length > 0) || @connection_close
177                   @read_state = :content
178               else
179                   dispatch_response
180                   @read_state = :base
181               end
182           else
183             @headers << ary.first
184             if @headers.length == 1
185               parse_response_line
186             elsif ary.first =~ /\Acontent-length:\s*/i
187               # Only take the FIRST content-length header that appears,
188               # which we can distinguish because @content_length is nil.
189               # TODO, it's actually a fatal error if there is more than one
190               # content-length header, because the caller is presumptively
191               # a bad guy. (There is an exploit that depends on multiple
192               # content-length headers.)
193               @content_length ||= $'.to_i
194             elsif ary.first =~ /\Aconnection:\s*close/i
195               @connection_close = true
196             end
197           end
198         else
199           @data << data
200           data = ""
201         end
202       when :content
203         # If there was no content-length header, we have to wait until the connection
204         # closes. Everything we get until that point is content.
205         # TODO: Must impose a content-size limit, and also must implement chunking.
206         # Also, must support either temporary files for large content, or calling
207         # a content-consumer block supplied by the user.
208         if @content_length
209           bytes_needed = @content_length - @content.length
210           @content += data[0, bytes_needed]
211           data = data[bytes_needed..-1] || ""
212           if @content_length == @content.length
213             dispatch_response
214             @read_state = :base
215           end
216         else
217           @content << data
218           data = ""
219         end
220       end
221     end
222   end
223
224
225   # We get called here when we have received an HTTP response line.
226   # It's an opportunity to throw an exception or trigger other exceptional
227   # handling.
228   def parse_response_line
229     if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
230       @status = $1.to_i
231     else
232       set_deferred_status :failed, {
233         :status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
234       }
235       close_connection
236     end
237   end
238   private :parse_response_line
239
240   def dispatch_response
241     @read_state = :base
242     set_deferred_status :succeeded, {
243       :content => @content,
244       :headers => @headers,
245       :status => @status
246     }
247     # TODO, we close the connection for now, but this is wrong for persistent clients.
248     close_connection
249   end
250
251   def unbind
252     if !@connected
253       set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
254     elsif (@read_state == :content and @content_length == nil)
255       dispatch_response
256     end
257   end
258 end
259
260
261 end
262 end
263
264
Note: See TracBrowser for help on using the browser.