summaryrefslogtreecommitdiff
path: root/tool/lib/webrick/httprequest.rb
diff options
context:
space:
mode:
authorHiroshi SHIBATA <[email protected]>2020-11-02 13:44:28 +0900
committerHiroshi SHIBATA <[email protected]>2020-12-10 18:06:25 +0900
commit5dc786bf86bb6e0da2639f88659598ec8b9db30d (patch)
tree87cc4d68088fc7c2616c7c1bef44c36ca10fa01f /tool/lib/webrick/httprequest.rb
parent46d3ea2c2569e2e5a9ee3e7e206f07f0f8b693f5 (diff)
Move webrick library into internal test toolchain
Notes
Notes: Merged: https://github.com/ruby/ruby/pull/3729
Diffstat (limited to 'tool/lib/webrick/httprequest.rb')
-rw-r--r--tool/lib/webrick/httprequest.rb636
1 files changed, 636 insertions, 0 deletions
diff --git a/tool/lib/webrick/httprequest.rb b/tool/lib/webrick/httprequest.rb
new file mode 100644
index 0000000000..d34eac7ecf
--- /dev/null
+++ b/tool/lib/webrick/httprequest.rb
@@ -0,0 +1,636 @@
+# frozen_string_literal: false
+#
+# httprequest.rb -- HTTPRequest Class
+#
+# Author: IPR -- Internet Programming with Ruby -- writers
+# Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
+# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
+# reserved.
+#
+# $IPR: httprequest.rb,v 1.64 2003/07/13 17:18:22 gotoyuzo Exp $
+
+require 'fiber'
+require 'uri'
+require_relative 'httpversion'
+require_relative 'httpstatus'
+require_relative 'httputils'
+require_relative 'cookie'
+
+module WEBrick
+
+ ##
+ # An HTTP request. This is consumed by service and do_* methods in
+ # WEBrick servlets
+
+ class HTTPRequest
+
+ BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ] # :nodoc:
+
+ # :section: Request line
+
+ ##
+ # The complete request line such as:
+ #
+ # GET / HTTP/1.1
+
+ attr_reader :request_line
+
+ ##
+ # The request method, GET, POST, PUT, etc.
+
+ attr_reader :request_method
+
+ ##
+ # The unparsed URI of the request
+
+ attr_reader :unparsed_uri
+
+ ##
+ # The HTTP version of the request
+
+ attr_reader :http_version
+
+ # :section: Request-URI
+
+ ##
+ # The parsed URI of the request
+
+ attr_reader :request_uri
+
+ ##
+ # The request path
+
+ attr_reader :path
+
+ ##
+ # The script name (CGI variable)
+
+ attr_accessor :script_name
+
+ ##
+ # The path info (CGI variable)
+
+ attr_accessor :path_info
+
+ ##
+ # The query from the URI of the request
+
+ attr_accessor :query_string
+
+ # :section: Header and entity body
+
+ ##
+ # The raw header of the request
+
+ attr_reader :raw_header
+
+ ##
+ # The parsed header of the request
+
+ attr_reader :header
+
+ ##
+ # The parsed request cookies
+
+ attr_reader :cookies
+
+ ##
+ # The Accept header value
+
+ attr_reader :accept
+
+ ##
+ # The Accept-Charset header value
+
+ attr_reader :accept_charset
+
+ ##
+ # The Accept-Encoding header value
+
+ attr_reader :accept_encoding
+
+ ##
+ # The Accept-Language header value
+
+ attr_reader :accept_language
+
+ # :section:
+
+ ##
+ # The remote user (CGI variable)
+
+ attr_accessor :user
+
+ ##
+ # The socket address of the server
+
+ attr_reader :addr
+
+ ##
+ # The socket address of the client
+
+ attr_reader :peeraddr
+
+ ##
+ # Hash of request attributes
+
+ attr_reader :attributes
+
+ ##
+ # Is this a keep-alive connection?
+
+ attr_reader :keep_alive
+
+ ##
+ # The local time this request was received
+
+ attr_reader :request_time
+
+ ##
+ # Creates a new HTTP request. WEBrick::Config::HTTP is the default
+ # configuration.
+
+ def initialize(config)
+ @config = config
+ @buffer_size = @config[:InputBufferSize]
+ @logger = config[:Logger]
+
+ @request_line = @request_method =
+ @unparsed_uri = @http_version = nil
+
+ @request_uri = @host = @port = @path = nil
+ @script_name = @path_info = nil
+ @query_string = nil
+ @query = nil
+ @form_data = nil
+
+ @raw_header = Array.new
+ @header = nil
+ @cookies = []
+ @accept = []
+ @accept_charset = []
+ @accept_encoding = []
+ @accept_language = []
+ @body = ""
+
+ @addr = @peeraddr = nil
+ @attributes = {}
+ @user = nil
+ @keep_alive = false
+ @request_time = nil
+
+ @remaining_size = nil
+ @socket = nil
+
+ @forwarded_proto = @forwarded_host = @forwarded_port =
+ @forwarded_server = @forwarded_for = nil
+ end
+
+ ##
+ # Parses a request from +socket+. This is called internally by
+ # WEBrick::HTTPServer.
+
+ def parse(socket=nil)
+ @socket = socket
+ begin
+ @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : []
+ @addr = socket.respond_to?(:addr) ? socket.addr : []
+ rescue Errno::ENOTCONN
+ raise HTTPStatus::EOFError
+ end
+
+ read_request_line(socket)
+ if @http_version.major > 0
+ read_header(socket)
+ @header['cookie'].each{|cookie|
+ @cookies += Cookie::parse(cookie)
+ }
+ @accept = HTTPUtils.parse_qvalues(self['accept'])
+ @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset'])
+ @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding'])
+ @accept_language = HTTPUtils.parse_qvalues(self['accept-language'])
+ end
+ return if @request_method == "CONNECT"
+ return if @unparsed_uri == "*"
+
+ begin
+ setup_forwarded_info
+ @request_uri = parse_uri(@unparsed_uri)
+ @path = HTTPUtils::unescape(@request_uri.path)
+ @path = HTTPUtils::normalize_path(@path)
+ @host = @request_uri.host
+ @port = @request_uri.port
+ @query_string = @request_uri.query
+ @script_name = ""
+ @path_info = @path.dup
+ rescue
+ raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'."
+ end
+
+ if /\Aclose\z/io =~ self["connection"]
+ @keep_alive = false
+ elsif /\Akeep-alive\z/io =~ self["connection"]
+ @keep_alive = true
+ elsif @http_version < "1.1"
+ @keep_alive = false
+ else
+ @keep_alive = true
+ end
+ end
+
+ ##
+ # Generate HTTP/1.1 100 continue response if the client expects it,
+ # otherwise does nothing.
+
+ def continue # :nodoc:
+ if self['expect'] == '100-continue' && @config[:HTTPVersion] >= "1.1"
+ @socket << "HTTP/#{@config[:HTTPVersion]} 100 continue#{CRLF}#{CRLF}"
+ @header.delete('expect')
+ end
+ end
+
+ ##
+ # Returns the request body.
+
+ def body(&block) # :yields: body_chunk
+ block ||= Proc.new{|chunk| @body << chunk }
+ read_body(@socket, block)
+ @body.empty? ? nil : @body
+ end
+
+ ##
+ # Prepares the HTTPRequest object for use as the
+ # source for IO.copy_stream
+
+ def body_reader
+ @body_tmp = []
+ @body_rd = Fiber.new do
+ body do |buf|
+ @body_tmp << buf
+ Fiber.yield
+ end
+ end
+ @body_rd.resume # grab the first chunk and yield
+ self
+ end
+
+ # for IO.copy_stream.
+ def readpartial(size, buf = ''.b) # :nodoc
+ res = @body_tmp.shift or raise EOFError, 'end of file reached'
+ if res.length > size
+ @body_tmp.unshift(res[size..-1])
+ res = res[0..size - 1]
+ end
+ buf.replace(res)
+ res.clear
+ # get more chunks - check alive? because we can take a partial chunk
+ @body_rd.resume if @body_rd.alive?
+ buf
+ end
+
+ ##
+ # Request query as a Hash
+
+ def query
+ unless @query
+ parse_query()
+ end
+ @query
+ end
+
+ ##
+ # The content-length header
+
+ def content_length
+ return Integer(self['content-length'])
+ end
+
+ ##
+ # The content-type header
+
+ def content_type
+ return self['content-type']
+ end
+
+ ##
+ # Retrieves +header_name+
+
+ def [](header_name)
+ if @header
+ value = @header[header_name.downcase]
+ value.empty? ? nil : value.join(", ")
+ end
+ end
+
+ ##
+ # Iterates over the request headers
+
+ def each
+ if @header
+ @header.each{|k, v|
+ value = @header[k]
+ yield(k, value.empty? ? nil : value.join(", "))
+ }
+ end
+ end
+
+ ##
+ # The host this request is for
+
+ def host
+ return @forwarded_host || @host
+ end
+
+ ##
+ # The port this request is for
+
+ def port
+ return @forwarded_port || @port
+ end
+
+ ##
+ # The server name this request is for
+
+ def server_name
+ return @forwarded_server || @config[:ServerName]
+ end
+
+ ##
+ # The client's IP address
+
+ def remote_ip
+ return self["client-ip"] || @forwarded_for || @peeraddr[3]
+ end
+
+ ##
+ # Is this an SSL request?
+
+ def ssl?
+ return @request_uri.scheme == "https"
+ end
+
+ ##
+ # Should the connection this request was made on be kept alive?
+
+ def keep_alive?
+ @keep_alive
+ end
+
+ def to_s # :nodoc:
+ ret = @request_line.dup
+ @raw_header.each{|line| ret << line }
+ ret << CRLF
+ ret << body if body
+ ret
+ end
+
+ ##
+ # Consumes any remaining body and updates keep-alive status
+
+ def fixup() # :nodoc:
+ begin
+ body{|chunk| } # read remaining body
+ rescue HTTPStatus::Error => ex
+ @logger.error("HTTPRequest#fixup: #{ex.class} occurred.")
+ @keep_alive = false
+ rescue => ex
+ @logger.error(ex)
+ @keep_alive = false
+ end
+ end
+
+ # This method provides the metavariables defined by the revision 3
+ # of "The WWW Common Gateway Interface Version 1.1"
+ # To browse the current document of CGI Version 1.1, see below:
+ # http://tools.ietf.org/html/rfc3875
+
+ def meta_vars
+ meta = Hash.new
+
+ cl = self["Content-Length"]
+ ct = self["Content-Type"]
+ meta["CONTENT_LENGTH"] = cl if cl.to_i > 0
+ meta["CONTENT_TYPE"] = ct.dup if ct
+ meta["GATEWAY_INTERFACE"] = "CGI/1.1"
+ meta["PATH_INFO"] = @path_info ? @path_info.dup : ""
+ #meta["PATH_TRANSLATED"] = nil # no plan to be provided
+ meta["QUERY_STRING"] = @query_string ? @query_string.dup : ""
+ meta["REMOTE_ADDR"] = @peeraddr[3]
+ meta["REMOTE_HOST"] = @peeraddr[2]
+ #meta["REMOTE_IDENT"] = nil # no plan to be provided
+ meta["REMOTE_USER"] = @user
+ meta["REQUEST_METHOD"] = @request_method.dup
+ meta["REQUEST_URI"] = @request_uri.to_s
+ meta["SCRIPT_NAME"] = @script_name.dup
+ meta["SERVER_NAME"] = @host
+ meta["SERVER_PORT"] = @port.to_s
+ meta["SERVER_PROTOCOL"] = "HTTP/" + @config[:HTTPVersion].to_s
+ meta["SERVER_SOFTWARE"] = @config[:ServerSoftware].dup
+
+ self.each{|key, val|
+ next if /^content-type$/i =~ key
+ next if /^content-length$/i =~ key
+ name = "HTTP_" + key
+ name.gsub!(/-/o, "_")
+ name.upcase!
+ meta[name] = val
+ }
+
+ meta
+ end
+
+ private
+
+ # :stopdoc:
+
+ MAX_URI_LENGTH = 2083 # :nodoc:
+
+ # same as Mongrel, Thin and Puma
+ MAX_HEADER_LENGTH = (112 * 1024) # :nodoc:
+
+ def read_request_line(socket)
+ @request_line = read_line(socket, MAX_URI_LENGTH) if socket
+ raise HTTPStatus::EOFError unless @request_line
+
+ @request_bytes = @request_line.bytesize
+ if @request_bytes >= MAX_URI_LENGTH and @request_line[-1, 1] != LF
+ raise HTTPStatus::RequestURITooLarge
+ end
+
+ @request_time = Time.now
+ if /^(\S+)\s+(\S++)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line
+ @request_method = $1
+ @unparsed_uri = $2
+ @http_version = HTTPVersion.new($3 ? $3 : "0.9")
+ else
+ rl = @request_line.sub(/\x0d?\x0a\z/o, '')
+ raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'."
+ end
+ end
+
+ def read_header(socket)
+ if socket
+ while line = read_line(socket)
+ break if /\A(#{CRLF}|#{LF})\z/om =~ line
+ if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH
+ raise HTTPStatus::RequestEntityTooLarge, 'headers too large'
+ end
+ @raw_header << line
+ end
+ end
+ @header = HTTPUtils::parse_header(@raw_header.join)
+ end
+
+ def parse_uri(str, scheme="http")
+ if @config[:Escape8bitURI]
+ str = HTTPUtils::escape8bit(str)
+ end
+ str.sub!(%r{\A/+}o, '/')
+ uri = URI::parse(str)
+ return uri if uri.absolute?
+ if @forwarded_host
+ host, port = @forwarded_host, @forwarded_port
+ elsif self["host"]
+ pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/n
+ host, port = *self['host'].scan(pattern)[0]
+ elsif @addr.size > 0
+ host, port = @addr[2], @addr[1]
+ else
+ host, port = @config[:ServerName], @config[:Port]
+ end
+ uri.scheme = @forwarded_proto || scheme
+ uri.host = host
+ uri.port = port ? port.to_i : nil
+ return URI::parse(uri.to_s)
+ end
+
+ def read_body(socket, block)
+ return unless socket
+ if tc = self['transfer-encoding']
+ case tc
+ when /\Achunked\z/io then read_chunked(socket, block)
+ else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}."
+ end
+ elsif self['content-length'] || @remaining_size
+ @remaining_size ||= self['content-length'].to_i
+ while @remaining_size > 0
+ sz = [@buffer_size, @remaining_size].min
+ break unless buf = read_data(socket, sz)
+ @remaining_size -= buf.bytesize
+ block.call(buf)
+ end
+ if @remaining_size > 0 && @socket.eof?
+ raise HTTPStatus::BadRequest, "invalid body size."
+ end
+ elsif BODY_CONTAINABLE_METHODS.member?(@request_method) && [email protected]
+ raise HTTPStatus::LengthRequired
+ end
+ return @body
+ end
+
+ def read_chunk_size(socket)
+ line = read_line(socket)
+ if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line
+ chunk_size = $1.hex
+ chunk_ext = $2
+ [ chunk_size, chunk_ext ]
+ else
+ raise HTTPStatus::BadRequest, "bad chunk `#{line}'."
+ end
+ end
+
+ def read_chunked(socket, block)
+ chunk_size, = read_chunk_size(socket)
+ while chunk_size > 0
+ begin
+ sz = [ chunk_size, @buffer_size ].min
+ data = read_data(socket, sz) # read chunk-data
+ if data.nil? || data.bytesize != sz
+ raise HTTPStatus::BadRequest, "bad chunk data size."
+ end
+ block.call(data)
+ end while (chunk_size -= sz) > 0
+
+ read_line(socket) # skip CRLF
+ chunk_size, = read_chunk_size(socket)
+ end
+ read_header(socket) # trailer + CRLF
+ @header.delete("transfer-encoding")
+ @remaining_size = 0
+ end
+
+ def _read_data(io, method, *arg)
+ begin
+ WEBrick::Utils.timeout(@config[:RequestTimeout]){
+ return io.__send__(method, *arg)
+ }
+ rescue Errno::ECONNRESET
+ return nil
+ rescue Timeout::Error
+ raise HTTPStatus::RequestTimeout
+ end
+ end
+
+ def read_line(io, size=4096)
+ _read_data(io, :gets, LF, size)
+ end
+
+ def read_data(io, size)
+ _read_data(io, :read, size)
+ end
+
+ def parse_query()
+ begin
+ if @request_method == "GET" || @request_method == "HEAD"
+ @query = HTTPUtils::parse_query(@query_string)
+ elsif self['content-type'] =~ /^application\/x-www-form-urlencoded/
+ @query = HTTPUtils::parse_query(body)
+ elsif self['content-type'] =~ /^multipart\/form-data; boundary=(.+)/
+ boundary = HTTPUtils::dequote($1)
+ @query = HTTPUtils::parse_form_data(body, boundary)
+ else
+ @query = Hash.new
+ end
+ rescue => ex
+ raise HTTPStatus::BadRequest, ex.message
+ end
+ end
+
+ PrivateNetworkRegexp = /
+ ^unknown$|
+ ^((::ffff:)?127.0.0.1|::1)$|
+ ^(::ffff:)?(10|172\.(1[6-9]|2[0-9]|3[01])|192\.168)\.
+ /ixo
+
+ # It's said that all X-Forwarded-* headers will contain more than one
+ # (comma-separated) value if the original request already contained one of
+ # these headers. Since we could use these values as Host header, we choose
+ # the initial(first) value. (apr_table_mergen() adds new value after the
+ # existing value with ", " prefix)
+ def setup_forwarded_info
+ if @forwarded_server = self["x-forwarded-server"]
+ @forwarded_server = @forwarded_server.split(",", 2).first
+ end
+ if @forwarded_proto = self["x-forwarded-proto"]
+ @forwarded_proto = @forwarded_proto.split(",", 2).first
+ end
+ if host_port = self["x-forwarded-host"]
+ host_port = host_port.split(",", 2).first
+ if host_port =~ /\A(\[[0-9a-fA-F:]+\])(?::(\d+))?\z/
+ @forwarded_host = $1
+ tmp = $2
+ else
+ @forwarded_host, tmp = host_port.split(":", 2)
+ end
+ @forwarded_port = (tmp || (@forwarded_proto == "https" ? 443 : 80)).to_i
+ end
+ if addrs = self["x-forwarded-for"]
+ addrs = addrs.split(",").collect(&:strip)
+ addrs.reject!{|ip| PrivateNetworkRegexp =~ ip }
+ @forwarded_for = addrs.first
+ end
+ end
+
+ # :startdoc:
+ end
+end