require 'tap/http/helpers' require 'net/http' #module Net # class HTTP # attr_reader :socket # end # class BufferedIO #include Prosperity::Acts::Monitorable #private #def rbuf_fill # tick_monitor # timeout(@read_timeout) { # @rbuf << @io.sysread(1024) # } #end #end #end module Tap module Http # Dispatch provides methods for constructing and submitting get and post # HTTP requests. module Dispatch REQUEST_KEYS = [:url, :request_method, :headers, :params, :redirection_limit] module_function # Constructs and submits a request to the url using the headers and parameters. # Returns the response from the submission. # # res = submit_request("http://www.google.com/search", {:request_method => 'get'}, {:q => 'tap rubyforge'}) # # => # # Notes: # - A request method must be specified in the headers; currently only get and # post are supported. See construct_post for supported post content types. # - A url or a url (as would result from URI.parse(url)) can be provided to # submit request. The Net::HTTP object performing the submission is passed # to the block, if given, before the request is made. def submit_request(config) # :yields: http url_or_uri = config[:url] params = config[:params] || {} headers = headerize_keys( config[:headers] || {}) request_method = (config[:request_method] || 'GET').to_s uri = url_or_uri.kind_of?(URI) ? url_or_uri : URI.parse(url_or_uri) uri.path = "/" if uri.path.empty? # construct the request based on the method request = case request_method when /^get$/i then construct_get(uri, headers, params) when /^post$/i then construct_post(uri, headers, params) else raise ArgumentError.new("Missing or unsupported request_method: #{request_method}") end # set the http version # if version = config[:http_version] # version_method = "version_#{version.to_s.gsub(".", "_")}".to_sym # if Object::Net::HTTP.respond_to?(version_method) # Object::Net::HTTP.send(version_method) # else # raise ArgumentError.new("unsupported http_version: #{version}") # end # end # submit the request res = Object::Net::HTTP.new(uri.host, uri.port).start do |http| yield(http) if block_given? http.request(request) end # fetch redirections redirection_limit = config[:redirection_limit] redirection_limit ? fetch_redirection(res, redirection_limit) : res end # Constructs a post query. The 'Content-Type' header determines the format of the body # content. If the content type is 'multipart/form-data', then the parameters will be # formatted using the boundary in the content-type header, if provided, or a randomly # generated boundary. # # Headers for the request are set in this method. If uri contains query parameters, # they will be included in the request URI. # # Supported content-types: # - application/x-www-form-urlencoded (the default) # - multipart/form-data # def construct_post(uri, headers, params) req = Object::Net::HTTP::Post.new( URI.encode("#{uri.path}#{construct_query(uri)}") ) headers = headerize_keys(headers) content_type = headers['Content-Type'] case content_type when /multipart\/form-data/i # extract the boundary if it exists content_type =~ /boundary=(.*)/i boundary = $1 || rand.to_s[2..20] req.body = format_multipart_form_data(boundary, params) headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}" headers['Content-Length'] = req.body.length else req.body = format_www_form_urlencoded(params) headers['Content-Type'] = "application/x-www-form-urlencoded" headers['Content-Length'] = req.body.length end headers.each_pair { |key, value| req[key] = value } req end # Constructs a get query. All parameters in uri and params are added to the # request URI. Headers for the request are set in this method. def construct_get(uri, headers, params) req = Object::Net::HTTP::Get.new( URI.encode("#{uri.path}#{construct_query(uri, params)}") ) headerize_keys(headers).each_pair { |key, value| req[key] = value } req end # Checks the type of the response; if it is a redirection, get the redirection, # otherwise return the response. # # Notes: # - The redirections will only recurse for the input redirection limit (default 10) # - Responses that are not Net::HTTPRedirection or Net::HTTPSuccess raise an error. def fetch_redirection(res, limit=10) raise ArgumentError, 'Could not follow all the redirections.' if limit == 0 case res when Object::Net::HTTPRedirection redirect = Object::Net::HTTP.get_response( URI.parse(res['location']) ) fetch_redirection(redirect, limit - 1) when Object::Net::HTTPSuccess then res else raise StandardError, res.error! end end # Normalizes the header keys to a titleized, dasherized string. # 'some_header' => 'Some-Header' # :some_header => 'Some-Header' # 'some header' => 'Some-Header' def headerize_keys(headers) result = {} headers.each_pair do |key, value| result[Helpers.headerize(key)] = value end result end # Returns a URI query constructed from the query in uri and the input parameters. # The query is not encoded, so you may need to URI.encode it later. def construct_query(uri, params={}) query = [] params.each_pair do |key, values| values = values.kind_of?(Array) ? values : [values] values.each { |value| query << "#{key}=#{value}" } end query << uri.query if uri.query "#{query.empty? ? '' : '?'}#{query.join('&')}" end def format_www_form_urlencoded(params={}) query = [] params.each_pair do |key, values| values = values.kind_of?(Array) ? values : [values] values.each { |value| query << "#{key}=#{value}" } end URI.encode( query.join('&') ) end # Returns a post body formatted as 'multipart/form-data'. Special formatting occures # if value in one of the key-value parameter pairs is an Array or Hash. # # Array values are treated as a multiple inputs for a single key; each array value # is assigned to the key. Hash values are treated as files, with all file-related # headers specified in the hash. # #-- # Example: # "--1234", {:key => 'value'} => # --1234 # Content-Disposition: form-data; name="key" # # value # --1234-- def format_multipart_form_data(boundary, params) body = [] params.each_pair do |key, values| values = values.kind_of?(Array) ? values : [values] values.each do |value| body << case value when Hash hash = headerize_keys(value) filename = hash.delete('Filename') || "" content = File.exists?(filename) ? File.read(filename) : "" header = "Content-Disposition: form-data; name=\"#{key.to_s}\"; filename=\"#{filename}\"\r\n" hash.each_pair { |key, value| header << "#{key}: #{value}\r\n" } "#{header}\r\n#{content}\r\n" else %Q{Content-Disposition: form-data; name="#{key.to_s}"\r\n\r\n#{value.to_s}\r\n} end end end body.collect {|p| "--#{boundary}\r\n#{p}" }.join('') + "--#{boundary}--\r\n" end end end end