はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Pythonで簡易HTTP静的ファイルサーバ -- 細工用にWSGIをひっかける

概要

この記事は技術的チャレンジ...というよりは、他の用途の都合、PythonWSGIのライブラリのさわりを目的外(?)使用した例です。

という意味で、WSGIの説明などではありませんのでご了承ください。

内容としては、

WSGIの仕組みで、

ワンライナーではないものの、

ワンライナー程度のステップ数でHTTP静的HTML簡易ホスティングサーバを実現する

というものになっています。

前置き

コンフィグファイル等も必要なくその分やれることも限られていて良いので、(ちょっとした静的HTML一式の表示確認用などに)コマンドラインから手軽に起動できるHTTPサーバはないんかね。

昔はともかく今だったらプログラミング言語の標準モジュールの範囲でも実現できるやつがあるあるんじゃないか、ないはずはないと思っていたら、... ありました。

例えばPythonだったら、

python -m http.server 8090

などとすると

起動したカレントディレクトリをドキュメントルートにして、静的コンテンツのホスティングが可能です。

http://localhost:8090/foo/bar.html

などでアクセスしてみてください。

Rubyだとどうでしょうか。

もちろんありました。

ruby -run -e httpd . -p 8090

有名でかなり昔から対応しているようです。

おもしろそうなので、他にもないかと調べて記事にしようかと思ったものの、ググったらすばらしいまとめがありましたので、そちらにリンクさせていただくことにします。

qiita.com

今だとDockerとかもあるでしょう。また、WAFのインストールのハードルも下がっているのですが、ワンライナーでいけるならその分本来の目的に集中できる/いくつか同時にサーバを起動して見比べなどもできるので、このようなワンライナーはありがたいですね。

あらまし(ワンライナーよりタイプ量が増えてもよいが標準ライブライの応用程度でいざとなれば独自の細工ができるHTTP静的ファイルサーバを気軽に起動したい)

ワンライナーでここまでいけるとすると少しだけ欲がでてきます。

というのも、私の経験ではボチボチある話なのですが、

User-Agentなどを元に、オリジンの静的HTMLのファイルを切り替えてやる...ということができると、デザイナーさんが作成したスマホ向けとPC向けの紙芝居HTMLのデモをインターネットに繋がなくても実施できる

あるいは

レスポンスのhtml中の特定のタグを書き換えてやる(もっとも端的なものは画像などをCDNサーバに配置のものにさしかえてやる)など、オリジンは静的HTMLだが少し細工をしたい

といったものです*1

どうにかこれらに手間をかけずに対応できないでしょうか。できればあまり凝った環境などを用意せずに、です。

残念ながらこれらは静的ホスティングの狭い方の守備範囲よりはちょいと広そうなので、先のワンライナーだけではだめそうです。

最終プロダクトでは静的どころかもっといろんな仕組みや要件を取り込んで念入りに実現することになるでしょうが、今の目的は、静的ホスティングでイケるかもう少し何か細工が必要かの見極めという前段の話です。

よって、WAF(略していましたが、Web Application Framework)を使うほどの手間をかけたくない、apacheやnginxを真面目に設定するのも今の時点では割りに合わないです。

方針:WSGIをつまみ食いする

ムシの良い話だとは思っていますが、何か都合の良い頃合いの良いしかけはないだろうか。

静的ホスティング以上、WAFなどの動的処理未満というところですので、プログラミング言語にミニチュアのWAF風のものがあれば良いのですが...

幸いPythonには、Python製のWAFを実現する場合に、従うことが推奨されている約束事(WSGI; Web Server Gateway Interface)というものがあるようです。

FlaskなどもWSGIにしたがっているとのこと。

また、この約束事に従うと、HTTP周辺のハンドリングは標準ライブラリにまかせることができそうですし、現にwsgiref.simple_server というものが使えそうです。

docs.python.org

静的ファイルホスティング on WSGIベースのソースコード

早速ですが、Pythonソースコード例を以下に示します。これを適当なファイル名で保存して、コマンドラインで起動すると、8090ポートで、起動ディレクトリをドキュメントルートにした静的ファイルホスティングのサーバが立ち上がります。

from wsgiref.simple_server import make_server
from collections import defaultdict
import pathlib

CONTENT_TYPE = defaultdict(lambda: 'application/octed-stream')
CONTENT_TYPE.update({
        '.html': 'text/html; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.js': 'text/javascript', '.json': 'application/json',
        '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif',
        '.css':'text/css'
    }
)

def endpoint(environ, start_response):
    p = environ['PATH_INFO']
    filepath = '.' + ( p + 'index.html'  if p.endswith('/') else p )
    headers = [('Content-type', CONTENT_TYPE[pathlib.Path(filepath).suffix])]
    with open(filepath,mode='br') as f:
        data = f.read()

    start_response('200 OK', headers)
    return [bytes(data)]

make_server('localhost', 8090, app=endpoint).serve_forever()

'PATH_INFO'というキーで、アクセスされたURIのパス情報が取得できるので、その後、ディレクトリ上のファイルを読み込み、それをレスポンスデータに戻しています。

今回は特に何もしていませんが、ファイル名を解析したり、読み込んだファイルの内容(上記でいうと変数dataに格納)を改変するなどすれば、オリジンの静的ファイルを元にいくつかアドホックなトリックを組み込むことができます。

上記の「environ」という変数はdict型の変数で、次のような情報が取得できます。

なお、環境によっては他にも取得できる項目がありますが、ここではいつものアレねというものを中心にあげています。

SERVER_NAME 1.0.0.127.in-addr.arpa
SERVER_PORT 8090
REMOTE_HOST 
CONTENT_LENGTH 
SERVER_PROTOCOL HTTP/1.1
SERVER_SOFTWARE WSGIServer/0.2
REQUEST_METHOD GET
PATH_INFO /foo/bar.html
QUERY_STRING bar=aaa&boo=bbb
REMOTE_ADDR 127.0.0.1
CONTENT_TYPE text/plain
HTTP_HOST localhost:8000
HTTP_CONNECTION keep-alive
HTTP_CACHE_CONTROL max-age=0
HTTP_UPGRADE_INSECURE_REQUESTS 1
HTTP_USER_AGENT Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.131 Safari/537.36
HTTP_SEC_FETCH_USER ?1
HTTP_ACCEPT text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
HTTP_SEC_FETCH_SITE none
HTTP_SEC_FETCH_MODE navigate
HTTP_ACCEPT_ENCODING gzip, deflate, br
HTTP_ACCEPT_LANGUAGE ja,en-US;q=0.9,en;q=0.8
HTTP_COOKIE __gads=ID=22edc85a3f0fd9be:T=1581602476:S=ALNI_MaJxNlUyXY_9qzIWOiLSbzE4_3qIA
wsgi.url_scheme http

このプログラムは、自身で卑下するわけではないですが、WSGI準拠と呼べるのかどうかというレベルです。

make_serverをimportしているだけで、WSGI準拠とは多分言えないです。その点ご注意ください。

また、エラーハンドリングはほとんどされておらず、当たり前のように常にステータス200を返すという実装です。

セキュリティ...は微塵も考慮されていないですね。ただし、今回はこれでいいのです。

http.server版

最初に紹介したPythonワンライナー版ですと、http.serverというモジュールを使いました。

http.serverは公式ドキュメントにも書いてあるとおり、プロダクトに利用する類のものと公式が言う類のものなので、WSGIよりやれることは狭まるようですが、今回程度であればあまり変わらないかもしれません。

http.serverのBaseHTTPRequestHandlerを継承して(特にdoGETをオーバーライドすると)、先のWSGI版の例とおおよそ同じ動作になる例が作れるのでこれを示します。

from http.server import BaseHTTPRequestHandler, HTTPServer
from collections import defaultdict
import pathlib

CONTENT_TYPE = defaultdict(lambda: 'application/octed-stream')
CONTENT_TYPE.update({
        '.html': 'text/html; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.js': 'text/javascript', '.json': 'application/json',
        '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif',
        '.css':'text/css'
    }
)

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        p = self.path
        self.send_response(200)
        filepath = '.' + (p + 'index.html' if p.endswith('/') else p)
        with open(filepath,mode='br') as f:
            data = f.read()
        self.send_header('Content-type', CONTENT_TYPE[pathlib.Path(filepath).suffix])
        self.end_headers()
        self.wfile.write(bytes(data))

with HTTPServer(('localhost',8090), MyHandler) as srv:
    srv.serve_forever()

なお、selfをdirしたところ次のような値が戻ってきました、直接httpヘッダーの項目をひとつずつ取得することはできなさそうですが、requestlineなどからどうにかできるのでしょうか(未確認)。

['MessageClass', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'address_string', 'client_address', 'close_connection', 'command', 'connection', 'date_time_string', 'default_request_version', 'disable_nagle_algorithm', 'do_GET', 'end_headers', 'error_content_type', 'error_message_format', 'finish', 'flush_headers', 'handle', 'handle_expect_100', 'handle_one_request', 'headers', 'log_date_time_string', 'log_error', 'log_message', 'log_request', 'monthname', 'parse_request', 'path', 'protocol_version', 'raw_requestline', 'rbufsize', 'request', 'request_version', 'requestline', 'responses', 'rfile', 'send_error', 'send_header', 'send_response', 'send_response_only', 'server', 'server_version', 'setup', 'sys_version', 'timeout', 'version_string', 'wbufsize', 'weekdayname', 'wfile']

docs.python.org

node.js版

同じ目的でのnode.js版です。mozillaの公式に記載がありました。node.jsが流行りだしたころによく見ましたね。

developer.mozilla.org

これの上記Pythonと同じ粗々度の例を作成しようと思いましたが、たまたまググってて模範的な例を見つけたので、こちらのリンクをご紹介することにしました。

*1:CMSのPoCってところでしょうか。