lib/webrick/httprequest.rb in rubysl-webrick-1.0.0 vs lib/webrick/httprequest.rb in rubysl-webrick-2.0.0

- old
+ new

@@ -6,46 +6,153 @@ # 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 'timeout' require 'uri' - require 'webrick/httpversion' require 'webrick/httpstatus' require 'webrick/httputils' require 'webrick/cookie' module WEBrick + ## + # An HTTP request. This is consumed by service and do_* methods in + # WEBrick servlets + class HTTPRequest - BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ] - BUFSIZE = 1024*4 - # Request line + BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ] # :nodoc: + + # :section: Request line + + ## + # The complete request line such as: + # + # GET / HTTP/1.1 + attr_reader :request_line - attr_reader :request_method, :unparsed_uri, :http_version - # Request-URI - attr_reader :request_uri, :host, :port, :path - attr_accessor :script_name, :path_info, :query_string + ## + # The request method, GET, POST, PUT, etc. - # Header and entity body - attr_reader :raw_header, :header, :cookies - attr_reader :accept, :accept_charset - attr_reader :accept_encoding, :accept_language + attr_reader :request_method - # Misc + ## + # 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 - attr_reader :addr, :peeraddr + + ## + # 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 @@ -70,12 +177,19 @@ @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 : [] @@ -96,10 +210,11 @@ 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 @@ -119,58 +234,130 @@ else @keep_alive = true end end - def body(&block) + ## + # 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 + ## + # 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 - @header.each{|k, v| - value = @header[k] - yield(k, value.empty? ? nil : value.join(", ")) - } + 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 + def to_s # :nodoc: ret = @request_line.dup @raw_header.each{|line| ret << line } ret << CRLF ret << body if body ret end - def fixup() + ## + # 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} occured.") @keep_alive = false @@ -178,15 +365,15 @@ @logger.error(ex) @keep_alive = false end end - def meta_vars - # This method provides the metavariables defined by the revision 3 - # of ``The WWW Common Gateway Interface Version 1.1''. - # (http://Web.Golux.Com/coar/cgi/) + # This method provides the metavariables defined by the revision 3 + # of "The WWW Common Gateway Interface Version 1.1" + # http://Web.Golux.Com/coar/cgi/ + def meta_vars meta = Hash.new cl = self["Content-Length"] ct = self["Content-Type"] meta["CONTENT_LENGTH"] = cl if cl.to_i > 0 @@ -219,15 +406,22 @@ meta end private + # :stopdoc: + + MAX_URI_LENGTH = 2083 # :nodoc: + def read_request_line(socket) - @request_line = read_line(socket) if socket + @request_line = read_line(socket, MAX_URI_LENGTH) if socket + if @request_line.bytesize >= MAX_URI_LENGTH and @request_line[-1, 1] != LF + raise HTTPStatus::RequestURITooLarge + end @request_time = Time.now raise HTTPStatus::EOFError unless @request_line - if /^(\S+)\s+(\S+?)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line + 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, '') @@ -240,32 +434,31 @@ while line = read_line(socket) break if /\A(#{CRLF}|#{LF})\z/om =~ line @raw_header << line end end - begin - @header = HTTPUtils::parse_header(@raw_header) - rescue => ex - raise HTTPStatus::BadRequest, ex.message - 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 self["host"] + 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 = scheme + uri.scheme = @forwarded_proto || scheme uri.host = host uri.port = port ? port.to_i : nil return URI::parse(uri.to_s) end @@ -276,14 +469,14 @@ when /chunked/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 = BUFSIZE < @remaining_size ? BUFSIZE : @remaining_size + while @remaining_size > 0 + sz = [@buffer_size, @remaining_size].min break unless buf = read_data(socket, sz) - @remaining_size -= buf.size + @remaining_size -= buf.bytesize block.call(buf) end if @remaining_size > 0 && @socket.eof? raise HTTPStatus::BadRequest, "invalid body size." end @@ -305,17 +498,12 @@ end def read_chunked(socket, block) chunk_size, = read_chunk_size(socket) while chunk_size > 0 - data = "" - while data.size < chunk_size - tmp = read_data(socket, chunk_size-data.size) # read chunk-data - break unless tmp - data << tmp - end - if data.nil? || data.size != chunk_size + data = read_data(socket, chunk_size) # read chunk-data + if data.nil? || data.bytesize != chunk_size raise BadRequest, "bad chunk data size." end read_line(socket) # skip CRLF block.call(data) chunk_size, = read_chunk_size(socket) @@ -323,24 +511,24 @@ read_header(socket) # trailer + CRLF @header.delete("transfer-encoding") @remaining_size = 0 end - def _read_data(io, method, arg) + def _read_data(io, method, *arg) begin - timeout(@config[:RequestTimeout]){ - return io.__send__(method, arg) + WEBrick::Utils.timeout(@config[:RequestTimeout]){ + return io.__send__(method, *arg) } rescue Errno::ECONNRESET return nil rescue TimeoutError raise HTTPStatus::RequestTimeout end end - def read_line(io) - _read_data(io, :gets, LF) + def read_line(io, size=4096) + _read_data(io, :gets, LF, size) end def read_data(io, size) _read_data(io, :read, size) end @@ -359,7 +547,37 @@ 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 + @forwarded_proto = self["x-forwarded-proto"] + if host_port = self["x-forwarded-host"] + host_port = host_port.split(",", 2).first + @forwarded_host, tmp = host_port.split(":", 2) + @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