[rubygems/rubygems] Vendor net-http and net-protocol in RubyGems

99d91c9ed2
This commit is contained in:
David Rodríguez 2023-01-29 21:24:11 +01:00 committed by Hiroshi SHIBATA
parent 6cefad7704
commit ce924ce1fb
33 changed files with 7158 additions and 75 deletions

View file

@ -0,0 +1,22 @@
Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
# for backward compatibility
# :enddoc:
class Gem::Net::HTTP
ProxyMod = ProxyDelta
deprecate_constant :ProxyMod
end
module Gem::Net::NetPrivate
HTTPRequest = ::Gem::Net::HTTPRequest
deprecate_constant :HTTPRequest
end
module Gem::Net
HTTPSession = HTTP
HTTPInformationCode = HTTPInformation
HTTPSuccessCode = HTTPSuccess
HTTPRedirectionCode = HTTPRedirection
HTTPRetriableCode = HTTPRedirection
HTTPClientErrorCode = HTTPClientError
HTTPFatalErrorCode = HTTPClientError
HTTPServerErrorCode = HTTPServerError
HTTPResponseReceiver = HTTPResponse
HTTPResponceReceiver = HTTPResponse # Typo since 2001
deprecate_constant :HTTPSession,
:HTTPInformationCode,
:HTTPSuccessCode,
:HTTPRedirectionCode,
:HTTPRetriableCode,
:HTTPClientErrorCode,
:HTTPFatalErrorCode,
:HTTPServerErrorCode,
:HTTPResponseReceiver,
:HTTPResponceReceiver
end

View file

@ -0,0 +1,34 @@
# frozen_string_literal: true
module Gem::Net
# Gem::Net::HTTP exception class.
# You cannot use Gem::Net::HTTPExceptions directly; instead, you must use
# its subclasses.
module HTTPExceptions
def initialize(msg, res) #:nodoc:
super msg
@response = res
end
attr_reader :response
alias data response #:nodoc: obsolete
end
class HTTPError < ProtocolError
include HTTPExceptions
end
class HTTPRetriableError < ProtoRetriableError
include HTTPExceptions
end
class HTTPClientException < ProtoServerError
include HTTPExceptions
end
class HTTPFatalError < ProtoFatalError
include HTTPExceptions
end
# We cannot use the name "HTTPServerError", it is the name of the response.
HTTPServerException = HTTPClientException # :nodoc:
deprecate_constant(:HTTPServerException)
end

View file

@ -0,0 +1,414 @@
# frozen_string_literal: true
#
# \HTTPGenericRequest is the parent of the Gem::Net::HTTPRequest class.
#
# Do not use this directly; instead, use a subclass of Gem::Net::HTTPRequest.
#
# == About the Examples
#
# :include: doc/net-http/examples.rdoc
#
class Gem::Net::HTTPGenericRequest
include Gem::Net::HTTPHeader
def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) # :nodoc:
@method = m
@request_has_body = reqbody
@response_has_body = resbody
if URI === uri_or_path then
raise ArgumentError, "not an HTTP URI" unless URI::HTTP === uri_or_path
hostname = uri_or_path.hostname
raise ArgumentError, "no host component for URI" unless (hostname && hostname.length > 0)
@uri = uri_or_path.dup
host = @uri.hostname.dup
host << ":" << @uri.port.to_s if @uri.port != @uri.default_port
@path = uri_or_path.request_uri
raise ArgumentError, "no HTTP request path given" unless @path
else
@uri = nil
host = nil
raise ArgumentError, "no HTTP request path given" unless uri_or_path
raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty?
@path = uri_or_path.dup
end
@decode_content = false
if Gem::Net::HTTP::HAVE_ZLIB then
if !initheader ||
!initheader.keys.any? { |k|
%w[accept-encoding range].include? k.downcase
} then
@decode_content = true if @response_has_body
initheader = initheader ? initheader.dup : {}
initheader["accept-encoding"] =
"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
end
end
initialize_http_header initheader
self['Accept'] ||= '*/*'
self['User-Agent'] ||= 'Ruby'
self['Host'] ||= host if host
@body = nil
@body_stream = nil
@body_data = nil
end
# Returns the string method name for the request:
#
# Gem::Net::HTTP::Get.new(uri).method # => "GET"
# Gem::Net::HTTP::Post.new(uri).method # => "POST"
#
attr_reader :method
# Returns the string path for the request:
#
# Gem::Net::HTTP::Get.new(uri).path # => "/"
# Gem::Net::HTTP::Post.new('example.com').path # => "example.com"
#
attr_reader :path
# Returns the URI object for the request, or +nil+ if none:
#
# Gem::Net::HTTP::Get.new(uri).uri
# # => #<URI::HTTPS https://jsonplaceholder.typicode.com/>
# Gem::Net::HTTP::Get.new('example.com').uri # => nil
#
attr_reader :uri
# Returns +false+ if the request's header <tt>'Accept-Encoding'</tt>
# has been set manually or deleted
# (indicating that the user intends to handle encoding in the response),
# +true+ otherwise:
#
# req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET>
# req['Accept-Encoding'] # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
# req.decode_content # => true
# req['Accept-Encoding'] = 'foo'
# req.decode_content # => false
# req.delete('Accept-Encoding')
# req.decode_content # => false
#
attr_reader :decode_content
# Returns a string representation of the request:
#
# Gem::Net::HTTP::Post.new(uri).inspect # => "#<Gem::Net::HTTP::Post POST>"
#
def inspect
"\#<#{self.class} #{@method}>"
end
##
# Don't automatically decode response content-encoding if the user indicates
# they want to handle it.
def []=(key, val) # :nodoc:
@decode_content = false if key.downcase == 'accept-encoding'
super key, val
end
# Returns whether the request may have a body:
#
# Gem::Net::HTTP::Post.new(uri).request_body_permitted? # => true
# Gem::Net::HTTP::Get.new(uri).request_body_permitted? # => false
#
def request_body_permitted?
@request_has_body
end
# Returns whether the response may have a body:
#
# Gem::Net::HTTP::Post.new(uri).response_body_permitted? # => true
# Gem::Net::HTTP::Head.new(uri).response_body_permitted? # => false
#
def response_body_permitted?
@response_has_body
end
def body_exist? # :nodoc:
warn "Gem::Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE
response_body_permitted?
end
# Returns the string body for the request, or +nil+ if there is none:
#
# req = Gem::Net::HTTP::Post.new(uri)
# req.body # => nil
# req.body = '{"title": "foo","body": "bar","userId": 1}'
# req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}"
#
attr_reader :body
# Sets the body for the request:
#
# req = Gem::Net::HTTP::Post.new(uri)
# req.body # => nil
# req.body = '{"title": "foo","body": "bar","userId": 1}'
# req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}"
#
def body=(str)
@body = str
@body_stream = nil
@body_data = nil
str
end
# Returns the body stream object for the request, or +nil+ if there is none:
#
# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
# req.body_stream # => nil
# require 'stringio'
# req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8>
# req.body_stream # => #<StringIO:0x0000027d1e5affa8>
#
attr_reader :body_stream
# Sets the body stream for the request:
#
# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
# req.body_stream # => nil
# require 'stringio'
# req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8>
# req.body_stream # => #<StringIO:0x0000027d1e5affa8>
#
def body_stream=(input)
@body = nil
@body_stream = input
@body_data = nil
input
end
def set_body_internal(str) #:nodoc: internal use only
raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
self.body = str if str
if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted?
self.body = ''
end
end
#
# write
#
def exec(sock, ver, path) #:nodoc: internal use only
if @body
send_request_with_body sock, ver, path, @body
elsif @body_stream
send_request_with_body_stream sock, ver, path, @body_stream
elsif @body_data
send_request_with_body_data sock, ver, path, @body_data
else
write_header sock, ver, path
end
end
def update_uri(addr, port, ssl) # :nodoc: internal use only
# reflect the connection and @path to @uri
return unless @uri
if ssl
scheme = 'https'
klass = URI::HTTPS
else
scheme = 'http'
klass = URI::HTTP
end
if host = self['host']
host.sub!(/:.*/m, '')
elsif host = @uri.host
else
host = addr
end
# convert the class of the URI
if @uri.is_a?(klass)
@uri.host = host
@uri.port = port
else
@uri = klass.new(
scheme, @uri.userinfo,
host, port, nil,
@uri.path, nil, @uri.query, nil)
end
end
private
class Chunker #:nodoc:
def initialize(sock)
@sock = sock
@prev = nil
end
def write(buf)
# avoid memcpy() of buf, buf can huge and eat memory bandwidth
rv = buf.bytesize
@sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n")
rv
end
def finish
@sock.write("0\r\n\r\n")
end
end
def send_request_with_body(sock, ver, path, body)
self.content_length = body.bytesize
delete 'Transfer-Encoding'
supply_default_content_type
write_header sock, ver, path
wait_for_continue sock, ver if sock.continue_timeout
sock.write body
end
def send_request_with_body_stream(sock, ver, path, f)
unless content_length() or chunked?
raise ArgumentError,
"Content-Length not given and Transfer-Encoding is not `chunked'"
end
supply_default_content_type
write_header sock, ver, path
wait_for_continue sock, ver if sock.continue_timeout
if chunked?
chunker = Chunker.new(sock)
IO.copy_stream(f, chunker)
chunker.finish
else
IO.copy_stream(f, sock)
end
end
def send_request_with_body_data(sock, ver, path, params)
if /\Amultipart\/form-data\z/i !~ self.content_type
self.content_type = 'application/x-www-form-urlencoded'
return send_request_with_body(sock, ver, path, URI.encode_www_form(params))
end
opt = @form_option.dup
require 'securerandom' unless defined?(SecureRandom)
opt[:boundary] ||= SecureRandom.urlsafe_base64(40)
self.set_content_type(self.content_type, boundary: opt[:boundary])
if chunked?
write_header sock, ver, path
encode_multipart_form_data(sock, params, opt)
else
require 'tempfile'
file = Tempfile.new('multipart')
file.binmode
encode_multipart_form_data(file, params, opt)
file.rewind
self.content_length = file.size
write_header sock, ver, path
IO.copy_stream(file, sock)
file.close(true)
end
end
def encode_multipart_form_data(out, params, opt)
charset = opt[:charset]
boundary = opt[:boundary]
require 'securerandom' unless defined?(SecureRandom)
boundary ||= SecureRandom.urlsafe_base64(40)
chunked_p = chunked?
buf = +''
params.each do |key, value, h={}|
key = quote_string(key, charset)
filename =
h.key?(:filename) ? h[:filename] :
value.respond_to?(:to_path) ? File.basename(value.to_path) :
nil
buf << "--#{boundary}\r\n"
if filename
filename = quote_string(filename, charset)
type = h[:content_type] || 'application/octet-stream'
buf << "Content-Disposition: form-data; " \
"name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
"Content-Type: #{type}\r\n\r\n"
if !out.respond_to?(:write) || !value.respond_to?(:read)
# if +out+ is not an IO or +value+ is not an IO
buf << (value.respond_to?(:read) ? value.read : value)
elsif value.respond_to?(:size) && chunked_p
# if +out+ is an IO and +value+ is a File, use IO.copy_stream
flush_buffer(out, buf, chunked_p)
out << "%x\r\n" % value.size if chunked_p
IO.copy_stream(value, out)
out << "\r\n" if chunked_p
else
# +out+ is an IO, and +value+ is not a File but an IO
flush_buffer(out, buf, chunked_p)
1 while flush_buffer(out, value.read(4096), chunked_p)
end
else
# non-file field:
# HTML5 says, "The parts of the generated multipart/form-data
# resource that correspond to non-file fields must not have a
# Content-Type header specified."
buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
buf << (value.respond_to?(:read) ? value.read : value)
end
buf << "\r\n"
end
buf << "--#{boundary}--\r\n"
flush_buffer(out, buf, chunked_p)
out << "0\r\n\r\n" if chunked_p
end
def quote_string(str, charset)
str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
str.gsub(/[\\"]/, '\\\\\&')
end
def flush_buffer(out, buf, chunked_p)
return unless buf
out << "%x\r\n"%buf.bytesize if chunked_p
out << buf
out << "\r\n" if chunked_p
buf.clear
end
def supply_default_content_type
return if content_type()
warn 'net/http: Content-Type did not set; using application/x-www-form-urlencoded', uplevel: 1 if $VERBOSE
set_content_type 'application/x-www-form-urlencoded'
end
##
# Waits up to the continue timeout for a response from the server provided
# we're speaking HTTP 1.1 and are expecting a 100-continue response.
def wait_for_continue(sock, ver)
if ver >= '1.1' and @header['expect'] and
@header['expect'].include?('100-continue')
if sock.io.to_io.wait_readable(sock.continue_timeout)
res = Gem::Net::HTTPResponse.read_new(sock)
unless res.kind_of?(Gem::Net::HTTPContinue)
res.decode_content = @decode_content
throw :response, res
end
end
end
end
def write_header(sock, ver, path)
reqline = "#{@method} #{path} HTTP/#{ver}"
if /[\r\n]/ =~ reqline
raise ArgumentError, "A Request-Line must not contain CR or LF"
end
buf = +''
buf << reqline << "\r\n"
each_capitalized do |k,v|
buf << "#{k}: #{v}\r\n"
end
buf << "\r\n"
sock.write buf
end
end

View file

@ -0,0 +1,981 @@
# frozen_string_literal: true
#
# The \HTTPHeader module provides access to \HTTP headers.
#
# The module is included in:
#
# - Gem::Net::HTTPGenericRequest (and therefore Gem::Net::HTTPRequest).
# - Gem::Net::HTTPResponse.
#
# The headers are a hash-like collection of key/value pairs called _fields_.
#
# == Request and Response Fields
#
# Headers may be included in:
#
# - A Gem::Net::HTTPRequest object:
# the object's headers will be sent with the request.
# Any fields may be defined in the request;
# see {Setters}[rdoc-ref:Gem::Net::HTTPHeader@Setters].
# - A Gem::Net::HTTPResponse object:
# the objects headers are usually those returned from the host.
# Fields may be retrieved from the object;
# see {Getters}[rdoc-ref:Gem::Net::HTTPHeader@Getters]
# and {Iterators}[rdoc-ref:Gem::Net::HTTPHeader@Iterators].
#
# Exactly which fields should be sent or expected depends on the host;
# see:
#
# - {Request fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields].
# - {Response fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields].
#
# == About the Examples
#
# :include: doc/net-http/examples.rdoc
#
# == Fields
#
# A header field is a key/value pair.
#
# === Field Keys
#
# A field key may be:
#
# - A string: Key <tt>'Accept'</tt> is treated as if it were
# <tt>'Accept'.downcase</tt>; i.e., <tt>'accept'</tt>.
# - A symbol: Key <tt>:Accept</tt> is treated as if it were
# <tt>:Accept.to_s.downcase</tt>; i.e., <tt>'accept'</tt>.
#
# Examples:
#
# req = Gem::Net::HTTP::Get.new(uri)
# req[:accept] # => "*/*"
# req['Accept'] # => "*/*"
# req['ACCEPT'] # => "*/*"
#
# req['accept'] = 'text/html'
# req[:accept] = 'text/html'
# req['ACCEPT'] = 'text/html'
#
# === Field Values
#
# A field value may be returned as an array of strings or as a string:
#
# - These methods return field values as arrays:
#
# - #get_fields: Returns the array value for the given key,
# or +nil+ if it does not exist.
# - #to_hash: Returns a hash of all header fields:
# each key is a field name; its value is the array value for the field.
#
# - These methods return field values as string;
# the string value for a field is equivalent to
# <tt>self[key.downcase.to_s].join(', '))</tt>:
#
# - #[]: Returns the string value for the given key,
# or +nil+ if it does not exist.
# - #fetch: Like #[], but accepts a default value
# to be returned if the key does not exist.
#
# The field value may be set:
#
# - #[]=: Sets the value for the given key;
# the given value may be a string, a symbol, an array, or a hash.
# - #add_field: Adds a given value to a value for the given key
# (not overwriting the existing value).
# - #delete: Deletes the field for the given key.
#
# Example field values:
#
# - \String:
#
# req['Accept'] = 'text/html' # => "text/html"
# req['Accept'] # => "text/html"
# req.get_fields('Accept') # => ["text/html"]
#
# - \Symbol:
#
# req['Accept'] = :text # => :text
# req['Accept'] # => "text"
# req.get_fields('Accept') # => ["text"]
#
# - Simple array:
#
# req[:foo] = %w[bar baz bat]
# req[:foo] # => "bar, baz, bat"
# req.get_fields(:foo) # => ["bar", "baz", "bat"]
#
# - Simple hash:
#
# req[:foo] = {bar: 0, baz: 1, bat: 2}
# req[:foo] # => "bar, 0, baz, 1, bat, 2"
# req.get_fields(:foo) # => ["bar", "0", "baz", "1", "bat", "2"]
#
# - Nested:
#
# req[:foo] = [%w[bar baz], {bat: 0, bam: 1}]
# req[:foo] # => "bar, baz, bat, 0, bam, 1"
# req.get_fields(:foo) # => ["bar", "baz", "bat", "0", "bam", "1"]
#
# req[:foo] = {bar: %w[baz bat], bam: {bah: 0, bad: 1}}
# req[:foo] # => "bar, baz, bat, bam, bah, 0, bad, 1"
# req.get_fields(:foo) # => ["bar", "baz", "bat", "bam", "bah", "0", "bad", "1"]
#
# == Convenience Methods
#
# Various convenience methods retrieve values, set values, query values,
# set form values, or iterate over fields.
#
# === Setters
#
# \Method #[]= can set any field, but does little to validate the new value;
# some of the other setter methods provide some validation:
#
# - #[]=: Sets the string or array value for the given key.
# - #add_field: Creates or adds to the array value for the given key.
# - #basic_auth: Sets the string authorization header for <tt>'Authorization'</tt>.
# - #content_length=: Sets the integer length for field <tt>'Content-Length</tt>.
# - #content_type=: Sets the string value for field <tt>'Content-Type'</tt>.
# - #proxy_basic_auth: Sets the string authorization header for <tt>'Proxy-Authorization'</tt>.
# - #set_range: Sets the value for field <tt>'Range'</tt>.
#
# === Form Setters
#
# - #set_form: Sets an HTML form data set.
# - #set_form_data: Sets header fields and a body from HTML form data.
#
# === Getters
#
# \Method #[] can retrieve the value of any field that exists,
# but always as a string;
# some of the other getter methods return something different
# from the simple string value:
#
# - #[]: Returns the string field value for the given key.
# - #content_length: Returns the integer value of field <tt>'Content-Length'</tt>.
# - #content_range: Returns the Range value of field <tt>'Content-Range'</tt>.
# - #content_type: Returns the string value of field <tt>'Content-Type'</tt>.
# - #fetch: Returns the string field value for the given key.
# - #get_fields: Returns the array field value for the given +key+.
# - #main_type: Returns first part of the string value of field <tt>'Content-Type'</tt>.
# - #sub_type: Returns second part of the string value of field <tt>'Content-Type'</tt>.
# - #range: Returns an array of Range objects of field <tt>'Range'</tt>, or +nil+.
# - #range_length: Returns the integer length of the range given in field <tt>'Content-Range'</tt>.
# - #type_params: Returns the string parameters for <tt>'Content-Type'</tt>.
#
# === Queries
#
# - #chunked?: Returns whether field <tt>'Transfer-Encoding'</tt> is set to <tt>'chunked'</tt>.
# - #connection_close?: Returns whether field <tt>'Connection'</tt> is set to <tt>'close'</tt>.
# - #connection_keep_alive?: Returns whether field <tt>'Connection'</tt> is set to <tt>'keep-alive'</tt>.
# - #key?: Returns whether a given key exists.
#
# === Iterators
#
# - #each_capitalized: Passes each field capitalized-name/value pair to the block.
# - #each_capitalized_name: Passes each capitalized field name to the block.
# - #each_header: Passes each field name/value pair to the block.
# - #each_name: Passes each field name to the block.
# - #each_value: Passes each string field value to the block.
#
module Gem::Net::HTTPHeader
MAX_KEY_LENGTH = 1024
MAX_FIELD_LENGTH = 65536
def initialize_http_header(initheader) #:nodoc:
@header = {}
return unless initheader
initheader.each do |key, value|
warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE
if value.nil?
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
@header[key.downcase.to_s] = [value]
end
end
end
def size #:nodoc: obsolete
@header.size
end
alias length size #:nodoc: obsolete
# Returns the string field value for the case-insensitive field +key+,
# or +nil+ if there is no such key;
# see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
#
# res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
# res['Connection'] # => "keep-alive"
# res['Nosuch'] # => nil
#
# Note that some field values may be retrieved via convenience methods;
# see {Getters}[rdoc-ref:Gem::Net::HTTPHeader@Getters].
def [](key)
a = @header[key.downcase.to_s] or return nil
a.join(', ')
end
# Sets the value for the case-insensitive +key+ to +val+,
# overwriting the previous value if the field exists;
# see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
#
# req = Gem::Net::HTTP::Get.new(uri)
# req['Accept'] # => "*/*"
# req['Accept'] = 'text/html'
# req['Accept'] # => "text/html"
#
# Note that some field values may be set via convenience methods;
# see {Setters}[rdoc-ref:Gem::Net::HTTPHeader@Setters].
def []=(key, val)
unless val
@header.delete key.downcase.to_s
return val
end
set_field(key, val)
end
# Adds value +val+ to the value array for field +key+ if the field exists;
# creates the field with the given +key+ and +val+ if it does not exist.
# see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
#
# req = Gem::Net::HTTP::Get.new(uri)
# req.add_field('Foo', 'bar')
# req['Foo'] # => "bar"
# req.add_field('Foo', 'baz')
# req['Foo'] # => "bar, baz"
# req.add_field('Foo', %w[baz bam])
# req['Foo'] # => "bar, baz, baz, bam"
# req.get_fields('Foo') # => ["bar", "baz", "baz", "bam"]
#
def add_field(key, val)
stringified_downcased_key = key.downcase.to_s
if @header.key?(stringified_downcased_key)
append_field_value(@header[stringified_downcased_key], val)
else
set_field(key, val)
end
end
private def set_field(key, val)
case val
when Enumerable
ary = []
append_field_value(ary, val)
@header[key.downcase.to_s] = ary
else
val = val.to_s # for compatibility use to_s instead of to_str
if val.b.count("\r\n") > 0
raise ArgumentError, 'header field value cannot include CR/LF'
end
@header[key.downcase.to_s] = [val]
end
end
private def append_field_value(ary, val)
case val
when Enumerable
val.each{|x| append_field_value(ary, x)}
else
val = val.to_s
if /[\r\n]/n.match?(val.b)
raise ArgumentError, 'header field value cannot include CR/LF'
end
ary.push val
end
end
# Returns the array field value for the given +key+,
# or +nil+ if there is no such field;
# see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
#
# res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
# res.get_fields('Connection') # => ["keep-alive"]
# res.get_fields('Nosuch') # => nil
#
def get_fields(key)
stringified_downcased_key = key.downcase.to_s
return nil unless @header[stringified_downcased_key]
@header[stringified_downcased_key].dup
end
# call-seq:
# fetch(key, default_val = nil) {|key| ... } -> object
# fetch(key, default_val = nil) -> value or default_val
#
# With a block, returns the string value for +key+ if it exists;
# otherwise returns the value of the block;
# ignores the +default_val+;
# see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
#
# res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
#
# # Field exists; block not called.
# res.fetch('Connection') do |value|
# fail 'Cannot happen'
# end # => "keep-alive"
#
# # Field does not exist; block called.
# res.fetch('Nosuch') do |value|
# value.downcase
# end # => "nosuch"
#
# With no block, returns the string value for +key+ if it exists;
# otherwise, returns +default_val+ if it was given;
# otherwise raises an exception:
#
# res.fetch('Connection', 'Foo') # => "keep-alive"
# res.fetch('Nosuch', 'Foo') # => "Foo"
# res.fetch('Nosuch') # Raises KeyError.
#
def fetch(key, *args, &block) #:yield: +key+
a = @header.fetch(key.downcase.to_s, *args, &block)
a.kind_of?(Array) ? a.join(', ') : a
end
# Calls the block with each key/value pair:
#
# res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
# res.each_header do |key, value|
# p [key, value] if key.start_with?('c')
# end
#
# Output:
#
# ["content-type", "application/json; charset=utf-8"]
# ["connection", "keep-alive"]
# ["cache-control", "max-age=43200"]
# ["cf-cache-status", "HIT"]
# ["cf-ray", "771d17e9bc542cf5-ORD"]
#
# Returns an enumerator if no block is given.
#
# Gem::Net::HTTPHeader#each is an alias for Gem::Net::HTTPHeader#each_header.
def each_header #:yield: +key+, +value+
block_given? or return enum_for(__method__) { @header.size }
@header.each do |k,va|
yield k, va.join(', ')
end
end
alias each each_header
# Calls the block with each field key:
#
# res = Gem::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"
#
# Returns an enumerator if no block is given.
#
# Gem::Net::HTTPHeader#each_name is an alias for Gem::Net::HTTPHeader#each_key.
def each_name(&block) #:yield: +key+
block_given? or return enum_for(__method__) { @header.size }
@header.each_key(&block)
end
alias each_key each_name
# Calls the block with each capitalized field name:
#
# res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
# res.each_capitalized_name do |key|
# p key if key.start_with?('C')
# end
#
# Output:
#
# "Content-Type"
# "Connection"
# "Cache-Control"
# "Cf-Cache-Status"
# "Cf-Ray"
#
# The capitalization is system-dependent;
# see {Case Mapping}[https://docs.ruby-lang.org/en/master/case_mapping_rdoc.html].
#
# Returns an enumerator if no block is given.
def each_capitalized_name #:yield: +key+
block_given? or return enum_for(__method__) { @header.size }
@header.each_key do |k|
yield capitalize(k)
end
end
# Calls the block with each string field value:
#
# res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
# res.each_value do |value|
# p value if value.start_with?('c')
# end
#
# Output:
#
# "chunked"
# "cf-q-config;dur=6.0000002122251e-06"
# "cloudflare"
#
# Returns an enumerator if no block is given.
def each_value #:yield: +value+
block_given? or return enum_for(__method__) { @header.size }
@header.each_value do |va|
yield va.join(', ')
end
end
# Removes the header for the given case-insensitive +key+
# (see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]);
# returns the deleted value, or +nil+ if no such field exists:
#
# req = Gem::Net::HTTP::Get.new(uri)
# req.delete('Accept') # => ["*/*"]
# req.delete('Nosuch') # => nil
#
def delete(key)
@header.delete(key.downcase.to_s)
end
# Returns +true+ if the field for the case-insensitive +key+ exists, +false+ otherwise:
#
# req = Gem::Net::HTTP::Get.new(uri)
# req.key?('Accept') # => true
# req.key?('Nosuch') # => false
#
def key?(key)
@header.key?(key.downcase.to_s)
end
# Returns a hash of the key/value pairs:
#
# req = Gem::Net::HTTP::Get.new(uri)
# req.to_hash
# # =>
# {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
# "accept"=>["*/*"],
# "user-agent"=>["Ruby"],
# "host"=>["jsonplaceholder.typicode.com"]}
#
def to_hash
@header.dup
end
# Like #each_header, but the keys are returned in capitalized form.
#
# Gem::Net::HTTPHeader#canonical_each is an alias for Gem::Net::HTTPHeader#each_capitalized.
def each_capitalized
block_given? or return enum_for(__method__) { @header.size }
@header.each do |k,v|
yield capitalize(k), v.join(', ')
end
end
alias canonical_each each_capitalized
def capitalize(name)
name.to_s.split(/-/).map {|s| s.capitalize }.join('-')
end
private :capitalize
# Returns an array of Range objects that represent
# the value of field <tt>'Range'</tt>,
# or +nil+ if there is no such field;
# see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]:
#
# req = Gem::Net::HTTP::Get.new(uri)
# req['Range'] = 'bytes=0-99,200-299,400-499'
# req.range # => [0..99, 200..299, 400..499]
# req.delete('Range')
# req.range # # => nil
#
def range
return nil unless @header['range']
value = self['Range']
# byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec )
# *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] )
# corrected collected ABNF
# http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1
# http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C
# http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5
unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value
raise Gem::Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'"
end
byte_range_set = $1
result = byte_range_set.split(/,/).map {|spec|
m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or
raise Gem::Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'"
d1 = m[1].to_i
d2 = m[2].to_i
if m[1] and m[2]
if d1 > d2
raise Gem::Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'"
end
d1..d2
elsif m[1]
d1..-1
elsif m[2]
-d2..-1
else
raise Gem::Net::HTTPHeaderSyntaxError, 'range is not specified'
end
}
# if result.empty?
# byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec
# but above regexp already denies it.
if result.size == 1 && result[0].begin == 0 && result[0].end == -1
raise Gem::Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length'
end
result
end
# call-seq:
# set_range(length) -> length
# set_range(offset, length) -> range
# set_range(begin..length) -> range
#
# Sets the value for field <tt>'Range'</tt>;
# see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]:
#
# With argument +length+:
#
# req = Gem::Net::HTTP::Get.new(uri)
# req.set_range(100) # => 100
# req['Range'] # => "bytes=0-99"
#
# With arguments +offset+ and +length+:
#
# req.set_range(100, 100) # => 100...200
# req['Range'] # => "bytes=100-199"
#
# With argument +range+:
#
# req.set_range(100..199) # => 100..199
# req['Range'] # => "bytes=100-199"
#
# Gem::Net::HTTPHeader#range= is an alias for Gem::Net::HTTPHeader#set_range.
def set_range(r, e = nil)
unless r
@header.delete 'range'
return r
end
r = (r...r+e) if e
case r
when Numeric
n = r.to_i
rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
when Range
first = r.first
last = r.end
last -= 1 if r.exclude_end?
if last == -1
rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
else
raise Gem::Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0
raise Gem::Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0
raise Gem::Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last
rangestr = "#{first}-#{last}"
end
else
raise TypeError, 'Range/Integer is required'
end
@header['range'] = ["bytes=#{rangestr}"]
r
end
alias range= set_range
# 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 = Gem::Net::HTTP.get_response(hostname, '/nosuch/1')
# res.content_length # => 2
# res = Gem::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
raise Gem::Net::HTTPHeaderSyntaxError, 'wrong Content-Length format'
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 = Gem::Net::HTTP::Post.new(_uri) # => #<Gem::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 = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
#
def content_length=(len)
unless len
@header.delete 'content-length'
return nil
end
@header['content-length'] = [len.to_i.to_s]
end
# 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 = Gem::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 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 = Gem::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
raise Gem::Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
return unless m[1] == 'bytes'
m[2].to_i .. m[3].to_i
end
# 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 = Gem::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 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 = Gem::Net::HTTP.get_response(hostname, '/todos/1')
# res['content-type'] # => "application/json; charset=utf-8"
# res.content_type # => "application/json"
#
def content_type
main = main_type()
return nil unless main
sub = sub_type()
if sub
"#{main}/#{sub}"
else
main
end
end
# 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 = Gem::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 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 = Gem::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('/')
return nil unless sub
sub.strip
end
# 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 = Gem::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(';')
list.shift
list.each do |param|
k, v = *param.split('=', 2)
result[k.strip] = v.strip
end
result
end
# 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 = Gem::Net::HTTP::Get.new(uri)
# req.set_content_type('application/json') # => ["application/json"]
#
# Gem::Net::HTTPHeader#content_type= is an alias for Gem::Net::HTTPHeader#set_content_type.
def set_content_type(type, params = {})
@header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
end
alias content_type= set_content_type
# 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}[https://docs.ruby-lang.org/en/master/URI.html#method-c-encode_www_form].
#
# With only argument +params+ given,
# sets the body to a URL-encoded string with the default separator <tt>'&'</tt>:
#
# req = Gem::Net::HTTP::Post.new('example.com')
#
# 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"
#
# Gem::Net::HTTPHeader#form_data= is an alias for Gem::Net::HTTPHeader#set_form_data.
def set_form_data(params, sep = '&')
query = URI.encode_www_form(params)
query.gsub!(/&/, sep) if sep != '&'
self.body = query
self.content_type = 'application/x-www-form-urlencoded'
end
alias form_data= set_form_data
# 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}[https://docs.ruby-lang.org/en/master/Enumerable.html#module-Enumerable-label-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 = Gem::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
@body = nil
@body_stream = nil
@form_option = formopt
case enctype
when /\Aapplication\/x-www-form-urlencoded\z/i,
/\Amultipart\/form-data\z/i
self.content_type = enctype
else
raise ArgumentError, "invalid enctype: #{enctype}"
end
end
# 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
# 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)
'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}
@header['proxy-connection']&.grep(token) {return true}
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}
@header['proxy-connection']&.grep(token) {return true}
false
end
end

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Gem::Net::HTTP::ProxyDelta #:nodoc: internal use only
private
def conn_address
proxy_address()
end
def conn_port
proxy_port()
end
def edit_path(path)
use_ssl? ? path : "http://#{addr_port()}#{path}"
end
end

View file

@ -0,0 +1,88 @@
# frozen_string_literal: true
# This class is the base class for \Gem::Net::HTTP request classes.
# The class should not be used directly;
# instead you should use its subclasses, listed below.
#
# == Creating a Request
#
# An request object may be created with either a URI or a string hostname:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('https://jsonplaceholder.typicode.com/')
# req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET>
# req = Gem::Net::HTTP::Get.new(uri.hostname) # => #<Gem::Net::HTTP::Get GET>
#
# And with any of the subclasses:
#
# req = Gem::Net::HTTP::Head.new(uri) # => #<Gem::Net::HTTP::Head HEAD>
# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
# req = Gem::Net::HTTP::Put.new(uri) # => #<Gem::Net::HTTP::Put PUT>
# # ...
#
# The new instance is suitable for use as the argument to Gem::Net::HTTP#request.
#
# == Request Headers
#
# A new request object has these header fields by default:
#
# req.to_hash
# # =>
# {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
# "accept"=>["*/*"],
# "user-agent"=>["Ruby"],
# "host"=>["jsonplaceholder.typicode.com"]}
#
# See:
#
# - {Request header Accept-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Accept-Encoding]
# and {Compression and Decompression}[rdoc-ref:Gem::Net::HTTP@Compression+and+Decompression].
# - {Request header Accept}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#accept-request-header].
# - {Request header User-Agent}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#user-agent-request-header].
# - {Request header Host}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#host-request-header].
#
# You can add headers or override default headers:
#
# # res = Gem::Net::HTTP::Get.new(uri, {'foo' => '0', 'bar' => '1'})
#
# This class (and therefore its subclasses) also includes (indirectly)
# module Gem::Net::HTTPHeader, which gives access to its
# {methods for setting headers}[rdoc-ref:Gem::Net::HTTPHeader@Setters].
#
# == Request Subclasses
#
# Subclasses for HTTP requests:
#
# - Gem::Net::HTTP::Get
# - Gem::Net::HTTP::Head
# - Gem::Net::HTTP::Post
# - Gem::Net::HTTP::Put
# - Gem::Net::HTTP::Delete
# - Gem::Net::HTTP::Options
# - Gem::Net::HTTP::Trace
# - Gem::Net::HTTP::Patch
#
# Subclasses for WebDAV requests:
#
# - Gem::Net::HTTP::Propfind
# - Gem::Net::HTTP::Proppatch
# - Gem::Net::HTTP::Mkcol
# - Gem::Net::HTTP::Copy
# - Gem::Net::HTTP::Move
# - Gem::Net::HTTP::Lock
# - Gem::Net::HTTP::Unlock
#
class Gem::Net::HTTPRequest < Gem::Net::HTTPGenericRequest
# Creates an HTTP request object for +path+.
#
# +initheader+ are the default headers to use. Gem::Net::HTTP adds
# Accept-Encoding to enable compression of the response body unless
# Accept-Encoding or Range are supplied in +initheader+.
def initialize(path, initheader = nil)
super self.class::METHOD,
self.class::REQUEST_HAS_BODY,
self.class::RESPONSE_HAS_BODY,
path, initheader
end
end

View file

@ -0,0 +1,425 @@
# frozen_string_literal: true
# HTTP/1.1 methods --- RFC2616
# \Class for representing
# {HTTP method GET}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#GET_method]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Properties:
#
# - Request body: optional.
# - Response body: yes.
# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes.
#
# Related:
#
# - Gem::Net::HTTP.get: sends +GET+ request, returns response body.
# - Gem::Net::HTTP#get: sends +GET+ request, returns response object.
#
class Gem::Net::HTTP::Get < Gem::Net::HTTPRequest
METHOD = 'GET'
REQUEST_HAS_BODY = false
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {HTTP method HEAD}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#HEAD_method]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Head.new(uri) # => #<Gem::Net::HTTP::Head HEAD>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Properties:
#
# - Request body: optional.
# - Response body: no.
# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes.
#
# Related:
#
# - Gem::Net::HTTP#head: sends +HEAD+ request, returns response object.
#
class Gem::Net::HTTP::Head < Gem::Net::HTTPRequest
METHOD = 'HEAD'
REQUEST_HAS_BODY = false
RESPONSE_HAS_BODY = false
end
# \Class for representing
# {HTTP method POST}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#POST_method]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# uri.path = '/posts'
# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
# req.body = '{"title": "foo","body": "bar","userId": 1}'
# req.content_type = 'application/json'
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Properties:
#
# - Request body: yes.
# - Response body: yes.
# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no.
# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes.
#
# Related:
#
# - Gem::Net::HTTP.post: sends +POST+ request, returns response object.
# - Gem::Net::HTTP#post: sends +POST+ request, returns response object.
#
class Gem::Net::HTTP::Post < Gem::Net::HTTPRequest
METHOD = 'POST'
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {HTTP method PUT}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PUT_method]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# uri.path = '/posts'
# req = Gem::Net::HTTP::Put.new(uri) # => #<Gem::Net::HTTP::Put PUT>
# req.body = '{"title": "foo","body": "bar","userId": 1}'
# req.content_type = 'application/json'
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Properties:
#
# - Request body: yes.
# - Response body: yes.
# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
#
class Gem::Net::HTTP::Put < Gem::Net::HTTPRequest
METHOD = 'PUT'
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {HTTP method DELETE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#DELETE_method]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# uri.path = '/posts/1'
# req = Gem::Net::HTTP::Delete.new(uri) # => #<Gem::Net::HTTP::Delete DELETE>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Properties:
#
# - Request body: optional.
# - Response body: yes.
# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
#
# Related:
#
# - Gem::Net::HTTP#delete: sends +DELETE+ request, returns response object.
#
class Gem::Net::HTTP::Delete < Gem::Net::HTTPRequest
METHOD = 'DELETE'
REQUEST_HAS_BODY = false
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {HTTP method OPTIONS}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#OPTIONS_method]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Options.new(uri) # => #<Gem::Net::HTTP::Options OPTIONS>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Properties:
#
# - Request body: optional.
# - Response body: yes.
# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
#
# Related:
#
# - Gem::Net::HTTP#options: sends +OPTIONS+ request, returns response object.
#
class Gem::Net::HTTP::Options < Gem::Net::HTTPRequest
METHOD = 'OPTIONS'
REQUEST_HAS_BODY = false
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {HTTP method TRACE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#TRACE_method]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Trace.new(uri) # => #<Gem::Net::HTTP::Trace TRACE>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Properties:
#
# - Request body: no.
# - Response body: yes.
# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
#
# Related:
#
# - Gem::Net::HTTP#trace: sends +TRACE+ request, returns response object.
#
class Gem::Net::HTTP::Trace < Gem::Net::HTTPRequest
METHOD = 'TRACE'
REQUEST_HAS_BODY = false
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {HTTP method PATCH}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PATCH_method]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# uri.path = '/posts'
# req = Gem::Net::HTTP::Patch.new(uri) # => #<Gem::Net::HTTP::Patch PATCH>
# req.body = '{"title": "foo","body": "bar","userId": 1}'
# req.content_type = 'application/json'
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Properties:
#
# - Request body: yes.
# - Response body: yes.
# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no.
# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
#
# Related:
#
# - Gem::Net::HTTP#patch: sends +PATCH+ request, returns response object.
#
class Gem::Net::HTTP::Patch < Gem::Net::HTTPRequest
METHOD = 'PATCH'
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
end
#
# WebDAV methods --- RFC2518
#
# \Class for representing
# {WebDAV method PROPFIND}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Propfind.new(uri) # => #<Gem::Net::HTTP::Propfind PROPFIND>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Related:
#
# - Gem::Net::HTTP#propfind: sends +PROPFIND+ request, returns response object.
#
class Gem::Net::HTTP::Propfind < Gem::Net::HTTPRequest
METHOD = 'PROPFIND'
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {WebDAV method PROPPATCH}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Proppatch.new(uri) # => #<Gem::Net::HTTP::Proppatch PROPPATCH>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Related:
#
# - Gem::Net::HTTP#proppatch: sends +PROPPATCH+ request, returns response object.
#
class Gem::Net::HTTP::Proppatch < Gem::Net::HTTPRequest
METHOD = 'PROPPATCH'
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {WebDAV method MKCOL}[http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Mkcol.new(uri) # => #<Gem::Net::HTTP::Mkcol MKCOL>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Related:
#
# - Gem::Net::HTTP#mkcol: sends +MKCOL+ request, returns response object.
#
class Gem::Net::HTTP::Mkcol < Gem::Net::HTTPRequest
METHOD = 'MKCOL'
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {WebDAV method COPY}[http://www.webdav.org/specs/rfc4918.html#METHOD_COPY]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Copy.new(uri) # => #<Gem::Net::HTTP::Copy COPY>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Related:
#
# - Gem::Net::HTTP#copy: sends +COPY+ request, returns response object.
#
class Gem::Net::HTTP::Copy < Gem::Net::HTTPRequest
METHOD = 'COPY'
REQUEST_HAS_BODY = false
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {WebDAV method MOVE}[http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Move.new(uri) # => #<Gem::Net::HTTP::Move MOVE>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Related:
#
# - Gem::Net::HTTP#move: sends +MOVE+ request, returns response object.
#
class Gem::Net::HTTP::Move < Gem::Net::HTTPRequest
METHOD = 'MOVE'
REQUEST_HAS_BODY = false
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {WebDAV method LOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_LOCK]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Lock.new(uri) # => #<Gem::Net::HTTP::Lock LOCK>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Related:
#
# - Gem::Net::HTTP#lock: sends +LOCK+ request, returns response object.
#
class Gem::Net::HTTP::Lock < Gem::Net::HTTPRequest
METHOD = 'LOCK'
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
end
# \Class for representing
# {WebDAV method UNLOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_UNLOCK]:
#
# require 'rubygems/net-http/lib/net/http'
# uri = URI('http://example.com')
# hostname = uri.hostname # => "example.com"
# req = Gem::Net::HTTP::Unlock.new(uri) # => #<Gem::Net::HTTP::Unlock UNLOCK>
# res = Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end
#
# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
#
# Related:
#
# - Gem::Net::HTTP#unlock: sends +UNLOCK+ request, returns response object.
#
class Gem::Net::HTTP::Unlock < Gem::Net::HTTPRequest
METHOD = 'UNLOCK'
REQUEST_HAS_BODY = true
RESPONSE_HAS_BODY = true
end

View file

@ -0,0 +1,738 @@
# frozen_string_literal: true
# This class is the base class for \Gem::Net::HTTP response classes.
#
# == About the Examples
#
# :include: doc/net-http/examples.rdoc
#
# == Returned Responses
#
# \Method Gem::Net::HTTP.get_response returns
# an instance of one of the subclasses of \Gem::Net::HTTPResponse:
#
# Gem::Net::HTTP.get_response(uri)
# # => #<Gem::Net::HTTPOK 200 OK readbody=true>
# Gem::Net::HTTP.get_response(hostname, '/nosuch')
# # => #<Gem::Net::HTTPNotFound 404 Not Found readbody=true>
#
# As does method Gem::Net::HTTP#request:
#
# req = Gem::Net::HTTP::Get.new(uri)
# Gem::Net::HTTP.start(hostname) do |http|
# http.request(req)
# end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
#
# \Class \Gem::Net::HTTPResponse includes module Gem::Net::HTTPHeader,
# which provides access to response header values via (among others):
#
# - \Hash-like method <tt>[]</tt>.
# - Specific reader methods, such as +content_type+.
#
# Examples:
#
# res = Gem::Net::HTTP.get_response(uri) # => #<Gem::Net::HTTPOK 200 OK readbody=true>
# res['Content-Type'] # => "text/html; charset=UTF-8"
# res.content_type # => "text/html"
#
# == Response Subclasses
#
# \Class \Gem::Net::HTTPResponse has a subclass for each
# {HTTP status code}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes].
# You can look up the response class for a given code:
#
# Gem::Net::HTTPResponse::CODE_TO_OBJ['200'] # => Gem::Net::HTTPOK
# Gem::Net::HTTPResponse::CODE_TO_OBJ['400'] # => Gem::Net::HTTPBadRequest
# Gem::Net::HTTPResponse::CODE_TO_OBJ['404'] # => Gem::Net::HTTPNotFound
#
# And you can retrieve the status code for a response object:
#
# Gem::Net::HTTP.get_response(uri).code # => "200"
# Gem::Net::HTTP.get_response(hostname, '/nosuch').code # => "404"
#
# The response subclasses (indentation shows class hierarchy):
#
# - Gem::Net::HTTPUnknownResponse (for unhandled \HTTP extensions).
#
# - Gem::Net::HTTPInformation:
#
# - Gem::Net::HTTPContinue (100)
# - Gem::Net::HTTPSwitchProtocol (101)
# - Gem::Net::HTTPProcessing (102)
# - Gem::Net::HTTPEarlyHints (103)
#
# - Gem::Net::HTTPSuccess:
#
# - Gem::Net::HTTPOK (200)
# - Gem::Net::HTTPCreated (201)
# - Gem::Net::HTTPAccepted (202)
# - Gem::Net::HTTPNonAuthoritativeInformation (203)
# - Gem::Net::HTTPNoContent (204)
# - Gem::Net::HTTPResetContent (205)
# - Gem::Net::HTTPPartialContent (206)
# - Gem::Net::HTTPMultiStatus (207)
# - Gem::Net::HTTPAlreadyReported (208)
# - Gem::Net::HTTPIMUsed (226)
#
# - Gem::Net::HTTPRedirection:
#
# - Gem::Net::HTTPMultipleChoices (300)
# - Gem::Net::HTTPMovedPermanently (301)
# - Gem::Net::HTTPFound (302)
# - Gem::Net::HTTPSeeOther (303)
# - Gem::Net::HTTPNotModified (304)
# - Gem::Net::HTTPUseProxy (305)
# - Gem::Net::HTTPTemporaryRedirect (307)
# - Gem::Net::HTTPPermanentRedirect (308)
#
# - Gem::Net::HTTPClientError:
#
# - Gem::Net::HTTPBadRequest (400)
# - Gem::Net::HTTPUnauthorized (401)
# - Gem::Net::HTTPPaymentRequired (402)
# - Gem::Net::HTTPForbidden (403)
# - Gem::Net::HTTPNotFound (404)
# - Gem::Net::HTTPMethodNotAllowed (405)
# - Gem::Net::HTTPNotAcceptable (406)
# - Gem::Net::HTTPProxyAuthenticationRequired (407)
# - Gem::Net::HTTPRequestTimeOut (408)
# - Gem::Net::HTTPConflict (409)
# - Gem::Net::HTTPGone (410)
# - Gem::Net::HTTPLengthRequired (411)
# - Gem::Net::HTTPPreconditionFailed (412)
# - Gem::Net::HTTPRequestEntityTooLarge (413)
# - Gem::Net::HTTPRequestURITooLong (414)
# - Gem::Net::HTTPUnsupportedMediaType (415)
# - Gem::Net::HTTPRequestedRangeNotSatisfiable (416)
# - Gem::Net::HTTPExpectationFailed (417)
# - Gem::Net::HTTPMisdirectedRequest (421)
# - Gem::Net::HTTPUnprocessableEntity (422)
# - Gem::Net::HTTPLocked (423)
# - Gem::Net::HTTPFailedDependency (424)
# - Gem::Net::HTTPUpgradeRequired (426)
# - Gem::Net::HTTPPreconditionRequired (428)
# - Gem::Net::HTTPTooManyRequests (429)
# - Gem::Net::HTTPRequestHeaderFieldsTooLarge (431)
# - Gem::Net::HTTPUnavailableForLegalReasons (451)
#
# - Gem::Net::HTTPServerError:
#
# - Gem::Net::HTTPInternalServerError (500)
# - Gem::Net::HTTPNotImplemented (501)
# - Gem::Net::HTTPBadGateway (502)
# - Gem::Net::HTTPServiceUnavailable (503)
# - Gem::Net::HTTPGatewayTimeOut (504)
# - Gem::Net::HTTPVersionNotSupported (505)
# - Gem::Net::HTTPVariantAlsoNegotiates (506)
# - Gem::Net::HTTPInsufficientStorage (507)
# - Gem::Net::HTTPLoopDetected (508)
# - Gem::Net::HTTPNotExtended (510)
# - Gem::Net::HTTPNetworkAuthenticationRequired (511)
#
# There is also the Gem::Net::HTTPBadResponse exception which is raised when
# there is a protocol error.
#
class Gem::Net::HTTPResponse
class << self
# true if the response has a body.
def body_permitted?
self::HAS_BODY
end
def exception_type # :nodoc: internal use only
self::EXCEPTION_TYPE
end
def read_new(sock) #:nodoc: internal use only
httpv, code, msg = read_status_line(sock)
res = response_class(code).new(httpv, code, msg)
each_response_header(sock) do |k,v|
res.add_field k, v
end
res
end
private
def read_status_line(sock)
str = sock.readline
m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or
raise Gem::Net::HTTPBadResponse, "wrong status line: #{str.dump}"
m.captures
end
def response_class(code)
CODE_TO_OBJ[code] or
CODE_CLASS_TO_OBJ[code[0,1]] or
Gem::Net::HTTPUnknownResponse
end
def each_response_header(sock)
key = value = nil
while true
line = sock.readuntil("\n", true).sub(/\s+\z/, '')
break if line.empty?
if line[0] == ?\s or line[0] == ?\t and value
value << ' ' unless value.empty?
value << line.strip
else
yield key, value if key
key, value = line.strip.split(/\s*:\s*/, 2)
raise Gem::Net::HTTPBadResponse, 'wrong header line format' if value.nil?
end
end
yield key, value if key
end
end
# next is to fix bug in RDoc, where the private inside class << self
# spills out.
public
include Gem::Net::HTTPHeader
def initialize(httpv, code, msg) #:nodoc: internal use only
@http_version = httpv
@code = code
@message = msg
initialize_http_header nil
@body = nil
@read = false
@uri = nil
@decode_content = false
@body_encoding = false
@ignore_eof = true
end
# The HTTP version supported by the server.
attr_reader :http_version
# The HTTP result code string. For example, '302'. You can also
# determine the response type by examining which response subclass
# the response object is an instance of.
attr_reader :code
# The HTTP result message sent by the server. For example, 'Not Found'.
attr_reader :message
alias msg message # :nodoc: obsolete
# The URI used to fetch this response. The response URI is only available
# if a URI was used to create the request.
attr_reader :uri
# Set to true automatically when the request did not contain an
# Accept-Encoding header from the user.
attr_accessor :decode_content
# Returns the value set by body_encoding=, or +false+ if none;
# see #body_encoding=.
attr_reader :body_encoding
# Sets the encoding that should be used when reading the body:
#
# - If the given value is an Encoding object, that encoding will be used.
# - Otherwise if the value is a string, the value of
# {Encoding#find(value)}[https://docs.ruby-lang.org/en/master/Encoding.html#method-c-find]
# will be used.
# - Otherwise an encoding will be deduced from the body itself.
#
# Examples:
#
# http = Gem::Net::HTTP.new(hostname)
# req = Gem::Net::HTTP::Get.new('/')
#
# http.request(req) do |res|
# p res.body.encoding # => #<Encoding:ASCII-8BIT>
# end
#
# http.request(req) do |res|
# res.body_encoding = "UTF-8"
# p res.body.encoding # => #<Encoding:UTF-8>
# end
#
def body_encoding=(value)
value = Encoding.find(value) if value.is_a?(String)
@body_encoding = value
end
# Whether to ignore EOF when reading bodies with a specified Content-Length
# header.
attr_accessor :ignore_eof
def inspect
"#<#{self.class} #{@code} #{@message} readbody=#{@read}>"
end
#
# response <-> exception relationship
#
def code_type #:nodoc:
self.class
end
def error! #:nodoc:
message = @code
message = "#{message} #{@message.dump}" if @message
raise error_type().new(message, self)
end
def error_type #:nodoc:
self.class::EXCEPTION_TYPE
end
# Raises an HTTP error if the response is not 2xx (success).
def value
error! unless self.kind_of?(Gem::Net::HTTPSuccess)
end
def uri= uri # :nodoc:
@uri = uri.dup if uri
end
#
# header (for backward compatibility only; DO NOT USE)
#
def response #:nodoc:
warn "Gem::Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE
self
end
def header #:nodoc:
warn "Gem::Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE
self
end
def read_header #:nodoc:
warn "Gem::Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE
self
end
#
# body
#
def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only
@socket = sock
@body_exist = reqmethodallowbody && self.class.body_permitted?
begin
yield
self.body # ensure to read body
ensure
@socket = nil
end
end
# Gets the entity body returned by the remote HTTP server.
#
# If a block is given, the body is passed to the block, and
# the body is provided in fragments, as it is read in from the socket.
#
# If +dest+ argument is given, response is read into that variable,
# with <code>dest#<<</code> method (it could be String or IO, or any
# other object responding to <code><<</code>).
#
# Calling this method a second or subsequent time for the same
# HTTPResponse object will return the value already read.
#
# http.request_get('/index.html') {|res|
# puts res.read_body
# }
#
# http.request_get('/index.html') {|res|
# p res.read_body.object_id # 538149362
# p res.read_body.object_id # 538149362
# }
#
# # using iterator
# http.request_get('/index.html') {|res|
# res.read_body do |segment|
# print segment
# end
# }
#
def read_body(dest = nil, &block)
if @read
raise IOError, "#{self.class}\#read_body called twice" if dest or block
return @body
end
to = procdest(dest, block)
stream_check
if @body_exist
read_body_0 to
@body = to
else
@body = nil
end
@read = true
return if @body.nil?
case enc = @body_encoding
when Encoding, false, nil
# Encoding: force given encoding
# false/nil: do not force encoding
else
# other value: detect encoding from body
enc = detect_encoding(@body)
end
@body.force_encoding(enc) if enc
@body
end
# Returns the string response body;
# note that repeated calls for the unmodified body return a cached string:
#
# path = '/todos/1'
# Gem::Net::HTTP.start(hostname) do |http|
# res = http.get(path)
# p res.body
# p http.head(path).body # No body.
# end
#
# Output:
#
# "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}"
# nil
#
def body
read_body()
end
# Sets the body of the response to the given value.
def body=(value)
@body = value
end
alias entity body #:nodoc: obsolete
private
# :nodoc:
def detect_encoding(str, encoding=nil)
if encoding
elsif encoding = type_params['charset']
elsif encoding = check_bom(str)
else
encoding = case content_type&.downcase
when %r{text/x(?:ht)?ml|application/(?:[^+]+\+)?xml}
/\A<xml[ \t\r\n]+
version[ \t\r\n]*=[ \t\r\n]*(?:"[0-9.]+"|'[0-9.]*')[ \t\r\n]+
encoding[ \t\r\n]*=[ \t\r\n]*
(?:"([A-Za-z][\-A-Za-z0-9._]*)"|'([A-Za-z][\-A-Za-z0-9._]*)')/x =~ str
encoding = $1 || $2 || Encoding::UTF_8
when %r{text/html.*}
sniff_encoding(str)
end
end
return encoding
end
# :nodoc:
def sniff_encoding(str, encoding=nil)
# the encoding sniffing algorithm
# http://www.w3.org/TR/html5/parsing.html#determining-the-character-encoding
if enc = scanning_meta(str)
enc
# 6. last visited page or something
# 7. frequency
elsif str.ascii_only?
Encoding::US_ASCII
elsif str.dup.force_encoding(Encoding::UTF_8).valid_encoding?
Encoding::UTF_8
end
# 8. implementation-defined or user-specified
end
# :nodoc:
def check_bom(str)
case str.byteslice(0, 2)
when "\xFE\xFF"
return Encoding::UTF_16BE
when "\xFF\xFE"
return Encoding::UTF_16LE
end
if "\xEF\xBB\xBF" == str.byteslice(0, 3)
return Encoding::UTF_8
end
nil
end
# :nodoc:
def scanning_meta(str)
require 'strscan'
ss = StringScanner.new(str)
if ss.scan_until(/<meta[\t\n\f\r ]*/)
attrs = {} # attribute_list
got_pragma = false
need_pragma = nil
charset = nil
# step: Attributes
while attr = get_attribute(ss)
name, value = *attr
next if attrs[name]
attrs[name] = true
case name
when 'http-equiv'
got_pragma = true if value == 'content-type'
when 'content'
encoding = extracting_encodings_from_meta_elements(value)
unless charset
charset = encoding
end
need_pragma = true
when 'charset'
need_pragma = false
charset = value
end
end
# step: Processing
return if need_pragma.nil?
return if need_pragma && !got_pragma
charset = Encoding.find(charset) rescue nil
return unless charset
charset = Encoding::UTF_8 if charset == Encoding::UTF_16
return charset # tentative
end
nil
end
def get_attribute(ss)
ss.scan(/[\t\n\f\r \/]*/)
if ss.peek(1) == '>'
ss.getch
return nil
end
name = ss.scan(/[^=\t\n\f\r \/>]*/)
name.downcase!
raise if name.empty?
ss.skip(/[\t\n\f\r ]*/)
if ss.getch != '='
value = ''
return [name, value]
end
ss.skip(/[\t\n\f\r ]*/)
case ss.peek(1)
when '"'
ss.getch
value = ss.scan(/[^"]+/)
value.downcase!
ss.getch
when "'"
ss.getch
value = ss.scan(/[^']+/)
value.downcase!
ss.getch
when '>'
value = ''
else
value = ss.scan(/[^\t\n\f\r >]+/)
value.downcase!
end
[name, value]
end
def extracting_encodings_from_meta_elements(value)
# http://dev.w3.org/html5/spec/fetching-resources.html#algorithm-for-extracting-an-encoding-from-a-meta-element
if /charset[\t\n\f\r ]*=(?:"([^"]*)"|'([^']*)'|["']|\z|([^\t\n\f\r ;]+))/i =~ value
return $1 || $2 || $3
end
return nil
end
##
# Checks for a supported Content-Encoding header and yields an Inflate
# wrapper for this response's socket when zlib is present. If the
# Content-Encoding is not supported or zlib is missing, the plain socket is
# yielded.
#
# If a Content-Range header is present, a plain socket is yielded as the
# bytes in the range may not be a complete deflate block.
def inflater # :nodoc:
return yield @socket unless Gem::Net::HTTP::HAVE_ZLIB
return yield @socket unless @decode_content
return yield @socket if self['content-range']
v = self['content-encoding']
case v&.downcase
when 'deflate', 'gzip', 'x-gzip' then
self.delete 'content-encoding'
inflate_body_io = Inflater.new(@socket)
begin
yield inflate_body_io
success = true
ensure
begin
inflate_body_io.finish
if self['content-length']
self['content-length'] = inflate_body_io.bytes_inflated.to_s
end
rescue => err
# Ignore #finish's error if there is an exception from yield
raise err if success
end
end
when 'none', 'identity' then
self.delete 'content-encoding'
yield @socket
else
yield @socket
end
end
def read_body_0(dest)
inflater do |inflate_body_io|
if chunked?
read_chunked dest, inflate_body_io
return
end
@socket = inflate_body_io
clen = content_length()
if clen
@socket.read clen, dest, @ignore_eof
return
end
clen = range_length()
if clen
@socket.read clen, dest
return
end
@socket.read_all dest
end
end
##
# read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF,
# etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip
# encoded.
#
# See RFC 2616 section 3.6.1 for definitions
def read_chunked(dest, chunk_data_io) # :nodoc:
total = 0
while true
line = @socket.readline
hexlen = line.slice(/[0-9a-fA-F]+/) or
raise Gem::Net::HTTPBadResponse, "wrong chunk size line: #{line}"
len = hexlen.hex
break if len == 0
begin
chunk_data_io.read len, dest
ensure
total += len
@socket.read 2 # \r\n
end
end
until @socket.readline.empty?
# none
end
end
def stream_check
raise IOError, 'attempt to read body out of block' if @socket.nil? || @socket.closed?
end
def procdest(dest, block)
raise ArgumentError, 'both arg and block given for HTTP method' if
dest and block
if block
Gem::Net::ReadAdapter.new(block)
else
dest || +''
end
end
##
# Inflater is a wrapper around Gem::Net::BufferedIO that transparently inflates
# zlib and gzip streams.
class Inflater # :nodoc:
##
# Creates a new Inflater wrapping +socket+
def initialize socket
@socket = socket
# zlib with automatic gzip detection
@inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
end
##
# Finishes the inflate stream.
def finish
return if @inflate.total_in == 0
@inflate.finish
end
##
# The number of bytes inflated, used to update the Content-Length of
# the response.
def bytes_inflated
@inflate.total_out
end
##
# Returns a Gem::Net::ReadAdapter that inflates each read chunk into +dest+.
#
# This allows a large response body to be inflated without storing the
# entire body in memory.
def inflate_adapter(dest)
if dest.respond_to?(:set_encoding)
dest.set_encoding(Encoding::ASCII_8BIT)
elsif dest.respond_to?(:force_encoding)
dest.force_encoding(Encoding::ASCII_8BIT)
end
block = proc do |compressed_chunk|
@inflate.inflate(compressed_chunk) do |chunk|
compressed_chunk.clear
dest << chunk
end
end
Gem::Net::ReadAdapter.new(block)
end
##
# Reads +clen+ bytes from the socket, inflates them, then writes them to
# +dest+. +ignore_eof+ is passed down to Gem::Net::BufferedIO#read
#
# Unlike Gem::Net::BufferedIO#read, this method returns more than +clen+ bytes.
# At this time there is no way for a user of Gem::Net::HTTPResponse to read a
# specific number of bytes from the HTTP response body, so this internal
# API does not return the same number of bytes as were requested.
#
# See https://bugs.ruby-lang.org/issues/6492 for further discussion.
def read clen, dest, ignore_eof = false
temp_dest = inflate_adapter(dest)
@socket.read clen, temp_dest, ignore_eof
end
##
# Reads the rest of the socket, inflates it, then writes it to +dest+.
def read_all dest
temp_dest = inflate_adapter(dest)
@socket.read_all temp_dest
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,84 @@
# frozen_string_literal: true
require_relative '../http'
if $0 == __FILE__
require 'open-uri'
File.foreach(__FILE__) do |line|
puts line
break if line.start_with?('end')
end
puts
puts "Gem::Net::HTTP::STATUS_CODES = {"
url = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv"
URI(url).read.each_line do |line|
code, mes, = line.split(',')
next if ['(Unused)', 'Unassigned', 'Description'].include?(mes)
puts " #{code} => '#{mes}',"
end
puts "} # :nodoc:"
end
Gem::Net::HTTP::STATUS_CODES = {
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
103 => 'Early Hints',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Content Too Large',
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
421 => 'Misdirected Request',
422 => 'Unprocessable Content',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Too Early',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
451 => 'Unavailable For Legal Reasons',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended (OBSOLETED)',
511 => 'Network Authentication Required',
} # :nodoc:

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
=begin
= net/https -- SSL/TLS enhancement for Gem::Net::HTTP.
This file has been merged with net/http. There is no longer any need to
require 'rubygems/net-http/lib/net/https' to use HTTPS.
See Gem::Net::HTTP for details on how to make HTTPS connections.
== Info
'OpenSSL for Ruby 2' project
Copyright (C) 2001 GOTOU Yuuzou <gotoyuzo@notwork.org>
All rights reserved.
== Licence
This program is licensed under the same licence as Ruby.
(See the file 'LICENCE'.)
=end
require_relative 'http'
require 'openssl'