determines if a http-proxy should be used with the CONNECT verb
# File lib/em-http/client.rb, line 364 def connect_proxy?; http_proxy? && (@options[:proxy][:use_connect] == true); end
start HTTP request once we establish connection to host
# File lib/em-http/client.rb, line 261 def connection_completed # if a socks proxy is specified, then a connection request # has to be made to the socks server and we need to wait # for a response code if socks_proxy? and @state == :response_header @state = :connect_socks_proxy send_socks_handshake # if we need to negotiate the proxy connection first, then # issue a CONNECT query and wait for 200 response elsif connect_proxy? and @state == :response_header @state = :connect_http_proxy send_request_header # if connecting via proxy, then state will be :proxy_connected, # indicating successful tunnel. from here, initiate normal http # exchange else @state = :response_header ssl = @options[:tls] || @options[:ssl] || {} start_tls(ssl) if @uri.scheme == "https" or @uri.port == 443 send_request_header send_request_body end end
assign disconnect callback for websocket
# File lib/em-http/client.rb, line 315 def disconnect(&blk) @disconnect = blk end
Response processing
# File lib/em-http/client.rb, line 523 def dispatch while case @state when :connect_socks_proxy parse_socks_response when :connect_http_proxy parse_response_header when :response_header parse_response_header when :chunk_header parse_chunk_header when :chunk_body process_chunk_body when :chunk_footer process_chunk_footer when :response_footer process_response_footer when :body process_body when :websocket process_websocket when :finished, :invalid break else raise RuntimeError, "invalid state: #{@state}" end end
# File lib/em-http/client.rb, line 485 def finished? @state == :finished || (@state == :body && @bytes_remaining.nil?) end
determines if there is enough data in the buffer
# File lib/em-http/client.rb, line 349 def has_bytes?(num) @data.size >= num end
assign a headers parse callback
# File lib/em-http/client.rb, line 320 def headers(&blk) @headers = blk end
determines if a proxy should be used that uses http-headers as proxy-mechanism
this is the default proxy type if none is specified
# File lib/em-http/client.rb, line 360 def http_proxy?; proxy? && [nil, :http].include?(@options[:proxy][:type]); end
# File lib/em-http/client.rb, line 338 def normalize_body @normalized_body ||= begin if @options[:body].is_a? Hash form_encode_body(@options[:body]) else @options[:body] end end end
Called when part of the body has been read
# File lib/em-http/client.rb, line 464 def on_body_data(data) if @content_decoder begin @content_decoder << data rescue HttpDecoders::DecoderError on_error "Content-decoder error" end else on_decoded_body_data(data) end end
# File lib/em-http/client.rb, line 476 def on_decoded_body_data(data) data.force_encoding @content_charset if @content_charset if @stream @stream.call(data) else @response << data end end
request failed, invoke errback
# File lib/em-http/client.rb, line 300 def on_error(msg, dns_error = false) @error = msg # no connection signature on DNS failures # fail the connection directly dns_error == true ? fail(self) : unbind end
request is done, invoke the callback
# File lib/em-http/client.rb, line 289 def on_request_complete begin @content_decoder.finalize! if @content_decoder rescue HttpDecoders::DecoderError on_error "Content-decoder error" end close_connection end
# File lib/em-http/client.rb, line 241 def post_init @parser = HttpClientParser.new @data = EventMachine::Buffer.new @chunk_header = HttpChunkHeader.new @response_header = HttpResponseHeader.new @parser_nbytes = 0 @redirects = 0 @response = '' @error = '' @headers = nil @last_effective_url = nil @content_decoder = nil @content_charset = nil @stream = nil @disconnect = nil @state = :response_header @socks_state = nil end
# File lib/em-http/client.rb, line 354 def proxy?; !@options[:proxy].nil?; end
# File lib/em-http/client.rb, line 458 def receive_data(data) @data << data dispatch end
raw data push from the client (WebSocket) should only be invoked after handshake, otherwise it will inject data into the header exchange
frames need to start with 0x00-0x7f byte and end with an 0xFF byte. Per spec, we can also set the first byte to a value betweent 0x80 and 0xFF, followed by a leading length indicator
# File lib/em-http/client.rb, line 332 def send(data) if @state == :websocket send_data("\x00#{data}\xff") end end
# File lib/em-http/client.rb, line 448 def send_request_body if @options[:body] body = normalize_body send_data body return elsif @options[:file] stream_file_data @options[:file], :http_chunks => false end end
# File lib/em-http/client.rb, line 387 def send_request_header query = @options[:query] head = @options[:head] ? munge_header_keys(@options[:head]) : {} file = @options[:file] proxy = @options[:proxy] body = normalize_body request_header = nil if http_proxy? # initialize headers for the http proxy head = proxy[:head] ? munge_header_keys(proxy[:head]) : {} head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization] # if we need to negotiate the tunnel connection first, then # issue a CONNECT query to the proxy first. This is an optional # flag, by default we will provide full URIs to the proxy if @state == :connect_http_proxy request_header = HTTP_REQUEST_HEADER % ['CONNECT', "#{@uri.host}:#{@uri.port}"] end end if websocket? head['upgrade'] = 'WebSocket' head['connection'] = 'Upgrade' head['origin'] = @options[:origin] || @uri.host else # Set the Content-Length if file is given head['content-length'] = File.size(file) if file # Set the Content-Length if body is given head['content-length'] = body.bytesize if body # Set the cookie header if provided if cookie = head.delete('cookie') head['cookie'] = encode_cookie(cookie) end # Set content-type header if missing and body is a Ruby hash if not head['content-type'] and options[:body].is_a? Hash head['content-type'] = "application/x-www-form-urlencoded" end end # Set the Host header if it hasn't been specified already head['host'] ||= encode_host # Set the User-Agent if it hasn't been specified head['user-agent'] ||= "EventMachine HttpClient" # Record last seen URL @last_effective_url = @uri # Build the request headers request_header ||= encode_request(@method, @uri, query, proxy) request_header << encode_headers(head) request_header << CRLF send_data request_header end
# File lib/em-http/client.rb, line 377 def send_socks_handshake # Method Negotiation as described on # http://www.faqs.org/rfcs/rfc1928.html Section 3 @socks_state = :method_negotiation methods = socks_methods send_data [5, methods.size].pack('CC') + methods.pack('C*') end
# File lib/em-http/client.rb, line 369 def socks_methods methods = [] methods << 2 if !options[:proxy][:authorization].nil? # 2 => Username/Password Authentication methods << 0 # 0 => No Authentication Required methods end
determines if a SOCKS5 proxy should be used
# File lib/em-http/client.rb, line 367 def socks_proxy?; proxy? && (@options[:proxy][:type] == :socks); end
assign a stream processing block
# File lib/em-http/client.rb, line 310 def stream(&blk) @stream = blk end
# File lib/em-http/client.rb, line 489 def unbind if finished? && (@last_effective_url != @uri) && (@redirects < @options[:redirects]) begin # update uri to redirect location if we're allowed to traverse deeper @uri = @last_effective_url # keep track of the depth of requests we made in this session @redirects += 1 # swap current connection and reassign current handler req = HttpOptions.new(@method, @uri, @options) reconnect(req.host, req.port) @response_header = HttpResponseHeader.new @state = :response_header @response = '' @data.clear rescue EventMachine::ConnectionError => e on_error(e.message, true) end else if finished? succeed(self) else @disconnect.call(self) if @state == :websocket and @disconnect fail(self) end end end
# File lib/em-http/client.rb, line 353 def websocket?; @uri.scheme == 'ws'; end