#
# httprequest.rb -- HTTPRequest Class
#
# Author: IPR -- Internet Programming with Ruby -- writers
# Copyright (C) 2000, 2001 TAKAHASHI Masayoshi, GOTOU Yuuzou
#
# $IPR: httprequest.rb,v 1.31 2002/02/13 20:02:51 gotoyuzo Exp $

require 'timeout'

require 'webrick/httpstatus'
require 'webrick/httputils'

module WEBrick

  class HTTPRequest

    # request message component
    attr_reader :request_line
    attr_reader :request_method, :request_uri, :http_version
    attr_reader :path, :query_string
    attr_reader :raw_header, :header
    attr_reader :body

    # properties
    attr_reader :cookies
    attr_reader :query
    attr_reader :config
    attr_reader :peeraddr
    attr_accessor :script_name, :path_info

    def initialize(config)
      @config = config
      @logger = config[:Logger]

      @request_line = nil
      @request_method = nil
      @request_uri = nil
      @query_string = nil
      @http_version = nil
      @header = Hash.new([].freeze)
      @raw_header = Array.new
      @body = ''

      @cookies = []
      @query = {}
      @path = nil
      @peer_addr = nil
      @script_name = nil
      @path_info = nil

      @keep_alive = false
    end

    def parse(socket)
      read_request_line(socket)
      read_header(socket)
      read_body(socket)

      @script_name = ""
      @path_info = @path.dup
      @query_string = @request_uri.query
      case @request_method
      when "GET", "HEAD"
        @query = HTTPUtils::parse_query(@query_string)
      else
        if self['content-type'] == "application/x-www-form-urlencoded"
          @query = HTTPUtils::parse_query(@body)
        end
      end
      @header['cookie'].each{ |cookie| @cookies += Cookie::parse(cookie) }
      @peeraddr = socket.respond_to?(:peeraddr) ? socket.peeraddr : []

      if /close/io =~ self["connection"]
        @keep_alive = false
      elsif /keep-alive/io =~ self["connection"]
        @keep_alive = true
      elsif @config[:HTTPVersion].to_f < 1.1
        @keep_alive = false
      elsif @http_version.to_f < 1.1
        @keep_alive = false
      else
        @keep_alive = true
      end
    end

    def read_request_line(socket)
      @request_line = read_line(socket)
      raise HTTPStatus::EOFError unless @request_line
      if /^(\S+)\s+(\S+)(\s+HTTP\/(\d+\.\d+))?/o =~ @request_line
        @request_method = $1
        @request_uri    = $2
        @http_version   = $4 || "0.9"
      else
        rl = @request_line.chomp
        raise HTTPStatus::BadRequest, "bad Request-Line `#{rl}'."
      end

      begin
        @request_uri = URI::parse(@request_uri)
        @path = HTTPUtils::unescape(@request_uri.path)
        @path = HTTPUtils::normalize_path(@path)
      rescue
        raise HTTPStatus::BadRequest, "bad URI `#@request_uri'."
      end
    end
    private :read_request_line

    def read_header(socket)
      field = nil
      while true
        line = read_line(socket)
        case line
        when /^([A-Za-z0-9_-]+):(.*)/om
          field, value = $1, $2
          field.downcase!
          @header[field] = [] unless @header.has_key?(field)
          @header[field].push(value)
        when /^[\t ]+(.*)/om
          value = $1
          raise HTTPStatus::BadRequest, "bad header `#{line}'." unless field
          @header[field][-1] << value
        when CRLF, LF
          break
        else
          raise HTTPStatus::BadRequest, "bad header `#{line}'."
        end
        @raw_header << line
      end

      @header.each{|key, val|
        val.each{|v|
          v.gsub!(/\A((#{CR}?#{LF})?\s+)+/om, "")
          v.gsub!(/((#{CR}?#{LF})?\s+)+\z/om, "")
          v.gsub!(/((#{CR}?#{LF})?\s+)+/om, " ")
        }
      }
    end
    private :read_header

    def read_body(socket)
      if tc = self['transfer-encoding']
        case tc
        when /chunked/io
          @body = read_chunked
        else
          raise HTTPStatus::NotImplemented, "Transfer-Encoding: #{tc}."
        end
      elsif self['content-length']
        size = self['content-length'].to_i
        @body = read_data(socket, size)
        if @body == nil || @body.size != size
          raise HTTPStatus::BadRequest, "invalid body size."
        end
      elsif @request_method == "PUT" || @request_method == "POST"
        raise HTTPStatus::LengthRequired
      end
    end
    private :read_body

    def read_chunk_size(io)
      line = read_line(io)
      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
    private :read_chunk_size

    def read_chunked(socket)
      body = ""
      chunk_size, = read_chank_size(socket)
      while chunk_size > 0
        data = read_data(socket, size) # read chunk-data
        if data == nil || data.size != size
          raise BadRequest, "bad chunk data size."
        end
        read_line(socket)              # skip CRLF
        body << data
        chunk_size, = read_chank_size(socket)
      end
      read_header()                    # trailer + CRLF
      @header.delete("transfer-encoding")
      body
    end
    private :read_chunked

    def [](header_name)
      value = @header[header_name.downcase]
      value.empty? ? nil : value.join(", ")
    end

    def each
      @header.each{|k, v|
        value = @header[k]
        yield(k, value.empty? ? nil : value.join(", "))
      }
    end

    def keep_alive?
      @keep_alive
    end

    def to_s
      ret = @request_line.dup
      ret << @raw_header.join
      ret << CRLF
      ret << @body
    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/)

      meta = Hash.new

      if /^(Basic|Digest)\s+(.+)/o =~ self["Authorization"]
        auth_type = $1
        userpass = $2
        meta["AUTH_TYPE"]       = auth_type
        if auth_type == "Basic"
          remote_user, = decode64(userpass).split(":", 2)
          meta["REMOTE_USER"]   = remote_user
        end
      end

      cl = self["Content-Length"]
      meta["CONTENT_LENGTH"]    = cl.to_i == 0 ? nil : cl
      ct = self["Content-Type"]
      meta["CONTENT_TYPE"]      = ct ? ct.dup : nil
      meta["GATEWAY_INTERFACE"] = "CGI/1.1"
      meta["PATH_INFO"]         = @path_info.dup
      #meta["PATH_TRANSLATED"]  = nil      # no plan to be provided
      meta["QUERY_STRING"]      = @query_string ? @query_string.dup : ""
      peeraddr = self.peeraddr
      meta["REMOTE_ADDR"]       = peeraddr[3]
      meta["REMOTE_HOST"]       = peeraddr[2]
      #meta["REMOTE_IDENT"]     = nil      # no plan to be provided
      meta["REQUEST_METHOD"]    = @request_method.dup
      meta["SCRIPT_NAME"]       = @script_name.dup
      meta["SERVER_NAME"]       = @config[:ServerName].dup
      meta["SERVER_PORT"]       = @config[:Port].to_s
      meta["SERVER_PROTOCOL"]   = "HTTP/" + @config[:HTTPVersion].to_s
      meta["SERVER_SOFTWARE"]   = @config[:ServerSoftware].dup

      self.each{|key, val|
        name = "HTTP_" + key
        name.gsub!(/-/o, "_")
        name.upcase!
        meta[name] = val
      }

      meta
    end

    def _read_data(io, method, arg)
      begin
        timeout(@config[:RequestTimeout]){
          return io.__send__(method, arg)
        }
      rescue Errno::ECONNRESET
        return nil
      rescue TimeoutError
        raise HTTPStatus::RequestTimeout
      end
    end
    private :_read_data

    def read_line(io)
      _read_data(io, :gets, LF)
    end
    private :read_line

    def read_data(io, size)
      _read_data(io, :read, size)
    end
    private :read_data

  end
end
