シェルスクリプトおばさん(43)の手づくりCookie

# Cookieを焼くのはおば(あ)さんの仕事？

錦糸町の街を歩いていたら[ステラおばさんのクッキー](http://www.auntstella.co.jp/)という名のクッキー屋さんがあった。一方、ネットをさまよっていたら、[Cookie Clicker](http://orteil.dashnet.org/cookieclicker/)というサイトがあって、ナゼかいかつい顔したグランマがクッキーを焼いていた。どうやらクッキーを焼くのはおば(あ)さんの得意技と相場が決まっているらしい。

シェルスクリプトおばさん[(43)](http://en.wikipedia.org/wiki/Thompson_shell#History)も、 **年齢的にそろそろCookie焼くのが得意な年代** ではないだろうか（女性だったのか!?）。ということで、シェルスクリプトでCookieを焼く方法を紹介する。「手作り」の看板に恥じぬよう、例によって、使うのはシェルスクリプトと標準UNIXコマンドだけだ。

# 1. 食べたCookieは何か聞く(クライアント→サーバー)

本来は焼いて食べさせてから聞くのが筋というのものだが、Webアプリを作るにあたっては、大抵クライアントがリクエストヘッダーに埋め込んだCookie情報を回収してから新しいものを発行するという手順を踏むのでこちらを先に解説する。

## まずはコードをどーん！

```sh:クライアントからのCookie情報を読み取る
# ===== Cookie文字列を取得・正規化 ===================================
#
# --- 準備 -----------------------------------------------------------
LF=$(printf '\\\n_');LF=${LF%_}                     # (1)sed用改行コードを用意(別Tipsで解説)
tmpf_cookie=$(mktemp -t "${0##*/}.cookie.XXXXXXXX") # (2)一時ファイル用意
[ $? -eq 0 ] || { echo 'tmpfile error' 1>&2; exit 1; }
trap "rm -f ${tmpf_cookie}; exit 0;"  EXIT HUP INT QUIT PIPE ALRM TERM
#
# --- Cookie変数を1行1行取り出す --------------------------------------
printf '%s' "${HTTP_COOKIE:-}"        | # 環境変数からCookie文字列取得
sed 's/[;,[:blank:]]\{1,\}/'"$LF"'/g' | # 1つの変数ごとに改行する(RFC2109準拠) ↑
grep -v '^[[:blank:]]*$'              | # 空行があれば除去                    |
# --- "%nn"をデコードする ------------- #        　　　　　　　　　　    　　　  |
tr '+' ' '                            | # 空白は"+"になっているのでこれを戻す   |
sed 's/%0[Dd]%0[Aa]/\\n/g'            | # CR+LFは実際の文字ではなく'\n'に置換　 |
sed 's/%0[DdAa]/\\n/g'                | # CR,LFそれぞれも同様　　　　          |
awk '                                 # # それ以外の文字を置換　　　　　　      |
BEGIN {                               # 　　　　　　　　　　　                 |
  for (i = 0; i < 256; i++) {         # 　　　　　　　　　　　                 |
    s = sprintf("%02X",i);            #                   この区間は長くて鬱陶しいので
    chr[s] = sprintf("%c",i);         #                   私は普段、
  }                                   #                   cgi-nameというコマンド一発で
}                                     #                   済ませている。超便利。
{                                     # 　　　　　　　　　　　                 |
  delim = index($0,"=");              # 　　　　　　　　　　　                 |
  val0  = substr($0,delim+1);         # 　　　　　　　　　　　                 |
  val   = "";                         # 　　　　　　　　　　　                 |
  while (match(val0,/%[0-9A-Fa-f][0-9A-Fa-f]/)) { # 　　　　　　　　　　       |
    hex  = toupper(substr(val0, RSTART+1, 2));    # 　　　　　　　　　　       |
    val  = val substr(val0,1,RSTART-1) chr[hex];  # 　　　　　　　　　　       |
    val0 = substr(val0, RSTART+3);    # 　　　　　　　　　　　                 |
  }                                   # 　　　　　　　　　　　                 |
  print substr($0,1, delim-1) " " val val0;       # 　　　　　　　　　　       |
}                                     # 　　　　　　　　　　　                 ↓
'                                     > $tmpf_cookie # 一時ファイルに保存

# ===== Cookie変数を実際に読む時は… =================================
#
# --- 'session_id'という変数の中身が欲しい場合 -----------------------
session_id=$(grep '^session_id ' "$tmpf_cookie" | sed 's/^[^ ]* //')
```

## コードの解説…1)環境変数から読みだして一時保存

最初にやるべき仕事は、後で何度も参照するのに便利なように渡ってきた文字列を加工して一時保存しておくことだ。

クライアントから送られてくるCookie文字列は、環境変数`$HTTP_COOKIE`に入っているのでそこから取ってくる。ただしクライアントがCookieを送信してこずに変数が未定義な場合もあるので${HTTP_COOKIE:-}と書いておき、set -uしている時でもコケないようにしておくこと。

取ってくるとき、一変数ごとに一行化しておく。冒頭にあるLFという変数を使うテクニックについては[該当Tips](http://qiita.com/richmikan@github/items/3c74212b0d8dec9bd00f)を参照。で、一変数一行化だけでなく、さらにパーセントエンコーディング(`%nn`というアレ)も解いておくとラクだ。ちなみに、ここらへん（${HTTP_COOKIE:-}の次の行から最後のAWKまで）のコードは毎回書くのが鬱陶しいのでコマンド化されているものを使うとラクだ。私は普段、これに相当するする作業を[cgi-nameという名で公開されているコマンド](https://uec.usp-lab.com/TUKUBAI_MAN/CGI/TUKUBAI_MAN.CGI?POMPA=MAN1_cgi-name)一発でやっている。([シェルスクリプト版のソースコード](https://github.com/usp-engineers-community/Open-usp-Tukubai/blob/master/COMMANDS.SH/cgi-name))

解いたら臆せず一時ファイルにしまう。一時ファイルなんて使うのヤダーとか言わない方がいい。そりゃprintfからAWKまでの全体を$(～)で囲んでシェル変数に格納しておけば一時ファイルなんぞ作らずに済むが、 **シェル変数のパフォーマンスは、文字列が長くなるほど悪い** のでかえって美しくないコードを書くことになる。シェルスクリプトを使うなら一時ファイルを使うのは半ば作法にしておいた方がいい。

## コードの解説…2)一時保存していた値の参照

使いやすいように格納しておいたら参照は簡単だ。前述のコードのように、grepで必要な変数の行に絞り込んでからsedで値部分だけ取り出せばいい。尚、 *変数と値の境界は各行の最初に出てくる空白1文字* にしてあるのでそのつもりで。

# 2. おまちどうさま。Cookieの焼き方(サーバー→クライアント)

さて次はCookieを発行してクライアントに送るやり方だ。これもまずはソースをご覧いただこう。おいしいわよ。 **(2014/02/02 17:58 RFC850形式の日時の作り方を間違えていたので修正 m(_ _;)m)**

```sh:Cookieを発行してクライアントに送る
# ===== Cookie文字列生成時に必要な材料を作成 =========================
#
# --- Cookieの有効期限を設定する -------------------------------------
expire=$(TZ=UTC+0 date +%Y%m%d%H%M%S |
         TZ=UTC+0 utconv             | # UNIX時間に変換
         awk  '{print $1+86400}'     | # 有効期限を1日としてみた
         TZ=UTC+0 utconv -r          | # UNIX時間から逆変換
         awk '{                      #   "Wdy, DD-Mon-YYYY HH:MM:SS GMT"形式に変換
           split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec",monthname);
           split("Sun Mon Tue Wed Thu Fri Sat",weekname);
           Y = substr($0, 1,4)*1; M = substr($0, 5,2)*1; D = substr($0, 7,2)*1;
           h = substr($0, 9,2)*1; m = substr($0,11,2)*1; s = substr($0,13,2)*1;
           Y2 = (M<3) ? Y-1 : Y; M2 = (M<3)? M+12 : M;
           w = (Y2+int(Y2/4)-int(Y2/100)+int(Y2/400)+int((M2*13+8)/5)+D)%7;
           printf("%s, %02d-%s-%04d %02d:%02d:%02d GMT\n",
                  weekname[w+1], D, monthname[M], Y, h, m, s);
         }'                          )
#
# --- Cookieの有効なパスを設定する -----------------------------------
mypath='/hogehoge/foo/bar'  # これは例なので実際はちゃんとしたものを設定すること
#
# --- このCGIが動いているのはHTTPか、それともHTTPSか -----------------
case "${HTTPS:-off}" in
  [Oo][Nn]) secure='; secure';;
  *)        secure=''        ;;
esac

# ===== Cookie文字列生成 =============================================
#
# "sessionid"や"timestamp"は例なので、必要なものを自分で設定しておく
cookie_id=$(printf '\nSet-Cookie: sessionid=%s; expires=%s; path=%s%s' \
                   "$sessionid" "$expire" "$mypath" "$secure"          )
cookie_ts=$(printf '\nSet-Cookie: timestamp=%s; expires=%s; path=%s%s' \
                   "$timestamp" "$expire" "$mypath" "$secure"          )

# ===== クライアントへ送信 ===========================================
#
cat <<-HTML_HEADER
	Content-Type: text/html$cookie_id$cookie_ts

HTML_HEADER

# ===== HTML本体を作る ===============================================
#
# ここからは、あなたに任せるわ。自由にお作りなさい。
#
#                           Aunt ShellScript
```

## コードの解説…1)有効期限文字列を作る

Cookieを発行する時に必要なものは、変数名とその値、だけではない。(1)有効期限、(2)有効パス、(3)HTTPSの時だけ有効にするかどうかのフラグ、という3つがある。というわけでまずは(1)の有効期限を作っておかねばならない。

手順は次のとおり、

1. 現在日時を求める

1. UNIX時間に変換

1. 有効期限分の秒数を加算

1. UNIX時間から逆変換

1. RFC850形式の日時フォーマットへ変換

である。しかし、このUNIX時間の変換・逆変換は、正直にコードを掲載するととてもながーくなってしまうので、[utconvという外部コマンド](https://gist.github.com/richmikan/8703117)にさせてもらった。このコマンドに関するTipsは[シェルスクリプトで時間計算を一人前にこなす](http://qiita.com/richmikan@github/items/14e32d099bd9bcc7f8e8)を参照してもらいたい。

それから、(2)の有効パスを作って、最後に(3)のHTTPSかどうかの判定を行う。Apacheで動かしているなら、自分がHTTPS上で動かされているCGIの時は`$HTTPS`という環境変数に"on"が入っているので、それでわかる。

## コードの解説…2)Cookie文字列をつくる

Cookie本体の文字列はprintfコマンドあたりで作ればよい。Cookie文字列の書式については[Studing HTTPさんのHTTP Cookies](http://www.studyinghttp.net/cookies)を見るのが超オススメ！

## コードの解説…3)クライアントへ送る

これは簡単。ヒアドキュメントでHTTPヘッダー文字列を作るようにして、そこに先程作ったCookie文字列変数を埋め込んでおけばいいだけだ。

# 私はこれで、セッション管理もやってるのよ

私がCookieを焼く主な目的は、セッション管理の必要なWebアプリを作るためだ。実際にシェルスクリプトで、ショッピングカートを作ったり、ログインの必要な管理画面を作ったりしている。

というわけで、[シェルスクリプトによるCGIのセッション管理](http://qiita.com/richmikan@github/items/ee77911602afc911858f)というTipsを用意したので興味があれば見てもらいたい。