前回、CGIのリダイレクトを使ってみました。
Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのリダイレクトを使ってみた - メモ的な思考的な
目次
- 環境
- PythonのCookieまわりの標準モジュールについて
- CookieのExpires属性の日付書式について
- http.cookies.SimpleCookieについて
- http.cookies.Morselについて
- ハードコーディングによるCookieを使ったCGIの流れ
- http.cookies.SimpleCookieを使ったCGIの流れ
- ソースコード
環境
なお、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の流れは、
とします。
ステータスコードもリダイレクトのものを設定したいため、NPHスクリプトとして作成します。
PythonのCookieまわりの標準モジュールについて
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をセットしてリダイレクトする部分です。
#!/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
を使って書いてみます。
#!/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