summaryrefslogtreecommitdiff
path: root/lib/net/http/header.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/net/http/header.rb')
-rw-r--r--lib/net/http/header.rb400
1 files changed, 280 insertions, 120 deletions
diff --git a/lib/net/http/header.rb b/lib/net/http/header.rb
index 856fb0a16b..5dcdcc7d74 100644
--- a/lib/net/http/header.rb
+++ b/lib/net/http/header.rb
@@ -1,4 +1,4 @@
-# frozen_string_literal: false
+# frozen_string_literal: true
#
# The \HTTPHeader module provides access to \HTTP headers.
#
@@ -179,6 +179,10 @@
# - #each_value: Passes each string field value to the block.
#
module Net::HTTPHeader
+ # The maximum length of HTTP header keys.
+ MAX_KEY_LENGTH = 1024
+ # The maximum length of HTTP header values.
+ MAX_FIELD_LENGTH = 65536
def initialize_http_header(initheader) #:nodoc:
@header = {}
@@ -189,6 +193,12 @@ module Net::HTTPHeader
warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE
else
value = value.strip # raise error for invalid byte sequences
+ if key.to_s.bytesize > MAX_KEY_LENGTH
+ raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..."
+ end
+ if value.to_s.bytesize > MAX_FIELD_LENGTH
+ raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}"
+ end
if value.count("\r\n") > 0
raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
end
@@ -207,9 +217,7 @@ module Net::HTTPHeader
# or +nil+ if there is no such key;
# see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]:
#
- # res = Net::HTTP.start(hostname) do |http|
- # http.get('/todos/1')
- # end
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
# res['Connection'] # => "keep-alive"
# res['Nosuch'] # => nil
#
@@ -261,6 +269,7 @@ module Net::HTTPHeader
end
end
+ # :stopdoc:
private def set_field(key, val)
case val
when Enumerable
@@ -288,14 +297,13 @@ module Net::HTTPHeader
ary.push val
end
end
+ # :startdoc:
# Returns the array field value for the given +key+,
# or +nil+ if there is no such field;
# see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]:
#
- # res = Net::HTTP.start(hostname) do |http|
- # http.get('/todos/1')
- # end
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
# res.get_fields('Connection') # => ["keep-alive"]
# res.get_fields('Nosuch') # => nil
#
@@ -305,7 +313,7 @@ module Net::HTTPHeader
@header[stringified_downcased_key].dup
end
- # :call-seq
+ # call-seq:
# fetch(key, default_val = nil) {|key| ... } -> object
# fetch(key, default_val = nil) -> value or default_val
#
@@ -314,9 +322,7 @@ module Net::HTTPHeader
# ignores the +default_val+;
# see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]:
#
- # res = Net::HTTP.start(hostname) do |http|
- # http.get('/todos/1')
- # end
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
#
# # Field exists; block not called.
# res.fetch('Connection') do |value|
@@ -343,9 +349,7 @@ module Net::HTTPHeader
# Calls the block with each key/value pair:
#
- # res = Net::HTTP.start(hostname) do |http|
- # http.get('/todos/1')
- # end
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
# res.each_header do |key, value|
# p [key, value] if key.start_with?('c')
# end
@@ -372,20 +376,18 @@ module Net::HTTPHeader
# Calls the block with each field key:
#
- # res = Net::HTTP.start(hostname) do |http|
- # http.get('/todos/1')
- # end
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
# res.each_key do |key|
# p key if key.start_with?('c')
# end
#
# Output:
#
- # "content-type"
- # "connection"
- # "cache-control"
- # "cf-cache-status"
- # "cf-ray"
+ # "content-type"
+ # "connection"
+ # "cache-control"
+ # "cf-cache-status"
+ # "cf-ray"
#
# Returns an enumerator if no block is given.
#
@@ -399,9 +401,7 @@ module Net::HTTPHeader
# Calls the block with each capitalized field name:
#
- # res = Net::HTTP.start(hostname) do |http|
- # http.get('/todos/1')
- # end
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
# res.each_capitalized_name do |key|
# p key if key.start_with?('C')
# end
@@ -427,9 +427,7 @@ module Net::HTTPHeader
# Calls the block with each string field value:
#
- # res = Net::HTTP.start(hostname) do |http|
- # http.get('/todos/1')
- # end
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
# res.each_value do |value|
# p value if value.start_with?('c')
# end
@@ -496,8 +494,8 @@ module Net::HTTPHeader
alias canonical_each each_capitalized
- def capitalize(name)
- name.to_s.split(/-/).map {|s| s.capitalize }.join('-')
+ def capitalize(name) # :nodoc:
+ name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze)
end
private :capitalize
@@ -554,7 +552,7 @@ module Net::HTTPHeader
result
end
- # :call-seq:
+ # call-seq:
# set_range(length) -> length
# set_range(offset, length) -> range
# set_range(begin..length) -> range
@@ -610,8 +608,15 @@ module Net::HTTPHeader
alias range= set_range
- # Returns an Integer object which represents the HTTP Content-Length:
- # header field, or +nil+ if that field was not provided.
+ # Returns the value of field <tt>'Content-Length'</tt> as an integer,
+ # or +nil+ if there is no such field;
+ # see {Content-Length request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-request-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/nosuch/1')
+ # res.content_length # => 2
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res.content_length # => nil
+ #
def content_length
return nil unless key?('Content-Length')
len = self['Content-Length'].slice(/\d+/) or
@@ -619,6 +624,20 @@ module Net::HTTPHeader
len.to_i
end
+ # Sets the value of field <tt>'Content-Length'</tt> to the given numeric;
+ # see {Content-Length response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-response-header]:
+ #
+ # _uri = uri.dup
+ # hostname = _uri.hostname # => "jsonplaceholder.typicode.com"
+ # _uri.path = '/posts' # => "/posts"
+ # req = Net::HTTP::Post.new(_uri) # => #<Net::HTTP::Post POST>
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.content_length = req.body.size # => 42
+ # req.content_type = 'application/json'
+ # res = Net::HTTP.start(hostname) do |http|
+ # http.request(req)
+ # end # => #<Net::HTTPCreated 201 Created readbody=true>
+ #
def content_length=(len)
unless len
@header.delete 'content-length'
@@ -627,20 +646,31 @@ module Net::HTTPHeader
@header['content-length'] = [len.to_i.to_s]
end
- # Returns "true" if the "transfer-encoding" header is present and
- # set to "chunked". This is an HTTP/1.1 feature, allowing
- # the content to be sent in "chunks" without at the outset
- # stating the entire content length.
+ # Returns +true+ if field <tt>'Transfer-Encoding'</tt>
+ # exists and has value <tt>'chunked'</tt>,
+ # +false+ otherwise;
+ # see {Transfer-Encoding response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#transfer-encoding-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Transfer-Encoding'] # => "chunked"
+ # res.chunked? # => true
+ #
def chunked?
return false unless @header['transfer-encoding']
field = self['Transfer-Encoding']
(/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
end
- # Returns a Range object which represents the value of the Content-Range:
- # header field.
- # For a partial entity body, this indicates where this fragment
- # fits inside the full entity body, as range of byte offsets.
+ # Returns a Range object representing the value of field
+ # <tt>'Content-Range'</tt>, or +nil+ if no such field exists;
+ # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Content-Range'] # => nil
+ # res['Content-Range'] = 'bytes 0-499/1000'
+ # res['Content-Range'] # => "bytes 0-499/1000"
+ # res.content_range # => 0..499
+ #
def content_range
return nil unless @header['content-range']
m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or
@@ -649,32 +679,66 @@ module Net::HTTPHeader
m[2].to_i .. m[3].to_i
end
- # The length of the range represented in Content-Range: header.
+ # Returns the integer representing length of the value of field
+ # <tt>'Content-Range'</tt>, or +nil+ if no such field exists;
+ # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Content-Range'] # => nil
+ # res['Content-Range'] = 'bytes 0-499/1000'
+ # res.range_length # => 500
+ #
def range_length
r = content_range() or return nil
r.end - r.begin + 1
end
- # Returns a content type string such as "text/html".
- # This method returns nil if Content-Type: header field does not exist.
+ # Returns the {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.content_type # => "application/json"
+ #
def content_type
- return nil unless main_type()
- if sub_type()
- then "#{main_type()}/#{sub_type()}"
- else main_type()
+ main = main_type()
+ return nil unless main
+
+ sub = sub_type()
+ if sub
+ "#{main}/#{sub}"
+ else
+ main
end
end
- # Returns a content type string such as "text".
- # This method returns nil if Content-Type: header field does not exist.
+ # Returns the leading ('type') part of the
+ # {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.main_type # => "application"
+ #
def main_type
return nil unless @header['content-type']
self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip
end
- # Returns a content type string such as "html".
- # This method returns nil if Content-Type: header field does not exist
- # or sub-type is not given (e.g. "Content-Type: text").
+ # Returns the trailing ('subtype') part of the
+ # {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.sub_type # => "json"
+ #
def sub_type
return nil unless @header['content-type']
_, sub = *self['Content-Type'].split(';').first.to_s.split('/')
@@ -682,9 +746,14 @@ module Net::HTTPHeader
sub.strip
end
- # Any parameters specified for the content type, returned as a Hash.
- # For example, a header of Content-Type: text/html; charset=EUC-JP
- # would result in type_params returning {'charset' => 'EUC-JP'}
+ # Returns the trailing ('parameters') part of the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.type_params # => {"charset"=>"utf-8"}
+ #
def type_params
result = {}
list = self['Content-Type'].to_s.split(';')
@@ -696,10 +765,12 @@ module Net::HTTPHeader
result
end
- # Sets the content type in an HTTP header.
- # The +type+ should be a full HTTP content type, e.g. "text/html".
- # The +params+ are an optional Hash of parameters to add after the
- # content type, e.g. {'charset' => 'iso-8859-1'}.
+ # Sets the value of field <tt>'Content-Type'</tt>;
+ # returns the new value;
+ # see {Content-Type request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-request-header]:
+ #
+ # req = Net::HTTP::Get.new(uri)
+ # req.set_content_type('application/json') # => ["application/json"]
#
# Net::HTTPHeader#content_type= is an alias for Net::HTTPHeader#set_content_type.
def set_content_type(type, params = {})
@@ -708,18 +779,38 @@ module Net::HTTPHeader
alias content_type= set_content_type
- # Set header fields and a body from HTML form data.
- # +params+ should be an Array of Arrays or
- # a Hash containing HTML form data.
- # Optional argument +sep+ means data record separator.
+ # Sets the request body to a URL-encoded string derived from argument +params+,
+ # and sets request header field <tt>'Content-Type'</tt>
+ # to <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The resulting request is suitable for HTTP request +POST+ or +PUT+.
+ #
+ # Argument +params+ must be suitable for use as argument +enum+ to
+ # {URI.encode_www_form}[rdoc-ref:URI.encode_www_form].
+ #
+ # With only argument +params+ given,
+ # sets the body to a URL-encoded string with the default separator <tt>'&'</tt>:
#
- # Values are URL encoded as necessary and the content-type is set to
- # application/x-www-form-urlencoded
+ # req = Net::HTTP::Post.new('example.com')
#
- # Example:
- # http.form_data = {"q" => "ruby", "lang" => "en"}
- # http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"}
- # http.set_form_data({"q" => "ruby", "lang" => "en"}, ';')
+ # req.set_form_data(q: 'ruby', lang: 'en')
+ # req.body # => "q=ruby&lang=en"
+ # req['Content-Type'] # => "application/x-www-form-urlencoded"
+ #
+ # req.set_form_data([['q', 'ruby'], ['lang', 'en']])
+ # req.body # => "q=ruby&lang=en"
+ #
+ # req.set_form_data(q: ['ruby', 'perl'], lang: 'en')
+ # req.body # => "q=ruby&q=perl&lang=en"
+ #
+ # req.set_form_data([['q', 'ruby'], ['q', 'perl'], ['lang', 'en']])
+ # req.body # => "q=ruby&q=perl&lang=en"
+ #
+ # With string argument +sep+ also given,
+ # uses that string as the separator:
+ #
+ # req.set_form_data({q: 'ruby', lang: 'en'}, '|')
+ # req.body # => "q=ruby|lang=en"
#
# Net::HTTPHeader#form_data= is an alias for Net::HTTPHeader#set_form_data.
def set_form_data(params, sep = '&')
@@ -731,53 +822,108 @@ module Net::HTTPHeader
alias form_data= set_form_data
- # Set an HTML form data set.
- # +params+ :: The form data to set, which should be an enumerable.
- # See below for more details.
- # +enctype+ :: The content type to use to encode the form submission,
- # which should be application/x-www-form-urlencoded or
- # multipart/form-data.
- # +formopt+ :: An options hash, supporting the following options:
- # :boundary :: The boundary of the multipart message. If
- # not given, a random boundary will be used.
- # :charset :: The charset of the form submission. All
- # field names and values of non-file fields
- # should be encoded with this charset.
- #
- # Each item of params should respond to +each+ and yield 2-3 arguments,
- # or an array of 2-3 elements. The arguments yielded should be:
- # * The name of the field.
- # * The value of the field, it should be a String or a File or IO-like.
- # * An options hash, supporting the following options, only
- # used for file uploads:
- # :filename :: The name of the file to use.
- # :content_type :: The content type of the uploaded file.
- #
- # Each item is a file field or a normal field.
- # If +value+ is a File object or the +opt+ hash has a :filename key,
- # the item is treated as a file field.
- #
- # If Transfer-Encoding is set as chunked, this sends the request using
- # chunked encoding. Because chunked encoding is HTTP/1.1 feature,
- # you should confirm that the server supports HTTP/1.1 before using
- # chunked encoding.
- #
- # Example:
- # req.set_form([["q", "ruby"], ["lang", "en"]])
- #
- # req.set_form({"f"=>File.open('/path/to/filename')},
- # "multipart/form-data",
- # charset: "UTF-8",
- # )
- #
- # req.set_form([["f",
- # File.open('/path/to/filename.bar'),
- # {filename: "other-filename.foo"}
- # ]],
- # "multipart/form-data",
- # )
- #
- # See also RFC 2388, RFC 2616, HTML 4.01, and HTML5
+ # Stores form data to be used in a +POST+ or +PUT+ request.
+ #
+ # The form data given in +params+ consists of zero or more fields;
+ # each field is:
+ #
+ # - A scalar value.
+ # - A name/value pair.
+ # - An IO stream opened for reading.
+ #
+ # Argument +params+ should be an
+ # {Enumerable}[rdoc-ref:Enumerable@Enumerable+in+Ruby+Classes]
+ # (method <tt>params.map</tt> will be called),
+ # and is often an array or hash.
+ #
+ # First, we set up a request:
+ #
+ # _uri = uri.dup
+ # _uri.path ='/posts'
+ # req = Net::HTTP::Post.new(_uri)
+ #
+ # <b>Argument +params+ As an Array</b>
+ #
+ # When +params+ is an array,
+ # each of its elements is a subarray that defines a field;
+ # the subarray may contain:
+ #
+ # - One string:
+ #
+ # req.set_form([['foo'], ['bar'], ['baz']])
+ #
+ # - Two strings:
+ #
+ # req.set_form([%w[foo 0], %w[bar 1], %w[baz 2]])
+ #
+ # - When argument +enctype+ (see below) is given as
+ # <tt>'multipart/form-data'</tt>:
+ #
+ # - A string name and an IO stream opened for reading:
+ #
+ # require 'stringio'
+ # req.set_form([['file', StringIO.new('Ruby is cool.')]])
+ #
+ # - A string name, an IO stream opened for reading,
+ # and an options hash, which may contain these entries:
+ #
+ # - +:filename+: The name of the file to use.
+ # - +:content_type+: The content type of the uploaded file.
+ #
+ # Example:
+ #
+ # req.set_form([['file', file, {filename: "other-filename.foo"}]]
+ #
+ # The various forms may be mixed:
+ #
+ # req.set_form(['foo', %w[bar 1], ['file', file]])
+ #
+ # <b>Argument +params+ As a Hash</b>
+ #
+ # When +params+ is a hash,
+ # each of its entries is a name/value pair that defines a field:
+ #
+ # - The name is a string.
+ # - The value may be:
+ #
+ # - +nil+.
+ # - Another string.
+ # - An IO stream opened for reading
+ # (only when argument +enctype+ -- see below -- is given as
+ # <tt>'multipart/form-data'</tt>).
+ #
+ # Examples:
+ #
+ # # Nil-valued fields.
+ # req.set_form({'foo' => nil, 'bar' => nil, 'baz' => nil})
+ #
+ # # String-valued fields.
+ # req.set_form({'foo' => 0, 'bar' => 1, 'baz' => 2})
+ #
+ # # IO-valued field.
+ # require 'stringio'
+ # req.set_form({'file' => StringIO.new('Ruby is cool.')})
+ #
+ # # Mixture of fields.
+ # req.set_form({'foo' => nil, 'bar' => 1, 'file' => file})
+ #
+ # Optional argument +enctype+ specifies the value to be given
+ # to field <tt>'Content-Type'</tt>, and must be one of:
+ #
+ # - <tt>'application/x-www-form-urlencoded'</tt> (the default).
+ # - <tt>'multipart/form-data'</tt>;
+ # see {RFC 7578}[https://www.rfc-editor.org/rfc/rfc7578].
+ #
+ # Optional argument +formopt+ is a hash of options
+ # (applicable only when argument +enctype+
+ # is <tt>'multipart/form-data'</tt>)
+ # that may include the following entries:
+ #
+ # - +:boundary+: The value is the boundary string for the multipart message.
+ # If not given, the boundary is a random string.
+ # See {Boundary}[https://www.rfc-editor.org/rfc/rfc7578#section-4.1].
+ # - +:charset+: Value is the character set for the form submission.
+ # Field names and values of non-file fields should be encoded with this charset.
#
def set_form(params, enctype='application/x-www-form-urlencoded', formopt={})
@body_data = params
@@ -793,21 +939,34 @@ module Net::HTTPHeader
end
end
- # Set the Authorization: header for "Basic" authorization.
+ # Sets header <tt>'Authorization'</tt> using the given
+ # +account+ and +password+ strings:
+ #
+ # req.basic_auth('my_account', 'my_password')
+ # req['Authorization']
+ # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA=="
+ #
def basic_auth(account, password)
@header['authorization'] = [basic_encode(account, password)]
end
- # Set Proxy-Authorization: header for "Basic" authorization.
+ # Sets header <tt>'Proxy-Authorization'</tt> using the given
+ # +account+ and +password+ strings:
+ #
+ # req.proxy_basic_auth('my_account', 'my_password')
+ # req['Proxy-Authorization']
+ # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA=="
+ #
def proxy_basic_auth(account, password)
@header['proxy-authorization'] = [basic_encode(account, password)]
end
- def basic_encode(account, password)
+ def basic_encode(account, password) # :nodoc:
'Basic ' + ["#{account}:#{password}"].pack('m0')
end
private :basic_encode
+ # Returns whether the HTTP session is to be closed.
def connection_close?
token = /(?:\A|,)\s*close\s*(?:\z|,)/i
@header['connection']&.grep(token) {return true}
@@ -815,6 +974,7 @@ module Net::HTTPHeader
false
end
+ # Returns whether the HTTP session is to be kept alive.
def connection_keep_alive?
token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i
@header['connection']&.grep(token) {return true}