Ubuntu18.04 + mod_python 3.3.1で、リクエスト・レスポンス・Cookieを試してみた

Ubuntu 18.04 に mod_python をセットアップし、いろいろと試した時のメモです。
mod_python - Apache / Python Integration

なお、mod_python

なことに注意します。

 
ドキュメントは以下を参考にしました。

 

目次

 

環境

 
なお、今回の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_pythonHello world する (publisher/汎用ハンドラ)

Ubuntu 18.04 に mod_python をセットアップし、 Hello world するまでの流れです。

以下を参考に、Apacheのセットアップを行っていきます。

なお、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を認識させておきます。

 
以下を参考に、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)

 
Apachemod_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 にて、UbuntuIPアドレスを確認しておきます。

$ 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)を使ってみます。

汎用ハンドラ用のPythonスクリプトを作成します。

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

 
変更内容は

とします。

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 )が渡される
  • レスポンスボディは、 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_cookiesCookieを取得し、 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 を用意します。内容は以下のとおりです。

  • Python標準モジュール time を使用して、現在の日付を出力
  • Pythonから渡される変数 query_string を表示

なお、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

 

その他参考