/
session.ex
140 lines (108 loc) · 3.9 KB
/
session.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
defmodule Plug.Session do
@moduledoc """
A plug to handle session cookies and session stores.
The session is accessed via functions on `Plug.Conn`. Cookies and
session have to be fetched with `Plug.Conn.fetch_session/1` before the
session can be accessed.
The session is also lazy. Once configured, a cookie header with the
session will only be sent to the client if something is written to the
session in the first place.
When using `Plug.Session`, also consider using `Plug.CSRFProtection`
to avoid Cross Site Request Forgery attacks.
## Session stores
See `Plug.Session.Store` for the specification session stores are required to
implement.
Plug ships with the following session stores:
* `Plug.Session.ETS`
* `Plug.Session.COOKIE`
## Options
* `:store` - session store module (required);
* `:key` - session cookie key (required);
* `:domain` - see `Plug.Conn.put_resp_cookie/4`;
* `:max_age` - see `Plug.Conn.put_resp_cookie/4`;
* `:path` - see `Plug.Conn.put_resp_cookie/4`;
* `:secure` - see `Plug.Conn.put_resp_cookie/4`;
* `:http_only` - see `Plug.Conn.put_resp_cookie/4`;
* `:same_site` - see `Plug.Conn.put_resp_cookie/4`;
* `:extra` - see `Plug.Conn.put_resp_cookie/4`;
Additional options can be given to the session store, see the store's
documentation for the options it accepts.
## Examples
plug Plug.Session, store: :ets, key: "_my_app_session", table: :session
"""
alias Plug.Conn
@behaviour Plug
@cookie_opts [:domain, :max_age, :path, :secure, :http_only, :extra, :same_site]
@impl true
def init(opts) do
store = Plug.Session.Store.get(Keyword.fetch!(opts, :store))
key = Keyword.fetch!(opts, :key)
cookie_opts = Keyword.take(opts, @cookie_opts)
store_opts = Keyword.drop(opts, [:store, :key] ++ @cookie_opts)
store_config = store.init(store_opts)
%{
store: store,
store_config: store_config,
key: key,
cookie_opts: cookie_opts
}
end
@impl true
def call(conn, config) do
Conn.put_private(conn, :plug_session_fetch, fetch_session(config))
end
defp fetch_session(config) do
%{store: store, store_config: store_config, key: key} = config
fn conn ->
{sid, session} =
if cookie = conn.cookies[key] do
store.get(conn, cookie, store_config)
else
{nil, %{}}
end
session = Map.merge(session, Map.get(conn.private, :plug_session, %{}))
conn
|> Conn.put_private(:plug_session, session)
|> Conn.put_private(:plug_session_fetch, :done)
|> Conn.register_before_send(before_send(sid, config))
end
end
defp before_send(sid, config) do
fn conn ->
case Map.get(conn.private, :plug_session_info) do
:write ->
value = put_session(sid, conn, config)
put_cookie(value, conn, config)
:drop ->
drop_session(sid, conn, config)
:renew ->
renew_session(sid, conn, config)
:ignore ->
conn
nil ->
conn
end
end
end
defp drop_session(sid, conn, config) do
if sid do
delete_session(sid, conn, config)
delete_cookie(conn, config)
else
conn
end
end
defp renew_session(sid, conn, config) do
if sid, do: delete_session(sid, conn, config)
value = put_session(nil, conn, config)
put_cookie(value, conn, config)
end
defp put_session(sid, conn, %{store: store, store_config: store_config}),
do: store.put(conn, sid, conn.private[:plug_session], store_config)
defp delete_session(sid, conn, %{store: store, store_config: store_config}),
do: store.delete(conn, sid, store_config)
defp put_cookie(value, conn, %{cookie_opts: cookie_opts, key: key}),
do: Conn.put_resp_cookie(conn, key, value, cookie_opts)
defp delete_cookie(conn, %{cookie_opts: cookie_opts, key: key}),
do: Conn.delete_resp_cookie(conn, key, cookie_opts)
end