diff options
Diffstat (limited to 'lib/net/http/header.rb')
| -rw-r--r-- | lib/net/http/header.rb | 400 |
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} |
