WEBrick::HTTPProxyServer で無理矢理リクエストヘッダを書き換えるプロクシサーバ

概要

全然需要なさそうだけど、個人的に必要だったので少し前に作ったもの。

レスポンスヘッダとか受け取ったコンテンツを書き換えるプロクシは一杯あるんだけど、リクエストヘッダまで書き換えるプロクシっていうのはあんまり無くて、自分が知っているものとしては Proxomitron 系ぐらい。

でも Proxomitron は設定ファイルを作るのがかなりの手間なので、シンプルなリクエストヘッダ書き換え用のプロクシサーバが欲しいと思って、Ruby + WEBrick::HTTPProxyServer で書いてみた。

参考にしたのは HTTPリクエストヘッダを付加したい - kkkkkkkk で、特異メソッドをオーバーライドしたりしてかなり強引なやり方なんだけど、とりあえず動く。

使い方

オプション "-r" で追加するリクエストヘッダを指定する。"one-field:value1,another-field:value2" みたいな感じで指定できる。

以下みたいにすると、常に uid を送り続ける Docomo 端末を偽装することができる。モバイルサイトの動作確認用とかに使えるかも。

ruby my_proxy.rb -r "user-agent:DoCoMo/2.0 P906i(c100;TB;W24H15),x-dcmguid:1234567"

あとは -b で bind するアドレスを指定したり、-p でポートを指定したりする。

ソース

長いけど MyProxyServer#run については WEBrickソースコードから持ってきている部分がほとんど。

  • my_proxy.rb
#!/usr/bin/env ruby
require 'webrick'
require 'webrick/httpproxy'
require 'optparse'
include WEBrick

# HTTPリクエストヘッダを付加したい - kkkkkkkk <http://d.hatena.ne.jp/kkkkkkkk/20060707/p1>
class MyProxyServer < HTTPProxyServer
  def run(sock)
    while true
      res = HTTPResponse.new(@config)
      req = HTTPRequest.new(@config)

      def req.read_header(socket)
        super
        begin
          @header = HTTPUtils::parse_header(@raw_header)
          @config[:RequestHeaders].each do |k,v|
            @header[k] = [v]
          end
        rescue => ex
          raise  HTTPStatus::BadRequest, ex.message
        end
      end

      server = self
      begin
        timeout = @config[:RequestTimeout]
        while timeout > 0
          break if IO.select([sock], nil, nil, 0.5)
          timeout = 0 if @status != :Running
          timeout -= 0.5
        end
        raise HTTPStatus::EOFError if timeout <= 0 || sock.eof?
        req.parse(sock)
        res.request_method = req.request_method
        res.request_uri = req.request_uri
        res.request_http_version = req.http_version
        res.keep_alive = req.keep_alive?
        server = lookup_server(req) || self
        if callback = server[:RequestCallback] || server[:RequestHandler]
          callback.call(req, res)
        end
        server.service(req, res)
      rescue HTTPStatus::EOFError, HTTPStatus::RequestTimeout => ex
        res.set_error(ex)
      rescue HTTPStatus::Error => ex
        @logger.error(ex.message)
        res.set_error(ex)
      rescue HTTPStatus::Status => ex
        res.status = ex.code
      rescue StandardError => ex
        @logger.error(ex)
        res.set_error(ex, true)
      ensure
        if req.request_line
          req.fixup()
          res.send_response(sock)
          server.access_log(@config, req, res)
        end
      end
      break if @http_version < "1.1"
      break unless req.keep_alive?
      break unless res.keep_alive?
    end
  end
end

# default settings
header_list = []
address = 'localhost'
port = 8080

# option parse
opt = OptionParser.new
opt.on('-r HEADERS', Array  ) {|v| header_list = v }
opt.on('-b ADDRESS', String ) {|v| address     = v }
opt.on('-p PORT',    Integer) {|v| port        = v }
opt.parse!

# create request headers
request_headers = {}
header_list.each do |header|
  field, value = header.split(':')
  request_headers[field] = value
end

# construct proxy server
server = MyProxyServer.new(
  :BindAddress => address,
  :Port => port,
  :Logger => WEBrick::Log::new($stderr, WEBrick::Log::DEBUG),
  :ProxyVia => false,
  :RequestHeaders => request_headers
)

# start server
Signal.trap('INT') { server.shutdown; }
server.start