概要
この記事は技術的チャレンジ...というよりは、他の用途の都合、PythonのWSGIのライブラリのさわりを目的外(?)使用した例です。
という意味で、WSGIの説明などではありませんのでご了承ください。
内容としては、
WSGIの仕組みで、
ワンライナーではないものの、
ワンライナー程度のステップ数でHTTP静的HTML簡易ホスティングサーバを実現する
というものになっています。
前置き
コンフィグファイル等も必要なくその分やれることも限られていて良いので、(ちょっとした静的HTML一式の表示確認用などに)コマンドラインから手軽に起動できるHTTPサーバはないんかね。
昔はともかく今だったらプログラミング言語の標準モジュールの範囲でも実現できるやつがあるあるんじゃないか、ないはずはないと思っていたら、... ありました。
例えばPythonだったら、
python -m http.server 8090
などとすると
起動したカレントディレクトリをドキュメントルートにして、静的コンテンツのホスティングが可能です。
http://localhost:8090/foo/bar.html
などでアクセスしてみてください。
Rubyだとどうでしょうか。
もちろんありました。
有名でかなり昔から対応しているようです。
おもしろそうなので、他にもないかと調べて記事にしようかと思ったものの、ググったらすばらしいまとめがありましたので、そちらにリンクさせていただくことにします。
今だと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 というものが使えそうです。
静的ファイルホスティング 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']
node.js版
同じ目的でのnode.js版です。mozillaの公式に記載がありました。node.jsが流行りだしたころによく見ましたね。
これの上記Pythonと同じ粗々度の例を作成しようと思いましたが、たまたまググってて模範的な例を見つけたので、こちらのリンクをご紹介することにしました。