WSGIで作る簡単ローカルHTTP Proxyサーバ

前提としては、

  • 社内のWiki(仮にintra.example.comとする)には外部から直接アクセスできない
  • ゲートウェイ(仮にgw.example.comとする)にはssh接続できる
  • ノートPCを使っていて、頻繁にLANの内部と外部を行き来する

という状況で、どこからでも社内のWikiが見られるようにしたい。

SSHのポートフォワーディングだけだと、http://localhost:8088/みたいなURLでアクセスすることになるので、リンクを辿れなかったり、VirtualHostを使っている場合はhostsを書き換える必要があったりして面倒。

SOCKSとかDynamicDNSとか調べたけどよくわからず、既存のHTTP Proxyサーバではちょうど良いものがなかったので、自分で作ることにした。
幸いWSGIには必要な部品は揃っているので、あとはそれを組み合わせるだけ。

virtualenv環境の構築とプロジェクトのひな形生成

$ python virtualenv.py --no-site-packages TinyWSGIProxy
$ cd TinyWSGIProxy
$ . bin/activate

(TinyWSGIProxy)$ easy_install Paste
(TinyWSGIProxy)$ easy_install PasteScript
(TinyWSGIProxy)$ paster create -t paste_deploy TinyWSGIProxy
Selected and implied templates:
  PasteScript#basic_package  A basic setuptools-enabled package
  PasteDeploy#paste_deploy   A web application deployed through paste.deploy

Variables:
  egg:      TinyWSGIProxy
  package:  tinywsgiproxy
  project:  TinyWSGIProxy
Enter version (Version (like 0.1)) ['']: 
Enter description (One-line description of the package) ['']: 
Enter long_description (Multi-line description (in reST)) ['']: 
Enter keywords (Space-separated keywords/tags) ['']: 
Enter author (Author name) ['']: 
Enter author_email (Author email) ['']: 
Enter url (URL of homepage) ['']: 
Enter license_name (License name) ['']: 
Enter zip_safe (True/False: if the package can be distributed as a .zip file) [False]: 
Creating template basic_package
Creating directory ./TinyWSGIProxy
  Recursing into +package+
    Creating ./TinyWSGIProxy/tinywsgiproxy/
    Copying __init__.py to ./TinyWSGIProxy/tinywsgiproxy/__init__.py
  Copying setup.cfg to ./TinyWSGIProxy/setup.cfg
  Copying setup.py_tmpl to ./TinyWSGIProxy/setup.py
Creating template paste_deploy
  Recursing into +package+
    Copying sampleapp.py_tmpl to ./TinyWSGIProxy/tinywsgiproxy/sampleapp.py
    Copying wsgiapp.py_tmpl to ./TinyWSGIProxy/tinywsgiproxy/wsgiapp.py
  Recursing into docs
    Creating ./TinyWSGIProxy/docs/
    Copying devel_config.ini_tmpl to ./TinyWSGIProxy/docs/devel_config.ini
Updating ./TinyWSGIProxy/setup.py
Updating ./TinyWSGIProxy/setup.py
************************************************************************
* Run "paster serve docs/devel_config.ini" to run the sample application
* on http://localhost:8080
************************************************************************
Running /Users/nozom/work/TinyWSGIProxy/bin/python setup.py egg_info
Adding PasteDeploy to paster_plugins.txt

(TinyWSGIProxy)$ cd TinyWSGIProxy

tinywsgiproxy/wsgiapp.py

from paste.proxy import TransparentProxy
from sqlalchemy import engine_from_config

import model


class ProxyAdmin(object):

    def __call__(self, environ, start_response):
        query = model.HostNameMapping.query.filter_by(enabled=True)
        mapping = query.filter_by(hostname=environ["HTTP_HOST"]).first()
        if mapping:
            proxy = TransparentProxy(force_host=mapping.mapped)
	else:
            proxy = TransparentProxy()
        return proxy(environ, start_response)


def make_app(global_conf, **kw):
    # Here we merge all the keys into one configuration
    # dictionary; you don't have to do this, but this
    # can be convenient later to add ad hoc configuration:
    conf = global_conf.copy()
    conf.update(kw)

    # Setup model
    model.metadata.bind = engine_from_config(conf, 'sqlalchemy.')
    model.setup_all()
    # model.create_all()

    # This is a WSGI application:
    app = ProxyAdmin()

    return app

tinywsgiproxy/model.py

from elixir import *

__all__ = ["HostNameMapping"]


class HostNameMapping(Entity):
    using_options(shortnames=True)

    hostname = Field(String(255), unique=True)
    mapped   = Field(String(255))
    enabled  = Field(Boolean)

development.ini

[DEFAULT]

[filter-app:main]
# This puts the interactive debugger in place:
use = egg:Paste#evalerror
next = devel

[app:devel]
# This application is meant for interactive development
use = egg:TinyWSGIProxy

sqlalchemy.url = sqlite:////%(here)s/data.db

filter-with = log

[filter:log]
use = egg:Paste#translogger

[app:test]
# While this version of the configuration is for non-iteractive
# tests (unit tests)
use = devel

[server:main]
use = egg:Paste#http

# Change to 0.0.0.0 to make public:
host = 127.0.0.1
port = 8088
  • DBのセットアップ
(TinyWSGIProxy)$ easy_install Elixir
(TinyWSGIProxy)$ easy_install pysqlite

(TinyWSGIProxy)$ python
>>> from sqlalchemy import create_engine
>>> import tinywsgiproxy.model
>>> tinywsgiproxy.model.metadata.bind = create_engine('sqlite:///data.db')
>>> tinywsgiproxy.model.setup_all()
>>> tinywsgiproxy.model.create_all()

CRUD機能はないのでコマンドラインからデータを入力する。

$ sqlite3
sqlite> insert into hostnamemapping (id, hostname, mapped, enabled) values (1, "intra.example.com", "localhost:40080", 1);
sqlite> .quit

実行

(TinyWSGIProxy)$ paster serve --reload development.ini

別のターミナルで

$ ssh -L 40080:intra.example.com:80 gw.example.com

を実行しておいて、ブラウザのプロキシ設定をlocalhost:8088に変更する。
http://intra.example.com/にアクセスすると、プロキシ経由でページが表示されるようになります。