# -*- coding: binary -*-
require 'uri'
#require 'rex/proto/http'
require 'rex/socket'
require 'rex/text'

require 'pp'

module Rex
module Proto
module Http

class ClientRequest

  DefaultUserAgent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)"
  DefaultConfig = {
    #
    # Regular HTTP stuff
    #
    'agent'                  => DefaultUserAgent,
    'cgi'                    => true,
    'cookie'                 => nil,
    'data'                   => '',
    'headers'                => nil,
    'raw_headers'            => '',
    'method'                 => 'GET',
    'path_info'              => '',
    'port'                   => 80,
    'proto'                  => 'HTTP',
    'query'                  => '',
    'ssl'                    => false,
    'uri'                    => '/',
    'vars_get'               => {},
    'vars_post'              => {},
    'version'                => '1.1',
    'vhost'                  => nil,

    #
    # Evasion options
    #
    'encode_params'          => true,
    'encode'                 => false,
    'uri_encode_mode'        => 'hex-normal', # hex-normal, hex-all, hex-noslashes, hex-random, u-normal, u-all, u-noslashes, u-random
    'uri_encode_count'       => 1,       # integer
    'uri_full_url'           => false,   # bool
    'pad_method_uri_count'   => 1,       # integer
    'pad_uri_version_count'  => 1,       # integer
    'pad_method_uri_type'    => 'space', # space, tab, apache
    'pad_uri_version_type'   => 'space', # space, tab, apache
    'method_random_valid'    => false,   # bool
    'method_random_invalid'  => false,   # bool
    'method_random_case'     => false,   # bool
    'version_random_valid'   => false,   # bool
    'version_random_invalid' => false,   # bool
    'version_random_case'    => false,   # bool
    'uri_dir_self_reference' => false,   # bool
    'uri_dir_fake_relative'  => false,   # bool
    'uri_use_backslashes'    => false,   # bool
    'pad_fake_headers'       => false,   # bool
    'pad_fake_headers_count' => 16,      # integer
    'pad_get_params'         => false,   # bool
    'pad_get_params_count'   => 8,       # integer
    'pad_post_params'        => false,   # bool
    'pad_post_params_count'  => 8,       # integer
    'uri_fake_end'           => false,   # bool
    'uri_fake_params_start'  => false,   # bool
    'header_folding'         => false,   # bool
    'chunked_size'           => 0,        # integer

    #
    # NTLM Options
    #
    'usentlm2_session' => true,
    'use_ntlmv2'       => true,
    'send_lm'         => true,
    'send_ntlm'       => true,
    'SendSPN'  => true,
    'UseLMKey' => false,
    'domain' => 'WORKSTATION',
    #
    # Digest Options
    #
    'DigestAuthIIS' => true
  }

  attr_reader :opts

  def initialize(opts={})
    @opts = DefaultConfig.merge(opts)
    @opts['headers'] ||= {}
  end

  def to_s

    # Start GET query string
    qstr = opts['query'] ? opts['query'].dup : ""

    # Start POST data string
    pstr = opts['data'] ? opts['data'].dup : ""

    if opts['cgi']
      uri_str = set_uri

      if (opts['pad_get_params'])
        1.upto(opts['pad_get_params_count'].to_i) do |i|
          qstr << '&' if qstr.length > 0
          qstr << set_encode_uri(Rex::Text.rand_text_alphanumeric(rand(32)+1))
          qstr << '='
          qstr << set_encode_uri(Rex::Text.rand_text_alphanumeric(rand(32)+1))
        end
      end

      opts['vars_get'].each_pair do |var,val|
        var = var.to_s

        qstr << '&' if qstr.length > 0
        qstr << (opts['encode_params'] ? set_encode_uri(var) : var)
        # support get parameter without value
        # Example: uri?parameter
        if val
          val = val.to_s
          qstr << '='
          qstr << (opts['encode_params'] ? set_encode_uri(val) : val)
        end
      end

      if (opts['pad_post_params'])
        1.upto(opts['pad_post_params_count'].to_i) do |i|
          rand_var = Rex::Text.rand_text_alphanumeric(rand(32)+1)
          rand_val = Rex::Text.rand_text_alphanumeric(rand(32)+1)
          pstr << '&' if pstr.length > 0
          pstr << (opts['encode_params'] ? set_encode_uri(rand_var) : rand_var)
          pstr << '='
          pstr << (opts['encode_params'] ? set_encode_uri(rand_val) : rand_val)
        end
      end

      opts['vars_post'].each_pair do |var,val|
        var = var.to_s
        val = val.to_s

        pstr << '&' if pstr.length > 0
        pstr << (opts['encode_params'] ? set_encode_uri(var) : var)
        pstr << '='
        pstr << (opts['encode_params'] ? set_encode_uri(val) : val)
      end
    else
      if opts['encode']
        qstr = set_encode_uri(qstr)
      end
      uri_str = set_uri
    end

    req = ''
    req << set_method
    req << set_method_uri_spacer()
    req << set_uri_prepend()

    if opts['encode']
      req << set_encode_uri(uri_str)
    else
      req << uri_str
    end


    if (qstr.length > 0)
      req << '?'
      req << qstr
    end

    req << set_path_info
    req << set_uri_append()
    req << set_uri_version_spacer()
    req << set_version
    req << set_host_header

    # If an explicit User-Agent header is set, then use that instead of
    # the default
    unless opts['headers'] and opts['headers'].keys.map{|x| x.downcase }.include?('user-agent')
      req << set_agent_header
    end

    # Similar to user-agent, only add an automatic auth header if a
    # manual one hasn't been provided
    unless opts['headers'] and opts['headers'].keys.map{|x| x.downcase }.include?('authorization')
      req << set_auth_header
    end

    req << set_cookie_header
    req << set_connection_header
    req << set_extra_headers

    req << set_content_type_header
    req << set_content_len_header(pstr.length)
    req << set_chunked_header()
    req << opts['raw_headers']
    req << set_body(pstr)
  end

  protected

  def set_uri
    uri_str = opts['uri'].dup
    if (opts['uri_dir_self_reference'])
      uri_str.gsub!('/', '/./')
    end

    if (opts['uri_dir_fake_relative'])
      buf = ""
      uri_str.split('/',-1).each do |part|
        cnt = rand(8)+2
        1.upto(cnt) { |idx|
          buf << "/" + Rex::Text.rand_text_alphanumeric(rand(32)+1)
        }
        buf << ("/.." * cnt)
        buf << "/" + part
      end
      uri_str = buf
    end

    if (opts['uri_full_url'])
      url = opts['ssl'] ? "https://" : "http://"
      url << opts['vhost']
      url << ((opts['port'] == 80) ? "" : ":#{opts['port']}")
      url << uri_str
      url
    else
      uri_str
    end
  end

  def set_encode_uri(str)
    a = str.to_s.dup
    opts['uri_encode_count'].times {
      a = Rex::Text.uri_encode(a, opts['uri_encode_mode'])
    }
    return a
  end

  def set_method
    ret = opts['method'].dup

    if (opts['method_random_valid'])
      ret = ['GET', 'POST', 'HEAD'][rand(3)]
    end

    if (opts['method_random_invalid'])
      ret = Rex::Text.rand_text_alpha(rand(20)+1)
    end

    if (opts['method_random_case'])
      ret = Rex::Text.to_rand_case(ret)
    end

    ret
  end

  def set_method_uri_spacer
    len = opts['pad_method_uri_count'].to_i
    set = " "
    buf = ""

    case opts['pad_method_uri_type']
    when 'tab'
      set = "\t"
    when 'apache'
      set = "\t \x0b\x0c\x0d"
    end

    while(buf.length < len)
      buf << set[ rand(set.length) ]
    end

    return buf
  end

  #
  # Return the padding to place before the uri
  #
  def set_uri_prepend
    prefix = ""

    if (opts['uri_fake_params_start'])
      prefix << '/%3fa=b/../'
    end

    if (opts['uri_fake_end'])
      prefix << '/%20HTTP/1.0/../../'
    end

    prefix
  end

  #
  # Return the HTTP path info
  # TODO:
  #  * Encode path information
  def set_path_info
    opts['path_info'] ? opts['path_info'] : ''
  end

  #
  # Return the padding to place before the uri
  #
  def set_uri_append
    # TODO:
    #  * Support different padding types
    ""
  end

  #
  # Return the spacing between the uri and the version
  #
  def set_uri_version_spacer
    len = opts['pad_uri_version_count'].to_i
    set = " "
    buf = ""

    case opts['pad_uri_version_type']
    when 'tab'
      set = "\t"
    when 'apache'
      set = "\t \x0b\x0c\x0d"
    end

    while(buf.length < len)
      buf << set[ rand(set.length) ]
    end

    return buf
  end

  #
  # Return the HTTP version string
  #
  def set_version
    ret = opts['proto'] + "/" + opts['version']

    if (opts['version_random_valid'])
      ret = opts['proto'] + "/" +  ['1.0', '1.1'][rand(2)]
    end

    if (opts['version_random_invalid'])
      ret = Rex::Text.rand_text_alphanumeric(rand(20)+1)
    end

    if (opts['version_random_case'])
      ret = Rex::Text.to_rand_case(ret)
    end

    ret << "\r\n"
  end

  #
  # Return a formatted header string
  #
  def set_formatted_header(var, val)
    if (self.opts['header_folding'])
      "#{var}:\r\n\t#{val}\r\n"
    else
      "#{var}: #{val}\r\n"
    end
  end

  #
  # Return the HTTP agent header
  #
  def set_agent_header
    opts['agent'] ? set_formatted_header("User-Agent", opts['agent']) : ""
  end

  def set_auth_header
    opts['authorization'] ? set_formatted_header("Authorization", opts['authorization']) : ""
  end

  #
  # Return the HTTP cookie header
  #
  def set_cookie_header
    opts['cookie'] ? set_formatted_header("Cookie", opts['cookie']) : ""
  end

  #
  # Return the HTTP connection header
  #
  def set_connection_header
    opts['connection'] ? set_formatted_header("Connection", opts['connection']) : ""
  end

  #
  # Return the content type header
  #
  def set_content_type_header
    opts['ctype'] ? set_formatted_header("Content-Type", opts['ctype']) : ""
  end

  #
  # Return the content length header
  def set_content_len_header(clen)
    return "" if opts['chunked_size'] > 0
    set_formatted_header("Content-Length", clen)
  end

  #
  # Return the HTTP Host header
  #
  def set_host_header
    return "" if opts['uri_full_url']
    host = opts['vhost']

    # IPv6 addresses must be placed in brackets
    if Rex::Socket.is_ipv6?(host)
      host = "[#{host}]"
    end

    # The port should be appended if non-standard
    if not [80,443].include?(opts['port'])
      host = host + ":#{opts['port']}"
    end

    set_formatted_header("Host", host)
  end

  #
  # Return a string of formatted extra headers
  #
  def set_extra_headers
    buf = ''

    if (opts['pad_fake_headers'])
      1.upto(opts['pad_fake_headers_count'].to_i) do |i|
        buf << set_formatted_header(
          Rex::Text.rand_text_alphanumeric(rand(32)+1),
          Rex::Text.rand_text_alphanumeric(rand(32)+1)
        )
      end
    end

    opts['headers'].each_pair do |var,val|
      buf << set_formatted_header(var, val)
    end

    buf
  end

  def set_chunked_header
    return "" if opts['chunked_size'] == 0
    set_formatted_header('Transfer-Encoding', 'chunked')
  end

  #
  # Return the HTTP seperator and body string
  #
  def set_body(bdata)
    return "\r\n" + bdata if opts['chunked_size'] == 0
    str = bdata.dup
    chunked = ''
    while str.size > 0
      chunk = str.slice!(0,rand(opts['chunked_size']) + 1)
      chunked << sprintf("%x", chunk.size) + "\r\n" + chunk + "\r\n"
    end
    "\r\n" + chunked + "0\r\n\r\n"
  end


end



end
end
end
