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