Included Modules

EventMachine::Protocols::HttpClient

Usage

 EventMachine.run {
   http = EventMachine::Protocols::HttpClient.request(
     :host => server,
     :port => 80,
     :request => "/index.html",
     :query_string => "parm1=value1&parm2=value2"
   )
   http.callback {|response|
     puts response[:status]
     puts response[:headers]
     puts response[:content]
   }
 }

Constants

MaxPostContentLength

Public Class Methods

request( args = {} ) click to toggle source

Arg list

:host => ‘ip/dns’, :port => fixnum, :verb => ‘GET’, :request => ‘path’, :basic_auth => {:username => ’’, :password => ’’}, :content => ‘content’, :contenttype => ‘text/plain’, :query_string => ’’, :host_header => ’’, :cookie => ’’

    # File lib/em/protocols/httpclient.rb, line 70
70:       def self.request( args = {} )
71:         args[:port] ||= 80
72:         EventMachine.connect( args[:host], args[:port], self ) {|c|
73:           # According to the docs, we will get here AFTER post_init is called.
74:           c.instance_eval {@args = args}
75:         }
76:       end

Public Instance Methods

connection_completed() click to toggle source

We send the request when we get a connection. AND, we set an instance variable to indicate we passed through here. That allows # to know whether there was a successful connection. NB: This naive technique won’t work when we have to support multiple requests on a single connection.

    # File lib/em/protocols/httpclient.rb, line 89
89:       def connection_completed
90:         @connected = true
91:         send_request @args
92:       end
dispatch_response() click to toggle source
     # File lib/em/protocols/httpclient.rb, line 242
242:       def dispatch_response
243:         @read_state = :base
244:         set_deferred_status :succeeded, {
245:           :content => @content,
246:           :headers => @headers,
247:           :status => @status
248:         }
249:         # TODO, we close the connection for now, but this is wrong for persistent clients.
250:         close_connection
251:       end
post_init() click to toggle source
    # File lib/em/protocols/httpclient.rb, line 78
78:       def post_init
79:         @start_time = Time.now
80:         @data = ""
81:         @read_state = :base
82:       end
receive_data(data) click to toggle source
     # File lib/em/protocols/httpclient.rb, line 161
161:       def receive_data data
162:         while data and data.length > 0
163:           case @read_state
164:           when :base
165:             # Perform any per-request initialization here and don't consume any data.
166:             @data = ""
167:             @headers = []
168:             @content_length = nil # not zero
169:             @content = ""
170:             @status = nil
171:             @read_state = :header
172:             @connection_close = nil
173:           when :header
174:             ary = data.split( /\r?\n/, 2 )
175:             if ary.length == 2
176:               data = ary.last
177:               if ary.first == ""
178:                 if (@content_length and @content_length > 0) || @connection_close
179:                   @read_state = :content
180:                 else
181:                   dispatch_response
182:                   @read_state = :base
183:                 end
184:               else
185:                 @headers << ary.first
186:                 if @headers.length == 1
187:                   parse_response_line
188:                 elsif ary.first =~ /\Acontent-length:\s*/
189:                   # Only take the FIRST content-length header that appears,
190:                   # which we can distinguish because @content_length is nil.
191:                   # TODO, it's actually a fatal error if there is more than one
192:                   # content-length header, because the caller is presumptively
193:                   # a bad guy. (There is an exploit that depends on multiple
194:                   # content-length headers.)
195:                   @content_length ||= $'.to_i
196:                 elsif ary.first =~ /\Aconnection:\s*close/
197:                   @connection_close = true
198:                 end
199:               end
200:             else
201:               @data << data
202:               data = ""
203:             end
204:           when :content
205:             # If there was no content-length header, we have to wait until the connection
206:             # closes. Everything we get until that point is content.
207:             # TODO: Must impose a content-size limit, and also must implement chunking.
208:             # Also, must support either temporary files for large content, or calling
209:             # a content-consumer block supplied by the user.
210:             if @content_length
211:               bytes_needed = @content_length - @content.length
212:               @content += data[0, bytes_needed]
213:               data = data[bytes_needed..1] || ""
214:               if @content_length == @content.length
215:                 dispatch_response
216:                 @read_state = :base
217:               end
218:             else
219:               @content << data
220:               data = ""
221:             end
222:           end
223:         end
224:       end
send_request(args) click to toggle source
     # File lib/em/protocols/httpclient.rb, line 94
 94:       def send_request args
 95:         args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
 96:         args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?
 97: 
 98:         verb = args[:verb].to_s.upcase
 99:         unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
100:           set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
101:           return # NOTE THE EARLY RETURN, we're not sending any data.
102:         end
103: 
104:         request = args[:request] || "/"
105:         unless request[0,1] == "/"
106:           request = "/" + request
107:         end
108: 
109:         qs = args[:query_string] || ""
110:         if qs.length > 0 and qs[0,1] != '?'
111:           qs = "?" + qs
112:         end
113: 
114:         version = args[:version] || "1.1"
115: 
116:         # Allow an override for the host header if it's not the connect-string.
117:         host = args[:host_header] || args[:host] || "_"
118:         # For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
119:         port = args[:port]
120: 
121:         # POST items.
122:         postcontenttype = args[:contenttype] || "application/octet-stream"
123:         postcontent = args[:content] || ""
124:         raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength
125: 
126:         # ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
127:         # TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
128:         req = [
129:           "#{verb} #{request}#{qs} HTTP/#{version}",
130:           "Host: #{host}:#{port}",
131:           "User-agent: Ruby EventMachine",
132:         ]
133: 
134:           if verb == "POST" || verb == "PUT"
135:             req << "Content-type: #{postcontenttype}"
136:             req << "Content-length: #{postcontent.length}"
137:           end
138: 
139:           # TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
140:           # Eventually we will want to deal intelligently with arrays and hashes.
141:           if args[:cookie]
142:             req << "Cookie: #{args[:cookie]}"
143:           end
144: 
145:           # Basic-auth stanza contributed by Matt Murphy.
146:           if args[:basic_auth]
147:             basic_auth_string = ["#{args[:basic_auth][:username]}:#{args[:basic_auth][:password]}"].pack('m').strip.gsub(/\n/,'')
148:             req << "Authorization: Basic #{basic_auth_string}"
149:           end
150: 
151:           req << ""
152:           reqstring = req.map {|l| "#{l}\r\n"}.join
153:           send_data reqstring
154: 
155:           if verb == "POST" || verb == "PUT"
156:             send_data postcontent
157:           end
158:       end
unbind() click to toggle source
     # File lib/em/protocols/httpclient.rb, line 253
253:       def unbind
254:         if !@connected
255:           set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
256:         elsif (@read_state == :content and @content_length == nil)
257:           dispatch_response
258:         end
259:       end

Private Instance Methods

parse_response_line() click to toggle source

We get called here when we have received an HTTP response line. It’s an opportunity to throw an exception or trigger other exceptional handling.

     # File lib/em/protocols/httpclient.rb, line 230
230:       def parse_response_line
231:         if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
232:           @status = $1.to_i
233:         else
234:           set_deferred_status :failed, {
235:             :status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
236:           }
237:           close_connection
238:         end
239:       end

Disabled; run with --debug to generate this.

[Validate]

Generated with the Darkfish Rdoc Generator 1.1.6.