以前、PythonでCGIのCookieを使ってみました。
Docker + Alpine3.5 + Apache2.4 + Python3.6で、CGIのCookieを使ってみた - メモ的な思考的な
その時は日本語をCookie値へ設定する方法が気になっていたものの、深入りしませんでした。
そこで今回、以下を参考に、日本語をCookie値へ設定する方法について調べたため、メモします。
- encodeURIComponentが世界基準だと誤解してた話 - Qiita
- URLエンコード/パーセントエンコードとPHP : PHP – FindxFine
- Allowed characters in cookies - Stack Overflow
なお、内容に誤りがあった場合にはご指摘いただけるとありがたいです。
目次
環境
まとめ
長いので最初にまとめます。
- Cookieの仕様はRFC6265にある
- RFC6265によると、Cookie値には日本語が使えない
- ただし、使えない文字の符号化方式については書かれていない
- 調べた範囲では、符号化方式としてURLエンコードを使うケースが多い
- URLエンコードには2種類ある
- Pythonでは以下の関数が使えそう
- RFC3986向き:
urllib.parse.quote()
- HTML5向き:
urllib.parse.quote_plus()
- RFC3986向き:
- ただし、両方とも引数がデフォルトのままだと、完全には準拠していない
- 両方とも引数
safe
を使うと準拠する- RFC3986向き:
quote(character, safe='~')
- HTML5向き:
quote_plus(character, safe='*')
- RFC3986向き:
- とはいえ、Cookie値のURLエンコード用に使うなら、
quote()
やquote_plus()
をデフォルトのまま使っても問題ない
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で使える文字は書いてあるものの、日本語などの使えない文字に対する符号化方式は書かれていないようです。
そのため、試しに、Pythonのhttp.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以外の文字の符号化について
cookie-octet以外の文字の符号化方式を調べてみると、よく使われているのはURLエンコードでした。
URLエンコードについて調べると、2種類の方法がありました。
そこで、両方の符号化方式を調べてみました。
RFC3986に基づくURLエンコード(パーセントエンコード)について
RFC3986にて、使用可能な文字が記載されています。
- RFC 3986 - Uniform Resource Identifier (URI): Generic Syntax
- Uniform Resource Identifier (URI): 一般的構文 - RFC3986 日本語訳の複製
また、RFC3986とその前のRFC2396における、使用可能な文字の比較などは以下が参考になりました。
- URIに使ってよい文字の話 - RFC2396 と RFC3986 - 本当は怖い情報科学
- URIで使用できる文字 - CyberLibrarian
- RFC3986を読みながらURLエンコードについて考えた - 情報アイランド
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 / "-" / "." / "_" / "~"
とのことです。
そのため、
-
.
_
~
以外がパーセントエンコードされれば良さそうです。
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