#pycamp #glnagano #nseg Python Boot Camp in 長野にTAとして参加しました

6/10にギークラボ長野で開催された「Python Boot Camp in 長野」にTAとして参加しました。

 
日々Pythonをさわっているのですが、使っても使っても新たな発見があり、楽しく過ごしています。

そんな中、「Python Boot Camp in 長野」が企画され、講師として寺田さんがいらっしゃるということを聞きました。

せっかくの機会なので参加したいと相談したところ、TA(講師アシスタント)枠での参加をすすめられたため、TAとして参加しました。

TAは初めての経験であり、うまくできるか不安でした。ただ、事前にいろいろと資料が配布されて説明していただけるなど、TAに対するサポートがありました。そのため、当日どんなふうに動けばよいかのイメージをつかむことができ、不安がやわらぎました。

 
当日の講義中は、寺田さんのお話を聞きつつ、参加者の間を動いてTAのお仕事をしていました。

特に、「WindowsPythonをインストールしたはずなのにPythonの対話モードが起動しない」と詰まっていたのを解決できた時はホッとしました。

ただ、うまくいかないと相談されたときには対応できたものの、参加者がうまくいっていないのか考え込んでいるのかを見分けることが難しく、このあたりは自分の課題だと感じました。

 
また、休憩中には、Python 3 エンジニア認定基礎試験の出題範囲である「Pythonチュートリアル 第3版」に、寺田さんのサインをいただきました。試験を受ける時のお守りにしたいと思います。
基礎試験 | 一般社団法人Pythonエンジニア育成推進協会

 
その後の懇親会でもPythonに関するお話をうかがうことができ、いろいろとためになりました。

 
最後になりましたが、「Python Boot Camp in 長野」を企画・運営・参加されたみなさま、ありがとうございました。

 

WindowsでインストールしたはずなのにPythonの対話モードが起動しない時

参考までに対応した時の行動をメモしておきます。

  • WindowsPythonをダウンロード・インストールしたものの、Pythonの対話モードが起動しないという相談を受ける
  • コマンドプロンプトからset PATHを入力し、Windows環境変数PATHpython.exeが登録されているか確認
    • 登録されていない
  • ダウンロードしたPythonインストーラーを確認
    • amd64という記載があったため、Pythonは64bit版でインストールされることを確認
  • よく使われるC:\Program FilesC:\Program Files (x86)Pythonがいるか確認
  • PyCharmがインストールされていたため、設定されていたinterpreterを確認
    • %USERPROFILE%\AppData\Local\Programs\Python\Python36が指定
  • 参加者に環境変数に上記パスを追加していただいた
    • GUIから操作する時のメモ
      • Windows10のGUIの場合、Win + Xキーでシステムを選択 > システムの詳細設定 > 環境変数ボタン
      • AppDataは隠しフォルダのため、コントロールパネルのエクスプローラーのオプションから、隠しフォルダを一時的に表示する
  • 再度、Pythonの対話モードを試したところ、無事に起動

 
原因としては、Pythonインストーラーのデフォルトが

  • Install for all usersにチェックが入っていないため、%USERPROFILE%\AppData\Local\Programs\Python\Python36にインストールされる
  • Add Python to environment variablesにチェックが入っていないため、環境変数PATHに追加されない

であり、インストール時にInstall Nowを選択してインストールしたためかなと思いました*1

f:id:thinkAmi:20170611082231p:plain

(自分のWindowsはPython3.6が導入済みのため、Python3.5.3のインストール画面のスクリーンショットになりますが、内容は同じです)

*1:自分はいつもCustomize installationを選択してデフォルトから変更していたため、現場では気づかなかったものの記事を書いている時に気づきました

Docker + Alpine3.5 + Apache2.4 + Python3.xで、mod_pythonをソースコードからインストールしてみた

以前、Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonソースコードからインストールしてみました。
Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonをソースコードからインストールしてみた - メモ的な思考的な

 
その時はPython2.x系で試したため、今回はPython3.x系でmod_pythonコンパイル&インストールし、Hello worldしてみます。

 
目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac12
  • Alpine3.5 + Apache2.4.25
    • 前回同様、Apacheはprefork MPMで動作するようにDockerfileを修正
  • Python3系は以下の2種類を試す
    • Alpine3.5のパッケージにあるPython3.5
    • Alpine EdgeのパッケージにあるPython3.6
      • Alpine3.6ならPython3.6が入るが、今回はAlpine3.5で検証
  • mod_python
    • GitHub上のmasterの最新版
    • 今回利用したコミットは8acf1b7

 
なお、動作させるためのDockerfileやhttpd.conf、Hello worldスクリプトは、以前のPython2系で使ったものを流用・一部修正していきます。

 

まとめ

長いので最初にまとめます。

  • 最新のmaster(8acf1b7)ではPython3.x系は動作しない*1
  • ブランチ 3.5.xソースコードであれば、Python3.x系は動作する
  • 最新のmasterで動作させるには、ビルド用のファイルをブランチ3.5.xのコミットへと戻す
    • configure〜make installはあまり詳しくないので、3.5.xから最新までの間のどこでビルド用のファイルが壊れたかまでは追っていない

 

準備:prefork MPMで動作するApache2.4のDockerイメージを作成

今回の件とはあまり関係ないですが、後で再利用できるよう、Apache2.4 + prefork MPMのDockerイメージを作成しておきます。

(長いので省略しますが)Dockerfileは以下を使います。
https://github.com/thinkAmi-sandbox/Docker_Apache-sample/blob/master/alpine_apache_prefork/Dockerfile

 
thinkami:httpd24_preforkという名前でDockerイメージを作成します。

$ docker image build -t thinkami:httpd24_prefork .

 

masterをコンパイル&インストー

mod_pythonのmasterをPython3.5でコンパイル&インストールするには、以前のDockerfileを以下の内容で修正します。

  • mod_pythonで使用するpythonpython3へと変更
  • mod_pythonをビルドする時に使うpython-devpython3-devへと変更
  • configureする時のwith-pythonオプションを--with-python=/usr/bin/python3へと変更
RUN apk --update --no-cache add python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    rm -r /root/.cache && \
    # mod_pythonで必要なパッケージを追加
    apk add --no-cache --virtual .mod_python_build_libs git && \
    apk --no-cache --virtual .mod_python_build_libs add python3-dev && \
    apk add --no-cache --virtual .mod_python_build_libs apache2-dev && \
    apk add --no-cache --virtual .mod_python_build_libs flex && \
    # ./configureで必要
    apk add --no-cache --virtual .mod_python_build_libs build-base && \
    # sudo make installで必要
    apk add --no-cache --virtual .mod_python_build_libs sudo && \
    # GitHubからソースコードを持ってきてインストール
    cd /tmp && \
    mkdir mod_python && \
    cd mod_python && \
    git clone https://github.com/grisha/mod_python.git . && \
    ./configure --with-apxs=/usr/local/apache2/bin/apxs --with-python=/usr/bin/python3 --with-flex=/usr/bin/flex && \
    make && \
    sudo make install && \
    # 不要なパッケージやソースコードを一括削除
    apk del .mod_python_build_libs && \
    rm -r /tmp/mod_python

 
docker image builddocker container runします。

# Dockerイメージのビルド
$ docker image build -t thinkami:python3_httpd24_mod_python_1 .
...
Successfully built e2669af0e6a4

# Dockerコンテナの起動 => エラーで起動せず
$ docker container run -p 8081:80 --name mod_python_1 -v `pwd`/htdocs/:/usr/local/apache2/htdocs thinkami:python3_httpd24_mod_python_1
httpd: Syntax error on line 19 of /usr/local/apache2/conf/httpd.conf: Cannot load modules/mod_python.so into server: Error relocating /usr/local/apache2/modules/mod_python.so: PyErr_BadArgument: symbol not found

 
エラーPyErr_BadArgument: symbol not foundが表示され、Dockerコンテナが起動しません。

ブランチ3.5.xをコンパイル&インストー

最新のmasterでは動作しなかったため、動作するブランチを探したところ、以下のIssueにコメントがありました。
mod_python 3.5.0 (with support for Python 3!) available for pre-release testing. · Issue #9 · grisha/mod_python

3.5.xブランチを使えばいいようです。

 
そのため、Dockerfileの中でgit cloneしている部分をmasterから3.5.xブランチへと修正します。

# git clone https://github.com/grisha/mod_python.git . && \
git clone -b 3.5.x https://github.com/grisha/mod_python.git . && \

 
docker image build & docker container runします。

$ docker image build -t thinkami:python3_httpd24_mod_python_2 .

$ docker container run -p 8081:80 --name mod_python_2 -v `pwd`/htdocs/:/usr/local/apache2/htdocs thinkami:python3_httpd24_mod_python_2
[Fri Jun 09 03:51:43.408568 2017] [:notice] [pid 1] mod_python: Creating 8 session mutexes based on 256 max processes and 0 max threads.
[Fri Jun 09 03:51:43.408632 2017] [:notice] [pid 1] mod_python: using mutex_directory /tmp 
[Fri Jun 09 03:51:43.445271 2017] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.25 (Unix) mod_python/3.5.0-9d223c3 Python/3.5.2 configured -- resuming normal operations
[Fri Jun 09 03:51:43.445310 2017] [core:notice] [pid 1] AH00094: Command line: 'httpd -D FOREGROUND'
[Fri Jun 09 03:52:06.023976 2017] [:notice] [pid 6] mod_python: (Re)importing module 'mptest'
172.17.0.1 - - [09/Jun/2017:03:52:05 +0000] "GET /test/mptest.py HTTP/1.1" 200 12

mod_pythonが起動したようです。

 
curlで動作確認をします。

$ curl localhost:8081/test/mptest.py -D - -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /test/mptest.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Fri, 09 Jun 2017 03:52:05 GMT
Date: Fri, 09 Jun 2017 03:52:05 GMT
< Server: Apache/2.4.25 (Unix) mod_python/3.5.0-9d223c3 Python/3.5.2
Server: Apache/2.4.25 (Unix) mod_python/3.5.0-9d223c3 Python/3.5.2
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Content-Type: text/plain
Content-Type: text/plain

< 
* Connection #0 to host localhost left intact
Hello World!

Hello worldが表示されたため、正常に動作しているようです。

 

masterにブランチ3.5.xをマージして、コンパイル&インストー

上記ではブランチ3.5.xで動かしましたが、3.5.xブランチの最新コミットは2013年10月であるため、それ以降に修正された不具合は反映されていません。

試しにブランチ3.5.xとmasterの差分を見ると、以下の4コミットでした。 Comparing master…3.5.x · grisha/mod_python

4コミットには以下の8ファイルが含まれていました。

 
そこで、masterにブランチ3.5.xを単純にマージし、動作するか試してみます。

 
Dockerfileでは

  • git cloneするのをmasterから3.5.xブランチへ
  • git mergeするために、ユーザの設定を任意の値へ

を修正します。

cd mod_python && \
# gitを使うためにユーザ設定を実施
git config --global user.email "you@example.com" && \
git config --global user.name "Your Name" && \
# 現在のmasterを取得
git clone https://github.com/grisha/mod_python.git . && \
# 3.5.xブランチを取得
git branch 3.5.x origin/3.5.x && \
# 3.5.xブランチをmasterにマージ
git merge 3.5.x && \
./configure --with-apxs=/usr/local/apache2/bin/apxs --with-python=/usr/bin/python3 --with-flex=/usr/bin/flex && \

 
docker image build & docker container runします。

$ docker image build -t thinkami:python3_httpd24_mod_python_3 .

$ docker container run -p 8081:80 --name mod_python_3 -v `pwd`/htdocs/:/usr/local/apache2/htdocs thinkami:python3_httpd24_mod_python_3
[Fri Jun 09 03:54:21.257839 2017] [:notice] [pid 1] mod_python: Creating 8 session mutexes based on 256 max processes and 0 max threads.
[Fri Jun 09 03:54:21.257983 2017] [:notice] [pid 1] mod_python: using mutex_directory /tmp 
[Fri Jun 09 03:54:21.391315 2017] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.25 (Unix) mod_python/3.5.0-7e4861f Python/3.5.2 configured -- resuming normal operations
[Fri Jun 09 03:54:21.393658 2017] [core:notice] [pid 1] AH00094: Command line: 'httpd -D FOREGROUND'

mod_pythonが起動したようです。

 
curlで動作確認をします。

$ curl localhost:8081/test/mptest.py -D - -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /test/mptest.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 500 Internal Server Error
HTTP/1.1 500 Internal Server Error
< Date: Fri, 09 Jun 2017 03:54:36 GMT
Date: Fri, 09 Jun 2017 03:54:36 GMT
< Server: Apache/2.4.25 (Unix) mod_python/3.5.0-7e4861f Python/3.5.2
Server: Apache/2.4.25 (Unix) mod_python/3.5.0-7e4861f Python/3.5.2
< Content-Length: 528
Content-Length: 528
< Connection: close
Connection: close
< Content-Type: text/html; charset=iso-8859-1
Content-Type: text/html; charset=iso-8859-1

< 
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>500 Internal Server Error</title>
</head><body>
<h1>Internal Server Error</h1>
<p>The server encountered an internal error or
misconfiguration and was unable to complete
your request.</p>
<p>Please contact the server administrator at 
 you@example.com to inform them of the time this error occurred,
 and the actions you performed just before this error.</p>
<p>More information about this error may be available
in the server error log.</p>
</body></html>
* Closing connection 0

Internal Server Errorになりました。

 
Docker containerのコンソールを見ると

[Fri Jun 09 03:54:36.714955 2017] [:error] [pid 6] make_obcallback: could not import mod_python.apache.\n
[Fri Jun 09 03:54:36.716445 2017] [:error] [pid 6] make_obcallback: Python path being used "['/usr/lib/python35.zip', '/usr/lib/python3.5/', '/usr/lib/python3.5/plat-linux', '/usr/lib/python3.5/lib-dynload']".
[Fri Jun 09 03:54:36.716560 2017] [:error] [pid 6] get_interpreter: no interpreter callback found.
[Fri Jun 09 03:54:36.716596 2017] [:error] [pid 6] [client 172.17.0.1:36616] python_handler: Can't get/create interpreter.
172.17.0.1 - - [09/Jun/2017:03:54:36 +0000] "GET /test/mptest.py HTTP/1.1" 500 528

と、interpreterのcallbackが見当たらないようです。

よって、単純なマージではダメなようです。

 

masterのビルド用ファイルをブランチ3.5.xの時点に戻して、コンパイル&インストー

差分のあった8ファイルのうちビルド用ファイル

を、ブランチ3.5.x時の状態に戻した上でコンパイル&インストールします。

また、コンパイルにはAPKのEdgeにあるPython3の最新版を利用してみます。

Dockerfileの変更は

# Python3まわり
RUN apk --update --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/main/ add python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    rm -r /root/.cache && \
    # python3-devもEdgeから持ってくる
    # そうでないと、Python3のバージョンが合わずに、Downgrading python3が発生する
    apk --no-cache --virtual .mod_python_build_libs --repository http://dl-cdn.alpinelinux.org/alpine/edge/main/ add python3-dev && \
...

# mod_pythonまわり
    cd mod_python && \
    # gitを使うためにユーザ設定を実施
    git config --global user.email "you@example.com" && \
    git config --global user.name "Your Name" && \
    # 現在のmasterを取得
    git clone https://github.com/grisha/mod_python.git . && \
    # 3.5.xブランチを取得
    git branch 3.5.x origin/3.5.x && \
    # 3.5.xブランチをmasterにマージ
    git merge 3.5.x && \
    # python3用に、masterブランチのうちの一部ファイルを3.5.xブランチの状態へ戻す => OK
    git checkout 2d013b353631feb7ffe29fe9946327a08fe55381 scripts/mod_python.in && \
    git checkout 54d42b1810d5abfffe5b407d8e763402a0d4b2f0 configure.in && \
    git checkout 9d223c39a8f27345d8c046d026468fc0c1ad5d53 configure && \
    git checkout 51f7b0366d66b58b5b82874fe8a2db346f77d7fc src/include/mod_python.h && \
    git checkout 51f7b0366d66b58b5b82874fe8a2db346f77d7fc src/include/mod_python.h.in && \
    git checkout affc26bd69b3f5e67e8233166ff0d40c43fb7302 src/mod_python.c && \
    # ビルド
    ./configure --with-apxs=/usr/local/apache2/bin/apxs --with-python=/usr/bin/python3 --with-flex=/usr/bin/flex && \
...

です。

 
docker image build & docker container runします。

$ docker image build -t thinkami:python3_httpd24_mod_python_4 .

$ docker container run -p 8081:80 --name mod_python_4 -v `pwd`/htdocs/:/usr/local/apache2/htdocs thinkami:python3_httpd24_mod_python_4
[Fri Jun 09 03:56:17.740963 2017] [:notice] [pid 1] mod_python: Creating 8 session mutexes based on 256 max processes and 0 max threads.
[Fri Jun 09 03:56:17.741173 2017] [:notice] [pid 1] mod_python: using mutex_directory /tmp 
[Fri Jun 09 03:56:17.959878 2017] [mpm_prefork:notice] [pid 1] AH00163: Apache/2.4.25 (Unix) mod_python/3.5.0-2b3da9d Python/3.6.1 configured -- resuming normal operations
[Fri Jun 09 03:56:17.960025 2017] [core:notice] [pid 1] AH00094: Command line: 'httpd -D FOREGROUND'

 
curlで動作確認をします。

$ curl localhost:8081/test/mptest.py -D - -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /test/mptest.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Fri, 09 Jun 2017 03:56:34 GMT
Date: Fri, 09 Jun 2017 03:56:34 GMT
< Server: Apache/2.4.25 (Unix) mod_python/3.5.0-2b3da9d Python/3.6.1
Server: Apache/2.4.25 (Unix) mod_python/3.5.0-2b3da9d Python/3.6.1
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Content-Type: text/plain
Content-Type: text/plain

< 
* Connection #0 to host localhost left intact
Hello World!

Hello Worldが表示されました。

念のため、Dockerのコンソールを見ると

[Fri Jun 09 03:56:35.416889 2017] [:notice] [pid 6] mod_python: (Re)importing module 'mptest'
172.17.0.1 - - [09/Jun/2017:03:56:34 +0000] "GET /test/mptest.py HTTP/1.1" 200 12

と、無事に動いているようです。

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Docker_Apache-sample

関係するディレクトリは以下の通りです。

  • alpine_apache_python3x_mod_python
    • 今回試したソースコード一式
    • executable_py36_all_in_oneは、Alpine3.5のDockerイメージから、Apache2.4(prefork) + EgeのPython3.x + mod_python が動作する環境を作るDockerfile
  • alpine_apache_prefork
    • Apache2.4をprefork MPMでビルドするDockerfile

*1:前回の記事にある通りこのコミットは自分のプルリクのものですが、動作しない原因は自分のコミットではありません、たぶん

初めてのOSSへのプルリクとマージ

会社のまわりの人たちが普通にプルリクを送ってるのを見ていいなーと思いつつ、プルリク未体験で過ごしてきました。

そんな中、ようやく初めてのプルリクとマージを体験できたので、メモしておきます。

 
目次

 

作成したプルリク

mod_pythonをPython3で使うとSyntaxErrorが出るのを修正するプルリクです。

github.com

 

見つけた経緯

前回、Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonソースコードからインストールしてみました。
Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonをソースコードからインストールしてみた - メモ的な思考的な

 
では、次はPython3で動かしてみようと思い、色々やってソースコードからビルドしたところ、

byte-compiling /usr/lib/python3.5/site-packages/mod_python/psp.py to psp.cpython-35.pyc
  File "/usr/lib/python3.5/site-packages/mod_python/psp.py", line 274
    raise et, ev, etb
            ^
SyntaxError: invalid syntax

byte-compiling /usr/lib/python3.5/site-packages/mod_python/python22.py to python22.cpython-35.pyc
byte-compiling /usr/lib/python3.5/site-packages/mod_python/version.py to version.cpython-35.pyc
byte-compiling /usr/lib/python3.5/site-packages/mod_python/apache.py to apache.cpython-35.pyc
writing byte-compilation script '/tmp/tmpu5k2chgw.py'
/usr/bin/python3 -OO /tmp/tmpu5k2chgw.py
  File "/usr/lib/python3.5/site-packages/mod_python/psp.py", line 274
    raise et, ev, etb
            ^
SyntaxError: invalid syntax

removing /tmp/tmpu5k2chgw.py

と、ログにSyntaxErrorが表示されました。

エラーにならず最後までいったものの、気になったのでソースコードを見たところ、

# lib/python/mod_python/psp.pyの270行目くらい
if PY2:
    raise et, ev, etb
else:
    raise et(ev).with_traceback(etb)

と、Python2系でしか使えないシンタックス raise et, ev, etbがありました。
Cheat Sheet: Writing Python 2-3 compatible code — Python-Future documentation

これが原因のようです。

 

プルリクの作成

以下を参考にプルリクを作成してみました。

 
流れは以下の通りです。

  • mod_pythonリポジトリで、既存のIssueやpull requestを読んで雰囲気をつかむ & 良さそうな表現を収集
  • mod_pythonリポジトリで、Issueを作成
    • この時に採番されたIssue番号をメモっておく
  • Forkボタンで自分のリポジトリにfork
  • ローカルにgit clone
  • ローカルでブランチを作成してcommit
  • 自分のリポジトリにブランチをpush
  • 自分のリポジトリにpull requestボタンが出るので押す
  • 頑張って英語でプルリクを作成*1
  • お返事を待つ

 
マージ or リジェクトまでどれくらいの時間がかかるのだろうと思っていたところ、1時間くらいでお返事が来ました。早い。

しかし、追加の説明が必要そうなお返事でした。descriptionで手を抜きすぎました。

そのため、必死に他のプルリクを参考に良さそうな英語表現を集めて返信したところ、無事にマージされました。

とても嬉しかったです。

 
あとはIssueを閉じて完了でした。

 

GitHub上のforkしたリポジトリmod_python本家に追従

せっかくなので、mod_pythonリポジトリの内容を、自分のForkしたリポジトリへと反映します。

# 本家をupstreamとして登録
$ git remote add upstream https://github.com/grisha/mod_python.git

# 本家にfetdh
$ git fetch upstream
remote: Counting objects: 1, done.
remote: Total 1 (delta 0), reused 1 (delta 0), pack-reused 0
Unpacking objects: 100% (1/1), done.
From https://github.com/grisha/mod_python
...
 * [new branch]      3.5.x                               -> upstream/3.5.x
...
 * [new branch]      master                              -> upstream/master
...

# 本家がリポジトリにいることを確認
$ git branch -a
* master
  py3-syntax-error-in-psppy
...
  remotes/origin/3.5.x
  remotes/origin/HEAD -> origin/master
...
  remotes/origin/master
...
  remotes/upstream/3.5.x
...
  remotes/upstream/master
...

# マージ
$ git merge upstream/master
Updating 1edbde6..8acf1b7
Fast-forward
 lib/python/mod_python/psp.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

# その後自分のリポジトリへpush

参考:GitHubでFork/cloneしたリポジトリを本家リポジトリに追従する - Qiita

 
その結果、自分のForkしたリポジトリThis branch is even with grisha:master. と表示されていることを確認します。

 

その他

プルリクの中でコミッターのかたに教わったPython2とPython3で互換性のある書き方をする方法のWikiが良かったので、ここでもリンクをはっておきます。python.orgにあるのが安心感があっていいです。
PortingToPy3k/BilingualQuickRef - Python Wiki

*1:コードで分かり合える世界だと、複雑な英語を要求されなくて済むのがありがたいです

Docker + Alpine3.5 + Apache2.4 + Python2.7で、mod_pythonをソースコードからインストールしてみた

今までPython3.x & Apache2.4.xを使って、CGIをひと通り試してきました。

次は、CGIを別のものに置き換えてWebアプリケーションを作りたくなりました。

PythonのWebアプリケーションというとWSGIが思い浮かびます。ただ、CGI以降WSGI以前に登場したものを使ってみたくなりました。

調べてみると、

がありました。

 
PyApacheは、

Runs with Python1.X/2.X under Apache 1.3.X/2.0 (configurable).

[Sunday 18th. January 2004]

PyApache module

との記載があり、現在では使えそうにありませんでした。

 
mod_snakeも

I no longer have the time or motivation to work on mod_snake, so instead of letting it sit here, I have decided to kill this project.

RIP - Mod Snake

Jon Travis Last modified: Thu May 9 19:00:01 PDT 2002

RIP - Mod Snake

とあり、プロジェクトが終了していました。

 
mod_python

などの記事から終了したのかなと思いました。

ただ、その後、

にある強い想いとともに復活したようです。

今年に入ってからもGitHub上でcommitがあり、最新版であるmod_python 3.5の日本語ドキュメントも存在することからも、プロジェクトとして動いているようでした。

また、Python3対応も

The latest release is 3.5.0. Key feature of mod_python 3.5.0 is Python 3 support.

mod_python - Apache / Python Integration

とあり、一応対応できているようでした。

 
そこで今回、以下を参考に、Python2系でmod_pythonをインストールし、Hello worldしてみます。
インストール — Mod_python 3.5.0-1330047 ドキュメント

 
目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac12
  • Alpine3.5 + Apache2.4.25 + Python 2.7.13
  • mod_python
    • GitHub上の最新版
    • 今回利用したコミットは8acf1b7

 

調査

Alpine Linuxのパッケージにはmod_pythonが無い

Docker + Alpine Linuxを使うため、Alpineのパッケージでmod_pythonを調べました。しかし、mod_pythonは無いようでした。
*python*での検索結果 | Alpine packages

 
Alpineでmod_pythonを使うには、自分でソースコードからインストールする必要がありそうでした。

 

mod_pythonコンパイル・インストールするために必要なパッケージについて

ドキュメントのインストールに記載がありました。
インストール — Mod_python 3.5.0-1330047 ドキュメント

Alpine Linuxの場合、以下のパッケージを用意すれば良さそうでした。

 

Dockerで配布しているApacheの状態について

今回はDockerで配布しているApache(2.4/alpine/Dockerfile)を使うことにします。
httpd - Docker Store

 
どんな状態のApacheなのかを調べてみます。

# Dockerコンテナを起動
$ docker container run -p 8081:80 --name httpd24 httpd:2.4.25-alpine

# 別のコンソールから、Apacheの状態を確認
$ docker container exec -it httpd24 httpd -V
Server version: Apache/2.4.25 (Unix)
Server built:   Mar  3 2017 21:57:00
Server's Module Magic Number: 20120211:67
Server loaded:  APR 1.5.2, APR-UTIL 1.5.4
Compiled using: APR 1.5.2, APR-UTIL 1.5.4
Architecture:   64-bit
Server MPM:     event
  threaded:     yes (fixed thread count)
    forked:     yes (variable process count)
Server compiled with....
 -D APR_HAS_SENDFILE
 -D APR_HAS_MMAP
 -D APR_HAVE_IPV6 (IPv4-mapped addresses enabled)
 -D APR_USE_SYSVSEM_SERIALIZE
 -D APR_USE_PTHREAD_SERIALIZE
 -D SINGLE_LISTEN_UNSERIALIZED_ACCEPT
 -D APR_HAS_OTHER_CHILD
 -D AP_HAVE_RELIABLE_PIPED_LOGS
 -D DYNAMIC_MODULE_LIMIT=256
 -D HTTPD_ROOT="/usr/local/apache2"
 -D SUEXEC_BIN="/usr/local/apache2/bin/suexec"
 -D DEFAULT_PIDLOG="logs/httpd.pid"
 -D DEFAULT_SCOREBOARD="logs/apache_runtime_status"
 -D DEFAULT_ERRORLOG="logs/error_log"
 -D AP_TYPES_CONFIG_FILE="conf/mime.types"
 -D SERVER_CONFIG_FILE="conf/httpd.conf"

Server MPM: eventのため、event MPMで動いているようです。

ただ、mod_pythonに関する記事ではpreforkで動いているのを目にするのが多かったため、今回はeventからpreforkへMPMを切り替えることにします。

 
Apache2.2まではコンパイル時にMPMを指定する必要がありましたが、Apache2.4からはLoadModuleだけでMPMが切り替えられそうでした。
MPMが普通のモジュールになった | Apache2.2の設定ファイルをApache2.4に移植するためにやったことまとめ - Qiita

 
そのため、デフォルトでインストールされるモジュールを見てみたところ、

# modulesディレクトリには、mpmモジュールが存在しない
$ docker container exec -it httpd24 pwd
/usr/local/apache2

$ docker container exec -it httpd24 ls -al modules/
...
-rwxr-xr-x    1 root     root        177416 Mar  3 21:57 mod_lua.so
-rwxr-xr-x    1 root     root         23488 Mar  3 21:57 mod_macro.so
-rwxr-xr-x    1 root     root         27920 Mar  3 21:57 mod_mime.so
-rwxr-xr-x    1 root     root         37208 Mar  3 21:57 mod_mime_magic.so
-rwxr-xr-x    1 root     root         46872 Mar  3 21:57 mod_negotiation.so
...

# コンパイル時のモジュールを確認
$ docker container exec -it httpd24 httpd -l
Compiled in modules:
  core.c
  mod_so.c
  http_core.c
  event.c

# ロードされているモジュールを確認
$ docker container exec -it httpd24 httpd -M
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
Loaded Modules:
 core_module (static)
 so_module (static)
 http_module (static)
 mpm_event_module (static)
 authn_file_module (shared)
 authn_core_module (shared)
 authz_host_module (shared)
 authz_groupfile_module (shared)
 authz_user_module (shared)
 authz_core_module (shared)
 access_compat_module (shared)
 auth_basic_module (shared)
 reqtimeout_module (shared)
 filter_module (shared)
 mime_module (shared)
 log_config_module (shared)
 env_module (shared)
 headers_module (shared)
 setenvif_module (shared)
 version_module (shared)
 unixd_module (shared)
 status_module (shared)
 autoindex_module (shared)
 dir_module (shared)
 alias_module (shared)

と、デフォルトではMPM系のモジュールが無いようです。

これより、公式のDockerイメージではprefork MPMで動かすのが難しいと考えました。

 
そのため、公式のDockerfileを一部修正し、Apacheソースコードからインストールする方針とします。

修正部分は、./configureする時のオプションにおける

  • --with-mpm=preforkで、デフォルトのMPMをpreforkにする
  • --enable-mpms-shared=allで、MPM系モジュールを生成する

の2点です。

 

準備

Dockerfile

今回用意するDockerfileは

です。

#--------------------------------------------------
# Apache2.4のインストール:公式のDockerfileを流用
#--------------------------------------------------
FROM alpine:3.5

# ensure www-data user exists
RUN set -x \
  && addgroup -g 82 -S www-data \
  && adduser -u 82 -D -S -G www-data www-data
# 82 is the standard uid/gid for "www-data" in Alpine
# http://git.alpinelinux.org/cgit/aports/tree/main/apache2/apache2.pre-install?h=v3.3.2
# http://git.alpinelinux.org/cgit/aports/tree/main/lighttpd/lighttpd.pre-install?h=v3.3.2
# http://git.alpinelinux.org/cgit/aports/tree/main/nginx-initscripts/nginx-initscripts.pre-install?h=v3.3.2

ENV HTTPD_PREFIX /usr/local/apache2
ENV PATH $HTTPD_PREFIX/bin:$PATH
RUN mkdir -p "$HTTPD_PREFIX" \
  && chown www-data:www-data "$HTTPD_PREFIX"
WORKDIR $HTTPD_PREFIX

ENV HTTPD_VERSION 2.4.25
ENV HTTPD_SHA1 bd6d138c31c109297da2346c6e7b93b9283993d2

# https://issues.apache.org/jira/browse/INFRA-8753?focusedCommentId=14735394#comment-14735394
ENV HTTPD_BZ2_URL https://www.apache.org/dyn/closer.cgi?action=download&filename=httpd/httpd-$HTTPD_VERSION.tar.bz2
# not all the mirrors actually carry the .asc files :'(
ENV HTTPD_ASC_URL https://www.apache.org/dist/httpd/httpd-$HTTPD_VERSION.tar.bz2.asc

# see https://httpd.apache.org/docs/2.4/install.html#requirements
RUN set -x \
  && runDeps=' \
    apr-dev \
    apr-util-dev \
    perl \
  ' \
  && apk add --no-cache --virtual .build-deps \
    $runDeps \
    ca-certificates \
    coreutils \
    dpkg-dev dpkg \
    gcc \
    gnupg \
    libc-dev \
    # mod_session_crypto
    libressl \
    libressl-dev \
    # mod_proxy_html mod_xml2enc
    libxml2-dev \
    # mod_lua
    lua-dev \
    make \
    # mod_http2
    nghttp2-dev \
    pcre-dev \
    tar \
    # mod_deflate
    zlib-dev \
  \
  && wget -O httpd.tar.bz2 "$HTTPD_BZ2_URL" \
  && echo "$HTTPD_SHA1 *httpd.tar.bz2" | sha1sum -c - \
# see https://httpd.apache.org/download.cgi#verify
  && wget -O httpd.tar.bz2.asc "$HTTPD_ASC_URL" \
  && export GNUPGHOME="$(mktemp -d)" \
  && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \
  && gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \
  && rm -r "$GNUPGHOME" httpd.tar.bz2.asc \
  \
  && mkdir -p src \
  && tar -xf httpd.tar.bz2 -C src --strip-components=1 \
  && rm httpd.tar.bz2 \
  && cd src \
  \
  && gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" \
  && ./configure \
    --build="$gnuArch" \
    --prefix="$HTTPD_PREFIX" \
    --enable-mods-shared=reallyall \
    # デフォルトはpreforkにする
    --with-mpm=prefork \
    # ただし、他のMPMもビルドする:この指定がない場合、MPMのmoduleがないので切り替えられない
    # http://tt4cs.blogspot.jp/2013/01/how-to-enable-dso-for-apache-2.4.html
    --enable-mpms-shared=all \
  && make -j "$(nproc)" \
  && make install \
  \
  && cd .. \
  && rm -r src man manual \
  \
  && sed -ri \
    -e 's!^(\s*CustomLog)\s+\S+!\1 /proc/self/fd/1!g' \
    -e 's!^(\s*ErrorLog)\s+\S+!\1 /proc/self/fd/2!g' \
    "$HTTPD_PREFIX/conf/httpd.conf" \
  \
  && runDeps="$runDeps $( \
    scanelf --needed --nobanner --recursive /usr/local \
      | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
      | sort -u \
      | xargs -r apk info --installed \
      | sort -u \
  )" \
  && apk add --virtual .httpd-rundeps $runDeps \
  && apk del .build-deps

# ローカルでchmod 755しておく
COPY httpd-foreground /usr/local/bin/

EXPOSE 80
CMD ["httpd-foreground"]


#--------------------------------------------------
# mod_pythonのインストール
#--------------------------------------------------
RUN apk --update --no-cache add python && \
    # mod_pythonで必要なパッケージを追加
    apk add --no-cache --virtual .mod_python_build_libs git && \
    apk add --no-cache --virtual .mod_python_build_libs python-dev && \
    apk add --no-cache --virtual .mod_python_build_libs apache2-dev && \
    apk add --no-cache --virtual .mod_python_build_libs flex && \
    # ./configureで必要
    apk add --no-cache --virtual .mod_python_build_libs build-base && \
    # sudo make installで必要
    apk add --no-cache --virtual .mod_python_build_libs sudo && \
    # GitHubからソースコードを持ってきてインストール
    cd /tmp && \
    mkdir mod_python && \
    cd mod_python && \
    git clone https://github.com/grisha/mod_python.git . && \
    ./configure --with-apxs=/usr/local/apache2/bin/apxs --with-python=/usr/bin/python --with-flex=/usr/bin/flex && \
    make && \
    sudo make install && \
    # 不要なパッケージやソースコードを一括削除
    apk del .mod_python_build_libs && \
    rm -r /tmp/mod_python


#--------------------------------------------------
# Apacheの設定
#--------------------------------------------------
# ローカルのhttpd.confをコピー
COPY httpd.conf /usr/local/apache2/conf/

 

httpd.conf

CGIで使ったものを流用します。

ただし、

  • CGI関係の記述を削除
  • MPMモジュールの指定を追加
    • LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
  • mod_python向けの記述を追加
    • LoadModule python_module modules/mod_python.so
  • mod_pythonのWebアプリを/usr/local/apache2/htdocs/testに配置するための記述を追加
    • <Directory "/usr/local/apache2/htdocs/test">ディレクティブ以下

を変更します。

# ServerRoot: The top of the directory tree
ServerRoot "/usr/local/apache2"

ServerName localhost
# Listen: Allows you to bind Apache to specific IP addresses and/or ports
Listen 80

# LoadModule
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule mime_module modules/mod_mime.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule env_module modules/mod_env.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule unixd_module modules/mod_unixd.so
LoadModule status_module modules/mod_status.so

# for mod_python
LoadModule python_module modules/mod_python.so

# for running prefork MPM
LoadModule mpm_prefork_module modules/mod_mpm_prefork.so


# unixd_module settings
User daemon
Group daemon

# 'Main' server configuration

# ServerAdmin: Your address, where problems with the server should be e-mailed.
ServerAdmin you@example.com

# Deny access to the entirety of your server's filesystem.
<Directory />
    AllowOverride none
    Require all denied
</Directory>

# DocumentRoot
DocumentRoot "/usr/local/apache2/htdocs"
<Directory "/usr/local/apache2/htdocs">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

<Directory "/usr/local/apache2/htdocs/test">
    AddHandler mod_python .py
    PythonHandler mptest
    PythonDebug On
</Directory>

# The following lines prevent .htaccess and .htpasswd files
<Files ".ht*">
    Require all denied
</Files>

# Log settings
ErrorLog /proc/self/fd/2
LogLevel warn

# log_config_module settings
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog /proc/self/fd/1 common

# mime_module settings
TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz

 

mod_pythonを使ったWebアプリ

チュートリアルにあったテストファイルを、ホストのpath/to/htdocs/testの中にmptest.pyとして用意します。

#!/usr/bin/python
# coding: utf-8
from mod_python import apache

def handler(req):
    req.content_type = 'text/plain'
    req.write("Hello World!")
    return apache.OK

Dockerコンテナ起動時に、このファイルをホストとDockerコンテナとで共有します。  
なお、shebangの値は、以下で調べたものを使いました。

$ docker container exec -it mod_python which python
/usr/bin/python

 

起動と動作確認

Dockerコンテナの起動

Dockerコンテナのイメージを作成し、起動します。

# Dockerコンテナイメージを作成
$ docker image build -t alpine:python27_httpd24_mod_python .

# Dockerコンテナを起動し、ホストのhtdocs以下をコンテナと共有
$ docker container run -p 8081:80 --name mod_python -v `pwd`/htdocs/:/usr/local/apache2/htdocs alpine:python27_httpd24_mod_python

 

Apacheの状況確認
# modulesディレクトリには、mpmモジュールが存在する
$ docker container exec -it mod_python pwd
/usr/local/apache2

$ docker container exec -it mod_python ls -al modules/
...
-rwxr-xr-x    1 root     root         37208 Jun  1 09:49 mod_mime_magic.so
-rwxr-xr-x    1 root     root         91720 Jun  1 09:49 mod_mpm_event.so
-rwxr-xr-x    1 root     root         48408 Jun  1 09:49 mod_mpm_prefork.so
-rwxr-xr-x    1 root     root         67720 Jun  1 09:49 mod_mpm_worker.so
...

# コンパイル時のモジュールを確認
$ docker container exec -it mod_python httpd -l
Compiled in modules:
  core.c
  mod_so.c
  http_core.c

# ロードされているモジュールを確認
$ docker container exec -it mod_python httpd -M
Loaded Modules:
 core_module (static)
 so_module (static)
 http_module (static)
 authz_core_module (shared)
 authz_host_module (shared)
 mime_module (shared)
 log_config_module (shared)
 env_module (shared)
 setenvif_module (shared)
 unixd_module (shared)
 status_module (shared)
 python_module (shared)
 mpm_prefork_module (shared)

# Vオプションで状況確認
$ docker container exec -it mod_python httpd -V
Server version: Apache/2.4.25 (Unix)
Server built:   Jun  1 2017 09:48:37
Server's Module Magic Number: 20120211:67
Server loaded:  APR 1.5.2, APR-UTIL 1.5.4
Compiled using: APR 1.5.2, APR-UTIL 1.5.4
Architecture:   64-bit
Server MPM:     prefork
  threaded:     no
    forked:     yes (variable process count)
Server compiled with....
 -D APR_HAS_SENDFILE
 -D APR_HAS_MMAP
 -D APR_HAVE_IPV6 (IPv4-mapped addresses enabled)
 -D APR_USE_SYSVSEM_SERIALIZE
 -D APR_USE_PTHREAD_SERIALIZE
 -D SINGLE_LISTEN_UNSERIALIZED_ACCEPT
 -D APR_HAS_OTHER_CHILD
 -D AP_HAVE_RELIABLE_PIPED_LOGS
 -D DYNAMIC_MODULE_LIMIT=256
 -D HTTPD_ROOT="/usr/local/apache2"
 -D SUEXEC_BIN="/usr/local/apache2/bin/suexec"
 -D DEFAULT_PIDLOG="logs/httpd.pid"
 -D DEFAULT_SCOREBOARD="logs/apache_runtime_status"
 -D DEFAULT_ERRORLOG="logs/error_log"
 -D AP_TYPES_CONFIG_FILE="conf/mime.types"
 -D SERVER_CONFIG_FILE="conf/httpd.conf"

mod_pythonがあり、prefork MPMで動作しているようです。

 

Webアプリの動作確認

curlを使って動作確認をします。

$ curl localhost:8081/test/mptest.py -D -
HTTP/1.1 200 OK
Date: Thu, 01 Jun 2017 10:38:23 GMT
Server: Apache/2.4.25 (Unix) mod_python/3.5.0-8acf1b7 Python/2.7.13
Transfer-Encoding: chunked
Content-Type: text/plain

Hello World!

動作しているようです。

 
ただ、コンテナを起動しているコンソールでは

[Thu Jun 01 10:39:29.700391 2017] [:notice] [pid 9] mod_python: (Re)importing module 'mptest'
172.17.0.1 - - [01/Jun/2017:10:39:29 +0000] "GET /test/mptest.py HTTP/1.1" 200 12
[Thu Jun 01 10:39:29.712520 2017] [:error] [pid 9] make_obcallback: could not import mod_python.apache.\n
ImportError: No module named mod_python.apache
[Thu Jun 01 10:39:29.712558 2017] [:error] [pid 9] make_obcallback: Python path being used "['/usr/lib/python27.zip', '/usr/lib/python2.7', '/usr/lib/python2.7/plat-linux2', '/usr/lib/python2.7/lib-tk', '/usr/lib/python2.7/lib-old', '/usr/lib/python2.7/lib-dynload']".
[Thu Jun 01 10:39:29.712566 2017] [:error] [pid 9] get_interpreter: no interpreter callback found.

と表示されていますが、現段階では気にしないことにします。

 
なお、DockerでApacheの再起動を普通にすると、

...
[Thu Jun 01 11:13:46.312764 2017] [core:notice] [pid 1] AH00052: child pid 129 exit signal Segmentation fault (11)
[Thu Jun 01 11:13:46.312770 2017] [core:error] [pid 1] AH00546: no record of generation 0 of exiting child 129
〜以降繰り返し〜
...

のようにSegmentation faultします。

 

ソースコード

GitHubに上げました。alpine_apache_python27_mod_pythonディレクトリの中が今回のものです。
thinkAmi-sandbox/Docker_Apache-sample

Pythonで、日本語をCookie値へ設定する方法を調べてみた

以前、PythonCGICookieを使ってみました。
Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのCookieを使ってみた - メモ的な思考的な

その時は日本語をCookie値へ設定する方法が気になっていたものの、深入りしませんでした。

 
そこで今回、以下を参考に、日本語をCookie値へ設定する方法について調べたため、メモします。

 
なお、内容に誤りがあった場合にはご指摘いただけるとありがたいです。

目次

 

環境

 

まとめ

長いので最初にまとめます。

 

Cookie仕様のRFC6265について

現在のCookieの仕様はRFC6265とのことです。

 
その 4.1.1. SyntaxにCookie値の仕様が記載されていました。
https://tools.ietf.org/html/rfc6265#section-4.1.1

cookie-pair       = cookie-name "=" cookie-value
cookie-name       = token
cookie-value      = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
cookie-octet      = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
                       ; US-ASCII characters excluding CTLs,
                       ; whitespace DQUOTE, comma, semicolon,
                       ; and backslash

cookie-octetを見ると、日本語をそのまま設定するのはダメそうです。

また、cookie-octetで使える文字は書いてあるものの、日本語などの使えない文字に対する符号化方式は書かれていないようです。

 
そのため、試しに、Pythonhttp.cookies.SimpleCookieを使いSet-Cookieヘッダを作成してみます。

try_python_SimpleCookie.py

from http.cookies import SimpleCookie
import binascii

# RFC6265 4.1.1.Syntax のcookie-octetより
# https://tools.ietf.org/html/rfc6265#section-4.1.1
ENABLE_OCTET = {
    'x21': '!',
    'x23': '#',
    'x24': '$',
    'x25': '%',
    'x26': '&',
    'x27': "'",
    'x28': '(',
    'x29': ')',
    'x2a': '*',
    'x2b': '+',
    'x2d': '-',
    'x2e': '.',
    'x2f': '/',
    # x30 ~ x39は0-9なので省略
    'x3a': ':',
    'x3c': '<',
    'x3d': '=',
    'x3e': '>',
    'x3f': '?',
    'x40': '@',
    # x41 ~ x5aはA-Zなので省略
    'x5b': '[',
    'x5d': ']',
    'x5e': '^',
    'x5f': '_',
    'x60': '`',
    # x61 ~ x7aはa-zなので省略
    'x7b': '{',
    'x7c': '|',
    'x7d': '}',
    'x7e': '~',
}

DISABLE_OCTET = {
    'x09': '\t', # 水平タブ
    'x0a': '\n', # LF(改行)
    'x0d': '\r', # CR
    'x22': '"',  # ダブルクォート
    'x2c': ',',  # カンマ
    'x3b': ';',  # セミコロン
    'x5c': '\\',  # バックスラッシュ
    'whitespace': '\x20'  # 半角スペース
}

STRINGS = ['a', 'あ', 'a,a', 'a;a', 'a a', 'a あ',]


def print_cookie_header_from_list(characters):
    for c in characters:
        try:
            cookie = SimpleCookie(f'key={c}')
            print(f'{c}: {type(cookie)} -> {cookie}')
        except Exception as ex_info:
            print(f'{c}: {ex_info}')


def print_cookie_header_from_dict(char_dict):
    for key, value in char_dict.items():
        # http://qiita.com/atsaki/items/6120cad2e3c448d774bf
        h = binascii.hexlify(value.encode('utf-8'))
        try:
            cookie = SimpleCookie(f'{key}={value}')
            print(f'{value} ({key}), hex:{h}, {type(cookie)} -> {cookie}')
        except Exception as ex_info:
            print(f'{key}: {ex_info}')


if __name__ == '__main__':
    print_cookie_header_from_dict(ENABLE_OCTET)
    print('-'*20)
    print_cookie_header_from_dict(DISABLE_OCTET)
    print('-'*20)
    print_cookie_header_from_list(STRINGS)

 
実行結果は以下の通りです。

$ python try_cookie_value.py
! (x21), hex:b'21', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x21=!
# (x23), hex:b'23', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x23=#
$ (x24), hex:b'24', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x24=$
% (x25), hex:b'25', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x25=%
& (x26), hex:b'26', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x26=&
' (x27), hex:b'27', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x27='
( (x28), hex:b'28', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x28=(
) (x29), hex:b'29', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x29=)
* (x2a), hex:b'2a', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2a=*
+ (x2b), hex:b'2b', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2b=+
- (x2d), hex:b'2d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2d=-
. (x2e), hex:b'2e', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2e=.
/ (x2f), hex:b'2f', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2f=/
: (x3a), hex:b'3a', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3a=:
< (x3c), hex:b'3c', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3c=<
= (x3d), hex:b'3d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3d==
> (x3e), hex:b'3e', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3e=>
? (x3f), hex:b'3f', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3f=?
@ (x40), hex:b'40', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x40=@
[ (x5b), hex:b'5b', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x5b=[
] (x5d), hex:b'5d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x5d=]
^ (x5e), hex:b'5e', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x5e=^
_ (x5f), hex:b'5f', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x5f=_
` (x60), hex:b'60', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x60=`
{ (x7b), hex:b'7b', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x7b={
| (x7c), hex:b'7c', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x7c=|
} (x7d), hex:b'7d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x7d=}
~ (x7e), hex:b'7e', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x7e=~
--------------------
     (x09), hex:b'09', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x09=

 (x0a), hex:b'0a', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x0a=
 (x0d), hex:b'0d', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x0d=
" (x22), hex:b'22', <class 'http.cookies.SimpleCookie'> -> 
, (x2c), hex:b'2c', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x2c=,
; (x3b), hex:b'3b', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: x3b=
\ (x5c), hex:b'5c', <class 'http.cookies.SimpleCookie'> -> 
  (whitespace), hex:b'20', <class 'http.cookies.SimpleCookie'> -> Set-Cookie: whitespace=
--------------------
a: <class 'http.cookies.SimpleCookie'> -> Set-Cookie: key=a
あ: <class 'http.cookies.SimpleCookie'> -> 
a,a: <class 'http.cookies.SimpleCookie'> -> Set-Cookie: key=a,a
a;a: <class 'http.cookies.SimpleCookie'> -> 
a a: <class 'http.cookies.SimpleCookie'> -> 
a あ: <class 'http.cookies.SimpleCookie'> -> Set-Cookie: key=a

 
例外は発生していませんが、http.cookies.SimpleCookieでは、cookie-octet以外の文字を使うとSet-Cookieヘッダが正しく作成されないようです。

 

cookie-octet以外の文字の符号化方式を調べてみると、よく使われているのはURLエンコードでした。

URLエンコードについて調べると、2種類の方法がありました。

 
そこで、両方の符号化方式を調べてみました。

 

RFC3986に基づくURLエンコード(パーセントエンコード)について

RFC3986にて、使用可能な文字が記載されています。

また、RFC3986とその前のRFC2396における、使用可能な文字の比較などは以下が参考になりました。

 
RFC3986より、予約されていない文字(=使用可能な文字)は

2.3.  Unreserved Characters

   Characters that are allowed in a URI but do not have a reserved
   purpose are called unreserved.  These include uppercase and lowercase
   letters, decimal digits, hyphen, period, underscore, and tilde.

      unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"

とのことです。

 
そのため、

  • -
  • .
  • _
  • ~

以外がパーセントエンコードされれば良さそうです。

 
PythonにはURLエンコードする関数として

  • urllib.parse.quote()
  • urllib.parse.quote_plus()

の2つがあります。

そこで、引数はデフォルトのまま両方の関数を試してみます。

try_url_encode.py

from urllib.parse import quote, quote_plus

WHITE_SPACE = '\x20'  # 半角スペース
ALPHABET = 'aA'
DIGIT = '0'
JAPANESE = 'あ'

RFC3986_RESERVED = (
    ":/?#[]@"  # gen-delims
    "!$&'()"   # sub-delims 1行目
    "*+,;="    # sub-delims 2行目
)
RFC3986_UNRESERVED = '-._~'


def main(func):
    print(func.__name__)
    print('-'*20)
    print('[WHITE_SPACE]')
    percent_encode(func, WHITE_SPACE)
    print('[ALPHABET]')
    percent_encode(func, ALPHABET)
    print('[DIGIT]')
    percent_encode(func, DIGIT)
    print('[JAPANESE]')
    percent_encode(func, JAPANESE)

    print('[RFC3986_RESERVED]')
    percent_encode(func, RFC3986_RESERVED)
    print('[RFC3986_UNRESERVED]')
    percent_encode(func, RFC3986_UNRESERVED)

def percent_encode(func, characters):
    for c in characters:
        print(f'{c}: {func(c)}')


if __name__ == '__main__':
    # quote()やquote_plus()の挙動確認
    print('-'*20)
    main(quote)
    print('-'*20)
    main(quote_plus)

 
urllib.parse.quote()の実行結果は  

--------------------
quote
--------------------
[WHITE_SPACE]
 : %20
[ALPHABET]
a: a
A: A
[DIGIT]
0: 0
[JAPANESE]
あ: %E3%81%82
[RFC3986_RESERVED]
:: %3A
/: /
?: %3F
#: %23
[: %5B
]: %5D
@: %40
!: %21
$: %24
&: %26
': %27
(: %28
): %29
*: %2A
+: %2B
,: %2C
;: %3B
=: %3D
[RFC3986_UNRESERVED]
-: -
.: .
_: _
~: %7E

となり、以下がRFC3986に準拠していません。

  • チルダ(~)が変換されてしまっている
  • スラッシュ(/)が変換されていない

 
一方、urllib.parse.quote_plus()の実行結果は、

--------------------
quote_plus
--------------------
[WHITE_SPACE]
 : +
[ALPHABET]
a: a
A: A
[DIGIT]
0: 0
[JAPANESE]
あ: %E3%81%82
[RFC3986_RESERVED]
:: %3A
/: %2F
?: %3F
#: %23
[: %5B
]: %5D
@: %40
!: %21
$: %24
&: %26
': %27
(: %28
): %29
*: %2A
+: %2B
,: %2C
;: %3B
=: %3D
[RFC3986_UNRESERVED]
-: -
.: .
_: _
~: %7E

となり、以下がRFC3986に準拠していません。

  • チルダ(~)が変換されてしまっている
  • 半角スペースが、RFC3986の予約語である+になっている

ただ、半角スペースの挙動は、urllib.parse.quote_plus()の公式ドキュメント通りです。

quote() と似ていますが、クエリ文字列を URL に挿入する時のために HTML フォームの値の空白をプラス記号「+」に置き換えます。オリジナルの文字列に「+」が存在した場合は safe に指定されている場合を除きエスケープされます。safe にデフォルト値は設定されていません。

urllib.parse.quote_plus() | 21.8. urllib.parse — URL を解析して構成要素にする — Python 3.6.1 ドキュメント

 
以上より、Pythonを使ってRFC3986に準拠するためには、urllib.parse.quote()の引数safeにチルダ(~)を設定すれば良さそうです。

try_url_encode.py

# 以下の関数を追加
def quote_rfc3986_strictly(character):
    # 半角スペースと/は%エンコードするが、チルダはそのまま
    return quote(character, safe='~')


if __name__ == '__main__':
    ...
    # 以下を追加
    main(quote_rfc3986_strictly)

 
結果です。スラッシュとチルダが仕様通りになりました。

--------------------
quote_rfc3986_strictly
--------------------
[WHITE_SPACE]
 : %20
[ALPHABET]
a: a
A: A
[DIGIT]
0: 0
[JAPANESE]
あ: %E3%81%82
[RFC3986_RESERVED]
:: %3A
/: %2F
?: %3F
#: %23
[: %5B
]: %5D
@: %40
!: %21
$: %24
&: %26
': %27
(: %28
): %29
*: %2A
+: %2B
,: %2C
;: %3B
=: %3D
[RFC3986_UNRESERVED]
-: -
.: .
_: _
~: ~

 

HTML5の仕様に基づくURLエンコードについて

HTML5の仕様では

If the byte is 0x20 (U+0020 SPACE if interpreted as ASCII) eplace the byte with a single 0x2B byte (“+” (U+002B) character if interpreted as ASCII).

If the byte is in the range 0x2A, 0x2D, 0x2E, 0x30 to 0x39, 0x41 to 0x5A, 0x5F, 0x61 to 0x7A Leave the byte as is.

Otherwise Let s be a string consisting of a U+0025 PERCENT SIGN character (%) followed by uppercase ASCII hex digits representing the hexadecimal value of the byte in question (zero-padded if necessary).

Encode the string s as US-ASCII, so that it is now a byte string.

Replace the byte in question in the name or value being processed by the bytes in s, preserving their relative order.

https://www.w3.org/TR/html5/forms.html#url-encoded-form-data

とのことです。

 
そのため、HTML5において、記号は

  • *
  • -
  • .
  • _

はそのまま、

  • (半角スペース)

+へと変換、それ以外はURLエンコードされれば良さそうです。

 
半角スペースを+へと変換する必要があるため、Pythonではurllib.parse.quote_plus()を使います。

そこで、以下を追加して試してみます。

try_url_encode.py

# 定数を追加
HTML5_ENABLE_OCTET = (
    '*'  # x2a
    '-'  # x2d
    '.'  # x2e
         # x30 ~ x39は0-9なので省略
         # x41 ~ x5aはA-Zなので省略
    '_'  # x5f
         # x61 ~ x7aはa-zなので省略
)

def main(func):
    ...
    # 以下を追加
    print('[HTML5_ENABLE_OCTET]')
    percent_encode(func, HTML5_ENABLE_OCTET)

 
quote_plus()の実行結果を見ると、*がURLエンコードされてしまっています。

--------------------
quote_plus
--------------------
...
[HTML5_ENABLE_OCTET]
*: %2A
-: -
.: .
_: _

 
そのため、urllib.parse.quote_plus()の引数safeアスタリスク(*)を追加すれば良さそうです。

try_url_encode.py

...
# 追加
def quote_html5_strictly(character):
    # *はそのまま、スペースは+にする
    return quote_plus(character, safe='*')


if __name__ == '__main__':
    ...
    # 追加
    print('-'*20)
    main(quote_html5_strictly))

 
実行してみます。アスタリスク(*)が変換されなくなりました。

--------------------
quote_html5_strictly
--------------------
[WHITE_SPACE]
 : +
[ALPHABET]
a: a
A: A
[DIGIT]
0: 0
[JAPANESE]
あ: %E3%81%82
[RFC3986_RESERVED]
:: %3A
/: %2F
?: %3F
#: %23
[: %5B
]: %5D
@: %40
!: %21
$: %24
&: %26
': %27
(: %28
): %29
*: *
+: %2B
,: %2C
;: %3B
=: %3D
[RFC3986_UNRESERVED]
-: -
.: .
_: _
~: %7E
[HTML5_ENABLE_OCTET]
*: *
-: -
.: .
_: _

 

Cookieで使えない文字の符号化を確認

RFC3986やHTML5におけるURLエンコードの仕様を確認したところで、Cookieで使えない文字の符号化を確認します。

以下を追加します。

try_url_encode.py

...
COOKIE_DISABLE_OCTET = (
    '\t'    # 水平タブ (x09)
    '\n'    # LF(改行) (x0a)
    '\r'    # CR (x0d)
    '"'     # ダブルクォート(x22)
    ','     # カンマ(x2c)
    ';'     # セミコロン(x3b)
    '\\'    # バックスラッシュ(x5c)
    '\x20'  # whitespace
)

def main(func):
    ...
    # 以下を追加
    print('[COOKIE_DISABLE_OCTET]')
    percent_encode(func, COOKIE_DISABLE_OCTET)

 
結果を見ると、いずれの方法でも、Cookieで使えない文字が符号化されたり、使える文字へと変換されました。

そのため、Cookie値のURLエンコード用に使うなら、quote()quote_plus()をデフォルトのまま使っても問題なさそうです。

--------------------
quote
--------------------
...
[COOKIE_DISABLE_OCTET]
    : %09

: %0A
: %0D
": %22
,: %2C
;: %3B
\: %5C
 : %20
--------------------
quote_plus
--------------------
...
[COOKIE_DISABLE_OCTET]
    : %09

: %0A
: %0D
": %22
,: %2C
;: %3B
\: %5C
 : +
--------------------
quote_rfc3986_strictly
--------------------
...
[COOKIE_DISABLE_OCTET]
    : %09

: %0A
: %0D
": %22
,: %2C
;: %3B
\: %5C
 : %20
--------------------
quote_html5_strictly
--------------------
...
[COOKIE_DISABLE_OCTET]
    : %09

: %0A
: %0D
": %22
,: %2C
;: %3B
\: %5C
 : +

 

その他

CookieのKey名について

CookieのKey名に関しては、こちらに詳しい内容がありました。
技術/HTTP/FormやCookieのkey名に"=“を含めたらどうなるのか? - Glamenv-Septzen.net

 

ソースコード

GitHubに上げました。e.g._urllib_parse_quote_quote_plusディレクトリの中が今回のものです。
thinkAmi-sandbox/python_misc_samples

Python + astモジュールを使ってソースコードを解析し、メソッドブロックや関数ブロックの定義行と最終行を取得する

Pythonソースコードを解析して、メソッドブロックや関数ブロックの定義行と最終行を取得することがありました。

Pythonでは標準モジュールのastを使ってソースコードを解析できるため、試した時のメモです。
32.2. ast — 抽象構文木 — Python 3.6.1 ドキュメント

なお、以下のページが大変参考になりました。ありがとうございました。
Python: ast (Abstract Syntax Tree: 抽象構文木) モジュールについて - CUBE SUGAR CONTAINER

 

環境

 

方法

上記の参考ブログ同様、astモジュールと再帰を組み合わせて解析します。

  • メソッドや関数の定義は、ast.FunctionDefクラス
  • メソッド名や関数名は、ast.FunctionDef.nameで取得
  • 行数はast.FunctionDef.linenoなどのlineno属性で取得
    • クラスによっては、lineno属性がないことに注意

 
collect_method_last_line_no.py

import ast

class Collection:
    def __init__(self, name='', def_line_no=0, last_line_no=0):
        self.name = name
        self.def_line_no = def_line_no
        self.last_line_no = last_line_no


class MethodLastLineNoCollector:
    def __init__(self):
        self.result = {}
        self.searched_line_no = 0

    def run(self, node):
        if isinstance(node, ast.FunctionDef):
            self.result[node.lineno] = Collection(node.name, def_line_no=node.lineno)

        for child in ast.iter_child_nodes(node):
            self.run(child)
        
        if hasattr(node, 'lineno'):
            # 探索した最終行を取得
            # 再帰で探すので、node.linenoは 1 > 2 > 3 > 2 > 1 となる
            if node.lineno > self.searched_line_no:
                self.searched_line_no = node.lineno

            # 再帰で探した時の帰りに、最終行を設定する
            else:
                if self.result.get(node.lineno):
                    self.result[node.lineno].last_line_no = self.searched_line_no


if __name__ == '__main__':
    FILENAME = 'target.py'
    with open(FILENAME, 'r') as f:
        source = f.read()

    tree = ast.parse(source, FILENAME)

    collector = MethodLastLineNoCollector()
    collector.run(tree)

    for v in collector.result.values():
        print(f'{v.name} -> def:{v.def_line_no}, last:{v.last_line_no}')

 
動作確認をします。例えば、

target.py

def foo_function():
    pass

    def innner_foo_function():
        pass


class Bar:
    def bar_method(self):
        pass

        def bar_inner_method(self):
            pass

    def bar_other_method(self):
        pass

    class InnerBar:
        def innter_bar_method(self):
            pass


if __name__ == "__main__":
    pass

というソースコードがあったとします。

 
collect_method_last_line_no.pyを実行します。

$ python collect_method_last_line_no.py 
foo_function -> def:1, last:5
innner_foo_function -> def:4, last:5
bar_method -> def:9, last:13
bar_inner_method -> def:12, last:13
bar_other_method -> def:15, last:16
innter_bar_method -> def:19, last:20

動作しているようです。

 

ソースコード

GitHubに上げました。collect_method_last_line_noディレクトリの中が今回のものです。
thinkAmi-sandbox/python_ast-sample

Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのCookieを使ってみた

前回、CGIのリダイレクトを使ってみました。
Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのリダイレクトを使ってみた - メモ的な思考的な

 
今回はCGICookieを使ってみます。

目次

 

環境

  • Mac OS X 10.11.6
  • Docker for Mac 17.03.1-ce-mac12
  • Alpine3.5 + Apache2.4.25 + Python 3.6.1

 
なお、Dockerfileは以前のものを流用し、Dockerは以下のコマンドで利用しています。

## DockerfileからDockerイメージをビルド
$ docker image build -t alpine:python3_httpd24_cookie .

## Dockerコンテナを起動し、CGIのディレクトリをホストと共有
$ docker container run -p 8081:80 --name cookie -v `pwd`/cgi/:/usr/local/apache2/cgi-bin/ alpine:python3_httpd24_cookie

 
また、今回のCGIの流れは、

  1. リダイレクト元ページでCookieをセット
  2. リダイレクト先へリダイレクト
  3. リダイレクト先でCookieの内容を表示

とします。

ステータスコードもリダイレクトのものを設定したいため、NPHスクリプトとして作成します。

 

PythonCookieまわりの標準モジュールについて

CookieまわりのPythonの標準モジュールについて、

  • http.cookies
    • Web クライアント 向けの HTTP クッキー処理
  • http.cookiejar
    • HTTP のクッキークラスで、基本的にはサーバサイドのコードで有用

の2つがあり、

http.cookiejar および http.cookies モジュールは互いに依存してはいません。

21.24. http.cookiejar — HTTP クライアント用の Cookie 処理 — Python 3.6.1 ドキュメント

とのことです。

CGIはサーバサイドのため、今回はhttp.cookiesを使います。
21.23. http.cookies — HTTPの状態管理 — Python 3.6.1 ドキュメント

 

CookieのExpires属性の日付書式について

Cookieの仕様を規定しているRFC6265では、rfc1123-dateと記載されています。
4.1.1. 構文 | RFC 6265 — HTTP State Management Mechanism (日本語訳)

rfc1123-dateの具体的な日付書式はRFC2616に記載されており、RFC1123形式とのことです。
ハイパーテキスト転送プロトコル – HTTP/1.1 - RFC2616 日本語訳の複製

 
Pythonにおいて、RFC1123形式で日付を取得する方法を探したところ、以下に記載がありました。
http - RFC 1123 Date Representation in Python? - Stack Overflow

いくつか挙げられていましたが、今回は標準モジュールのemail.utils.formatdate()を使うことにします*1
email.utils.formatdate() | 19.1.14. email.utils: 多方面のユーティリティ — Python 3.6.1 ドキュメント

 

http.cookies.SimpleCookieについて

まずはCookieオブジェクトであるhttp.cookies.SimpleCookieを使ってみます。

 

Cookieの読み込み

CGIではCookie環境変数HTTP_COOKIEに設定されるため、それを読み込んでみます。

読み込み方法としては

  • __init__()
  • load()

の2つがあります。

COOKIE_STR = 'foo=ham; bar=spam'

# __init__()を使ったCookieの読み込みと表示
cookie = SimpleCookie(COOKIE_STR)

for key, morsel in cookie.items():
    print(morsel)
    # => Set-Cookie: foo=ham
    #    Set-Cookie: bar=spam


# load()を使ったCookieの読み込みと表示
cookie = SimpleCookie()
cookie.load(COOKIE_STR)

for key, morsel in cookie.items():
    # => Set-Cookie: foo=ham
    #    Set-Cookie: bar=spam

 
また、dictの値もCookieとして読み込めます。

data = {
    'foo': 'ham',
    'bar': 'spam',
}
cookie = SimpleCookie(data)

for key, morsel in cookie.items():
    print(morsel)
    # => Set-Cookie: foo=ham
    #    Set-Cookie: bar=spam

 

Set-Cookieヘッダの出力

output()メソッドを使います。

なお、内部では__str__()output()エイリアスとして設定されているため、print文にSimpleCookieオブジェクトを渡しても良いです。
https://github.com/python/cpython/blob/v3.6.1/Lib/http/cookies.py#L402https://github.com/python/cpython/blob/v3.6.1/Lib/http/cookies.py#L531

cookie = SimpleCookie()
cookie['foo'] = 'ham'

print(cookie.output())
# => Set-Cookie: foo=ham
print(cookie)
# => Set-Cookie: foo=ham

 

output()で引数headerを設定

デフォルト値はSet-Cookie:ですが、別の値を設定したい場合には引数headerを使います。

cookie = SimpleCookie()
cookie['foo'] = 'ham'

print(cookie.output(header='hoge'))
# => hoge foo=ham

 

output()で引数sepを設定

複数Set-Cookieヘッダ間の区切り文字はCRLF(の8進数エスケープシーケンス表記)がデフォルト値ですが、変更したい場合は引数sepを使います。

data = {
    'foo': 'ham',
    'bar': 'spam',
}
cookie = SimpleCookie(data)
print(cookie.output(sep='++++'))
# => Set-Cookie: bar=spam++++Set-Cookie: foo=ham

 

output()で引数attrsを設定

複数のCookie属性がある場合で、ある属性のみ出力したい場合にはoutput()の引数attrsを使います。

# 複数属性を持つCookieを作成
cookie = SimpleCookie()
cookie['foo'] = 'ham'
cookie['foo']['path'] = '/bar/baz'
cookie['foo']['domain'] = 'example.com'
# RFC1123形式でexpiresを設定する
cookie['foo']['expires'] = formatdate(timeval=None, localtime=False, usegmt=True)
cookie['foo']['max-age'] = 100
cookie['foo']['httponly'] = True
cookie['foo']['secure'] = True

# attrsを使わない場合
print(cookie.output())
# => Set-Cookie: foo=ham; Domain=example.com; expires=Sat, 20 May 2017 11:37:36 GMT; HttpOnly; Max-Age=100; Path=/bar/baz; Secure

# attrsに文字列を渡す場合
print(cookie.output(attrs='expires'))
# => Set-Cookie: foo=ham; expires=Sat, 20 May 2017 11:39:12 GMT

# attrsに要素1のリストを渡す場合
print(cookie.output(attrs=['expires']))
# => Set-Cookie: foo=ham; expires=Sat, 20 May 2017 11:39:12 GMT

# attrsにリストを渡す場合
print(cookie.output(attrs=['max-age', 'httponly']))
# => Set-Cookie: foo=ham; HttpOnly; Max-Age=100

 

埋め込み可能なJavaScript snippetを出力

js_output()を使います。

# 複数属性を持つCookieを作成
cookie = SimpleCookie()
cookie['foo'] = 'ham'
cookie['foo']['path'] = '/bar/baz'
cookie['foo']['domain'] = 'example.com'
cookie['foo']['expires'] = formatdate(timeval=None, localtime=False, usegmt=True)
cookie['foo']['max-age'] = 100
cookie['foo']['httponly'] = True
cookie['foo']['secure'] = True

print(cookie.js_output())
# =>
# 
# <script type="text/javascript">
# <!-- begin hiding
# document.cookie = "foo=ham; Domain=example.com; expires=Sat, 20 May 2017 11:39:12 GMT; HttpOnly; Max-Age=100; Path=/bar/baz; Secure";
# // end hiding -->
# </script>
# 

 

Cookie値として日本語を設定
data = {
    'foo': 'ham',
    'bar': 'spam eggs',
    'baz': 'あ',
}
cookie = SimpleCookie(data)
print(cookie)
# => Set-Cookie: bar="spam eggs"
#   Set-Cookie: baz="あ"
#   Set-Cookie: foo=ham

と、値がダブルクォートで囲まれました。

パーセントエンコーディングをしたほうがよいのかもしれませんが、今回は深入りしません。

 

http.cookies.Morselについて

Morselの属性やメソッドを使用

SimpleCookieと同様に色々なメソッドがあるため、それぞれ使ってみます。

print('[{}]:'.format(inspect.getframeinfo(inspect.currentframe())[2]))
m = Morsel()
m.set('foo', 'bar', 'baz')

print(f'key: {m.key}')
# => key: foo
print(f'value: {m.value}')
# => value: bar
print(f'coded_value: {m.coded_value}')
# => coded_value: baz

print(m)
# => Set-Cookie: foo=baz

print(m.output())
# => Set-Cookie: foo=baz

print(m.OutputString())
# => foo=baz

print(m.js_output())
# =>
# <script type="text/javascript">
# <!-- begin hiding
# document.cookie = "foo=baz";
# // end hiding -->
# </script>

 

Morsel.coded_valueについて

Morselオブジェクトでは、set()メソッドを使って、実際に送信する形式にエンコードされたcookie値をMorsel.coded_valueへ設定します。

上記で見た通り、Morsel.output()では

m = Morsel()
m.set('foo', 'bar', 'baz')

print(m.output())
# => Set-Cookie: foo=baz

と、coded_valueの設定値がSet-Cookieヘッダの値として使われます。

 
一方、SimpleCookieクラスでCookieを設定した場合、coded_valueがどうなるかを見たところ、

cookie = SimpleCookie()
cookie['foo'] = 'ham'

print(f'key: {m.key}')
# => key: foo
print(f'value: {m.value}')
# => value: ham
print(f'coded_value: {m.coded_value}')
# => coded_value: ham

と、valueとcoded_valueが同じ値になりました。

 

ハードコーディングによるCookieを使ったCGIの流れ

上記でhttp.cookiesモジュールを見ましたが、一度戻ってハードコーディングによるCookieを使ったCGIの流れを実装してみます。

まずはCookieをセットしてリダイレクトする部分です。

nph-set_cookie_by_hardcode.py

#!/usr/bin/python3

# HTTPレスポンスヘッダ
import os
# ステータスコードとメッセージはPythonの定数を使う
# https://docs.python.jp/3/library/http.html
from http import HTTPStatus

# CGIの環境変数からプロトコルとバージョンを取得する
protocol = os.environ.get('SERVER_PROTOCOL')

# HTTPレスポンスヘッダ
# NPHなので、ステータスラインも記述する
print(f'{protocol} {HTTPStatus.FOUND.value} {HTTPStatus.FOUND.phrase}')
print('Location: /cgi-bin/done_redirect_with_cookie.py')
print('Set-Cookie: foo=bar; HttpOnly; Path=/example')
print('Set-Cookie: hoge=fuga')
print('')

次はリダイレクト後に受け取ったCookieを表示する部分です。

done.py

#!/usr/bin/python3

from http.cookies import SimpleCookie
import os

# HTTPレスポンスヘッダ
print('Content-Type: text/plain;charset=utf-8')
print('')

# レスポンスボディ
# 環境変数にセットされたCookieを表示する
env_cookie = os.environ.get('HTTP_COOKIE')
print('environ ver:')
print(env_cookie)
print('-'*20)

# SimpleCookieを使って表示する
print('SimpleCookie ver:')
cookie = SimpleCookie(env_cookie)
print(type(cookie))
print(cookie)

 
curlを使って確認します。

$ curl -b cookie.txt -c cookie.txt -L -D - http://localhost:8081/cgi-bin/nph-set_cookie_by_hardcode.py -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /cgi-bin/nph-set_cookie_by_hardcode.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 302 Found
HTTP/1.1 302 Found
< Location: /cgi-bin/done.py
Location: /cgi-bin/done.py
* Added cookie foo="bar" for domain localhost, path /cgi-bin/, expire 0
< Set-Cookie: foo=bar; HttpOnly;
Set-Cookie: foo=bar; HttpOnly;
* Added cookie hoge="fuga" for domain localhost, path /cgi-bin/, expire 0
< Set-Cookie: hoge=fuga
Set-Cookie: hoge=fuga
* no chunk, no close, no size. Assume close to signal end

< 
* Closing connection 0
* Issue another request to this URL: 'http://localhost:8081/cgi-bin/done.py'
* Hostname localhost was found in DNS cache
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#1)
> GET /cgi-bin/done.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> Cookie: foo=bar; hoge=fuga
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Sun, 21 May 2017 08:54:51 GMT
Date: Sun, 21 May 2017 08:54:51 GMT
< Server: Apache/2.4.25 (Unix)
Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
Content-Type: text/plain;charset=utf-8

< 
environ ver:
foo=bar; hoge=fuga
--------------------
SimpleCookie ver:
<class 'http.cookies.SimpleCookie'>
Set-Cookie: foo=bar
Set-Cookie: hoge=fuga

Cookieが正しくセットされているようです。

 
なお、curlで使われるCookie.txtファイルは、実行後は以下のようになりました。

# Netscape HTTP Cookie File
# http://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost FALSE   /cgi-bin/   FALSE   0   foo bar
localhost   FALSE   /cgi-bin/   FALSE   0   hoge    fuga

 

http.cookies.SimpleCookieを使ったCGIの流れ

今度は同じ内容をhttp.cookies.SimpleCookieを使って書いてみます。

nph-set_cookie_by_module.py

#!/usr/bin/python3

# HTTPレスポンスヘッダ
import os
# ステータスコードとメッセージはPythonの定数を使う
# https://docs.python.jp/3/library/http.html
from http import HTTPStatus
from http.cookies import SimpleCookie

# CGIの環境変数からプロトコルとバージョンを取得する
protocol = os.environ.get('SERVER_PROTOCOL')

# HTTPレスポンスヘッダ
# NPHなので、ステータスラインも記述する
print(f'{protocol} {HTTPStatus.FOUND.value} {HTTPStatus.FOUND.phrase}')
print('Location: /cgi-bin/done.py')

# Cookie
cookie = SimpleCookie()
cookie['foo'] = 'bar'
cookie['foo']['httponly'] = True
cookie['hoge'] = 'fuga'

print(cookie.output())
print('')

 
リダイレクト後(done.py)は同じのため、省略します。

 
curlで確認します。

$ curl -b cookie.txt -c cookie.txt -L -D - http://localhost:8081/cgi-bin/nph-set_cookie_by_module.py -v
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /cgi-bin/nph-set_cookie_by_module.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> Cookie: foo=bar; hoge=fuga
> 
< HTTP/1.1 302 Found
HTTP/1.1 302 Found
< Location: /cgi-bin/done.py
Location: /cgi-bin/done.py
* Replaced cookie foo="bar" for domain localhost, path /cgi-bin/, expire 0
< Set-Cookie: foo=bar; HttpOnly
Set-Cookie: foo=bar; HttpOnly
* Replaced cookie hoge="fuga" for domain localhost, path /cgi-bin/, expire 0
< Set-Cookie: hoge=fuga
Set-Cookie: hoge=fuga
* no chunk, no close, no size. Assume close to signal end

< 
* Closing connection 0
* Issue another request to this URL: 'http://localhost:8081/cgi-bin/done.py'
* Hostname localhost was found in DNS cache
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#1)
> GET /cgi-bin/done.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> Cookie: foo=bar; hoge=fuga
> 
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
< Date: Sun, 21 May 2017 08:57:51 GMT
Date: Sun, 21 May 2017 08:57:51 GMT
< Server: Apache/2.4.25 (Unix)
Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
Content-Type: text/plain;charset=utf-8

< 
environ ver:
foo=bar; hoge=fuga
--------------------
SimpleCookie ver:
<class 'http.cookies.SimpleCookie'>
Set-Cookie: foo=bar
Set-Cookie: hoge=fuga
* Connection #1 to host localhost left intact

同じ結果となりました。

 

ソースコード

GitHubに上げました。alpine_apache_python36_cookieディレクトリの中が今回のものです。
thinkAmi-sandbox/Docker_Apache-sample

*1:公式ドキュメントにはLegacy APIと書かれています。ただ、推奨しないなどの記載は特にないため、使っても大丈夫かと思います。参考:https://docs.python.jp/3/library/email.html