-
Notifications
You must be signed in to change notification settings - Fork 250
/
Copy pathcookie_jar.rb
251 lines (205 loc) · 7.2 KB
/
cookie_jar.rb
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
# frozen_string_literal: true
require 'uri'
require 'time'
module Rack
module Test
# Represents individual cookies in the cookie jar. This is considered private
# API and behavior of this class can change at any time.
class Cookie # :nodoc:
include Rack::Utils
# The name of the cookie, will be a string
attr_reader :name
# The value of the cookie, will be a string or nil if there is no value.
attr_reader :value
# The raw string for the cookie, without options. Will generally be in
# name=value format is name and value are provided.
attr_reader :raw
def initialize(raw, uri = nil, default_host = DEFAULT_HOST)
@default_host = default_host
uri ||= default_uri
# separate the name / value pair from the cookie options
@raw, options = raw.split(/[;,] */n, 2)
@name, @value = parse_query(@raw, ';').to_a.first
@options = Hash[parse_query(options, ';').map { |k, v| [k.downcase, v] }]
if domain = @options['domain']
@exact_domain_match = false
domain[0] = '' if domain[0] == '.'
else
# If the domain attribute is not present in the cookie,
# the domain must match exactly.
@exact_domain_match = true
@options['domain'] = (uri.host || default_host)
end
# Set the path for the cookie to the directory containing
# the request if it isn't set.
@options['path'] ||= uri.path.sub(/\/[^\/]*\Z/, '')
end
# Wether the given cookie can replace the current cookie in the cookie jar.
def replaces?(other)
[name.downcase, domain, path] == [other.name.downcase, other.domain, other.path]
end
# Whether the cookie has a value.
def empty?
@value.nil? || @value.empty?
end
# The explicit or implicit domain for the cookie.
def domain
@options['domain']
end
# Whether the cookie has the secure flag, indicating it can only be sent over
# an encrypted connection.
def secure?
@options.key?('secure')
end
# Whether the cookie has the httponly flag, indicating it is not available via
# a javascript API.
def http_only?
@options.key?('httponly')
end
# The explicit or implicit path for the cookie.
def path
([*@options['path']].first.split(',').first || '/').strip
end
# A Time value for when the cookie expires, if the expires option is set.
def expires
Time.parse(@options['expires']) if @options['expires']
end
# Whether the cookie is currently expired.
def expired?
expires && expires < Time.now
end
# Whether the cookie is valid for the given URI.
def valid?(uri)
uri ||= default_uri
uri.host = @default_host if uri.host.nil?
!!((!secure? || (secure? && uri.scheme == 'https')) &&
uri.host =~ Regexp.new("#{'^' if @exact_domain_match}#{Regexp.escape(domain)}$", Regexp::IGNORECASE))
end
# Cookies that do not match the URI will not be sent in requests to the URI.
def matches?(uri)
!expired? && valid?(uri) && uri.path.start_with?(path)
end
# Order cookies by name, path, and domain.
def <=>(other)
[name, path, domain.reverse] <=> [other.name, other.path, other.domain.reverse]
end
# A hash of cookie options, including the cookie value, but excluding the cookie name.
def to_h
hash = @options.merge(
'value' => @value,
'HttpOnly' => http_only?,
'secure' => secure?
)
hash.delete('httponly')
hash
end
alias to_hash to_h
private
# The default URI to use for the cookie, including just the host.
def default_uri
URI.parse('//' + @default_host + '/')
end
end
# Represents all cookies for a session, handling adding and
# removing cookies, and finding which cookies apply to a given
# request. This is considered private API and behavior of this
# class can change at any time.
class CookieJar # :nodoc:
DELIMITER = '; '.freeze
def initialize(cookies = [], default_host = DEFAULT_HOST)
@default_host = default_host
@cookies = cookies.sort!
end
# Ensure the copy uses a distinct cookies array.
def initialize_copy(other)
super
@cookies = @cookies.dup
end
# Return the value for first cookie with the given name, or nil
# if no such cookie exists.
def [](name)
name = name.to_s
@cookies.each do |cookie|
return cookie.value if cookie.name == name
end
nil
end
# Set a cookie with the given name and value in the
# cookie jar.
def []=(name, value)
merge("#{name}=#{Rack::Utils.escape(value)}")
end
# Return the first cookie with the given name, or nil if
# no such cookie exists.
def get_cookie(name)
@cookies.each do |cookie|
return cookie if cookie.name == name
end
nil
end
# Delete all cookies with the given name from the cookie jar.
def delete(name)
@cookies.reject! do |cookie|
cookie.name == name
end
nil
end
# Add a string of raw cookie information to the cookie jar,
# if the cookie is valid for the given URI.
# Cookies should be separated with a newline.
def merge(raw_cookies, uri = nil)
return unless raw_cookies
raw_cookies = raw_cookies.split("\n") if raw_cookies.is_a? String
raw_cookies.each do |raw_cookie|
next if raw_cookie.empty?
cookie = Cookie.new(raw_cookie, uri, @default_host)
self << cookie if cookie.valid?(uri)
end
end
# Add a Cookie to the cookie jar.
def <<(new_cookie)
@cookies.reject! do |existing_cookie|
new_cookie.replaces?(existing_cookie)
end
@cookies << new_cookie
@cookies.sort!
end
# Return a raw cookie string for the cookie header to
# use for the given URI.
def for(uri)
buf = String.new
delimiter = nil
each_cookie_for(uri) do |cookie|
if delimiter
buf << delimiter
else
delimiter = DELIMITER
end
buf << cookie.raw
end
buf
end
# Return a hash cookie names and cookie values for cookies in the jar.
def to_hash
cookies = {}
@cookies.each do |cookie|
cookies[cookie.name] = cookie.value
end
cookies
end
private
# Yield each cookie that matches for the URI.
#
# The cookies are sorted by most specific first. So, we loop through
# all the cookies in order and add it to a hash by cookie name if
# the cookie can be sent to the current URI. It's added to the hash
# so that when we are done, the cookies will be unique by name and
# we'll have grabbed the most specific to the URI.
def each_cookie_for(uri)
@cookies.each do |cookie|
yield cookie if !uri || cookie.matches?(uri)
end
end
end
end
end