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