初めての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

Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのリダイレクトを使ってみた

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

今回はCGIのリダイレクトを使ってみます。

目次

 

環境

  • 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_redirect .

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

 
また、動作確認にはcurlを使います。
インターネットにアクセスしよう 番外編 : curlの使い方 - Cocoaはやっぱり!

 

Locationヘッダを使ったリダイレクト

今回はLocationヘッダを使ったリダイレクトを試します。

LocationヘッダのURLの仕様は、RFC7231にあり、

Location = URI-reference

7.1.2. Location | RFC 7231 — HTTP/1.1: Semantics and Content (日本語訳)

とのことです。

URI-referenceの定義は、RFC3986にあり、

URI-reference は、URI か相対的参照のどちらかである。 URI-reference の先頭がコロン分離記号を従えるスキームの構文に合致しなければ、URI-reference は相対的参照である。

4.1. URI 参照 | Uniform Resource Identifier (URI): 一般的構文 - RFC3986 日本語訳の複製

とのことです。

そのため、今回は相対的参照として、ホスト以下を記載します。

以上をもとに、

  • リダイレクト前:redirect.py
  • リダイレクト後:done_redirect.py

の2ファイルを用意します。

redirect.py

#!/usr/bin/python3

# HTTPレスポンスヘッダ
print('Location: /cgi-bin/done_redirect.py')
print('')

 
done_redirect.py

#!/usr/bin/python3

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

print('Hello')

 
curlLオプションでリダイレクトに対応し、vオプションで実際の往復を見てみます。

$ curl -L -v http://localhost:8081/cgi-bin/redirect.py
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /cgi-bin/redirect.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Sat, 20 May 2017 21:22:27 GMT
< Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
< 
Hello
* Connection #0 to host localhost left intact

リダイレクトが行われているようですが、ステータスコードが200になっていました。

 

NPHスクリプトによる、Locationヘッダを使ったリダイレクト

ステータスコードも自由に変更できる方法を探したところ、書籍「CGIプログラミング 第2版」の3章にApacheNPH (Non Parsed Headers)スクリプトを使う方法が紹介されていました*1

CGIプログラミング

CGIプログラミング

 
また、RFC3875などにも記載があります。

 
書籍ではPerlを使っていましたが、今回はPythonで書いてみます。

NPHスクリプトの場合、

  • ファイル名の接頭辞にnph-を付ける
  • 少なくとも、ステータス行、Conent-Typeヘッダ、Serverヘッダを出力する

とのことですので、以下のPythonスクリプトを書きました。

nph-redirect_hardcord.py

#!/usr/bin/python3

# HTTPレスポンスヘッダ
# NPHなので、ステータスラインも記述する
print('HTTP/1.1 302 Found')
print('Location: /cgi-bin/done_redirect.py')
print('Server: hoge')
print('')

 
curlにて確認します。

$ curl -L -v http://localhost:8081/cgi-bin/nph-redirect_hardcord.py
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#0)
> GET /cgi-bin/nph-redirect_hardcord.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< Location: /cgi-bin/done_redirect.py
< Server: hoge
* 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_redirect.py'
* Hostname localhost was found in DNS cache
*   Trying ::1...
* Connected to localhost (::1) port 8081 (#1)
> GET /cgi-bin/done_redirect.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Sat, 20 May 2017 22:14:46 GMT
< Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
< 
Hello
* Connection #1 to host localhost left intact

302リダイレクトになりました。

 

NPHスクリプト + 環境変数による、Locationヘッダを使ったリダイレクト

上記のNPHスクリプトでは色々とハードコーディングしていたため、

を使うように変更します。

 
nph-redirect_environ.py

#!/usr/bin/python3

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

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

# HTTPレスポンスヘッダ
print(f'{protocol} {HTTPStatus.FOUND.value} {HTTPStatus.FOUND.phrase}')
print('Location: /cgi-bin/done_redirect.py')
print(f'Server: {server}')
print('')

 
curlで確認します。

$ curl -L -v http://localhost:8081/cgi-bin/nph-redirect_environ.py

> GET /cgi-bin/nph-redirect_environ.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< Location: /cgi-bin/done_redirect.py
< Server: Apache/2.4.25 (Unix)
< 
> GET /cgi-bin/done_redirect.py HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.43.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Sat, 20 May 2017 22:23:31 GMT
< Server: Apache/2.4.25 (Unix)
< Transfer-Encoding: chunked
< Content-Type: text/plain;charset=utf-8
< 
Hello

同じく、302リダイレクトになりました。

 

その他

NPHスクリプトを使う際、環境によっては以下のエラーが出るようです。
apache - Apache2 sends two HTTP headers with a mapped “nph-” CGI - Stack Overflow

 

ソースコード

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

*1:旧版の英語版であれば、オライリーの「O'Reilly Open Books Project」により公開されています:http://www.oreilly.com/openbook/cgi/ch03_08.html

Google I/O 2017 Extended Live Viewing 信州に参加しました

5/17の深夜に開催された、Google I/O 2017 Extended Live Viewing 信州に参加しました。

久しぶりの参加であり、長野県でGoogle I/O 2017 Extendedが毎年開催されるのはありがたいと感じています。

 
信州会場は基調講演前から開場していたこともあり、他の参加者のみなさんと交流を深められ、楽しい時間を過ごせました。

基調講演のYoutube配信は英語 + 英語字幕でしたが、

というライフハックなどもあり、自分なりに楽しむことができました。英語のカンファレンスに参加すると、英語学習意欲の増加するので良いです。

 
その中でも、個人的には「Google Lens」と「Google Photo books」が気になり、またどのプレゼンターもAIを推していたのが印象に残りました。
photo books | Google Photos - All your photos organized and easy to find

なお、詳細については、すでに他のサイトで記載されているので省略します。

 
今後の予定としては、6/10(土)にGoogle I/O 報告会があります。
Google I/O 2017 報告会 信州会場 - connpass

例年同様、ゲストパネラーとして Google I/O参加のDeveloperExpertの方も参加されるようですので、いろいろなお話がうかがえそうです。

 
最後になりましたが、開催してくださった関係者のみなさま、ありがとうございました。