この記事は2019年7月6日に開催されたTama Ruby会議01での発表「たのしいOSSコードリーディング: Let's read "cookies"🍪」を詳細解説するものです。
Railsアプリケーションで使用されるcookies
メソッドを題材に、このメソッドがどのように実装されているかを読んでいきます。
読みきれなかった部分・知識が曖昧な部分が残っているため、先輩方からの技術的指摘をお待ちしています。
当日の発表資料はこちら
調査環境
- Rails 6.0.0.rc1
- TraceLocation 0.9.3.1
そもそもcookiesメソッドとは
cookies[:hoge] = "fuga"
Railsが用意しているメソッド。
Cookieに「hoge=fuga」を設定するときに使用。
事前知識
①Cookie
クライアントからサーバーへリクエストを送る際、HTTPリクエストヘッダはCookieヘッダを含んでいる。
クライアント👩💻 → リクエスト[Cookie: hoge=fuga] → サーバー👨💻
Cookieヘッダの中身はこんな感じ。
NAME1=OPAQUE_STRING1; NAME2=OPAQUE_STRING2 ...
サーバーからクライアントにレスポンスを送る際、HTTPレスポンスヘッダはSet-Cookieヘッダを含んでいる。
クライアント👩💻 ← レスポンス[Set-Cookie: hoge=fuga] ← サーバー👨💻
Set-Cookieヘッダの中身はこんな感じ。
NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; ...
いずれの場合も、Cookieは次のように名前と値を一つのペアとしている🍪
Cookieの名前=Cookieの値
(Rubyスクリプトの中で扱う際、ハッシュの形式{名前: 値}
に変換したりする)
②Rackミドルウェア
Rackとは
- 「Rack」は次のふたつの意味を持っている
- WebアプリケーションとWebサーバーを繋ぐプロトコル
- WebアプリケーションとWebサーバーを繋ぐライブラリ
- Railsアプリケーションは、「Rackプロトコルを満たし、かつRackライブラリを内部で使うRackアプリケーション」
Rackミドルウェアとは
- Webアプリケーションが持っているべき、「特定の汎用的な機能」を切り出したRackライブラリ
- Railsアプリケーションは多くのRackミドルウェアを使用している
- 今回見ていく
ActionDispatch::Cookies
はRailsで使用されているRackミドルウェアのひとつ
$ rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
(略)...
ActionDispatch::Cookies
とは?
- Railsアプリケーションで使用されているRackミドルウェアのひとつ
- Cookieを保存するために使う
$ rails middleware
(略)...
use ActionDispatch::Cookies # ←これ
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Myapp::Application.routes
それでは早速読んでいきましょう🍪
cookies[:hoge] = "fuga"
の全体図
trace_locationを使用すると、cookiesメソッドを呼んだ際、次のように一連の処理が走ることが確認できます。
Logged by TraceLocation gem at 2019-06-05 20:31:31 +0900
https://github.com/yhirano55/trace_location
[Tracing events] C: Call, R: Return
C actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12 [ActionController::Cookies#cookies]
C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11 [ActionDispatch::Request#cookie_jar]
C rack-2.0.7/lib/rack/request.rb:58 [Rack::Request::Env#fetch_header]
R rack-2.0.7/lib/rack/request.rb:60 [Rack::Request::Env#fetch_header]
R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:15 [ActionDispatch::Request#cookie_jar]
R actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:14 [ActionController::Cookies#cookies]
C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374 [ActionDispatch::Cookies::CookieJar#[]=]
C actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:350 [ActionDispatch::Cookies::CookieJar#handle_options]
R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:370 [ActionDispatch::Cookies::CookieJar#handle_options]
R actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:392 [ActionDispatch::Cookies::CookieJar#[]=]
Result: fuga
要約するとこんな感じ
[Rails]ActionDispatch::Cookiesミドルウェアにアクセスする
[Rails][Rack]リクエストから既存のCookieヘッダを見つけて返す
見つからなかった場合は空のCookieをセットする
[Rails]ActionDispatch::Cookiesのインスタンスを生成
インスタンス変数を初期化する
[Rails]ActionDispatch::Cookiesのインスタンス変数に
新しいCookieの値を追加する
[Rails]ミドルウェアがアプリケーションを返すタイミングで
インスタンス変数の内容をSet-Cookieヘッダに書き込む(?) ← ここは処理を追いきれず…
RailsとRackの処理を行ったり来たりするため、手元にソースコードがある方は確認しながら進むことをお勧めします。
ひとつずつ読んでいきましょう🍪
rails/actionpack/lib/action_controller/metal/cookies.rb
7 C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12#cookies
Rails側から処理を追います🍪
まずはActionController::Cookies#cookies
メソッドを読んでみましょう。
ActionController::Cookies#cookies
11 private
12 def cookies
13 request.cookie_jar
14 end
一番最初に呼ばれる#cookies
メソッドの中身はこんな感じ、レシーバに対して#cookie_jar
メソッドを呼んでいるだけ。
レシーバのrequest
はActionDispatch::Request
のインスタンス(※include先で定義されている)。
続いて#cookie_jar
メソッドを読んでみましょう。
rails/actionpack/lib/action_dispatch/middleware/cookies.rb
8 C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11#cookie_jar
引き続きRails🍪
ActionDispatch::Request#cookie_jar
11 def cookie_jar
12 fetch_header("action_dispatch.cookies") do
13 self.cookie_jar = Cookies::CookieJar.build(self, cookies)
14 end
15 end
#cookie_jar
メソッドの中身はこんな感じ。
一行目でRackのメソッドであるfetch_header
(Rack::Request::Env#fetch_header
)を呼んでいます。
続いてRack::Request::Env#fetch_header
を読んでみましょう。
rack/lib/rack/request.rb
9 C /vendor/bundle/ruby/2.6.0/gems/rack-2.0.7/lib/rack/request.rb:58#fetch_header
Rack側に移ります🍪
Rack::Request::Env#fetch_header
69 def fetch_header(name, &block)
70 @env.fetch(name, &block)
71 end
#fetch_header
メソッドの中身はこんな感じ。
ここで登場するオブジェクトを確認しましょう。
nameとは
引数のname
は"action_dispatch.cookies"
(参考)
12 fetch_header("action_dispatch.cookies") do
13 self.cookie_jar = Cookies::CookieJar.build(self, cookies)
14 end
&blockとは
引数の&block
はfetch_header
に渡されているブロック
self.cookie_jar = Cookies::CookieJar.build(self, cookies)
。
(参考)
12 fetch_header("action_dispatch.cookies") do
13 self.cookie_jar = Cookies::CookieJar.build(self, cookies)
14 end
レシーバ@env
とは
HTTPヘッダを表すハッシュ。
例えばこんなRackアプリがあった場合
['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
真ん中の{'Content-Type' => 'text/html'}
というハッシュが、Rack上では@env
というインスタンス変数に入っています。
つまり…
fetch_header
は、ヘッダである@env
ハッシュに対して、Hash#fetchメソッドを呼ぶための処理。
すなわち
-
@env
が"action_dispatch.cookies"
というkeyを持っていた場合:- ➡︎そのvalueが返る
-
@env
が"action_dispatch.cookies"
というkeyを持っていなかった場合;- ➡︎ブロック
self.cookie_jar = Cookies::CookieJar.build(self, cookies)
が実行される
- ➡︎ブロック
続いて、ブロックself.cookie_jar = Cookies::CookieJar.build(self, cookies)
が実行された場合の処理を追います。
rails/actionpack/lib/action_dispatch/middleware/cookies.rb
self.cookie_jar =
は、ActionDispatch::Request#cookie_jar=
としてメソッド定義されています。
早速読んでみましょう🍪
ActionDispatch::Request#cookie_jar=
28 def cookie_jar=(jar)
29 set_header "action_dispatch.cookies", jar
30 end
ここで登場する引数のjar
の正体はCookies::CookieJar.build(self, cookies)
ですが、この処理は後で読むので一旦パス。
二行目で、再びRackのメソッドであるset_header
を呼んでいます。
#set_header
の処理を読んでみましょう。
rack/lib/rack/request.rb
再びRackへ🍪
Rack::Request::Env#set_header
79 def set_header(name, v)
80 @env[name] = v
81 end
#fetch_header
メソッドの中身はこんな感じ。
ここで登場するオブジェクトを確認しましょう。
引数nameとは
引数のname
は"action_dispatch.cookies"
引数vとは
v
は先程のjar
(つまりCookies::CookieJar.build(self, cookies)
)
具体的には
@env["action_dispatch.cookies"] = Cookies::CookieJar.build(self, cookies)
という処理が走っています。
このとき、返り値はCookies::CookieJar.build(self, cookies)
になります。
ここまでのまとめ
アプリケーションでcookies
メソッドが呼ばれると、次の2つの処理が走る。
-
@env
ハッシュにおける"action_dispatch.cookies"
keyの存在を確認する。 - 存在する場合はvalueを返す。
存在しない場合は新たに{"action_dispatch.cookies": *Cookies::CookieJar.build(self, cookies)*}
というペアをつくる。
Let's read Rack::Request::Helpers#cookies🍪
ここからは、先ほどパスしたCookies::CookieJar.build(self, cookies)
を読んでいきます。
ActionDispatch::Cookies::CookieJar.build
11 def cookie_jar
12 fetch_header("action_dispatch.cookies") do
13 self.cookie_jar = Cookies::CookieJar.build(self, cookies)
14 end
15 end
↑13行目でself.cookie_jar =
に渡しているCookies::CookieJar.build(self, cookies)
が、その前に
注目
Cookies::CookieJar.build(self, cookies)
ここで引数に入れたものは何?
(self, cookies)
self
は一番最初に出てきたrequest
cookies
は、これもRackのメソッドを呼んでいます。
rack/lib/rack/request.rb
cookiesの正体を探るため、Rack::Request::Helpers#cookies
メソッドを読んでいきましょう🍪
Rack::Request::Helpers#cookies
215 def cookies
216 hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
217 set_header(k, {})
218 end
219 string = get_header HTTP_COOKIE
220
221 return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
222 hash.replace Utils.parse_cookies_header get_header HTTP_COOKIE
223 set_header(RACK_REQUEST_COOKIE_STRING, string)
224 hash
225 end
#cookies
の中身はこんな感じ。
一行づつ
216 hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
217 set_header(k, {})
218 end
fatch_header
は先程と同じメソッドで、@env
ハッシュに対してRACK_REQUEST_COOKIE_HASH
keyをfetch
します。
- keyがあった場合
- ➡️valueを返す
- keyがなかった場合
- ➡️これも先程登場した
set_header
メソッドを使用して、
@env
ハッシュにRACK_REQUEST_COOKIE_HASH: {}
というペアを追加する - 返り値は空の
{}
- ➡️これも先程登場した
(参考)
79 def set_header(name, v)
80 @env[name] = v
81 end
いずれの場合も、返り値をhash
に代入しています。
RACK_REQUEST_COOKIE_HASHとは?
ここで登場する、RACK_REQUEST_COOKIE_HASH
の正体は、rack.request.cookie_hash
。
Rackがサーバーに対して問い合わせするためのメソッド。
返り値はこんな感じ。
(例)
{"_todo_session"=>"BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg==--c34c5fc6de928cde391cccd2b710547c7aab1d06"}
(↑の例はたまたま下記の記事中で見つけたものです)
Rails で捕捉されない例外が発生したらメールを送る
つづきまして
219 string = get_header HTTP_COOKIE
HTTP_COOKIE
を引数として、get_header
メソッドを呼んでいます。
get_header
はRack::Request::Env#get_header
で、定義は以下の通り。
63 def get_header(name)
64 @env[name]
65 end
@env
ハッシュからHTTP_COOKIE
keyを探し、valueを返し、返り値がstring
に代入されます。
ただし、初めてCookieを使用する場合は@env
ハッシュにHTTP_COOKIE
keyが見つからないため、nil
が返ります。
HTTP_COOKIEとは?
ここで登場するHTTP_COOKIE
の正体は、リクエストメッセージに含まれるメタ変数で、Cookie
ヘッダを返しています。
中身はこんな感じ(Cookieの名前と値を=で結ぶことでペアとして表現している)
(例)
(↑の例も先ほどの記事よりお借りしました)
※メタ変数について:
参照
例えば HTTP というプロトコルにおいては、 HTTP_USER_AGENT という名前のメタ変数が HTTP 要求メッセージの User-Agent: 欄の値を提供することになっています
つづき
初めてCookieを使用する場合、ここまでで
-
hash
に{}
-
string
にget_header HTTP_COOKIE
の返り値(初めてCookieを使用する場合はnil
)
が代入されていることになります。
221 return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
RACK_REQUEST_COOKIE_STRING
を引数に、Rack::Request::Env#get_header
メソッドを呼んでstring
と比較。
(参考)
63 def get_header(name)
64 @env[name]
65 end
RACK_REQUEST_COOKIE_STRINGとは?
ここで登場するRACK_REQUEST_COOKIE_STRING
の正体はrack.request.cookie_string
で、こちらもサーバーに対して問い合わせするためのメソッド。
返り値はこんな感じ。
(例)
_todo_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY1OWJmM2I3YWE3YzlhY2UzMmM5Mzk3NjZlMzJkNjU2BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMW5vOTdFQzRtQURmMDJoVlYzT2ZSK1hxQmw3M0ZrNFRwOGpCZWdXbmx3V1k9BjsARg%3D%3D--c34c5fc6de928cde391cccd2b710547c7aab1d06
(御多分に洩れず↑の例も先ほどの記事より)
…HTTP_COOKIE
と同じですね!
つづき
221 return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
==
の場合はここでreturn hash
なぜ?
次行以降はリクエストヘッダからやって来たhash
(初めてのCookieを使用する場合は空の{}
)をHTTP_COOKIE
で更新する処理となっています。
ハッシュの中身が変更されていない場合、置き換える必要がないため。
初めてCookieを使用する場合、string
と get_header(RACK_REQUEST_COOKIE_STRING)
の返り値はいずれもnilのため、ここでcookies
メソッドの処理はhash
(空の{}
)を返して終了します。
つづき(すでにCookieが存在する場合のみ)
222 hash.replace Utils.parse_cookies_header string
ここでは、
Utils.parse_cookies_header(string)
の返り値{ Cookieの名前: Cookieの値 }
で、hash
(fetch_header(RACK_REQUEST_COOKIE_HASH)
の返り値)をreplace
しています。
Utils.parse_cookies_header
はRack::Utils
のメソッドで、
詳しくは読みませんが、
string
(Cookieの名前=Cookieの値
)を{Cookieの名前: Cookieの値}
のようなハッシュに変換する役割を担っています。
(参考)
209 def parse_cookies_header(header)
215 cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
216 cookies.each_with_object({}) { |(k, v), hash| hash[k] = Array === v ? v.first : v }
217 end
(↑each_with_object
を使用して{}
に名前と値を代入している)
つづき(すでにCookieが存在する場合のみ)
223 set_header(RACK_REQUEST_COOKIE_STRING, string)
224 hash
225 end
すなわち、ここで
@env[RACK_REQUEST_COOKIE_STRING] = string
という処理を実行。
(参考)
79 def set_header(name, v)
80 @env[name] = v
81 end
最後にhash
({Cookieの名前: Cookieの値}
) を返しています。
ここまでのまとめ
メソッドcookies
を呼ぶとき
- 初めてCookieを使用する場合: ヘッダ(
@env
)に{RACK_REQUEST_COOKIE_HASH: {} }
が追加される- 最終的な返り値は空の
{}
- 最終的な返り値は空の
- すでにCookieが存在する場合: ヘッダ(
@env
)に{RACK_REQUEST_COOKIE_STRING: リクエストメッセージのCookieヘッダ}
が追加される- 最終的な返り値は
{Cookieの名前: Cookieの値}
- 最終的な返り値は
改めましてActionDispatch::Cookies::CookieJar.build
actionpack/lib/action_dispatch/middleware/cookies.rb
お待たせしました。Cookies::CookieJar.build
を読んでいきましょう。
再びRailsへ🍪
ActionDispatch::Cookies::CookieJar.build
170 # class Cookies
# ...
272 # class CookieJar
# ...
289 def self.build(req, cookies)
290 new(req).tap do |jar|
291 jar.update(cookies)
292 end
293 end
Cookies::CookieJar.build
の中身はこんな感じ。
tap
を使用して処理をチェーンしているのは、最終的にできあがったCookies::CookieJarインスタンスを返すためではないかと考えられます。
(tap
を挟まない場合、返り値がjar.update
の実行結果になる)
ここで登場するオブジェクトを確認しましょう。
reqとは
引数のreq
は一番最初のrequest
(ActionDispatch::Request
のインスタンス)
cookiesとは
cookies
は先ほど読んだRack::Request::Helpers#cookies
の返り値({Cookieの名前: Cookieの値}
あるいは空の{}
)
Cookies::CookieJar.buildは何をやっているのか
ここでの役割は2つ
-
Cookies::CoookieJar
のインスタンスを作る - できたインスタンスに対して
Cookies::CookieJar#update
メソッドを呼ぶ
順番に処理を読んでいきます。
new(req)
170 # class Cookies
# ...
272 # class CookieJar
# ...
289 def self.build(req, cookies)
290 new(req).tap do |jar| # ←
291 jar.update(cookies)
292 end
293 end
new(req)
(インスタンス化)すると、initialize
が呼ばれます。
295 attr_reader :request
296
297 def initialize(request)
298 @set_cookies = {}
299 @delete_cookies = {}
300 @request = request
301 @cookies = {}
302 @committed = false
303 end
ActionDispatch::Cookies::CookieJar#initialize
は各インスタンス変数を初期化するだけの処理。
@request
には引数で渡されている一番最初のrequest
が入ります。
attr_reader
によってメソッドとしてアクセスできるようになります。
つづいてtap以下の処理
170 # class Cookies
# ...
272 # class CookieJar
# ...
290 new(req).tap do |jar|
291 jar.update(cookies) # ←
292 end
tap
ブロックの中で実行されているupdate
メソッドはこちら
334 def update(other_hash)
335 @cookies.update other_hash.stringify_keys
336 self
337 end
引数other_hashとは
引数other_hash
は先ほどのcookies
、つまり{Cookieの名前: Cookieの値}
もしくは空の{}
。
@cookies
とは
@cookies
はinitialize
の中で初期化したこの部分
301 @cookies = {}
ここでは、この空の{}
に対してupdate
メソッドを呼んでいます。
Hash#update
メソッドはHash#merge!
のエイリアス。
処理を実行すると、こうなります。
@cookies = {'Cookieの名前': 'Cookieの値'}
(other_hash
が空の{}
の場合は、何も起きない)
その後、self
(=jar
、つまりCookies::CookieJar.build
でできたインスタンス)を返しています。
(余談)
↑の方でtap
を使った理由を「インスタンスを返すため」と記述しているのですが、ここでもインスタンス自身を返しているため、どちらか片方の処理で良いのではという疑惑あり…PRチャンス?
また、先述の通りこのインスタンスは
13 self.cookie_jar = Cookies::CookieJar.build(self, cookies)
ここでself.cookie_jar=
メソッドに引数として渡されて、
28 def cookie_jar=(jar)
29 set_header "action_dispatch.cookies", jar
30 end
ここで@env = {"action_dispatch.cookies": できあがったインスタンス}
としてヘッダーに追加されます。
ここまでのまとめ
アプリケーションからcookies
メソッドを呼ぶと、結果的には次のようなリクエストヘッダができる
@env = {
"action_dispatch.cookies": Cookies::CookieJarのインスタンス},
# 初めてCookieを使用する場合
RACK_REQUEST_COOKIE_HASH: {},
# すでにCookieが存在する場合
RACK_REQUEST_COOKIE_STRING: リクエストメッセージから受け取ったCookieヘッダ
}
最後に[]=
cookies[:hoge] = :fuga
cookies
に値を代入する処理を見ていきます🍪
actionpack/lib/action_dispatch/middleware/cookies.rb
14 C /vendor/bundle/ruby/2.6.0/gems/actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374#[]=
ここからはRailsのお話🍪
#[]=
メソッドを読んでいきましょう。
ActionDispatch::Cookies::CookieJar#[]=
170 # class Cookies
# ...
272 # class CookieJar
# ...
372 # Sets the cookie named +name+. The second argument may be the cookie's
373 # value or a hash of options as documented above.
374 def []=(name, options)
375 if options.is_a?(Hash)
376 options.symbolize_keys!
377 value = options[:value]
378 else
379 value = options
380 options = { value: value }
381 end
382
383 handle_options(options)
384
385 if @cookies[name.to_s] != value || options[:expires]
386 @cookies[name.to_s] = value
387 @set_cookies[name.to_s] = options
388 @delete_cookies.delete(name.to_s)
389 end
390
391 value
392 end
ActionDispatch::Cookies::CookieJar#[]=
の中身はこんな感じ。
一行ずつ
374 def []=(name, options)
これを定義すると、
[:name] = options
という形で値を代入できるようになります。
それではここで、Railsアプリからcookies[]= に渡せる値を確認しましょう🍪
cookie[:hoge] = "fuga"
のように文字列を渡すことが多いと思いますが、オプションを細かく設定する場合、右辺にはハッシュを渡すこともできます。
cookies[:hoge] = {
value: 'Cookieの値',
expires: 'Cookieの有効期限',
path: 'Cookieが適用されるパス(デフォルトは/)',
domain: 'Cookieが適用されるドメイン',
tld_length: 'domain: :allの時、TLDの一部として解釈される短い(3文字以下の)ドメインを使用するときに、TLDの長さを明示的に設定',
secure: '暗号化通信のみを有効化(デフォルトはfalse)',
httponly: 'スクリプト経由もしくはHTTP通信のみを有効化(デフォルトはfalse)'
}
これを踏まえて、引き続き処理を読んでいきます。
①引数optionsがハッシュの場合
375 if options.is_a?(Hash)
376 options.symbolize_keys!
377 value = options[:value]
378 else
cookies[:hoge] = {"value" => "fuga}
の場合、symbolize_keys!
で{:value => "fuga"}
に変換。
変数value
に"fuga"
を代入。
②引数optionsがハッシュ以外の場合
378 else
379 value = options
380 options = { value: value }
381 end
cookies[:hoge] = "fuga"
の場合、変数value
に"fuga"
を代入。
options
に {value: "fuga"}
を再代入。
今の状態
①②いずれの場合も、
-
options
は{value: "fuga"}
-
value
はfuga
となっている。
つづき
383 handle_options(options)
同じクラスのhandle_options(options)
を呼びます。
ActionDispatch::Cookies::CookieJar#handle_options
handle_options
はoptions
をいい感じにするためのメソッドです。
350 def handle_options(options) # :nodoc:
351 if options[:expires].respond_to?(:from_now)
352 options[:expires] = options[:expires].from_now
353 end
354
355 options[:path] ||= "/"
356
357 if options[:domain] == :all || options[:domain] == "all"
358 # If there is a provided tld length then we use it otherwise default domain regexp.
359 domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
360
361 # If host is not ip and matches domain regexp.
362 # (ip confirms to domain regexp so we explicitly check for ip)
363 options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
364 ".#{$&}"
365 end
366 elsif options[:domain].is_a? Array
367 # If host matches one of the supplied domains without a dot in front of it.
368 options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
369 end
370 end
中身はこんな感じ。
一行ずつ
351 if options[:expires].respond_to?(:from_now)
352 options[:expires] = options[:expires].from_now
353 end
cookies
メソッドに有効期限を設定した場合(例:1.week
)、from_now
で具体的な日時に変換してoptions[:expires]
に再代入。
from_now
はsince
のエイリアス。
355 options[:path] ||= "/"
cookies
メソッドにパスが指定されていない
場合、ルートパスを設定(デフォルト値として)
357 if options[:domain] == :all || options[:domain] == "all"
358 # If there is a provided tld length then we use it otherwise default domain regexp.
359 domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
360
361 # If host is not ip and matches domain regexp.
362 # (ip confirms to domain regexp so we explicitly check for ip)
363 options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
364 ".#{$&}"
365 end
366 elsif options[:domain].is_a? Array
367 # If host matches one of the supplied domains without a dot in front of it.
368 options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
369 end
370 end
誰か正規表現に強い方…………
-
[:domain]
オプションが:all
or"all"
の場合- [:tld_length]が指定されている場合は
/([^.]+\.?){#{options[:tld_length]}}$/
- そうでない場合は
DOMAIN_REGEXP
を変数
domain_regexp
に代入
(※DOMAIN_REGEXP
は/[^.]*\.([^.]*|..\...|...\...)$/
)リクエストメッセージのホストが
/^[\d.]+$/
に一致しない場合&&変数domain_regexp
に一致する場合はoptions[:domain]
を".#{$&}"
で置き換える - [:tld_length]が指定されている場合は
-
[:domain]
オプションが配列の場合-
options[:domain]
を、リクエストメッセージのホストが含まれる要素で置き換える
-
ここでやったこと
- 有効期限を具体的な日時に変換
- パスの設定がない場合にデフォルト値を設定
- ドメインをいい感じに再代入(?)
ActionDispatch::Cookies::CookieJar#[]=のつづき
今の状態
①②いずれの場合も、
-
options
は{value: 'fuga'}
-
value
はfuga
となっています。
ActionDispatch::Cookies::CookieJar#[]=のつづき
385 if @cookies[name.to_s] != value || options[:expires]
386 @cookies[name.to_s] = value
387 @set_cookies[name.to_s] = options
388 @delete_cookies.delete(name.to_s)
389 end
390
391 value
392 end
@cookies["hoge"]
のvalueが"fuga"
でない場合(つまり値が変わっている場合)、もしくは有効期限の設定がない場合に、
-
@cookies["hoge"]
に"fuga"
を代入 -
@set_cookies
にoption{value: 'fuga'}
を代入 -
@delete_cookies
から[:hoge]
を削除する
をして、最後に"fuga"
を返します。
最後に
trace_locationでログに残っていたのはここまでですが、
先述の通りActionDispatch::Cookies
はcall
メソッドを持ったRackミドルウェアとして実装されており、
Railsアプリケーションから使用されています。
$ rails middleware
use Webpacker::DevServerProxy
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use WebConsole::Middleware
use ActionDispatch::DebugExceptions
use ActionDispatch::ActionableExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies # ←ここ
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Myapp::Application.routes
このことから、Railsアプリケーションにリクエストがあった際、Rackアプリとして次の処理が呼ばれることがわかります。
170 # class Cookies
# ...
637 def initialize(app)
638 @app = app
639 end
640
641 def call(env)
642 request = ActionDispatch::Request.new env
643
644 status, headers, body = @app.call(env)
645
646 if request.have_cookie_jar?
647 cookie_jar = request.cookie_jar
648 unless cookie_jar.committed?
649 cookie_jar.write(headers)
650 if headers[HTTP_HEADER].respond_to?(:join)
651 headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
652 end
653 end
654 end
655
656 [status, headers, body]
657 end
中身は詳しく追っていきませんが、
647 cookie_jar = request.cookie_jar
648 unless cookie_jar.committed?
649 cookie_jar.write(headers)
650 if headers[HTTP_HEADER].respond_to?(:join)
651 headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
652 end
653 end
この辺りでヘッダーへの書き込みが行われていることが確認できました。
今回追うことができなかった処理・残された疑問点
-
"rack.request.cookie_hash"
がヘッダーに書き込まれるタイミング -
Cookies::CookieJar#write
→Cookies::CookieJar#make_set_cookie_header
から、最終的に呼ばれる::Rack::Utils.add_cookie_to_header
(↑の資料では未出)- ヘッダーに書き込む文字列の成形を行なっていることが確認できるが、その後実際にヘッダーに書き込まれるタイミング
おつかれさまでした🍪
参考資料
- Ruby on Railsチュートリアル 第9章 発展的なログイン機構 9.1.2 ログイン状態の保持
- RFC 6265 HTTP 状態管理の仕組み( “HTTP cookie” )
- SuikaWiki メタ変数 - プロトコル依存のメタ変数
- Rack: a Ruby Webserver Interface
- TraceLocation
おまけ
TraceLocationで作成したログは次の通りです(マークダウン形式)
Generated by trace_location at 2019-06-05 20:28:50 +0900
actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:12
ActionController::Cookies#cookies
def cookies
request.cookie_jar
end
# called from /Users/misakishioi/Projects/myapp/app/controllers/blogs_controller.rb:8
actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:11
ActionDispatch::Request#cookie_jar
def cookie_jar
fetch_header("action_dispatch.cookies") do
self.cookie_jar = Cookies::CookieJar.build(self, cookies)
end
end
# called from actionpack-6.0.0.rc1/lib/action_controller/metal/cookies.rb:13
rack-2.0.7/lib/rack/request.rb:58
Rack::Request::Env#fetch_header
def fetch_header(name, &block)
@env.fetch(name, &block)
end
# called from actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:12
actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:374
ActionDispatch::Cookies::CookieJar#[]=
def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
value = options[:value]
else
value = options
options = { value: value }
end
handle_options(options)
if @cookies[name.to_s] != value || options[:expires]
@cookies[name.to_s] = value
@set_cookies[name.to_s] = options
@delete_cookies.delete(name.to_s)
end
value
end
# called from /Users/misakishioi/Projects/myapp/app/controllers/blogs_controller.rb:8
actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:350
ActionDispatch::Cookies::CookieJar#handle_options
def handle_options(options) # :nodoc:
if options[:expires].respond_to?(:from_now)
options[:expires] = options[:expires].from_now
end
options[:path] ||= "/"
if options[:domain] == :all || options[:domain] == "all"
# If there is a provided tld length then we use it otherwise default domain regexp.
domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
# If host is not ip and matches domain regexp.
# (ip confirms to domain regexp so we explicitly check for ip)
options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
".#{$&}"
end
elsif options[:domain].is_a? Array
# If host matches one of the supplied domains without a dot in front of it.
options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
end
end
# called from actionpack-6.0.0.rc1/lib/action_dispatch/middleware/cookies.rb:383