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] } }
: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
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
# 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
# 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
# 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
# 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
# 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
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.
Generated with the Darkfish Rdoc Generator 1.1.6.