Ubuntu 18.04 に mod_python
をセットアップし、いろいろと試した時のメモです。
mod_python - Apache / Python Integration
なお、mod_pythonは
なことに注意します。
ドキュメントは以下を参考にしました。
- 公式版:Mod_python Documentation — Mod_python v3.5.0-3.5.0 documentation
- 日本語版:Mod_python ドキュメント — Mod_python 3.5.0-1330047 ドキュメント
目次
環境
- VM上に構築
- ホストは Mac OSX
- Ubuntu 18.04 日本語版
ubuntu-ja-18.04.1-desktop-amd64.iso
を使って、VMに最小構成でインストール
- Python 2.7系
- mod_python 3.3.1
なお、今回のmod_python のコード例は、汎用ハンドラ(generic handler)版で書いています。publisherハンドラ版を使う場合は、記述を省略できるかもしれません。
また、ディレクトリ構造などは以下です。
my@my-virtual-machine:/var/www$ tree . ├── html │ └── index.html # デフォルトのファイル └── mptest └── mp ├── form.html ├── generic_handler.py ├── publisher_handler.py └── template.html my@my-virtual-machine:/var/www/mptest/mp$ ls -al 合計 24 drwxr-xr-x 2 root root 4096 9月 24 09:06 . drwxr-xr-x 3 root root 4096 9月 24 06:26 .. -rw-r--r-- 1 root root 1238 9月 24 08:11 form.html -rw-r--r-- 1 root root 3125 9月 24 08:59 generic_handler.py -rw-r--r-- 1 root root 215 9月 24 06:37 publisher_handler.py -rw-r--r-- 1 root root 232 9月 24 08:36 template.html
mod_python で Hello world する (publisher/汎用ハンドラ)
Ubuntu 18.04 に mod_python をセットアップし、 Hello world するまでの流れです。
以下を参考に、Apacheのセットアップを行っていきます。
- How To Install the Apache Web Server on Ubuntu 18.04 | DigitalOcean
- UbuntuのApache設定ファイル、どうやるんだっけ? - Qiita
なお、Apacheの設定は「動けばいい」くらいの適当さです。
Macからsshできるようにします。今回、最小パッケージでインストールしたため、openssh-serverをインストールします。
$ sudo apt install openssh-server
Pythonのバージョンを確認したところ、Python自体がなかったため、追加で Python2.7 をインストールします。
# Pythonのバージョン確認 $ python --version Command 'python' not found, but can be installed with: sudo apt install python3 sudo apt install python sudo apt install python-minimal You also have python3 installed, you can run 'python3' instead. # Python2.7系をインストール my@my-virtual-machine:~$ sudo apt install python $ python --version Python 2.7.15rc1 # Pythonの場所を確認 $ which python /usr/bin/python
mod_python開発のため、PyCharm Communityをインストールします。
PyCharmのドキュメントに従い、snapパッケージでPyCharmをインストールします。
- 参考
# snapパッケージを使うため、snapdをインストール $ sudo apt install -y snapd $ sudo snap install pycharm-community --classic pycharm-community 2018.2.4 from 'jetbrains' installed
PyCharm Communityを起動します。
$ pycharm-community
プロジェクトを作成、システムのPythonを認識させておきます。
- Settings > Project:<project_name> > Project Interpreter の歯車マーク > Add
- System Interpreter > /usr/bin/python
以下を参考に、Apache2とmod_pythonをインストールします。
How to install and configure mod_python in apache 2 server | BHOU STUDIO
$ sudo apt install apache2 libapache2-mod-python
ApacheのMPMを確認します。
$ apachectl -V Server version: Apache/2.4.29 (Ubuntu) ... Server MPM: event threaded: yes (fixed thread count) forked: yes (variable process count)
event MPMでも動くと思いますが、とりあえず prefork MPM に変えてみます。
# mpm_eventを無効化 $ sudo a2dismod mpm_event # mpm_preforkを有効化 $ sudo a2enmod mpm_prefork # Apacheの再起動 $ sudo systemctl restart apache2 # 確認 $ apachectl -V ... Server MPM: prefork threaded: no forked: yes (variable process count)
Apacheにmod_pythonの設定を追加します。
$ cd /etc/apache2/sites-available/ $ sudo vi mod_python.conf
publisherハンドラを使うように設定します。
mod_python.conf
<VirtualHost *:80> ServerName example.com DocumentRoot /var/www/mptest/ <Directory "/var/www/mptest/mp"> AddHandler mod_python py PythonHandler mod_python.publisher PythonDebug On </Directory> </VirtualHost>
mod_python.conf を有効化するとともに、デフォルトを無効化します。
# mod_pythonを有効化 $ sudo a2ensite mod_python.conf Enabling site mod_python. To activate the new configuration, you need to run: systemctl reload apache2 # デフォルトを無効化 $ sudo a2dissite 000-default.conf
Apacheをリロードします。
$ sudo systemctl reload apache2
実際のpublisher_handlerを作成します。
$ cd ./mptest/mp $ sudo vi publisher_handler.py
中身は以下のとおりです。
publisher_handler.py
# /usr/bin/python # -*- codeing: utf-8 -*- def index(req): return 'publisher' def hello(req): return 'Hello world'
念のため、Apacheを再起動します。
sudo systemctl restart apache2
動作確認です。まずは ip address show
にて、UbuntuのIPアドレスを確認しておきます。
$ ip address show 2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 link/ether 00:0c:29:5b:ce:9f brd ff:ff:ff:ff:ff:ff inet 192.168.69.155/24 brd 192.168.69.255 scope global dynamic noprefixroute ens33 valid_lft 1717sec preferred_lft 1717sec inet6 fe80::f391:9b22:de93:f759/64 scope link noprefixroute valid_lft forever preferred_lft forever
別のターミナルを開いて動作確認します。publisherハンドラの動作確認ができました。
$ curl http://192.168.69.156/mp/publisher_handler.py publisher $ curl http://192.168.69.156/mp/publisher_handler.py/index publisher $ curl http://192.168.69.156/mp/publisher_handler.py/hello Hello world
なお、mod_pythonのハンドラは他にもあります。
そこで次は、汎用ハンドラ(generic handler)を使ってみます。
generic_handler.py
# /usr/bin/python # -*- codeing: utf-8 -*- from mod_python import apache def handler(req): req.write('Hello world') return apache.OK
mod_pythonの設定を変更します。
$ sudo vi /etc/apache2/sites-available/mod_python.conf
変更内容は
PythonHandler generic_handler
を追加PythonHandler mod_python.publisher
をコメントアウト
とします。
mod_python.conf
<VirtualHost *:80> ServerName example.com DocumentRoot /var/www/mptest/ <Directory "/var/www/mptest/mp"> AddHandler mod_python py # for generic handler PythonHandler generic_handler # for publisher handler # PythonHandler mod_python.publisher PythonDebug On </Directory> </VirtualHost>
Apacheを再起動します。
$ sudo systemctl restart apache2
別のターミナルで確認します。汎用ハンドラでも動作確認できました。
$ curl http://192.168.69.156/mp/generic_handler.py Hello world
引き続き、汎用ハンドラ使用時のリクエストとレスポンスをみていきます。
汎用ハンドラ使用時のリクエストとレスポンス
上記で見た通り、汎用ハンドラは
handler()
関数を作成- 引数として request オブジェクト (以降
req
)が渡される
- 引数として request オブジェクト (以降
- レスポンスボディは、
req.write()
を使う - 処理を終了する時は、
apache.OK
などを使う
な実装です。
また、 mod_python では、レスポンスは req オブジェクトに設定します。リクエストオブジェクトとレスポンスオブジェクトが一体化しているようです。
リクエスト時のHTTPメソッド
req.method
で取得します。
req.write('request.method: {}\n'.format(req.method)) # => GET
リクエスト時のHTTPヘッダ
req.headers_in
に含まれます。
headers_inは dict like object なので、items()などで取得できます。
for k, v in req.headers_in.items(): req.write('headers_in: key -> {} / value -> {}\n'.format(k, v)) # => headers_in: key -> Host / value -> 192.168.69.156 # => headers_in: key -> User-Agent / value -> curl/7.43.0 # => headers_in: key -> Accept / value -> */*
クライアントのIPアドレスなど
req.get_remote_host()
を使います。
引数に apache.REMOTE_NOLOOKUP
を指定するとIPアドレスが取得できます。
他の値は以下の公式ドキュメントに記載されています。
https://mod-python-doc-ja.readthedocs.io/ja/latest/pythonapi.html#apache.request.get_remote_host
req.write('request.get_remote_host(): {}\n'.format( req.get_remote_host(apache.REMOTE_NOLOOKUP))) # => 192.168.69.1
リクエストのあったファイル名
req.filename
を使います。
req.write('request.filename: {}\n'.format(req.filename)) # => /var/www/mpytest/mp/generic_handler.py
クエリストリングの取得
req.args
を使います。
req.write('request.args: {}\n'.format(req.args))
curlで確認します。
$ curl "http://192.168.69.156/mp/generic_handler.py?foo=1&bar=2&baz=3" request.args: foo=1&bar=2&baz=3
ただ、 req.args だと文字列として取得するため、使い勝手があまり良くなさそうです。
もしクエリストリングをオブジェクトとして取得したい場合は、 req オブジェクトを mod_python.util.FieldStorage
へ引数として渡せばよさそうです。
python - mod_python and getting the QUERY_STRING using env_vars() - Stack Overflow
fields = util.FieldStorage(req) for k, v in fields.items(): req.write('FieldStorage: key -> {} / value -> {}\n'.format(k, v))
curlで確認します。
$ curl "http://192.168.69.156/mp/generic_handler.py?foo=1&bar=2&baz=3" ... FieldStorage: key -> foo / value -> 1 FieldStorage: key -> bar / value -> 2 FieldStorage: key -> baz / value -> 3
なお、FieldStorageも dict like object です。
そのため、
fields.get('psp', None)
if '404' in fields
などが使えます。
POSTデータの取得
以下のようなフォームがあったとします。
form.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Input Form</title> </head> <body> <form action="/mp/generic_handler.py" method="post" name="myform"> <!-- input --> <label for="id_subject">Subject</label> <input type="text" name="subject" id="id_subject"> <!-- radio button --> <p> <label for="id_apple">Apple</label> <input id="id_apple" type="radio" name="fruit" value="apple"> <label for="id_mandarin">Mandarin</label> <input id="id_mandarin" type="radio" name="fruit" value="mandarin"> </p> <!-- select --> <p> <label for="id_quantity">Quantity</label> <select id="id_quantity" name="quantity"> <option id="id_select_1" name="select_1" value="one">1</option> <option id="id_select_2" name="select_2" value="two">2</option> </select> </p> <!-- checkbox --> <p> <label for="id_takeout">Takeout?</label> <input type="checkbox" id="id_takeout" name="takeout" value="takeout_yes"> </p> <!-- hidden --> <input type="hidden" id="id_hidden_value" name="hidden_valude" value="Oh, hidden value"> <p><input type="submit"></p> </form> </body> </html>
POSTされたデータを取得するには、FieldStorageを使います。クエリストリングと同じです。
fields = util.FieldStorage(req) for k, v in fields.items(): req.write('FieldStorage: key -> {} / value -> {}\n'.format(k, v))
ブラウザからフォームを送信してみます。hiddenも含め、値を取得できています。
# http://192.168.69.156/mp/form.html にアクセスし、入力した結果 request.method: POST ... FieldStorage: key -> subject / value -> web subject FieldStorage: key -> fruit / value -> mandarin FieldStorage: key -> quantity / value -> one FieldStorage: key -> takeout / value -> takeout_yes FieldStorage: key -> hidden_valude / value -> Oh, hidden value
CGI環境変数の取得
CGI環境変数とは以下のようなものです。
CGI Programming 101: Chapter 3: CGI Environment Variables
mod_pythonの場合、 req.add_common_vars()
後に、 req.subprocess_env
から取得します。
req.add_common_vars() env = req.subprocess_env # CGI環境変数 HTTP_HOST を取得 host = env.get('HTTP_HOST') req.write('subprocess_env(HTTP_HOST): {}\n'.format(host))
curlで確認してみます。
$ curl "http://192.168.69.156/mp/generic_handler.py?env1=1" subprocess_env(HTTP_HOST): 192.168.69.156
なお、add_common_vars()を使わないと取得できません。
env = req.subprocess_env host = env.get('HTTP_HOST') req.write('subprocess_env(HTTP_HOST): {}\n'.format(host))
curl結果です。
$ curl "http://192.168.69.156/mp/generic_handler.py?env2=1" subprocess_env(HTTP_HOST): None
また、 req.add_cgi_vars()
もあるようですが、mod_python 3.4.1以降でないと使えないようです。
参考:Issue 2550821: mod_python 3.4.1 - add_cgi_vars() instead of add_common_vars() - Roundup tracker
req.add_cgi_vars() env = req.subprocess_env req.write(env.get('HTTP_HOST', 'foo'))
curl結果です。mod_pythonエラーが発生しています。
$ curl "http://192.168.69.156/mp/generic_handler.py?env3=1" MOD_PYTHON ERROR ... Traceback (most recent call last): ... File "/var/www/mptest/mp/generic_handler.py", line 63, in handler req.add_cgi_vars() AttributeError: 'mp_request' object has no attribute 'add_cgi_vars'
Cookieの取得・設定
mod_pythonでは、リクエストCookieとレスポンスCookieは区別せず、同一の mod_python.Cookie
を使います。
Cookie.get_cookies
でCookieを取得し、 Cookie.add_cookie()
でCookieをセットします。
# Cookieの読込 cookies = Cookie.get_cookies(req) if 'counter' in cookies: # 更新したいキーのCookieを取得 c = cookies['counter'] c.value = int(c.value) + 1 # Cookieをセット Cookie.add_cookie(req, c) else: Cookie.add_cookie(req, 'counter', '1')
レスポンスの content_type を設定
req.content_type
に設定します。
# text/plainの場合 req.content_type = 'text/plain' # text/htmlの場合 req.content_type = 'text/html'
独自のHTTPヘッダを追加
req.headers_out
に設定します。
req.headers_out['X-My-header'] = 'hello world'
curlで確認します。
$ curl --include http://192.168.69.156/mp/generic_handler.py HTTP/1.1 200 OK Date: Sun, 23 Sep 2018 22:11:02 GMT Server: Apache/2.4.29 (Ubuntu) X-My-header: hello world
HTMLテンプレート(PSP)を使用したレスポンス
mod_pythonには、Python Server Pager (PSP)と呼ばれるHTMLテンプレートがあります。
テンプレート記法は以下に記載があります。
https://mod-python-doc-ja.readthedocs.io/ja/latest/pythonapi.html#module-psp
以下のテンプレート template.html
を用意します。内容は以下のとおりです。
なお、template.htmlは、generic_handler.pyと同じディレクトリに入れておきます。
template.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>PSP template</title> </head> <body> <% import time %> <p>Now: <%=time.strftime("%Y/%m/%d") %></p> <p>Query String: <%=query_string %></p> </body> </html>
Python側では、以下のコードでPSPテンプレートを表示します。
# content_typeを設定し、HTMLとして表示 req.content_type = 'text/html' # テンプレートを指定してPSPオブジェクトを生成 template = psp.PSP(req, filename='template.html') # クエリストリング:pspの値をPSPテンプレートへと渡す template.run({'query_string': fields.get('psp', None)}) return apache.OK
curlで確認します。
$ curl "http://192.168.69.156/mp/generic_handler.py?psp=use_template" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>PSP template</title> </head> <body> <p>Now: 2018/09/24</p> <p>Query String: use_template</p> </body> </html>
404ページを表示
apache.OKの代わりに、 apache.HTTP_NOT_FOUND
を使います。
if '404' in fields: return apache.HTTP_NOT_FOUND
curlで確認します。
$ curl "http://192.168.69.156/mp/generic_handler.py?404=1" <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>404 Not Found</title> </head><body> <h1>Not Found</h1> <p>The requested URL /mp/generic_handler.py was not found on this server.</p> <hr> <address>Apache/2.4.29 (Ubuntu) Server at 192.168.69.156 Port 80</address> </body></html>
エラーページを表示
apache.OKの代わりに、 apache.SERVER_RETURN
を使います。
if 'error' in fields: return apache.SERVER_RETURN
curlで確認します。
$ curl "http://192.168.69.156/mp/generic_handler.py?error=1" <pre> MOD_PYTHON ERROR ...
リダイレクト
mod_python.util.redirect()
を使います。
if 'redirect' in fields: util.redirect(req, 'https://www.google.co.jp')
curlで確認します。
$ curl "http://192.168.69.156/mp/generic_handler.py?redirect=1" <p>The document has moved <a href="https://www.google.co.jp">here</a></p> # リダイレクトを追跡 $ curl -L --include "http://192.168.69.156/mp/generic_handler.py?redirect=1" HTTP/1.1 302 Found ... Location: https://www.google.co.jp HTTP/1.1 200 OK ... Server: gws ... Set-Cookie: 1P_JAR=2018-09-23-23; expires=Tue, 23-Oct-2018 23:43:13 GMT; path=/; domain=.google.co.jp
req.write()使用上の注意
req.write()でレスポンスボディを設定します。
ただ、req.write()後にHTTPヘッダ系を修正しても反映されません。Cookieも同様ですので、注意が必要です。
if 'note' in fields: req.write('set after body\n') Cookie.add_cookie(req, 'after_write', 'yes') req.headers_out['X-After-Write'] = 'oh' return apache.OK
curlで確認します。
- HTTPヘッダ:X-After-Write
- Cookie:after_write
が設定されていません。
$ curl --include "http://192.168.69.156/mp/generic_handler.py?note=1" HTTP/1.1 200 OK Date: Sun, 23 Sep 2018 23:56:38 GMT Server: Apache/2.4.29 (Ubuntu) X-My-header: hello world Cache-Control: no-cache="set-cookie" Set-Cookie: counter=1 Vary: Accept-Encoding Transfer-Encoding: chunked Content-Type: text/plain set after body
試しに、req.write()前に移動してみます。
Cookie.add_cookie(req, 'after_write', 'yes') req.headers_out['X-After-Write'] = 'oh' req.write('set after body\n') return apache.OK
curlで確認します。
- HTTPヘッダ:X-After-Write
- Cookie:after_write
が設定されています。
$ curl --include "http://192.168.69.156/mp/generic_handler.py?note=1" HTTP/1.1 200 OK Date: Sun, 23 Sep 2018 23:59:11 GMT Server: Apache/2.4.29 (Ubuntu) X-My-header: hello world Cache-Control: no-cache="set-cookie" Set-Cookie: counter=1 Set-Cookie: after_write=yes X-After-Write: oh Vary: Accept-Encoding Transfer-Encoding: chunked Content-Type: text/plain set after body
ソースコード
GitHubに上げました。
thinkAmi-sandbox/mod_python-sample