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