lib/httpclient/http.rb in httpclient-2.1.5.2 vs lib/httpclient/http.rb in httpclient-2.1.6

- old
+ new

@@ -93,11 +93,11 @@ CRLF = "\r\n" # Represents HTTP message header. class Headers - # HTTP version in a HTTP header. Float. + # HTTP version in a HTTP header. String. attr_accessor :http_version # Size of body. nil when size is unknown (e.g. chunked response). attr_reader :body_size # Request/Response is chunked or not. attr_accessor :chunked @@ -107,11 +107,11 @@ # Request only. Requested URI. attr_accessor :request_uri # Request only. Requested query. attr_accessor :request_query # Request only. Requested via proxy or not. - attr_accessor :request_via_proxy + attr_accessor :request_absolute_uri # Response only. HTTP status attr_reader :status_code # Response only. HTTP status reason phrase. attr_accessor :reason_phrase @@ -149,18 +149,18 @@ } # Creates a Message::Headers. Use init_request, init_response, or # init_connect_request for acutual initialize. def initialize - @http_version = 1.1 + @http_version = '1.1' @body_size = nil @chunked = false @request_method = nil @request_uri = nil @request_query = nil - @request_via_proxy = nil + @request_absolute_uri = nil @status_code = nil @reason_phrase = nil @body_type = nil @@ -176,22 +176,22 @@ def init_connect_request(uri) @is_request = true @request_method = 'CONNECT' @request_uri = uri @request_query = nil - @http_version = 1.0 + @http_version = '1.0' end # Placeholder URI object for nil uri. NIL_URI = URI.parse('http://nil-uri-given/') # Initialize this instance as a general request. def init_request(method, uri, query = nil) @is_request = true @request_method = method @request_uri = uri || NIL_URI @request_query = query - @request_via_proxy = false + @request_absolute_uri = false end # Initialize this instance as a response. def init_response(status_code) @is_request = false @@ -283,15 +283,42 @@ # Returns an Array of header values for the given key. def [](key) get(key).collect { |item| item[1] } end + def create_query_uri() + if @request_method == 'CONNECT' + return "#{@request_uri.host}:#{@request_uri.port}" + end + path = @request_uri.path + path = '/' if path.nil? or path.empty? + if query_str = create_query_part() + path += "?#{query_str}" + end + path + end + + def create_query_part() + query_str = nil + if @request_uri.query + query_str = @request_uri.query + end + if @request_query + if query_str + query_str += "&#{Message.create_query_part_str(@request_query)}" + else + query_str = Message.create_query_part_str(@request_query) + end + end + query_str + end + private def request_line - path = create_query_uri(@request_uri, @request_query) - if @request_via_proxy + path = create_query_uri() + if @request_absolute_uri path = "#{ @request_uri.scheme }://#{ @request_uri.host }:#{ @request_uri.port }#{ path }" end "#{ @request_method } #{ path } HTTP/#{ @http_version }#{ CRLF }" end @@ -321,11 +348,11 @@ if @chunked set('Transfer-Encoding', 'chunked') elsif @body_size and (keep_alive or @body_size != 0) set('Content-Length', @body_size.to_s) end - if @http_version >= 1.1 + if @http_version >= '1.1' and get('Host').empty? if @request_uri.port == @request_uri.default_port # GFE/1.3 dislikes default port number (returns 404) set('Host', "#{@request_uri.host}") else set('Host', "#{@request_uri.host}:#{@request_uri.port}") @@ -356,33 +383,10 @@ end def charset_label(charset) CHARSET_MAP[charset] || 'us-ascii' end - - def create_query_uri(uri, query) - if @request_method == 'CONNECT' - return "#{uri.host}:#{uri.port}" - end - path = uri.path - path = '/' if path.nil? or path.empty? - query_str = nil - if uri.query - query_str = uri.query - end - if query - if query_str - query_str += "&#{Message.create_query_part_str(query)}" - else - query_str = Message.create_query_part_str(query) - end - end - if query_str - path += "?#{query_str}" - end - path - end end # Represents HTTP message body. class Body @@ -412,11 +416,13 @@ end # Initialize this instance as a response. def init_response(body = nil) @body = body - if @body.respond_to?(:size) + if @body.respond_to?(:bytesize) + @size = @body.bytesize + elsif @body.respond_to?(:size) @size = @body.size else @size = nil end end @@ -436,10 +442,11 @@ if Message.file?(part) reset_pos(part) while !part.read(@chunk_size, buf).nil? dev << buf end + part.rewind else dev << part end end elsif @body @@ -494,11 +501,11 @@ elsif boundary and Message.multiparam_query?(body) @body = build_query_multipart_str(body, boundary) @size = @body.size else @body = Message.create_query_part_str(body) - @size = @body.size + @size = @body.bytesize end end def remember_pos(io) # IO may not support it (ex. IO.pipe) @@ -515,11 +522,11 @@ dev << dump_chunk(buf) end end def dump_chunk(str) - dump_chunk_size(str.size) + (str + CRLF) + dump_chunk_size(str.bytesize) + (str + CRLF) end def dump_last_chunk dump_chunk_size(0) end @@ -553,14 +560,14 @@ # use chunked upload @size = nil end elsif @body[-1].is_a?(String) @body[-1] += part.to_s - @size += part.to_s.size if @size + @size += part.to_s.bytesize if @size else @body << part.to_s - @size += part.to_s.size if @size + @size += part.to_s.bytesize if @size end end def parts if @as_stream @@ -581,15 +588,25 @@ param_str = params_from_file(value).collect { |k, v| "#{k}=\"#{v}\"" }.join("; ") if value.respond_to?(:mime_type) content_type = value.mime_type + elsif value.respond_to?(:content_type) + content_type = value.content_type else - content_type = Message.mime_type(value.path) + path = value.respond_to?(:path) ? value.path : nil + content_type = Message.mime_type(path) end headers << %{Content-Disposition: form-data; name="#{attr}"; #{param_str}} headers << %{Content-Type: #{content_type}} + elsif attr.is_a?(Hash) + h = attr + value = h[:content] + h.each do |h_key, h_val| + headers << %{#{h_key}: #{h_val}} if h_key != :content + end + remember_pos(value) if Message.file?(value) else headers << %{Content-Disposition: form-data; name="#{attr}"} end parts.add(headers.join(CRLF) + CRLF + CRLF) parts.add(value) @@ -599,11 +616,12 @@ parts end def params_from_file(value) params = {} - params['filename'] = File.basename(value.path || '') + path = value.respond_to?(:path) ? value.path : nil + params['filename'] = File.basename(path || '') # Creation time is not available from File::Stat if value.respond_to?(:mtime) params['modification-date'] = value.mtime.rfc822 end if value.respond_to?(:atime) @@ -722,31 +740,30 @@ 'application/octet-stream' end end # Returns true if the given HTTP version allows keep alive connection. - # version:: Float + # version:: String def keep_alive_enabled?(version) - version >= 1.1 + version >= '1.1' end # Returns true if the given query (or body) has a multiple parameter. def multiparam_query?(query) query.is_a?(Array) or query.is_a?(Hash) end # Returns true if the given object is a File. In HTTPClient, a file is; # * must respond to :read for retrieving String chunks. - # * must respond to :path and returns a path for Content-Disposition. # * must respond to :pos and :pos= to rewind for reading. # Rewinding is only needed for following HTTP redirect. Some IO impl # defines :pos= but raises an Exception for pos= such as StringIO # but there's no problem as far as using it for non-following methods # (get/post/etc.) def file?(obj) - obj.respond_to?(:read) and obj.respond_to?(:path) and - obj.respond_to?(:pos) and obj.respond_to?(:pos=) + obj.respond_to?(:read) and obj.respond_to?(:pos) and + obj.respond_to?(:pos=) end def create_query_part_str(query) # :nodoc: if multiparam_query?(query) escape_query(query) @@ -756,24 +773,51 @@ query.to_s end end def escape_query(query) # :nodoc: - query.collect { |attr, value| + query.sort_by { |attr, value| attr.to_s }.collect { |attr, value| if value.respond_to?(:read) value = value.read end escape(attr.to_s) << '=' << escape(value.to_s) }.join('&') end # from CGI.escape def escape(str) # :nodoc: - str.gsub(/([^ a-zA-Z0-9_.-]+)/n) { - '%' + $1.unpack('H2' * $1.size).join('%').upcase - }.tr(' ', '+') + if defined?(Encoding::ASCII_8BIT) + str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(/([^ a-zA-Z0-9_.-]+)/) { + '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase + }.tr(' ', '+') + else + str.gsub(/([^ a-zA-Z0-9_.-]+)/n) { + '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase + }.tr(' ', '+') + end end + + # from CGI.parse + def parse(query) + params = Hash.new([].freeze) + query.split(/[&;]/n).each do |pairs| + key, value = pairs.split('=',2).collect{|v| unescape(v) } + if params.has_key?(key) + params[key].push(value) + else + params[key] = [value] + end + end + params + end + + # from CGI.unescape + def unescape(string) + string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do + [$1.delete('%')].pack('H*') + end + end end # HTTP::Message::Headers:: message header. attr_accessor :header @@ -811,16 +855,27 @@ def body=(body) @body = body @header.body_size = @body.size if @header end - # Returns HTTP version in a HTTP header. Float. - def version + # Returns HTTP version in a HTTP header. String. + def http_version @header.http_version end - # Sets HTTP version in a HTTP header. Float. + # Sets HTTP version in a HTTP header. String. + def http_version=(http_version) + @header.http_version = http_version + end + + VERSION_WARNING = 'Message#version (Float) is deprecated. Use Message#http_version (String) instead.' + def version + warn(VERSION_WARNING) + @header.http_version.to_f + end + def version=(version) + warn(VERSION_WARNING) @header.http_version = version end # Returns HTTP status code in response. Integer. def status