読者です 読者をやめる 読者になる 読者になる

俺の備忘録

個人的な備忘録です。

Pythonで簡易HTTP Proxy作成

python

サマリ

GUI(ブラウザ)とサーバ間でやり取りしているデータをどうにかして見やすくしたいと思い、HTTP Proxyプログラムを自作することにした。 ブラウザのデバッガについてるネットワーク監視機能でいいじゃんという声もあると思うが、 あれって検索しにくいんだよね。 ざーっと通信発生させて、その後、grepでガーッと検索したい。 ということで、Pythonで簡易HTTP Proxyサーバを作成してみた。

ここで載せるコードをベースにログ機能をもう少し強化してから実践投入する予定!

ポイント

作りはだいたい以下。
- ブラウザからリクエストを受け付けたら、転送処理に必要な情報を集めるために、HTTPのヘッダだけとりあえず全て読みだす。
- 転送先ホストとポートは、Hostヘッダから取得可能。
- 基本的にはブラウザと転送先ホストの通信を仲介するだけでOK。具体的にはブラウザ/転送先ホストからデータが送られてきたら、それをそのまま転送先ホスト/ブラウザに送っておけば大丈夫。
- httpsの場合も、ほとんど同じだが、こちらは通信を仲介する前に、ブラウザにHTTP/1.0 200 Connection establishedを返しておけばよいだけ。
- httpsかどうかは、MethodがCONNECTになっているのでそれで判断できる。

できていないこと(まだ怪しいこと)

  • httpsのサイトを覗いていると固まることが多々ある
  • Transfer-Encodingがchunkedの場合など動くか確認していない
  • (おそらく)他にもいっぱい

コード

以下、コード。 なお、コメントを頑張って英語で書いているがおそらく雰囲気しか合っていないので注意。

SimpleProxyServer.py

#!/usr/bin/python
# -*- coding: utf-8 -*-
import logging
import traceback
import socket
import select
import ConfigParser
import datetime


class HttpDataContainer:
    # Class for storing http data
    def __init__(self):
        self._date_time = datetime.datetime.now()
        self._request_data = ''
        self._response_data = ''

    def append_request_data(self, data):
        self._request_data += data

    def append_response_data(self, data):
        self._response_data += data

    @property
    def date_time(self):
        return self._date_time

    @property
    def request_data(self):
        return self._request_data

    @property
    def response_data(self):
        return self._response_data

    def get_request_header(self):
        return self._request_data[:self._request_data.find('\r\n\r\n') + 4]

    def get_response_header(self):
        return self._response_data[:self._response_data.find('\r\n\r\n') + 4]


class HttpHeaderHelper:
    # Class to analyze Http headers
    def __init__(self, header_raw_data):
        # constructor
        self._header_raw_data = header_raw_data
        self._header_dict = self.create_header_dict()

    def get_first_row(self):
        return self._header_raw_data[:self._header_raw_data .find('\r\n')]

    def get_http_method(self):
        # get http method
        return self._header_raw_data[:self._header_raw_data.find(' ')]

    def get_url(self):
        first_space = self._header_raw_data.find(' ')
        second_space = self._header_raw_data.find(' ', first_space + 1)
        return self._header_raw_data[first_space:second_space]

    def is_https(self):
        return self.get_http_method() == 'CONNECT'

    def get_forward_to(self):
        # get forward host and port
        host_header_value = self._header_dict.get('Host')
        if ':' in host_header_value:
            # Host and port number are specified
            return host_header_value.split(':')[0], int(host_header_value.split(':')[1])

        if self.is_https():
            # default https port number
            return host_header_value, 443

        # default http port number
        return host_header_value, 80

    def create_header_dict(self):
        # create http header dictionary
        header_dict = dict()

        for i, row in enumerate(self._header_raw_data.splitlines()):
            if i == 0:
                continue
            if ': ' in row:
                key = row[:row.find(': ')]
                value = row[row.find(': ') + 2:]
                header_dict[key] = value
        return header_dict

    def get_content_length(self):
        # get content-length
        if 'Content-Length' in self._header_dict:
            return int(self._header_dict['Content-Length'])
        return 0

    def is_chunked(self):
        # return chunked or not
        return 'Transfer-Encoding' in self._header_dict\
               and self._header_dict['Transfer-Encoding'] == 'chunked'

    @property
    def header_dict(self):
        return self._header_raw_data


class ProxyServer:
    # Proxy Server Class
    def __init__(self, config):
        # constructor
        self._host = ''
        self._port = config.get_default_port()
        self._server_sock = None
        self._config = config
        self._buff_size = config.get_default_buff_size()
        self._writer = open(config.get_output_path(), config.get_output_mode())

    @staticmethod
    def _create_forward_socket(host, port):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect((host, port))
        return sock

    def _mediate(self, client_sock, forward_sock, http_data_container, is_https):
        # send data already received
        if http_data_container.request_data and not is_https:
            forward_sock.sendall(http_data_container.request_data)

        # mediate communication for given sockets
        socks = [client_sock, forward_sock]
        close = False
        while True:
            r_socks, w_socks, x_socks = select.select(socks,
                                                      [],
                                                      socks,
                                                      self._config.get_default_timeout())
            try:
                if not r_socks:
                    break
                if x_socks:
                    break

                for r_sock in r_socks:
                    to_sock = client_sock
                    recv_from_client = False
                    if r_sock is client_sock:
                        to_sock = forward_sock
                        recv_from_client = True

                    data = r_sock.recv(self._buff_size)

                    if recv_from_client:
                        http_data_container.append_request_data(data)
                    else:
                        http_data_container.append_response_data(data)

                    if not data:
                        close = True
                        break
                    to_sock.sendall(data)
                if close:
                    break
            except Exception:
                logging.error(traceback.format_exc())

    def _handle_request(self, client_sock, client_address):
        forward_socket = None
        try:
            container = HttpDataContainer()

            # receive request data until the end of header
            req_data = client_sock.recv(self._buff_size)
            while '\r\n\r\n' not in req_data:
                req_data += client_sock.recv(self._buff_size)
            container.append_request_data(req_data)

            header_helper = HttpHeaderHelper(container.get_request_header())

            logging.info("{0} from {1}".format(header_helper.get_first_row(),
                                               client_address[0]))

            # get forward host info
            forward_host, forward_port = header_helper.get_forward_to()

            forward_socket = ProxyServer._create_forward_socket(forward_host, forward_port)

            # http or https ?
            is_https = header_helper.is_https()
            if is_https:
                # send  200 status code to web browser
                established = 'HTTP/1.0 200 Connection established\r\n\r\n'
                client_sock.sendall(established)
                container.append_response_data(established)
                self._puts_result(container)

            self._mediate(client_sock, forward_socket, container, is_https)

            if not is_https:
                self._puts_result(container)

        finally:
            # close sockets
            if forward_socket:
                try:
                    forward_socket.close()
                except IOError:
                    pass
            if client_sock:
                try:
                    client_sock.close()
                except IOError:
                    pass

    def _init_server_sock(self):
        self._server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self._server_sock.bind((self._host, self._port))
        self._server_sock.listen(256)
        logging.info('Proxy server started(port=%d).', self._port)

    def _puts_result(self, http_data_container):
        # puts http request, response to the file
        if self._config.get_output_enable():
            writer = self._writer
            writer.write(http_data_container.date_time.strftime("%Y/%m/%d %H:%M:%S"))
            writer.write('\n')
            writer.write(http_data_container.request_data)
            writer.write(http_data_container.response_data)
            writer.flush()

    def start(self):
        # Start proxy server
        self._init_server_sock()

        while True:
            # wait request form web browser
            client_sock, client_address = self._server_sock.accept()

            # handle request
            self._handle_request(client_sock, client_address)

    def stop(self):
        # stop proxy server

        # close socket
        if self._server_sock:
            try:
                self._server_sock.close()
            except IOError:
                pass

        # close writer
        if self._writer:
            try:
                self._writer.close()
            except IOError:
                pass
        logging.info('Proxy server stopped.')


class ProxyServerConfig:
    # Class to manage Proxy Server Config

    _CONFIG_PATH = './config.ini'

    def __init__(self):
        # constructor
        # load config file
        self._ini_file = ConfigParser.SafeConfigParser()
        self._ini_file.read(ProxyServerConfig._CONFIG_PATH)

    def get_default_port(self):
        return self._ini_file.getint('default', 'port')

    def get_default_buff_size(self):
        return self._ini_file.getint('default', 'buff.size')

    def get_default_timeout(self):
        return self._ini_file.getint('default', 'timeout')

    def get_output_mode(self):
        return self._ini_file.get('output', 'mode')

    def get_output_path(self):
        return self._ini_file.get('output', 'path')

    def get_output_enable(self):
        return self._ini_file.getboolean('output', 'enable')


if __name__ == "__main__":
    server = None
    try:
        # init logger
        logging.basicConfig(format='%(asctime)s    %(process)d    %(thread)x    %(levelname)s    %(message)s')
        logging.getLogger().setLevel(logging.INFO)

        # init config
        config = ProxyServerConfig()

        # Start proxy server
        server = ProxyServer(config)
        server.start()
    except KeyboardInterrupt as e:
        logging.info('Ctrl-c was pressed.')
    except Exception:
        logging.error(traceback.format_exc())
    finally:
        if server:
            server.stop()

config.ini(設定ファイル)

[default]
port=8080
timeout=30
buff.size=4096

[output]
enable=True
mode=w
path=./output.log

参考にしたサイト

https://github.com/inaz2/proxy2 http://memo.saitodev.com/home/python_network_programing/

github

githubに上げた。

https://github.com/magayengineer/PyProxy

修正歴

  • 2016/11/28 コードを微修正
  • 2016/11/29 コードを微修正
  • 2016/11/29 コードを修正。設定ファイル読み込み、結果出力実装。
  • 2016/12/2 githubへのリンクを追加