File: webrick/httprequest.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: WEBrick#19
  class: HTTPRequest#21
inherits from
  Object ( Builtin-Module )
has properties
constant: BODY_CONTAINABLE_METHODS #22
constant: BUFSIZE #23
attribute: request_line [R] #26
attribute: request_method [R] #27
attribute: unparsed_uri [R] #27
attribute: http_version [R] #27
attribute: request_uri [R] #30
attribute: host [R] #30
attribute: port [R] #30
attribute: path [R] #30
attribute: script_name [RW] #31
attribute: path_info [RW] #31
attribute: query_string [RW] #31
attribute: raw_header [R] #34
attribute: header [R] #34
attribute: cookies [R] #34
attribute: accept [R] #35
attribute: accept_charset [R] #35
attribute: accept_encoding [R] #36
attribute: accept_language [R] #36
attribute: user [RW] #39
attribute: addr [R] #40
attribute: peeraddr [R] #40
attribute: attributes [R] #41
attribute: keep_alive [R] #42
attribute: request_time [R] #43
method: initialize / 1 #45
method: parse (1/2) / 1 #77
method: body / 1 #124
method: query #130
method: content_length #137
method: content_type #141
method: [] / 1 #145
method: each #152
method: keep_alive? #159
method: to_s #163
method: fixup #171
method: meta_vars (1/2) #183
method: read_request_line / 1 #224
method: read_header / 1 #238
method: parse_uri (1/2) / 2 #248
method: read_body / 2 #269
method: read_chunk_size / 1 #293
method: read_chunked / 2 #304
method: _read_data / 3 #325
method: read_line / 1 #337
method: read_data / 2 #341
method: parse_query #345

Class Hierarchy

Object ( Builtin-Module )
  HTTPRequest ( WEBrick ) #21

Code

   1  #
   2  # httprequest.rb -- HTTPRequest Class
   3  #
   4  # Author: IPR -- Internet Programming with Ruby -- writers
   5  # Copyright (c) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
   6  # Copyright (c) 2002 Internet Programming with Ruby writers. All rights
   7  # reserved.
   8  #
   9  # $IPR: httprequest.rb,v 1.64 2003/07/13 17:18:22 gotoyuzo Exp $
  10 
  11  require 'timeout'
  12  require 'uri'
  13 
  14  require 'webrick/httpversion'
  15  require 'webrick/httpstatus'
  16  require 'webrick/httputils'
  17  require 'webrick/cookie'
  18 
  19  module WEBrick
  20 
  21    class HTTPRequest
  22      BODY_CONTAINABLE_METHODS = [ "POST", "PUT" ]
  23      BUFSIZE = 1024*4
  24 
  25      # Request line
  26      attr_reader :request_line
  27      attr_reader :request_method, :unparsed_uri, :http_version
  28 
  29      # Request-URI
  30      attr_reader :request_uri, :host, :port, :path
  31      attr_accessor :script_name, :path_info, :query_string
  32 
  33      # Header and entity body
  34      attr_reader :raw_header, :header, :cookies
  35      attr_reader :accept, :accept_charset
  36      attr_reader :accept_encoding, :accept_language
  37 
  38      # Misc
  39      attr_accessor :user
  40      attr_reader :addr, :peeraddr
  41      attr_reader :attributes
  42      attr_reader :keep_alive
  43      attr_reader :request_time
  44 
  45      def initialize(config)
  46        @config = config
  47        @logger = config[:Logger]
  48 
  49        @request_line = @request_method =
  50          @unparsed_uri = @http_version = nil
  51 
  52        @request_uri = @host = @port = @path = nil
  53        @script_name = @path_info = nil
  54        @query_string = nil
  55        @query = nil
  56        @form_data = nil
  57 
  58        @raw_header = Array.new
  59        @header = nil
  60        @cookies = []
  61        @accept = []
  62        @accept_charset = []
  63        @accept_encoding = []
  64        @accept_language = []
  65        @body = ""
  66 
  67        @addr = @peeraddr = nil
  68        @attributes = {}
  69        @user = nil
  70        @keep_alive = false
  71        @request_time = nil
  72 
  73        @remaining_size = nil
  74        @socket = nil
  75      end
  76 
  77      def parse(socket=nil)
  78        @socket = socket
  79        begin
  80          @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : []
  81          @addr = socket.respond_to?(:addr) ? socket.addr : []
  82        rescue Errno::ENOTCONN
  83          raise HTTPStatus::EOFError
  84        end
  85 
  86        read_request_line(socket)
  87        if @http_version.major > 0
  88          read_header(socket)
  89          @header['cookie'].each{|cookie|
  90            @cookies += Cookie::parse(cookie)
  91          }
  92          @accept = HTTPUtils.parse_qvalues(self['accept'])
  93          @accept_charset = HTTPUtils.parse_qvalues(self['accept-charset'])
  94          @accept_encoding = HTTPUtils.parse_qvalues(self['accept-encoding'])
  95          @accept_language = HTTPUtils.parse_qvalues(self['accept-language'])
  96        end
  97        return if @request_method == "CONNECT"
  98        return if @unparsed_uri == "*"
  99 
 100        begin
 101          @request_uri = parse_uri(@unparsed_uri)
 102          @path = HTTPUtils::unescape(@request_uri.path)
 103          @path = HTTPUtils::normalize_path(@path)
 104          @host = @request_uri.host
 105          @port = @request_uri.port
 106          @query_string = @request_uri.query
 107          @script_name = ""
 108          @path_info = @path.dup
 109        rescue
 110          raise HTTPStatus::BadRequest, "bad URI `#{@unparsed_uri}'."
 111        end
 112 
 113        if /close/io =~ self["connection"]
 114          @keep_alive = false
 115        elsif /keep-alive/io =~ self["connection"]
 116          @keep_alive = true
 117        elsif @http_version < "1.1"
 118          @keep_alive = false
 119        else
 120          @keep_alive = true
 121        end
 122      end
 123 
 124      def body(&block)
 125        block ||= Proc.new{|chunk| @body << chunk }
 126        read_body(@socket, block)
 127        @body.empty? ? nil : @body
 128      end
 129 
 130      def query
 131        unless @query
 132          parse_query()
 133        end
 134        @query
 135      end
 136 
 137      def content_length
 138        return Integer(self['content-length'])
 139      end
 140 
 141      def content_type
 142        return self['content-type']
 143      end
 144 
 145      def [](header_name)
 146        if @header
 147          value = @header[header_name.downcase]
 148          value.empty? ? nil : value.join(", ")
 149        end
 150      end
 151 
 152      def each
 153        @header.each{|k, v|
 154          value = @header[k]
 155          yield(k, value.empty? ? nil : value.join(", "))
 156        }
 157      end
 158 
 159      def keep_alive?
 160        @keep_alive
 161      end
 162 
 163      def to_s
 164        ret = @request_line.dup
 165        @raw_header.each{|line| ret << line }
 166        ret << CRLF
 167        ret << body if body
 168        ret
 169      end
 170 
 171      def fixup()
 172        begin
 173          body{|chunk| }   # read remaining body
 174        rescue HTTPStatus::Error => ex
 175          @logger.error("HTTPRequest#fixup: #{ex.class} occured.")
 176          @keep_alive = false
 177        rescue => ex
 178          @logger.error(ex)
 179          @keep_alive = false
 180        end
 181      end
 182 
 183      def meta_vars
 184        # This method provides the metavariables defined by the revision 3
 185        # of ``The WWW Common Gateway Interface Version 1.1''.
 186        # (http://Web.Golux.Com/coar/cgi/)
 187 
 188        meta = Hash.new
 189 
 190        cl = self["Content-Length"]
 191        ct = self["Content-Type"]
 192        meta["CONTENT_LENGTH"]    = cl if cl.to_i > 0
 193        meta["CONTENT_TYPE"]      = ct.dup if ct
 194        meta["GATEWAY_INTERFACE"] = "CGI/1.1"
 195        meta["PATH_INFO"]         = @path_info ? @path_info.dup : ""
 196       #meta["PATH_TRANSLATED"]   = nil      # no plan to be provided
 197        meta["QUERY_STRING"]      = @query_string ? @query_string.dup : ""
 198        meta["REMOTE_ADDR"]       = @peeraddr[3]
 199        meta["REMOTE_HOST"]       = @peeraddr[2]
 200       #meta["REMOTE_IDENT"]      = nil      # no plan to be provided
 201        meta["REMOTE_USER"]       = @user
 202        meta["REQUEST_METHOD"]    = @request_method.dup
 203        meta["REQUEST_URI"]       = @request_uri.to_s
 204        meta["SCRIPT_NAME"]       = @script_name.dup
 205        meta["SERVER_NAME"]       = @host
 206        meta["SERVER_PORT"]       = @port.to_s
 207        meta["SERVER_PROTOCOL"]   = "HTTP/" + @config[:HTTPVersion].to_s
 208        meta["SERVER_SOFTWARE"]   = @config[:ServerSoftware].dup
 209 
 210        self.each{|key, val|
 211          next if /^content-type$/i =~ key
 212          next if /^content-length$/i =~ key
 213          name = "HTTP_" + key
 214          name.gsub!(/-/o, "_")
 215          name.upcase!
 216          meta[name] = val
 217        }
 218 
 219        meta
 220      end
 221 
 222      private
 223 
 224      def read_request_line(socket)
 225        @request_line = read_line(socket) if socket
 226        @request_time = Time.now
 227        raise HTTPStatus::EOFError unless @request_line
 228        if /^(\S+)\s+(\S+?)(?:\s+HTTP\/(\d+\.\d+))?\r?\n/mo =~ @request_line
 229          @request_method = $1
 230          @unparsed_uri   = $2
 231          @http_version   = HTTPVersion.new($3 ? $3 : "0.9")
 232        else
 233          rl = @request_line.sub(/\x0d?\x0a\z/o, '')
 234          raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'."
 235        end
 236      end
 237 
 238      def read_header(socket)
 239        if socket
 240          while line = read_line(socket)
 241            break if /\A(#{CRLF}|#{LF})\z/om =~ line
 242            @raw_header << line
 243          end
 244        end
 245        @header = HTTPUtils::parse_header(@raw_header.join)
 246      end
 247 
 248      def parse_uri(str, scheme="http")
 249        if @config[:Escape8bitURI]
 250          str = HTTPUtils::escape8bit(str)
 251        end
 252        str.sub!(%r{\A/+}o, '/')
 253        uri = URI::parse(str)
 254        return uri if uri.absolute?
 255        if self["host"]
 256          pattern = /\A(#{URI::REGEXP::PATTERN::HOST})(?::(\d+))?\z/n
 257          host, port = *self['host'].scan(pattern)[0]
 258        elsif @addr.size > 0
 259          host, port = @addr[2], @addr[1]
 260        else
 261          host, port = @config[:ServerName], @config[:Port]
 262        end
 263        uri.scheme = scheme
 264        uri.host = host
 265        uri.port = port ? port.to_i : nil
 266        return URI::parse(uri.to_s)
 267      end
 268 
 269      def read_body(socket, block)
 270        return unless socket
 271        if tc = self['transfer-encoding']
 272          case tc
 273          when /chunked/io then read_chunked(socket, block)
 274          else raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}."
 275          end
 276        elsif self['content-length'] || @remaining_size
 277          @remaining_size ||= self['content-length'].to_i
 278          while @remaining_size > 0 
 279            sz = BUFSIZE < @remaining_size ? BUFSIZE : @remaining_size
 280            break unless buf = read_data(socket, sz)
 281            @remaining_size -= buf.size
 282            block.call(buf)
 283          end
 284          if @remaining_size > 0 && @socket.eof?
 285            raise HTTPStatus::BadRequest, "invalid body size."
 286          end
 287        elsif BODY_CONTAINABLE_METHODS.member?(@request_method)
 288          raise HTTPStatus::LengthRequired
 289        end
 290        return @body
 291      end
 292 
 293      def read_chunk_size(socket)
 294        line = read_line(socket)
 295        if /^([0-9a-fA-F]+)(?:;(\S+))?/ =~ line
 296          chunk_size = $1.hex
 297          chunk_ext = $2
 298          [ chunk_size, chunk_ext ]
 299        else
 300          raise HTTPStatus::BadRequest, "bad chunk `#{line}'."
 301        end
 302      end
 303 
 304      def read_chunked(socket, block)
 305        chunk_size, = read_chunk_size(socket)
 306        while chunk_size > 0
 307          data = ""
 308          while data.size < chunk_size
 309            tmp = read_data(socket, chunk_size-data.size) # read chunk-data
 310            break unless tmp
 311            data << tmp
 312          end
 313          if data.nil? || data.size != chunk_size
 314            raise BadRequest, "bad chunk data size."
 315          end
 316          read_line(socket)                    # skip CRLF
 317          block.call(data)
 318          chunk_size, = read_chunk_size(socket)
 319        end
 320        read_header(socket)                    # trailer + CRLF
 321        @header.delete("transfer-encoding")
 322        @remaining_size = 0
 323      end
 324 
 325      def _read_data(io, method, arg)
 326        begin
 327          timeout(@config[:RequestTimeout]){
 328            return io.__send__(method, arg)
 329          }
 330        rescue Errno::ECONNRESET
 331          return nil
 332        rescue TimeoutError
 333          raise HTTPStatus::RequestTimeout
 334        end
 335      end
 336 
 337      def read_line(io)
 338        _read_data(io, :gets, LF)
 339      end
 340 
 341      def read_data(io, size)
 342        _read_data(io, :read, size)
 343      end
 344 
 345      def parse_query()
 346        begin
 347          if @request_method == "GET" || @request_method == "HEAD"
 348            @query = HTTPUtils::parse_query(@query_string)
 349          elsif self['content-type'] =~ /^application\/x-www-form-urlencoded/
 350            @query = HTTPUtils::parse_query(body)
 351          elsif self['content-type'] =~ /^multipart\/form-data; boundary=(.+)/
 352            boundary = HTTPUtils::dequote($1)
 353            @query = HTTPUtils::parse_form_data(body, boundary)
 354          else
 355            @query = Hash.new
 356          end
 357        rescue => ex
 358          raise HTTPStatus::BadRequest, ex.message
 359        end
 360      end
 361    end
 362  end