class EventMachine::Protocols::HttpClient

Note: This class is deprecated and will be removed. Please use EM-HTTP-Request instead.

@example

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

new() click to toggle source
# File lib/em/protocols/httpclient.rb, line 66
def initialize
  warn "HttpClient is deprecated and will be removed. EM-Http-Request should be used instead."
end
request( args = {} ) click to toggle source

@param args [Hash] The request arguments @option args [String] :host The host IP/DNS name @option args [Integer] :port The port to connect too @option args [String] :verb The request type [GET | POST | DELETE | PUT] @option args [String] :request The request path @option args [Hash] :basic_auth The basic auth credentials (:username and :password) @option args [String] :content The request content @option args [String] :contenttype The content type (e.g. text/plain) @option args [String] :query_string The query string @option args [String] :host_header The host header to set @option args [String] :cookie Cookies to set

# File lib/em/protocols/httpclient.rb, line 81
def self.request( args = {} )
  args[:port] ||= 80
  EventMachine.connect( args[:host], args[:port], self ) {|c|
    # According to the docs, we will get here AFTER post_init is called.
    c.instance_eval {@args = args}
  }
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 unbind 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 100
def connection_completed
  @connected = true
  send_request @args
end
dispatch_response() click to toggle source
# File lib/em/protocols/httpclient.rb, line 258
def dispatch_response
  @read_state = :base
  set_deferred_status :succeeded, {
    :content => @content,
    :headers => @headers,
    :status => @status
  }
  # TODO, we close the connection for now, but this is wrong for persistent clients.
  close_connection
end
post_init() click to toggle source
# File lib/em/protocols/httpclient.rb, line 89
def post_init
  @start_time = Time.now
  @data = ""
  @read_state = :base
end
receive_data(data) click to toggle source
# File lib/em/protocols/httpclient.rb, line 177
def receive_data data
  while data and data.length > 0
    case @read_state
    when :base
      # Perform any per-request initialization here and don't consume any data.
      @data = ""
      @headers = []
      @content_length = nil # not zero
      @content = ""
      @status = nil
      @read_state = :header
      @connection_close = nil
    when :header
      ary = data.split( /\r?\n/, 2 )
      if ary.length == 2
        data = ary.last
        if ary.first == ""
          if (@content_length and @content_length > 0) || @connection_close
            @read_state = :content
          else
            dispatch_response
            @read_state = :base
          end
        else
          @headers << ary.first
          if @headers.length == 1
            parse_response_line
          elsif ary.first =~ /\Acontent-length:\s*/
            # Only take the FIRST content-length header that appears,
            # which we can distinguish because @content_length is nil.
            # TODO, it's actually a fatal error if there is more than one
            # content-length header, because the caller is presumptively
            # a bad guy. (There is an exploit that depends on multiple
            # content-length headers.)
            @content_length ||= $'.to_i
          elsif ary.first =~ /\Aconnection:\s*close/
            @connection_close = true
          end
        end
      else
        @data << data
        data = ""
      end
    when :content
      # If there was no content-length header, we have to wait until the connection
      # closes. Everything we get until that point is content.
      # TODO: Must impose a content-size limit, and also must implement chunking.
      # Also, must support either temporary files for large content, or calling
      # a content-consumer block supplied by the user.
      if @content_length
        bytes_needed = @content_length - @content.length
        @content += data[0, bytes_needed]
        data = data[bytes_needed..-1] || ""
        if @content_length == @content.length
          dispatch_response
          @read_state = :base
        end
      else
        @content << data
        data = ""
      end
    end
  end
end
send_request(args) click to toggle source
# File lib/em/protocols/httpclient.rb, line 105
def send_request args
  args[:verb] ||= args[:method] # Support :method as an alternative to :verb.
  args[:verb] ||= :get # IS THIS A GOOD IDEA, to default to GET if nothing was specified?

  verb = args[:verb].to_s.upcase
  unless ["GET", "POST", "PUT", "DELETE", "HEAD"].include?(verb)
    set_deferred_status :failed, {:status => 0} # TODO, not signalling the error type
    return # NOTE THE EARLY RETURN, we're not sending any data.
  end

  request = args[:request] || "/"
  unless request[0,1] == "/"
    request = "/" + request
  end

  qs = args[:query_string] || ""
  if qs.length > 0 and qs[0,1] != '?'
    qs = "?" + qs
  end

  version = args[:version] || "1.1"

  # Allow an override for the host header if it's not the connect-string.
  host = args[:host_header] || args[:host] || "_"
  # For now, ALWAYS tuck in the port string, although we may want to omit it if it's the default.
  port = args[:port].to_i != 80 ? ":#{args[:port]}" : ""

  # POST items.
  postcontenttype = args[:contenttype] || "application/octet-stream"
  postcontent = args[:content] || ""
  raise "oversized content in HTTP POST" if postcontent.length > MaxPostContentLength

  # ESSENTIAL for the request's line-endings to be CRLF, not LF. Some servers misbehave otherwise.
  # TODO: We ASSUME the caller wants to send a 1.1 request. May not be a good assumption.
  req = [
    "#{verb} #{request}#{qs} HTTP/#{version}",
    "Host: #{host}#{port}",
    "User-agent: Ruby EventMachine",
  ]

    if verb == "POST" || verb == "PUT"
      req << "Content-type: #{postcontenttype}"
      req << "Content-length: #{postcontent.length}"
    end

    # TODO, this cookie handler assumes it's getting a single, semicolon-delimited string.
    # Eventually we will want to deal intelligently with arrays and hashes.
    if args[:cookie]
      req << "Cookie: #{args[:cookie]}"
    end

    # Allow custom HTTP headers, e.g. SOAPAction
    args[:custom_headers].each do |k,v|
      req << "#{k}: #{v}"
    end if args[:custom_headers]

    # Basic-auth stanza contributed by Matt Murphy.
    if args[:basic_auth]
      basic_auth_string = ["#{args[:basic_auth][:username]}:#{args[:basic_auth][:password]}"].pack('m').strip.gsub(/\n/,'')
      req << "Authorization: Basic #{basic_auth_string}"
    end

    req << ""
    reqstring = req.map {|l| "#{l}\r\n"}.join
    send_data reqstring

    if verb == "POST" || verb == "PUT"
      send_data postcontent
    end
end
unbind() click to toggle source
# File lib/em/protocols/httpclient.rb, line 269
def unbind
  if !@connected
    set_deferred_status :failed, {:status => 0} # YECCCCH. Find a better way to signal no-connect/network error.
  elsif (@read_state == :content and @content_length == nil)
    dispatch_response
  end
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 246
def parse_response_line
  if @headers.first =~ /\AHTTP\/1\.[01] ([\d]{3})/
    @status = $1.to_i
  else
    set_deferred_status :failed, {
      :status => 0 # crappy way of signifying an unrecognized response. TODO, find a better way to do this.
    }
    close_connection
  end
end