-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathcookie.cr
275 lines (240 loc) · 8.36 KB
/
cookie.cr
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
require "./common"
module HTTP
# Represents a cookie with all its attributes. Provides convenient
# access and modification of them.
class Cookie
property name : String
property value : String
property path : String
property expires : Time?
property domain : String?
property secure : Bool
property http_only : Bool
property extension : String?
def_equals_and_hash name, value, path, expires, domain, secure, http_only
def initialize(@name : String, value : String, @path : String = "/",
@expires : Time? = nil, @domain : String? = nil,
@secure : Bool = false, @http_only : Bool = false,
@extension : String? = nil)
@name = URI.unescape name
@value = URI.unescape value
end
def to_set_cookie_header
path = @path
expires = @expires
domain = @domain
String.build do |header|
header << "#{URI.escape @name}=#{URI.escape value}"
header << "; domain=#{domain}" if domain
header << "; path=#{path}" if path
header << "; expires=#{HTTP.rfc1123_date(expires)}" if expires
header << "; Secure" if @secure
header << "; HttpOnly" if @http_only
header << "; #{@extension}" if @extension
end
end
def to_cookie_header
"#{@name}=#{URI.escape value}"
end
def expired?
if e = expires
e < Time.now
else
false
end
end
# :nodoc:
module Parser
module Regex
CookieName = /[^()<>@,;:\\"\/\[\]?={} \t\x00-\x1f\x7f]+/
CookieOctet = /[!#-+\--:<-\[\]-~]/
CookieValue = /(?:"#{CookieOctet}*"|#{CookieOctet}*)/
CookiePair = /(?<name>#{CookieName})=(?<value>#{CookieValue})/
DomainLabel = /[A-Za-z0-9\-]+/
DomainIp = /(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/
Time = /(?:\d{2}:\d{2}:\d{2})/
Month = /(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/
Weekday = /(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)/
Wkday = /(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)/
PathValue = /[^\x00-\x1f\x7f;]+/
DomainValue = /(?:#{DomainLabel}(?:\.#{DomainLabel})?|#{DomainIp})+/
Zone = /(?:UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[+-]?\d{4})/
RFC1036Date = /#{Weekday}, \d{2}-#{Month}-\d{2} #{Time} GMT/
RFC1123Date = /#{Wkday}, \d{1,2} #{Month} \d{2,4} #{Time} #{Zone}/
ANSICDate = /#{Wkday} #{Month} (?:\d{2}| \d) #{Time} \d{4}/
SaneCookieDate = /(?:#{RFC1123Date}|#{RFC1036Date}|#{ANSICDate})/
ExtensionAV = /(?<extension>[^\x00-\x1f\x7f]+)/
HttpOnlyAV = /(?<http_only>HttpOnly)/i
SecureAV = /(?<secure>Secure)/i
PathAV = /Path=(?<path>#{PathValue})/i
DomainAV = /Domain=(?<domain>#{DomainValue})/i
MaxAgeAV = /Max-Age=(?<max_age>[0-9]*)/i
ExpiresAV = /Expires=(?<expires>#{SaneCookieDate})/i
CookieAV = /(?:#{ExpiresAV}|#{MaxAgeAV}|#{DomainAV}|#{PathAV}|#{SecureAV}|#{HttpOnlyAV}|#{ExtensionAV})/
end
CookieString = /(?:^|; )#{Regex::CookiePair}/
SetCookieString = /^#{Regex::CookiePair}(?:; #{Regex::CookieAV})*$/
def parse_cookies(header)
header.scan(CookieString).each do |pair|
yield Cookie.new(pair["name"], pair["value"])
end
end
def parse_cookies(header)
cookies = [] of Cookie
parse_cookies(header) { |cookie| cookies << cookie }
cookies
end
def parse_set_cookie(header)
match = header.match(SetCookieString)
return unless match
expires = if max_age = match["max_age"]?
Time.now + max_age.to_i.seconds
else
parse_time(match["expires"]?)
end
Cookie.new(
match["name"], match["value"],
path: match["path"]? || "/",
expires: expires,
domain: match["domain"]?,
secure: match["secure"]? != nil,
http_only: match["http_only"]? != nil,
extension: match["extension"]?
)
end
private def parse_time(string)
return unless string
HTTP.parse_time(string)
end
extend self
end
end
# Represents a collection of cookies as it can be present inside
# a HTTP request or response.
class Cookies
include Enumerable(Cookie)
# Create a new instance by parsing the `Cookie` and `Set-Cookie`
# headers in the given `HTTP::Headers`.
#
# See `HTTP::Request#cookies` and `HTTP::Client::Response#cookies`.
def self.from_headers(headers) : self
new.tap { |cookies| cookies.fill_from_headers(headers) }
end
# Filling cookies by parsing the `Cookie` and `Set-Cookie`
# headers in the given `HTTP::Headers`.
def fill_from_headers(headers)
if values = headers.get?("Cookie")
values.each do |header|
Cookie::Parser.parse_cookies(header) { |cookie| self << cookie }
end
end
if values = headers.get?("Set-Cookie")
values.each do |header|
Cookie::Parser.parse_set_cookie(header).try { |cookie| self << cookie }
end
end
self
end
# Create a new empty instance.
def initialize
@cookies = {} of String => Cookie
end
# Set a new cookie in the collection with a string value.
# This creates a never expiring, insecure, not HTTP only cookie with
# no explicit domain restriction and the path `/`.
#
# ```
# request = HTTP::Request.new "GET", "/"
# request.cookies["foo"] = "bar"
# ```
def []=(key, value : String)
self[key] = Cookie.new(key, value)
end
# Set a new cookie in the collection to the given `HTTP::Cookie`
# instance. The name attribute must match the given *key*, else
# `ArgumentError` is raised.
#
# ```
# response = HTTP::Client::Response.new(200)
# response.cookies["foo"] = HTTP::Cookie.new("foo", "bar", "/admin", Time.now + 12.hours, secure: true)
# ```
def []=(key, value : Cookie)
unless key == value.name
raise ArgumentError.new("Cookie name must match the given key")
end
@cookies[key] = value
end
# Get the current `HTTP::Cookie` for the given *key*.
#
# ```
# request.cookies["foo"].value # => "bar"
# ```
def [](key)
@cookies[key]
end
# Get the current `HTTP::Cookie` for the given *key* or `nil` if none is set.
#
# ```
# request = HTTP::Request.new "GET", "/"
# request.cookies["foo"]? # => nil
# request.cookies["foo"] = "bar"
# request.cookies["foo"]?.try &.value # > "bar"
# ```
def []?(key)
@cookies[key]?
end
# Returns `true` if a cookie with the given *key* exists.
#
# ```
# request.cookies.has_key?("foo") # => true
# ```
def has_key?(key)
@cookies.has_key?(key)
end
# Add the given *cookie* to this collection, overrides an existing cookie
# with the same name if present.
#
# ```
# response.cookies << HTTP::Cookie.new("foo", "bar", http_only: true)
# ```
def <<(cookie : Cookie)
self[cookie.name] = cookie
end
# Yields each `HTTP::Cookie` in the collection.
def each(&block : Cookie ->)
@cookies.values.each do |cookie|
yield cookie
end
end
# Returns an iterator over the cookies of this collection.
def each
@cookies.each_value
end
# Whether the collection contains any cookies.
def empty?
@cookies.empty?
end
# Adds `Cookie` headers for the cookies in this collection to the
# given `HTTP::Header` instance and returns it. Removes any existing
# `Cookie` headers in it.
def add_request_headers(headers)
headers.delete("Cookie")
headers.add("Cookie", map(&.to_cookie_header).join("; ")) unless empty?
headers
end
# Adds `Set-Cookie` headers for the cookies in this collection to the
# given `HTTP::Header` instance and returns it. Removes any existing
# `Set-Cookie` headers in it.
def add_response_headers(headers)
headers.delete("Set-Cookie")
each do |cookie|
headers.add("Set-Cookie", cookie.to_set_cookie_header)
end
headers
end
# Returns this collection as a plain `Hash`.
def to_h
@cookies.dup
end
end
end