TL; DR
/etc/apache2/conf-available/security.conf に次を追加します。root の UID: 0, GID: 0 を範囲に含めています。
<IfModule mpm_itk_module> LimitUIDRange 0 6000 LimitGIDRange 0 6000 </IfModule>
/etc/apache2/conf-available/security.conf に次を追加します。root の UID: 0, GID: 0 を範囲に含めています。
<IfModule mpm_itk_module> LimitUIDRange 0 6000 LimitGIDRange 0 6000 </IfModule>
Apache2 MPM-ITK 環境下の情報が少なすぎて解決するのに丸一日かかってしまいました。もうこんな目に遭うのはご免なので記録します。前回の記事「Python CGI プログラマのための WSGI 移行記録」の続きから書きます。環境は同じく Apache2 MPM-ITK on Debian 9 Jessie です。
mod_wsgi には embedded mode と daemon mode があります。
embedded mode は呼び出される度にプロセスを起動して実行するのでほぼ CGI と同じ実行方式です。プロセス ID をしばらく観察していると、すぐには終了せずにしばらく残存してから終了するみたいです。CGI は都度終了するのでそこが違いでしょうか。
daemon mode は文字通り、デーモンとして起動したまま待機しています。プロセス ID を観察しているとずっと同じまま動作していることがわかります。
Apache2 の設定に AddHandler wsgi-script だけを設定すると embedded mode で実行されます。daemon mode で実行するには設定を加える必要があります。
Apache2 の VirtualHost に次の 2 行を加えます。
<VirtualHost *.443> ... WSGIDaemonProcess wsgi_app socket-user=USERNAME WSGIProcessGroup wsgi_app ... </VirtualHost>
保存したら Apache2 の再読込を行います。
$ sudo systemctl reload apache2
これで完了です。気になる方は次のような WSGI スクリプトを実行してみて、PID が間を置いたリロードで PID が変化するかどうかを試してみてください。
# coding: utf-8 import os def applicatoin(e, r): r('200 OK', [('Content-type', 'text/html; charset=utf-8')]) yield '<p>PID: {}</p>'.format(os.getpid()).encode('utf-8')
MPM-ITK 環境では socket-user オプションを指定しないと次のようなエラーを吐きます。
503 Service Unavailable
The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.
このとき Apache2 の error ログには次のようなものが残ります。
[Thu Oct 11 15:04:38.059487 2018] [wsgi:error] [pid 15574] (13)Permission denied: [client 153.194.19.27:55658] mod_wsgi (pid=15574): Unable to connect to WSGI daemon process 'wsgi_app' on '/var/run/wsgi.15557.0.1.sock' as user with uid=1001.
これを見たら、パーミッションが不足していると思ってディレクトリのオーナーやパーミッションの変更ばかり試してハマりました。権限があるところもパーミッションが 777 のところを指定しても自体は解決しません。
socket-user ではなく user と group オプションもありますがこれまた何も解決してくれず、おかげで丸一日悩む羽目になりました。
以下はおまけ程度の内容です。
実際に embedded mode と daemon mode でどれくらいの差があるのかを、Chrome の開発者モードに表示されるページの読込時間で試してみました。上記の PID を表示するだけの WSGI スクリプトをひたすら再読込するだけで、単位は ms です。
embedded | daemon | |
---|---|---|
初回 | 100 | 100 |
直後 | 30 | 30 |
間欠 | 100 | 70 |
数回リロードして安定した数値を拾っているだけなのでかなり雑です。目安程度にしてください。直後というのは概ね数秒以内です。間欠は 10 秒以上間を空けています。
サーバを起動した最初の 1 回目は同じですが、それ以後は daemon mode の方が有利ですね。開発時ならともかく、運用時はまず再起動することはないので daemon mode を選ばない理由はありませんね。
MPM-ITK を選んでいるのは複数ユーザのパーミッション分離を行うためで、Apache2 もユーザ別に設定ファイルを書いています。仮に foo さんと bar さんと hoge さんがいるとすると、設定ファイルは以下のようにしておけば、それぞれで WSGI スクリプトが daemon mode で動きます。
WSGIDaemonProcess 直後の名前は WSGIProcessGroup と揃えておけば好きな名前をつけて構いません。今回は WSGI であることがわかり、ユーザ別に分けたかったので wsgi_username の形にしました。
<VirtualHost *.443> ... ServerName foo.example.com ... WSGIDaemonProcess wsgi_foo socket-user=foo WSGIProcessGroup wsgi_foo ... </VirtualHost>
<VirtualHost *.443> ... ServerName bar.example.com ... WSGIDaemonProcess wsgi_bar socket-user=bar WSGIProcessGroup wsgi_bar ... </VirtualHost>
<VirtualHost *.443> ... ServerName hoge.example.com ... WSGIDaemonProcess wsgi_hoge socket-user=hoge WSGIProcessGroup wsgi_hoge ... </VirtualHost>
Apache2 MPM-ITK on Debian 9 Jessie を想定しています。
10 年近く書いてきた CGI を、昨今の WSGI に準拠したものへと移行するに当って大変苦労した記録です。技術的なことだけではなく、心情的なことも含んでいます。
まず最初に書いてしまうと、CGI も WSGI も呼び出しと結果の受け渡しが違うだけで、プログラムの内容や動作には殆ど関係ありません(細かく言うと実行の仕方も違うはずですが、そこはどちらかというとサーバの都合だと思うので今回は考慮しません)。なので単に今まで書いた CGI を WSGI に移行するだけなら、あまりメリットは感じないどころか環境構築が煩雑過ぎて疲れるだけです。
そもそもが CGI をどのような用途に使っているかによって、WSGI に移行すべきかどうかの判断が異なってきます。Raspberry pi やそれに近い組込み系の環境で単なる Web インターフェースとして使うのであれば、CGI の簡潔さは今なお有用です。ターミナル上で HTML の出力を確認しながらデバッグといったことも CGI なら簡単です。
CGI の代替となる技術が登場した背景には、CGI の実行に「無駄」が多いとされたことがあります。CGI へのアクセスがある度に Web サーバはプロセス(インタープリタ等)を起動します。アクセス数が多くなるとこの手順はサーバにとって負荷になります。インタープリタは常に同じものを使うことが多いので、当然ながらインタープリタは常時起動しておけばいいのではないかという考えに至ります。Web サーバと Web アプリケーション間のやり取りには様々なインターフェースが開発されましたが、それを統一したものの一つが WSGI という規格です。Python という言語の中から生れた WSGI はその後、他の言語にも派生しました。
さて、いよいよ Apache2 で WSGI を使って Python スクリプトを動かす準備に入ります。既に Apache2 で CGI が動いているなら実は変更することは 2 つです。
mod_wsgi は Debian 環境で apt 一発インストールです。このとき、Python 2.x を使うのか Python 3.x を使うのかによってパッケージが異なるので注意しましょう。今回は Python 3.x を使う予定なので Python 3.x に対応した “libapache2-mod-wsgi-py3” をインストールします。パッケージがあるか確認してインストールします。
$ sudo apt search mod-wsgi Sorting... Done Full Text Search... Done libapache2-mod-wsgi/stable,now 4.5.11-1 amd64 Python WSGI adapter module for Apache libapache2-mod-wsgi-py3/stable,now 4.5.11-1 amd64 Python 3 WSGI adapter module for Apache $ sudo apt install libapache2-mod-wsgi-py3
“sudo a2enmod wsgi” としなくても自動で有効化されていました。
Apache2 の設定ファイル(*.conf)に加えるのは次の一行だけ。<Directory /var/www> </Directory> の中などに書き加えましょう。
AddHandler wsgi-script .wsgi .py
WSGI はインターフェースの名前であって、スクリプト自体は Python だから拡張子を変えるのは間違っている気もしますが、それを言ったらどんな言語でも拡張子を .cgi とするのもおかしな話になるので、気にしないでおきましょう。
わかりにくいと思うので 000-default.conf を書き換えた例を示します。今回は実験用の独立したサーバなので全て許可した設定していますが、本番環境ではくれぐれも必要のないものまで許可しないようにしましょう。
... DocumentRoot /var/www <Directory /var/www> AllowOverride all Options ExecCGI AddHandler wsgi-script .wsgi .py Require all granted </Directory> ...
設定を書き換えたら Apache2 を restart または reload します。
$ sudo systemctl reload apache2
または
$ sudo systemctl restart apache2
動作を確認するために簡単な WSGI スクリプトを書いてみます。基本の Hello, world. で。
def application(environ, start_response): start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')]) yield '<p>Hello, world!</p>'.encode('utf-8')
これを /var/www/hello.wsgi として保存して http://[サーバのIPアドレス等]/hello.wsgi にアクセスして表示されたら完了です。CGI と違って chmod +x で実行権限を与える必要もありません。
ついでに調べた情報についても記しておきます。
def application() は呼び出し時にこの名前が呼ばれるので必須です。面倒だからと def app() にすると “404 Not Found” と怒られます。
引数の environ, start_response は別名にしても問題ありません。つまり、引数名使ったキーワード呼び出しは使わず、引数の順番しか定義されていません。これは仕様書でもはっきり次のように書かれています。
The application object must accept two positional arguments. For the sake of illustration, we have named them environ and start_response, but they are not required to have these names. A server or gateway must invoke the application object using positional (not keyword) arguments. (E.g. by calling result = application(environ, start_response) as shown above.)
PEP 333 — Python Web Server Gateway Interface v1.0 | Python.org
なので、例えば次のように書いても良いわけです。殆どが environ, start_response で書かれているようなので、そんなところで要らない個性を発揮する必要はないと思いますが、さっと動作確認したいときには省略したくなりますからね。
def application(e, r): r('200 OK', [('Content-type', 'text/html; charset=utf-8')]) yield '<p>Hello, world!</p>'.encode('utf-8')
application() を呼び出したときの戻り値は Iteratable である必要があるので、例えば複数の出力をしたい場合は次の 2 通りのパターンで行えます。結果はどちらも同じです。普通に return バイト列
とやると 500 Internal Server Error でハマります。
def application(environ, start_response): start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')]) yield '<title>Hello, WSGI!</title>'.encode('utf-8') yield '<p>This is a WSGI sample.</p>'.encode('utf-8')
def application(environ, start_response): start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')]) return ['<title>Hello, WSGI!</title>'.encode('utf-8'), <p>Hello, world!</p>'.encode('utf-8')]
この記事を書くに当って、れお氏(@reoreo125)には多大なる貢献を頂戴したこと、誠に感謝申し上げます。
print 文で UnicodeEncodeError が出ているなら次のコードで stdout をラップしましょう。
import io, sys sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding = 'utf-8')
CGI を書いていると出力する HTML の中で定型の部分が多くあることに気付きます。例えば <!DOCTYPE html> から </head> までは、どのページでもそう多く変化することはないと思います。
CGI という仕組み上、最終的な出力は殆どが HTML です。そこに着目したのが PHP という言語で HTML の中にコードを埋めるという逆の発想で、これはこれで理に適っている仕組みです。静的なページに近い場合はこれでも良いかなと思いますが、ロジックを組むことが多いのでやっぱり Python で書きたい、でも保守性も考えたいとなったときに活躍するのがテンプレートエンジンです。
pip が使えたら、とりあえずインストールします。
$ pip install jinja2
最も基本的な使い方、単純に placeholder として使う場合です。
from jinja2 import Template t = Template('こんにちは、{{ name }}さん') print(t.render(name = 'foo')) print(t.render(name = 'bar'))
こんにちは、fooさん こんにちは、barさん
{{ name }}
のスペースをなくして {{name}}
としても結果は同じですが、公式ドキュメントは全てスペースありで記述されているので統一します。
HTML をプログラムから分離するのが今回のゴールなので、テンプレートは別ファイルとして用意します。ファイルからテンプレートを読み込むのに FileSystemLoader を使います。
from jinja2 import Environment, FileSystemLoader env = Environment(loader = FileSystemLoader('./templates')) t = env.get_template('base.tpl') print(t.render(title = 'Sample Page', body = '<p>This is a Jinja2 sample page.</p>'))
<title>{{ title }}</title> <body>{{ body }} </body>
<title>Sample Page</title> <body><p>This is a Jinja2 sample page.</p></body>
鋭い方はもうお気づきだと思いますが、先の例では <p> タグがそのままテンプレートに埋め込まれています。これでは XSS の危険性があるので、エスケープ処理をする必要があります。Jinja2 には自動でエスケープ処理をしてくれる仕組みがあります。
先程のソースの Environment を作成するときに autoescape を指定するだけです。
from jinja2 import Environment, FileSystemLoader env = Environment(loader = FileSystemLoader('./templates'), autoescape = True) t = env.get_template('base.tpl') print(t.render(title = 'Sample Page', body = '<p>This is a Jinja2 sample page.</p>'))
<title>Sample Page</title> <body><p>This is a sample page.</p></body>
select_autoescape() という、テンプレートの拡張子で autoescape をするかどうか決める仕組みもあります。import select_autoescape を加えて Environment の引数に autoescape = select_autoescape(('html.', 'xml'))
とすれば、テンプレートの拡張が html か xml のときだけエスケープが働きます。
漏れが出たときに XSS のリスクとなるので避けたいところですが、テンプレート側で一つ一つエスケープする方法もあります。
{{ body }} {{ body|escape }} {{ body|e }}
<p>This is a sample page.</p> <p>This is a sample page.</p> <p>This is a sample page.</p>
基本的に autoescape は有効にしておくべきですが、エスケープしてほしくない箇所があった場合は override して部分的に有効化および無効化できます。
テンプレート側の {% autoescape true %}
と {% endautoescape %}
で挟まれば部分は有効化され、逆に {% autoescape false %}
と {% endautoescape %}
で挟まれた部分は無効化されます。
ここまでは Python 側の話でしたが、ここからはテンプレート側の話です。
Python の # と同じでコメントを記述できます。
for, if, macro/call, filter といった制御文を記述します。例えば for をつかって 1 から 5 までの連続した数値をリストアイテムとして出力する場合は次のようになります。
<ul> {% for i in range(1, 6) %} {{ i }} {% endfor %} </ul>
<ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul>
元々 {% … %} があった部分は空行になって出力されます。空行の抑制は後述します。
変数の出力で使いますが、計算式を入れて計算結果を表示したりもできます。組込みのフィルタも多数用意されているため、値の加工も容易です。
辞書から値を取出すのに . 区切り形式を使えます。Python で dict['key']
とするところを dict.key
で表現できます。
次のテンプレートと出力を基準として trim_blocks, lstrip_block, {%- … %} を指定したときの出力の違いを観察します。
<ul> {% for i in range(1, 6) %} <li>{{ i ** 2 }}</li> {% endfor %} </ul>
<ul> <li>1</li> <li>4</li> <li>9</li> <li>16</li> <li>25</li> </ul>
<ul> <li>1</li> <li>4</li> <li>9</li> <li>16</li> <li>25</li> </ul>
<ul> <li>1</li> <li>4</li> <li>9</li> <li>16</li> <li>25</li> </ul>
<ul> <li>1</li> <li>4</li> <li>9</li> <li>16</li> <li>25</li> </ul>
trim_blocks = False, lstrip_blocks = False とした上で、ソースを下記のように変更。
<ul> {%- for i in range(1, 6) %} <li>{{ i ** 2 }}</li> {%- endfor %} </ul>
<ul> <li>1</li> <li>4</li> <li>9</li> <li>16</li> <li>25</li> </ul>
Web でフォームを入力中に文字を消そうと Backspace を叩いたつもりが、テキストボックスからフォーカスが外れていて Web ブラウザのページ遷移が発生してしまって全て消えたという経験は誰しもが一度はあると思います。
最近の Web ブラウザの中には、フォーム入力中でページ遷移が発生しそうになると事前に注意をしてくれるものもあります。
そういった環境でなくても、JavaScript で定期的にフォームの内容を自動的に保存してしまおうというのが今回の趣旨です。
<script type="text/javascript"> $(function() { // Web Storageにフォームを保存 function saveForm() { // Key Valueでid, valueを格納 let f = {}; $("input").each(function () { f[$(this).attr("id")] = $(this).val(); }); // 辞書をJSON文字列化してWebStorageに保存 localStorage.setItem("form", JSON.stringify(f)); } // Web Storageからフォームを読込み function loadForm() { let f = JSON.parse(localStorage.getItem("form")); for (key in f) $("#" + key).val(f[key]); } // 前回の入力を読み込む loadForm(); // 自動保存のインターバルタイマー(5秒毎) setInterval(saveForm, 5000); }); </script>
WebStorage は key = value 形式で情報を保存します。セッション限り有効な sessionStorage と永続的な localStorage があります。今回は、間違ってウィンドウを閉じても復元したいので localStorage を選びました。
localStorage.setItem(key, value); localStorage.getItem(key);
form 要素内の input 要素を順に走査して key, value に id, value の形式で保存すれば良いのですが、WebStorage を他の用途に使うことも考えられるので、JSON にして “form” という key に保存することにしました。
let f = {}; $("input").each(function () { f[$(this).attr("id")] = $(this).val(); });
これで一旦全ての input 要素を id = value 形式で辞書に格納しています。注意点としては id 属性を key としているので、id 属性がないと保存されません。ここは name 属性にしたり、適宜変更する形になると思います。
localStorage.setItem("form", JSON.stringify(f));
辞書を JSON 文字列化して一つの key に保存しています。これで無駄に key を消費せずに保存できます。
JSON で保存したので、読み込むときも JSON として処理する必要があります。
let f = JSON.parse(localStorage.getItem("form"))
id 属性で拾った理由は、値をフォームにセットし直すときにセレクタが簡易に書けるからです。
for (key in f) $("#" + key).val(f[key]);
定期的に自動で保存するために、インターバルタイマーで 5 秒置きに saveForm() を呼び出すようにしました。
setInterval(saveForm, 5000);
これでページが読み込まれてから 5 秒置きに自動でフォームの内容が保存されるようになりました。