Cookieに対応する

Cookieとは

Cookieとは……と書きかけたのですが、 どうもこれを正確に説明するのはちょっと難しいようです。 まずは世間一般の説明を引用してみます。

以下はWikipediaより。

http://ja.wikipedia.org/wiki/HTTP_cookie

HTTP cookie(エイチティーティーピークッキー、単にクッキーとも表記される)は、 RFC 6265などで定義されたHTTPにおけるウェブサーバとウェブブラウザ間で 状態を管理するプロトコル、 またそこで用いられるウェブブラウザに保存された情報のことを指す。 ユーザ識別やセッション管理を実現する目的などに利用される。

次はe-WordsのIT用語辞典。

http://e-words.jp/w/Cookie.html

Webサイトの提供者が、Webブラウザを通じて訪問者のコンピュータに 一時的にデータを書き込んで保存させる仕組み。

どちらも間違ってはいないと思うのですが、 Cookieは必ずしも「状態を管理する」(Wikipediaより)ためのものではないですし、 必ずしも「Webサイトの提供者(=Webサーバ?)が」(IT用語辞典)発行する ものでもありません。

Cookieは、サーバ側で発行する場合、 HTTPレスポンスヘッダを使って、ブラウザに送られます。 ブラウザは、それを指定された期間だけ保持し、 以後、そのサーバにリクエストを送るときには、 単純な画像取得等を含むすべて※1 のHTTPリクエストにおいて、 HTTPリクエストヘッダにCookie情報を埋め込みます。

以前にも少し触れましたが、 HTTPはステートレスなプロトコルです。 Aさんが使っているブラウザがサーバに立て続けに2回リクエストを送ったとして、 サーバには、1回目のリクエストと2回目のリクエストはまったく独立したものとして 届きます。これでは、 「ショッピングサイトにログインしてカートに商品を登録していく」 といったことができません。

しかし、Cookieを使って、ブラウザごとに固有のIDを送付すれば、 以後のリクエストにはすべてそのIDが付いてきますから、 サーバ側でどのブラウザからの接続であるかを識別できるわけです。 現在、Cookieの使用法としては、これが最も重要な使用法といえるでしょう。 ただし、Cookieの仕組み自体は、その用途に限定されるものではありません。 たとえばサイトのデザインや文字サイズを選択できるような機能を作ったときには、 ユーザごとの設定をCookieで保持しておくことができるでしょう。

また、CookieはJavaScriptを用いてクライアント側で発行することも可能です。 一例として、うちのWebサイトの掲示板では ハンドル名やパスワードを毎回入力しなくてよいようにするためにCookieを 使用していますが、このCookieはクライアント側で生成しています。 そして、このCookieは、サーバに送られはしますが、 うちの掲示板では、サーバ側でそれを無視しています。この用途では、単に 「ブラウザにちょっとした情報を覚えておいてもらう」ためだけに Cookieを使用しているわけです。

ごちゃごちゃ説明するより、 ここを読んでいるような人にとっては 実際のHTTPリクエスト/レスポンスを見たほうが理解が早いと思いますので、 HttpFoxでキャプチャしながらGoogle(google.co.jp)につないでみます (実験前に、ブラウザのCookieとキャッシュはクリアしています)。


(クリックで拡大)

これを見ると、初回のgoogle.co.jpへのアクセス (キャプチャで反転している行)に対するレスポンスヘッダで、 以下のふたつが返されていることがわかります。

上の「PREF=ID=ddbf……」のほうについて、全体を見ると、 以下のようになっています。

Set-Cookie:PREF=ID=ddbfd8d2cf17bd19:FF=0:TM=1396769006:(行継続)
LM=1396769006:S=MCu-T1FuYci762u-; (行継続)
expires=Tue, 05-Apr-2016 07:23:26 GMT; path=/; domain=.google.co.jp

私のブラウザはFirefoxなのですが、Firefoxのメニューから 「オプション」→「プライバシー」→「Cookieの表示」にて google.co.jpのCookieを表示すると、以下のようになっています。

ここからわかることは以下です。

次は、google.co.jpへのアクセスで飛んだリダイレクトの中から、 2行目を見てみます。


(クリックで拡大)

リクエストヘッダで、以下が送られていることがわかります。

Cookie:PREF=ID=ddbfd8d2cf17bd19:FF=0:TM=1396769006:(行継続)
LM=1396769006:S=MCu-T1FuYci762u-;(行継続)
 NID=67=DdYtbVxACQ6-1ZrAUQo8gl3-_WHZ-sTFRSxW1BlgxwtZPt8Cwp(行継続)
YdvFQyngTvjtgppWNMtHtmxdPYSexfEvcqo2V-nDoAIWk5765l1(行継続)
sipCtNq69tZ6ttiX4rX9c7373fB

サーバから送られてくるレスポンスヘッダでは、 「Set-Cookie」という名前で、(Googleの場合は)NID, PREFという ふたつの値が送られてきたところ、 クライアントから送るリクエストヘッダでは、 「Cookie」という名前で、NID, PREFの両方を一度に送っていることがわかります。

単なる画像取得のためのリクエストヘッダでも、 同様にCookieが送られています(下図参照)。


(クリックで拡大)

まとめると、サーバとクライアントの間では、 以下のようなやり取りが行われています。

Cookieの容量制限

ブラウザにより異なりますが、Cookieの数やサイズには制限があります。

新しめのブラウザの制限については、たとえば以下のページに記載がありますが、

http://d.hatena.ne.jp/hosikiti/20130925/1380098776

Webサイトを作る側としては、最新ブラウザの(一番ゆるいであろう) 制限に合わせたのでは古いブラウザを使っている閲覧者が困るわけで、 規格化された中では一番古い RFC2109 の6.3を見ると以下の記述があります。

これを見ると、「ホストもしくはドメイン名ごとに最低20個」 という制限がかなりきついように見えます ※2

先のGoogleのCookieの例では、「PREF」という名前のCookieに対し、 「ID=ddbfd8d2cf17bd19:FF=0:TM=1396769006:(後略)」という値が 設定されていました。これを見ると、本当に保持したいのは IDやFFやTMであるところ、 ドメインあたりのCookieの総数制限にひっかからないよう、 ひとつのCookieにたくさんの値を突っ込んでいると推測できます。 ひとつのCookieあたり4096バイトという制限は、 ドメインごとに20Cookieという制限より実用上ゆるいからです。

クライアント側で発行するCookie

上記のGoogleの例では、PREF, NIDというふたつのCookieについて、 最初に登場したのはレスポンスヘッダでした。 つまり、このふたつのCookieについては、サーバ側で発行されています。

ただし、Cookieはクライアント側で発行することもできます。

このWebサイト(kmaebashi.com)の掲示板においても クライアント側でCookieを発行しています。 その用途は、ハンドル名、URL、削除用パスワードについて、 次回投稿時に再入力しなくてよいようにすることです。 テスト掲示板で その動きを見てみましょう。

ハンドル名、件名、リンクURL、本文、削除用パスワードを入れて 掲示板に投稿し(下のほうにある「ほげぴよ」はspam対策です。 こちらを参照してください。)、投稿します。


(クリックで拡大)

うちの掲示板では、投稿すると、プレビュー画面が表示されます。 それを表示するためのリクエストとレスポンスをHttpFoxにて表示したものが以下です。


(クリックで拡大)

クライアントから、Cookieリクエストヘッダが送られていることがわかります。 そして、このケースでは、サーバはこのリクエストを無視しています (だからレスポンスにもSet-Cookieヘッダは入っていません)。

上記を実現しているJavaScriptについては、 こちらを参照してください。

最近は、Cookieの用途もいろいろと多様化していますが、 以前は、このように掲示板のハンドル名等をクライアント側で 保持するという用途が割と主流だった頃がありました。

そのような用途で結構使われていたにも関わらず、 「Cookieとは」とかでぐぐって調べるとサーバ側で発行する Cookieのことばかり書いてあり、 そのくせ具体的なJavaScriptのコードサンプルを探すと クライアントで発行する例がぞろぞろひっかかってくる、という、 なんだかちょっといびつな状況が、 ことWebアプリケーションの勉強においては、 今でもいろいろ続いているような気がします。

Cookieの仕様

さきほどのGoogleのCookieの例において、Set-Cookieヘッダには、 Cookieの名前と値のほか、以下のような情報が付与されていました。

expires=Tue, 05-Apr-2016 07:23:26 GMT; path=/; domain=.google.co.jp

このexpiresやpathやdomainを、Cookieの属性と呼びます。

Cookieの属性について、Cookieに関する最新のRFCである RFC6265を 参照すると、以下が記載されています。

Expires属性
Cookieの有効期限を「Tue, 05-Apr-2016 07:23:26 GMT」 のような形式でブラウザに指示します(ただし、その期限まで、 必ず保持されるとは限らない)。省略時は、現在のセッションの終了時 (たいていはブラウザを閉じたとき)まで保持します。
サーバが、Expires属性に過去の日付を指定すると、 クライアントはそのCookieを削除します。
Max-Age属性
Cookieの有効期限を、失効までの秒数で指示します(Expires同様、 その期限まで必ず保持されるとは限らない)。 省略時は、現在のセッションの終了時(たいていはブラウザを閉じたとき) まで保持します。 Expires属性同様、過去の日付を指定すると、 クライアントはそのCookieを削除します。
Domain属性
Cookieの送信先のホストを後方一致で指定します。 省略時は、ブラウザは現在の通信相手のホストに対してのみCookieを返すので、 通常はこの属性は指定する必要はありません(セキュリティ的には、 指定しないのが最も安全)。 Domain属性は、www.example.comで発行したCookieをwww2.example.comにも 返したいといったように、 サブドメインやホストをまたがったCookieを発行する際に指定します。
Path属性
Cookieの送信先のパスを指定します。 省略時解釈は現在のリクエストURLのディレクトリ部分になります。
Domain同様、この属性も、 複数のディレクトリで同じCookieを共有したいときに使うもので、 これを指定することで セキュリティが高まるわけではありません
Secure属性
Secure属性を指定すると、そのCookieはSSL(https)通信以外では 送信されなくなります。
認証キーの入ったCookieなどについて盗聴を防ぐために指定しますが、 共有SSLサーバなどで同一ドメインに信頼できないサイトがある場合、 攻撃者に上書きされる可能性があるので注意が必要です。
HttpOnly属性
HttpOnly属性を指定すると、HTTP以外――現状では事実上JavaScriptからの アクセスを禁止するようブラウザに指示します。
XSS脆弱性などで攻撃者にスクリプトを仕込まれて、 Cookieの値を勝手に取得される、 といった攻撃を防ぐことができます。

上記を見ると、Expires属性とMax-Age属性が ほぼ同じ機能を持っているように見えます。 Expires属性は、Netscapeが最初にCookieを導入した際から存在した属性ですが、 Max-Age属性は、IETFが標準化を試みた RFC2109から登場しました。 しかし、対応ブラウザがなかなか増えなかったためか、 昔ながらのExpires属性が今でも広く使われています。 JavaのServlet APIにおいても、 デフォルトではNetscape仕様のCookieが送られます ( setVersion()メソッドにより変更可能)。 それにしては、有効期限を設定するメソッド名はsetMaxAge()であり、 指定するのもMax-Age風に秒数なので紛らわしいですが……

Henacatに追加するメソッド

では、開発中のへなちょこサーブレットコンテナHenacatに、 Cookieを発行する仕組みを組み込むことにします。

Henacatでは、Servlet APIにおいてCookieに関連するクラスとメソッドのうち、 以下を実装するものとします。

Cookieクラス

Cookieクラスは、ひとつのCookieを表現するクラスです。 コンストラクタのほか、各種属性のgetter/setterがあります。

Cookie(String name, String value)
Cookieクラスのコンストラクタです。 Cookieの名前と値を指定してCookieを生成します。
void setDomain(String pattern)
Domain属性を指定します。未指定の場合、Domain属性を出力しません。
Domain属性は、ドメイン名のフォーマットに従わなければなりませんが、 Tomcatの実装においては特にエラーチェックもしておらず、 正しいドメイン名を設定することはアプリケーションに任されているようですので Henacatでも特にチェックはしないことにします。
String getDomain()
Domain属性を取得します。
void setMaxAge(int expiry)
Cookieの有効期限を指定します。引数は秒数です。
前述の通り、メソッド名がsetMaxAge()で引数が秒数なので Max-Age属性を付与するかと思いきや、 デフォルトではExpires属性を付与します(現状では Expires属性が広く使われていても、 いずれはMax-Age属性に置き換わるだろう、 という期待からこのメソッド名になっているのでしょうか……)。
なお、Henacatでは、CookieのsetVersion()メソッドを実装しないので、 結局Expires属性しか付与できません。 setVersion()属性を実装することも、 Max-Age属性を付与することもさして難しいことではないので、 興味のある方は追加してみてください。
Expires属性、Max-Age属性ともに、 過去の日付を与えることでCookieを削除できますが、 Servlet APIでは、setMaxAge()の引数に0を与えることで 過去日付が送信されます(Tomcatの実装では、1970年1月1日0時0分10秒を返します。 なぜ10秒?)。 負の値を与えると、Expires属性自体が付与されません。
int getMaxAge()
setMaxAge()で指定した有効期限を取得します。デフォルトでは-1です。 負の値なので、上記の通り、デフォルトではExpires属性は付与されません。
void setPath(String uri)
Path属性を設定します。 Domain属性同様、 Tomcatの実装においては特にエラーチェックもしていないようですので、 Henacatでも特にチェックはしないことにします。
String getPath()
Path属性を取得します。
void setSecure(boolean flag)
Secure属性を設定します。
boolean getSecure()
Secure属性を取得します。
void setHttpOnly(boolean httpOnly)
HttpOnly属性を設定します。
boolean isHttpOnly()
HttpOnly属性を取得します。
String getName()
Cookie名を取得します。setName()メソッドはないので、 コンストラクタで指定したCookie名は変更できません。
void setValue(String newValue)
Cookieの値を設定します。
String getValue()
Cookieの値を取得します。
HttpServletRequestインタフェース

クライアントから送られてきたCookieをアプリケーションが 取得できるように、HttpServletRequestにメソッドを追加します。

Cookie[] getCookies()
クライアントから送られてきたCookieを、配列として全件取得します。
HttpServletResponseインタフェース

サーバからクライアントにCookieを送り返すため、 HttpServletResponseにメソッドを追加します。

void addCookie(Cookie cookie)
クライアントに送り返すCookieを設定します。 複数回呼び出すことで、複数のCookieをクライアントに送付できます。

Henacatにおける実装

Cookieクラス

まずはCookieの状態を保持するオブジェクト、Cookieクラスから。

  1: package com.kmaebashi.henacat.servletinterfaces;
  2: 
  3: public class Cookie {
  4:     private String name;
  5:     private String value;
  6:     private String domain;
  7:     private int maxAge = -1;
  8:     private String path;
  9:     private boolean secure;
 10:     private boolean httpOnly;
 11: 
 12:     public Cookie(String name, String value) {
 13:         this.name = name;
 14:         this.value = value;
 15:     }
 16: 
 17:     public void setDomain(String pattern) {
 18:         this.domain = pattern;
 19:     }
 20: 
 21:     public String getDomain() {
 22:         return this.domain;
 23:     }
 24: 
 25:     public void setMaxAge(int expiry) {
 26:         this.maxAge = expiry;
 27:     }
 28: 
 29:     public int getMaxAge() {
 30:         return this.maxAge;
 31:     }
 32: 
 33:     public void setPath(String uri) {
 34:         this.path = uri;
 35:     }
 36: 
 37:     public String getPath() {
 38:         return this.path;
 39:     }
 40: 
 41:     public void setSecure(boolean flag) {
 42:         this.secure = flag;
 43:     }
 44: 
 45:     public boolean getSecure() {
 46:         return this.secure;
 47:     }
 48: 
 49:     public void setHttpOnly(boolean httpOnly) {
 50:         this.httpOnly = httpOnly;
 51:     }
 52: 
 53:     public boolean isHttpOnly() {
 54:         return this.httpOnly;
 55:     }
 56: 
 57:     public String getName() {
 58:         return this.name;
 59:     }
 60: 
 61:     public void setValue(String newValue) {
 62:         this.value = newValue;
 63:     }
 64: 
 65:     public String getValue() {
 66:         return this.value;
 67:     }
 68: }

なんのことはありません。 単に、Cookieヘッダでクライアントから受け取ったり、 Set-Cookieヘッダでクライアントに送る情報を詰め込むための構造体的なクラスです。

Cookieの受信

クライアントから受け取ったCookieヘッダの内容を元に Cookieクラスのインスタンスを生成する処理は、 HttpServletRequestImpl内に記述しました。 以下は今回の修正箇所の抜粋です。

 41:     @Override
 42:     public Cookie[] getCookies() {
 43:         return this.cookies;
 44:     }
 45: 
 46:     private static Cookie[] parseCookies(String cookieString) {
 47:         if (cookieString == null) {
 48:             return null;
 49:         }
 50:         String[] cookiePairArray = cookieString.split(";");
 51:         Cookie[] ret = new Cookie[cookiePairArray.length];
 52:         int cookieCount = 0;
 53: 
 54:         for (String cookiePair : cookiePairArray) {
 55:             String[] pair = cookiePair.split("=", 2);
 56: 
 57:             ret[cookieCount] = new Cookie(pair[0], pair[1]);
 58:             cookieCount++;
 59:         }
 60: 
 61:         return ret;
 62:     }
 63: 
 64:     HttpServletRequestImpl(String method, Map<String, String> requestHeader,
 65:                            Map<String, String> parameterMap) {
 66:         this.method = method;
 67:         this.requestHeader = requestHeader;
 68:         this.cookies = parseCookies(requestHeader.get("COOKIE"));
 69:         this.parameterMap = parameterMap;
 70:     }
 71: }

41行目〜44行目はインタフェースとしてアプリケーションに公開している getCookies()メソッドです。インスタンスフィールドとして保持している Cookieの配列を返しているだけです。

Cookieヘッダの解釈は、46行目からのparseCookies()で行っています。 クライアントから送られるCookieヘッダは、単に「(Cookie名)=(値)」 をセミコロン区切りで並べてあるだけなので、 「;」でsplit()した後「=」でsplitして、Cookieの名前と値を取得しています。

Cookieの値にはセミコロンが含まれないことは RFC6265では保証されていますが、 「=」については保証されていません(それどころか、 上記のGoogleの例でも「=」は含まれていました)。 Cookieヘッダに「A=B=C」と記述されていたらそれは 「A」という名前のCookieの値が「B=C」である、 と解釈しなければいけません。ここでは、それを実現するために、 split()メソッドの第2引数に「2」を渡しています。 split()メソッドの仕様の詳細については こちらをどうぞ。

全体的な流れについて

サーバからCookieをクライアントに送付するとき、 Cookieは、HTTPレスポンスヘッダとして送付されます。 前回のHenacatの実装では、 getWriter()を呼んだ時点でレスポンスヘッダの出力まで行ってしまっていました。 これでは、getWriter()の呼び出し以降にHttpServletResponseの addCookie()を呼び出しても無駄です。 レスポンスヘッダはもう出力されてしまっているからです。

addCookie()はgetWriter()の呼び出し前に行っておけ、 というルールにするのはアプリケーションプログラマにとって結構不便ですし、 Tomcat等ではgetWriter()以後のaddCookie()も(ある程度?)効くわけですから、 いくらへなちょこサーブレットコンテナといっても ここは合わせておきたいところです。

そこで、今回、Henacatでは、 アプリケーションがgetWriter()に対して出力した内容を すべてバッファリングするようにしました。

この処理を記述しているのはServletService.javaです。 該当箇所を抜粋します。

 68:         ByteArrayOutputStream outputBuffer =  new ByteArrayOutputStream();
 69:         HttpServletResponseImpl resp
 70:             = new HttpServletResponseImpl(outputBuffer);
 71:         info.servlet.service(req, resp);
 72: 
 73:         ResponseHeaderGenerator hg
 74:             = new ResponseHeaderGeneratorImpl(resp.cookies);
 75:         SendResponse.sendOkResponseHeader(output, resp.contentType, hg);
 76:         resp.printWriter.flush();
 77:         byte[] outputBytes = outputBuffer.toByteArray();
 78:         for (byte b: outputBytes) {
 79:             output.write((int)b);
 80:         }
 81:     }
 82: }

見てのとおり、バッファとしてByteArrayOutputStreamを作成し、 HttpServletResponseにはそれを渡しています。 そして、71行目でサーブレットのservice()メソッドを呼び出した後、 75行目でSendResponse.sendOkResponseHeader()にてレスポンスヘッダを出力し、 その後、ByteArrayOutputStreamに蓄積した内容を出力しています。

その途中、73行目でResponseHeaderGeneratorImplというクラスを newしており、それをSendResponse.sendOkResponseHeader()に渡しています。 このResponseHeaderGeneratorImplが、今回Set-Cookieヘッダを生成している箇所です。

Set-Cookieヘッダの生成

以下がResponseHeaderGeneraterImpl.javaです。新規クラスなので全掲載します。

  1: package com.kmaebashi.henacat.servletimpl;
  2: import com.kmaebashi.henacat.util.*;
  3: import com.kmaebashi.henacat.servletinterfaces.*;
  4: import java.util.*;
  5: import java.io.*;
  6: import java.text.*;
  7: 
  8: class ResponseHeaderGeneratorImpl implements ResponseHeaderGenerator {
  9:     private ArrayList<Cookie> cookies;
 10: 
 11:     private static String getCookieDateString(Calendar cal) {
 12:         DateFormat df = new SimpleDateFormat("EEE, dd-MMM-yyyy HH:mm:ss",
 13:                                              Locale.US);
 14:         df.setTimeZone(cal.getTimeZone());
 15:         Date now = cal.getTime();
 16:         return df.format(cal.getTime()) + " GMT";
 17:     }
 18: 
 19:     public void generate(OutputStream output) throws IOException {
 20:         for (Cookie cookie : cookies) {
 21:             String header;
 22:             header = "Set-Cookie: "
 23:                 + cookie.getName() + "=" + cookie.getValue();
 24: 
 25:             if (cookie.getDomain() != null) {
 26:                 header += "; Domain=" + cookie.getDomain();
 27:             }
 28:             if (cookie.getMaxAge() > 0) {
 29:                 Calendar cal
 30:                     = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
 31:                 cal.add(Calendar.SECOND, cookie.getMaxAge());
 32:                 header += "; Expires=" + getCookieDateString(cal);
 33:             } else if (cookie.getMaxAge() == 0) {
 34:                 Calendar cal
 35:                     = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
 36:                 cal.set(1970, 0, 1, 0, 0, 10);
 37:                 header += "; Expires=" + getCookieDateString(cal);
 38:             }
 39:             if (cookie.getPath() != null) {
 40:                 header += "; Path=" + cookie.getPath();
 41:             }
 42:             if (cookie.getSecure()) {
 43:                 header += "; Secure";
 44:             }
 45:             if (cookie.isHttpOnly()) {
 46:                 header += "; HttpOnly";
 47:             }
 48:             Util.writeLine(output, header);
 49:         }
 50:     }
 51: 
 52:     ResponseHeaderGeneratorImpl(ArrayList<Cookie> cookies) {
 53:         this.cookies = cookies;
 54:     }
 55: }

19行目からのgenerate()メソッドがSet-Cookieヘッダを生成しているところです。

22〜23行目で名前と値を出力し、25行目以降で各属性を出力しています。 属性の順序は、Tomcatの出力に合わせました。

基本的にCookieクラスに設定されている内容を出力しているだけですが、 多少面倒なことをしているのはExpires属性の出力です(28〜38行目)。 前述の通り、Servlet APIでは、setMaxAge()の引数が0の時は、 過去日付(Tomcatの場合1970年1月1日0時0分0秒)を、 負の値を与えるとExpires属性自体を出力しないことになっています。 Henacatの実装では、28行目からが正の値の時、つまり現在時刻に 指定した秒数を加算して出力する処理で、 33行目からが過去日付を出力する処理です。

11行目からのgetCookieDateString()メソッドは、 Cookieに付与する日付をフォーマットするための下請けメソッドです。

日付をフォーマットする下請けメソッドは、Henacatでは utilパッケージのUtil.javaにgetDateStringUtc()メソッドとして 既に用意してあります。 にもかかわらずなぜこれが必要になったのかは こちらの補足を参照してください。

ResponseHeaderGeneratorImplクラスのgenerate()メソッドを呼び出しているのは、 SendResponse.sendOkResponseHeader()です。 次はこれを見ていきます。

SendResponseクラス

SendResponseクラスに今回追加したメソッドは、 以下のsendOkResponseHeader()メソッドです(6〜19行目)。 21行目以降のsendOkResponse()メソッドは元からあったものです ※3。 (このクラスにはsendNotFoundResponse()等のメソッドもあるのですが、 今回記載は省略しています)。

  5: public class SendResponse {
  6:     public static void sendOkResponseHeader(OutputStream output,
  7:                                             String contentType,
  8:                                             ResponseHeaderGenerator hg)
  9:         throws IOException {
 10:         Util.writeLine(output, "HTTP/1.1 200 OK");
 11:         Util.writeLine(output, "Date: " + Util.getDateStringUtc());
 12:         Util.writeLine(output, "Server: Henacat");
 13:         Util.writeLine(output, "Connection: close");
 14:         Util.writeLine(output, "Content-type: " + contentType);
 15:         hg.generate(output);
 16:         Util.writeLine(output, "");
 17:     }
 18: 
 19:     public static void sendOkResponse(OutputStream output, InputStream fis,
 20:                                       String ext) throws Exception {
 21:         Util.writeLine(output, "HTTP/1.1 200 OK");
 22:         Util.writeLine(output, "Date: " + Util.getDateStringUtc());
 23:         Util.writeLine(output, "Server: Henacat");
 24:         Util.writeLine(output, "Connection: close");
 25:         Util.writeLine(output, "Content-type: "
 26:                        + Util.getContentType(ext));
 27:         Util.writeLine(output, "");
 28: 
 29:         int ch;
 30:         while ((ch = fis.read()) != -1) {
 31:             output.write(ch);
 32:         }
 33:     }

sendOkResponse()メソッドは、 通常のWebページを出力するときに使用するもので、 webserverパッケージのServerThreadクラスから呼び出されています。 このメソッドは、レスポンスボディも出力しています。

それに対し、sendOkResponseHeader()は、サーブレットの出力で使用するもので、 servletimplパッケージのServletServiceクラスから呼び出され、 ヘッダのみを出力します。 中身を見ればわかるように似たようなメソッドなので、utilパッケージの SendResponseクラスにまとめているわけです。 「これだけ似た実装なら、こんなコピペプログラミングじみたことやってないで、 ヘッダ出力の機能を切り出して共通化すべきなのでは」 という指摘は大いにわかるのですが、企画の趣旨上、 出力しているものをなるべく生で書いたほうがわかりやすいのでは、 という趣旨でこうしています。

そして、sendOkResponseHeader()メソッドは、 ResponseHeaderGeneratorインタフェース経由で、 ResponseHeaderGeneratorImplのgenarete()メソッドを呼び出しています (15行目) このように、わざわざResponseHeaderGeneratorインタフェースを作成し、 実装であるResponseHeaderGeneratorImplを分けているのは以下の理由によります。

まず、SendResponseクラスはutilパッケージに存在します。 このパッケージは、 逆にwebserverパッケージ、servletimplパッケージから使用されますが、 utilパッケージ自体はどのパッケージにも依存しないようにすべきだと思われます (なにせutilパッケージですし)。

今回は、構造上、ResponseHeaderGeneratorはservletimplに置きたいところですが、 それをutilパッケージから呼び出すとutilがservletimplに依存することに なってしまうので、インタフェースを介すことで依存関係を絶っているわけです (ResponseHeaderGeneratorインタフェース自体はutilパッケージに配置してあります)。

今回のケースでこれが「よい設計」なのかどうかは異論もあるかと思いますが、 私はこうしてみました。

上記のソース一式をzipで固めたものは、 ここからダウンロードできます。

Cookieの日付形式について

Henacatの実装では、CookieのExpires属性の日付をフォーマットするために、 ResponseHeaderGeneratorImpl.javaにて、 getCookieDateString()というメソッドを作っています。

これを見て、

あれ? HTTPで日付なんてあちこちに出てくるわけで、 日付をフォーマットするメソッドなんかどこかにひとつ作っておけばいいだろう。

なぜCookie専用のメソッドなんか作るんだ。コピペ脳かよ!

と思う人がいるかもしれません。

ところが不思議なことに、Cookieの日付は、 HTTPの別のところの日付とはフォーマットが異なります。 たとえばこちらで挙げた Dateレスポンスヘッダのフォーマットは以下のようになっています。

Tue, 30 Jul 2013 17:47:09 GMT

それに対し、Set-Cookieのフォーマットは以下です。

Tue, 05-Apr-2016 07:23:26 GMT

Set-Cookieの方では、日、月、年の間にハイフンが入っています。

実のところ、Cookieの最新の仕様であるRFC6265ではこの日付は RFC2616, Section 3.3.1で定義されると書いてあって、 RFC2616というのはつまりHTTP1.1の仕様ですから、 Cookieの日付フォーマットは HTTPレスポンスヘッダ等のフォーマットと同じでよい(というか、 最新のRFCでは同じにすることが求められている)はずなのですが、 Netscapeの当初の仕様書ではハイフン入りのフォーマットになっていて、 現状、これが引き継がれているようです。

参考: Cookieの日付形式

今後について

Cookieの送受信ができるようになったので、 次はこれを使ってセッションを実装しようと考えています。

そこまでできれば、Webアプリケーションの「本当の基礎」はひととおり 眺めたことになるかなあ、と思います。

2014/12/01公開


前のページ | 次のページ | ひとつ上のページに戻る | トップページに戻る