http-cookie 0.1.5 → 1.0.0.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +3 -1
- data/lib/http/cookie.rb +269 -141
- data/lib/http/cookie/scanner.rb +215 -0
- data/lib/http/cookie/version.rb +1 -1
- data/lib/http/cookie_jar.rb +1 -1
- data/lib/http/cookie_jar/hash_store.rb +2 -2
- data/test/test_http_cookie.rb +94 -22
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9f5ccaec04a2b731f1120d21079f05fda32dc05f
|
4
|
+
data.tar.gz: aea993fb843b5218522f7a12adf189112dd8a477
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f02c9afbb827b9a35e2417e4104488d3cfc237ac9ec8007c6b77b96b61c2b5c1f3a74f10660211ff1e4e2ee5000f28ddb1052148ce3cba5ccc1d2d3d49e135a8
|
7
|
+
data.tar.gz: 8316457b65faeb9d19449bf49e11b920d53d9c5c8e37edf8d99d02b32fcf30350dff5c270cad654a8e0d4dcc45950894d59496450a7fde2f14e339a1b5b0d077
|
data/.travis.yml
CHANGED
data/lib/http/cookie.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# :markup: markdown
|
1
2
|
require 'http/cookie/version'
|
2
3
|
require 'time'
|
3
4
|
require 'uri'
|
@@ -7,11 +8,12 @@ module HTTP
|
|
7
8
|
autoload :CookieJar, 'http/cookie_jar'
|
8
9
|
end
|
9
10
|
|
10
|
-
# In Ruby < 1.9.3 URI() does not accept
|
11
|
+
# In Ruby < 1.9.3 URI() does not accept a URI object.
|
11
12
|
if RUBY_VERSION < "1.9.3"
|
12
13
|
begin
|
13
14
|
URI(URI(''))
|
14
15
|
rescue
|
16
|
+
# :nodoc:
|
15
17
|
def URI(url)
|
16
18
|
url.is_a?(URI) ? url : URI.parse(url)
|
17
19
|
end
|
@@ -20,13 +22,17 @@ end
|
|
20
22
|
|
21
23
|
# This class is used to represent an HTTP Cookie.
|
22
24
|
class HTTP::Cookie
|
23
|
-
# Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at
|
25
|
+
# Maximum number of bytes per cookie (RFC 6265 6.1 requires 4096 at
|
26
|
+
# least)
|
24
27
|
MAX_LENGTH = 4096
|
25
|
-
# Maximum number of cookies per domain (RFC 6265 6.1 requires 50 at
|
28
|
+
# Maximum number of cookies per domain (RFC 6265 6.1 requires 50 at
|
29
|
+
# least)
|
26
30
|
MAX_COOKIES_PER_DOMAIN = 50
|
27
|
-
# Maximum number of cookies total (RFC 6265 6.1 requires 3000 at
|
31
|
+
# Maximum number of cookies total (RFC 6265 6.1 requires 3000 at
|
32
|
+
# least)
|
28
33
|
MAX_COOKIES_TOTAL = 3000
|
29
34
|
|
35
|
+
# :stopdoc:
|
30
36
|
UNIX_EPOCH = Time.at(0)
|
31
37
|
|
32
38
|
PERSISTENT_PROPERTIES = %w[
|
@@ -52,31 +58,91 @@ class HTTP::Cookie
|
|
52
58
|
end
|
53
59
|
private :check_string_type
|
54
60
|
end
|
61
|
+
# :startdoc:
|
55
62
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
63
|
+
# The cookie name. It may not be nil or empty.
|
64
|
+
#
|
65
|
+
# Trying to set a value with the normal setter method will raise
|
66
|
+
# ArgumentError only when it contains any of these characters:
|
67
|
+
# control characters (\x00-\x1F and \x7F), space and separators
|
68
|
+
# `,;\"=`.
|
69
|
+
#
|
70
|
+
# Note that RFC 6265 4.1.1 lists more characters disallowed for use
|
71
|
+
# in a cookie name, which are these: `<>@:/[]?{}`. Using these
|
72
|
+
# characters will reduce interoperability.
|
73
|
+
#
|
74
|
+
# :attr_accessor: name
|
75
|
+
|
76
|
+
# The cookie value.
|
77
|
+
#
|
78
|
+
# Trying to set a value with the normal setter method will raise an
|
79
|
+
# ArgumentError only when it contains any of these characters:
|
80
|
+
# control characters (\x00-\x1F and \x7F).
|
81
|
+
#
|
82
|
+
# Note that RFC 6265 4.1.1 lists more characters disallowed for use
|
83
|
+
# in a cookie value, which are these: ` ",;\`. Using these
|
84
|
+
# characters will reduce interoperability.
|
85
|
+
#
|
86
|
+
# :attr_accessor: value
|
60
87
|
|
61
|
-
|
88
|
+
# The cookie domain.
|
89
|
+
#
|
90
|
+
# Setting a domain with a leading dot implies that the #for_domain
|
91
|
+
# flag should be turned on. The setter accepts a `DomainName`
|
92
|
+
# object as well as a string-like.
|
93
|
+
#
|
94
|
+
# :attr_accessor: domain
|
62
95
|
|
63
|
-
|
64
|
-
|
96
|
+
# The path attribute value.
|
97
|
+
#
|
98
|
+
# The setter treats an empty path ("") as the root path ("/").
|
99
|
+
#
|
100
|
+
# :attr_accessor: path
|
101
|
+
|
102
|
+
# The origin of the cookie.
|
103
|
+
#
|
104
|
+
# Setting this will initialize the #domain and #path attribute
|
105
|
+
# values if unknown yet.
|
106
|
+
#
|
107
|
+
# :attr_accessor: origin
|
108
|
+
|
109
|
+
# The Expires attribute value as a Time object.
|
110
|
+
#
|
111
|
+
# The setter method accepts a Time object, a string representation
|
112
|
+
# of date/time, or `nil`.
|
113
|
+
#
|
114
|
+
# Note that #max_age and #expires are mutually exclusive. Setting
|
115
|
+
# \#max_age resets #expires to nil, and vice versa.
|
116
|
+
#
|
117
|
+
# :attr_accessor: expires
|
118
|
+
|
119
|
+
# The Max-Age attribute value as an integer, the number of seconds
|
120
|
+
# before expiration.
|
121
|
+
#
|
122
|
+
# The setter method accepts an integer, or a string-like that
|
123
|
+
# represents an integer which will be stringified and then
|
124
|
+
# integerized using #to_i.
|
125
|
+
#
|
126
|
+
# Note that #max_age and #expires are mutually exclusive. Setting
|
127
|
+
# \#max_age resets #expires to nil, and vice versa.
|
128
|
+
#
|
129
|
+
# :attr_accessor: max_age
|
65
130
|
|
66
131
|
# :call-seq:
|
67
132
|
# new(name, value)
|
68
133
|
# new(name, value, attr_hash)
|
69
134
|
# new(attr_hash)
|
70
135
|
#
|
71
|
-
# Creates a cookie object. For each key of
|
136
|
+
# Creates a cookie object. For each key of `attr_hash`, the setter
|
72
137
|
# is called if defined. Each key can be either a symbol or a
|
73
138
|
# string, downcased or not.
|
74
139
|
#
|
75
140
|
# This methods accepts any attribute name for which a setter method
|
76
|
-
# is defined. Beware, however, any error (typically
|
77
|
-
# a setter method raises will be passed through.
|
141
|
+
# is defined. Beware, however, any error (typically
|
142
|
+
# `ArgumentError`) a setter method raises will be passed through.
|
78
143
|
#
|
79
144
|
# e.g.
|
145
|
+
#
|
80
146
|
# new("uid", "a12345")
|
81
147
|
# new("uid", "a12345", :domain => 'example.org',
|
82
148
|
# :for_domain => true, :expired => Time.now + 7*86400)
|
@@ -86,11 +152,12 @@ class HTTP::Cookie
|
|
86
152
|
@version = 0 # Netscape Cookie
|
87
153
|
|
88
154
|
@origin = @domain = @path =
|
89
|
-
@secure = @httponly =
|
90
155
|
@expires = @max_age =
|
91
156
|
@comment = nil
|
92
|
-
|
157
|
+
@secure = @httponly = false
|
158
|
+
@session = true
|
93
159
|
@created_at = @accessed_at = Time.now
|
160
|
+
|
94
161
|
case args.size
|
95
162
|
when 2
|
96
163
|
self.name, self.value = *args
|
@@ -129,45 +196,43 @@ class HTTP::Cookie
|
|
129
196
|
end
|
130
197
|
end
|
131
198
|
|
132
|
-
|
133
|
-
# +domain+. If it is false, this cookie will be sent only to the
|
134
|
-
# host indicated by the +domain+.
|
135
|
-
attr_accessor :for_domain
|
136
|
-
alias for_domain? for_domain
|
199
|
+
autoload :Scanner, 'http/cookie/scanner'
|
137
200
|
|
138
201
|
class << self
|
139
|
-
|
140
|
-
|
141
|
-
#
|
142
|
-
#
|
143
|
-
#
|
144
|
-
def normalize_path(
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
uri.path.empty? ? uri + '/' : uri
|
149
|
-
else
|
150
|
-
uri.empty? ? '/' : uri
|
151
|
-
end
|
202
|
+
# Normalizes a given path. If it is empty or it is a relative
|
203
|
+
# path, the root path '/' is returned.
|
204
|
+
#
|
205
|
+
# If a URI object is given, returns a new URI object with the path
|
206
|
+
# part normalized.
|
207
|
+
def normalize_path(path)
|
208
|
+
return path + normalize_path(path.path) if URI === path
|
209
|
+
# RFC 6265 5.1.4
|
210
|
+
path.start_with?('/') ? path : '/'
|
152
211
|
end
|
153
212
|
|
154
|
-
# Parses a Set-Cookie header value
|
213
|
+
# Parses a Set-Cookie header value `set_cookie` into an array of
|
155
214
|
# Cookie objects. Parts (separated by commas) that are malformed
|
156
215
|
# or invalid are silently ignored. For example, a cookie that a
|
157
216
|
# given origin is not allowed to issue is not included in the
|
158
217
|
# resulted array.
|
159
218
|
#
|
219
|
+
# Any Max-Age attribute value found is converted to an expires
|
220
|
+
# value computing from the current time so that expiration check
|
221
|
+
# (#expired?) can be performed.
|
222
|
+
#
|
160
223
|
# If a block is given, each cookie object is passed to the block.
|
161
224
|
#
|
162
225
|
# Available option keywords are below:
|
163
226
|
#
|
164
|
-
#
|
165
|
-
#
|
166
|
-
#
|
167
|
-
#
|
227
|
+
# `origin`
|
228
|
+
# : The cookie's origin URI/URL
|
229
|
+
#
|
230
|
+
# `date`
|
231
|
+
# : The base date used for interpreting Max-Age attribute values
|
168
232
|
# instead of the current time
|
169
|
-
#
|
170
|
-
#
|
233
|
+
#
|
234
|
+
# `logger`
|
235
|
+
# : Logger object useful for debugging
|
171
236
|
def parse(set_cookie, options = nil, *_, &block)
|
172
237
|
_.empty? && !options.is_a?(String) or
|
173
238
|
raise ArgumentError, 'HTTP::Cookie equivalent for Mechanize::Cookie.parse(uri, set_cookie[, log]) is HTTP::Cookie.parse(set_cookie, :origin => uri[, :logger => log]).'
|
@@ -180,83 +245,44 @@ class HTTP::Cookie
|
|
180
245
|
date ||= Time.now
|
181
246
|
|
182
247
|
[].tap { |cookies|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
next
|
188
|
-
end
|
189
|
-
|
190
|
-
first_elem, *cookie_elem = c.split(/;+/)
|
191
|
-
first_elem.strip!
|
192
|
-
key, value = first_elem.split(/\=/, 2)
|
248
|
+
s = Scanner.new(set_cookie, logger)
|
249
|
+
until s.eos?
|
250
|
+
name, value, attrs = s.scan_cookie
|
251
|
+
break if name.nil? || name.empty?
|
193
252
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
cookie_elem.each do |pair|
|
202
|
-
pair.strip!
|
203
|
-
key, value = pair.split(/=/, 2) #/)
|
204
|
-
next unless key
|
205
|
-
case value # may be nil
|
206
|
-
when /\A"(.*)"\z/
|
207
|
-
value = $1.gsub(/\\(.)/, "\\1")
|
208
|
-
end
|
209
|
-
|
210
|
-
case key.downcase
|
211
|
-
when 'domain'
|
212
|
-
next unless value && !value.empty?
|
213
|
-
begin
|
214
|
-
cookie.domain = value
|
253
|
+
cookie = new(name, value)
|
254
|
+
attrs.each { |aname, avalue|
|
255
|
+
begin
|
256
|
+
case aname
|
257
|
+
when 'domain'
|
258
|
+
cookie.domain = avalue
|
215
259
|
cookie.for_domain = true
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
cookie.
|
233
|
-
rescue
|
234
|
-
logger.warn("Couldn't parse max age '#{value}'") if logger
|
235
|
-
end
|
236
|
-
when 'comment'
|
237
|
-
next unless value
|
238
|
-
cookie.comment = value
|
239
|
-
when 'version'
|
240
|
-
next unless value
|
241
|
-
begin
|
242
|
-
cookie.version = Integer(value)
|
243
|
-
rescue
|
244
|
-
logger.warn("Couldn't parse version '#{value}'") if logger
|
245
|
-
cookie.version = nil
|
260
|
+
when 'path'
|
261
|
+
cookie.path = avalue
|
262
|
+
when 'expires'
|
263
|
+
# RFC 6265 4.1.2.2
|
264
|
+
# The Max-Age attribute has precedence over the Expires
|
265
|
+
# attribute.
|
266
|
+
cookie.expires = avalue unless cookie.max_age
|
267
|
+
when 'max-age'
|
268
|
+
cookie.max_age = avalue
|
269
|
+
when 'comment'
|
270
|
+
cookie.comment = avalue
|
271
|
+
when 'version'
|
272
|
+
cookie.version = avalue
|
273
|
+
when 'secure'
|
274
|
+
cookie.secure = avalue
|
275
|
+
when 'httponly'
|
276
|
+
cookie.httponly = avalue
|
246
277
|
end
|
247
|
-
|
248
|
-
|
249
|
-
when 'httponly'
|
250
|
-
cookie.httponly = true
|
278
|
+
rescue => e
|
279
|
+
logger.warn("Couldn't parse #{aname} '#{avalue}': #{e}") if logger
|
251
280
|
end
|
252
|
-
|
253
|
-
|
254
|
-
cookie.secure ||= false
|
255
|
-
cookie.httponly ||= false
|
281
|
+
}
|
256
282
|
|
257
|
-
#
|
258
|
-
|
259
|
-
cookie.
|
283
|
+
# Have `expires` set instead of `max_age`, so that
|
284
|
+
# expiration check (`expired?`) can be performed.
|
285
|
+
cookie.expires = date + cookie.max_age if cookie.max_age
|
260
286
|
|
261
287
|
if origin
|
262
288
|
begin
|
@@ -270,22 +296,46 @@ class HTTP::Cookie
|
|
270
296
|
yield cookie if block_given?
|
271
297
|
|
272
298
|
cookies << cookie
|
273
|
-
|
299
|
+
end
|
274
300
|
}
|
275
301
|
end
|
276
302
|
end
|
277
303
|
|
304
|
+
attr_reader :name
|
305
|
+
|
306
|
+
# See #name.
|
278
307
|
def name=(name)
|
279
|
-
|
308
|
+
name = check_string_type(name) or
|
309
|
+
raise TypeError, "#{name.class} is not a String"
|
310
|
+
if name.empty?
|
280
311
|
raise ArgumentError, "cookie name cannot be empty"
|
281
|
-
elsif name.match(/[\x00-\
|
282
|
-
raise ArgumentError, "cookie name
|
312
|
+
elsif name.match(/[\x00-\x20\x7F,;\\"=]/)
|
313
|
+
raise ArgumentError, "invalid cookie name"
|
283
314
|
end
|
315
|
+
# RFC 6265 4.1.1
|
316
|
+
# cookie-name may not match:
|
317
|
+
# /[\x00-\x20\x7F()<>@,;:\\"\/\[\]?={}]/
|
284
318
|
@name = name
|
285
319
|
end
|
286
320
|
|
287
|
-
|
288
|
-
|
321
|
+
attr_reader :value
|
322
|
+
|
323
|
+
# See #value.
|
324
|
+
def value=(value)
|
325
|
+
value = check_string_type(value) or
|
326
|
+
raise TypeError, "#{value.class} is not a String"
|
327
|
+
if value.match(/[\x00-\x1F\x7F]/)
|
328
|
+
raise ArgumentError, "invalid cookie value"
|
329
|
+
end
|
330
|
+
# RFC 6265 4.1.1
|
331
|
+
# cookie-name may not match:
|
332
|
+
# /[^\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/
|
333
|
+
@value = value
|
334
|
+
end
|
335
|
+
|
336
|
+
attr_reader :domain
|
337
|
+
|
338
|
+
# See #domain.
|
289
339
|
def domain=(domain)
|
290
340
|
if DomainName === domain
|
291
341
|
@domain_name = domain
|
@@ -310,13 +360,29 @@ class HTTP::Cookie
|
|
310
360
|
raise NoMethodError, 'HTTP::Cookie equivalent for Mechanize::CookieJar#set_domain() is #domain=().'
|
311
361
|
end
|
312
362
|
|
313
|
-
#
|
363
|
+
# Returns the domain attribute value as a DomainName object.
|
364
|
+
attr_reader :domain_name
|
365
|
+
|
366
|
+
# The domain flag.
|
367
|
+
#
|
368
|
+
# If this flag is true, this cookie will be sent to any host in the
|
369
|
+
# \#domain, including the host domain itself. If it is false, this
|
370
|
+
# cookie will be sent only to the host indicated by the #domain.
|
371
|
+
attr_accessor :for_domain
|
372
|
+
alias for_domain? for_domain
|
373
|
+
|
374
|
+
attr_reader :path
|
375
|
+
|
376
|
+
# See #path.
|
314
377
|
def path=(path)
|
378
|
+
path = check_string_type(path) or
|
379
|
+
raise TypeError, "#{path.class} is not a String"
|
315
380
|
@path = HTTP::Cookie.normalize_path(path)
|
316
381
|
end
|
317
382
|
|
318
|
-
|
319
|
-
|
383
|
+
attr_reader :origin
|
384
|
+
|
385
|
+
# See #origin.
|
320
386
|
def origin=(origin)
|
321
387
|
@origin.nil? or
|
322
388
|
raise ArgumentError, "origin cannot be changed once it is set"
|
@@ -328,14 +394,58 @@ class HTTP::Cookie
|
|
328
394
|
@origin = origin
|
329
395
|
end
|
330
396
|
|
331
|
-
#
|
332
|
-
#
|
397
|
+
# The secure flag.
|
398
|
+
#
|
399
|
+
# A cookie with this flag on should only be sent via a secure
|
400
|
+
# protocol like HTTPS.
|
401
|
+
attr_accessor :secure
|
402
|
+
alias secure? secure
|
403
|
+
|
404
|
+
# The HttpOnly flag.
|
405
|
+
#
|
406
|
+
# A cookie with this flag on should be hidden from a client script.
|
407
|
+
attr_accessor :httponly
|
408
|
+
alias httponly? httponly
|
409
|
+
|
410
|
+
# The session flag.
|
411
|
+
#
|
412
|
+
# A cookie with this flag on should be hidden from a client script.
|
413
|
+
attr_reader :session
|
414
|
+
alias session? session
|
415
|
+
|
416
|
+
attr_reader :expires
|
417
|
+
|
418
|
+
# See #expires.
|
333
419
|
def expires=(t)
|
334
420
|
case t
|
335
421
|
when nil, Time
|
336
|
-
@expires = t
|
337
422
|
else
|
338
|
-
|
423
|
+
t = Time.parse(t)
|
424
|
+
end
|
425
|
+
@max_age = nil
|
426
|
+
@session = t.nil?
|
427
|
+
@expires = t
|
428
|
+
end
|
429
|
+
|
430
|
+
alias expires_at expires
|
431
|
+
alias expires_at= expires=
|
432
|
+
|
433
|
+
attr_reader :max_age
|
434
|
+
|
435
|
+
# See #max_age.
|
436
|
+
def max_age=(sec)
|
437
|
+
@expires = nil
|
438
|
+
case sec
|
439
|
+
when Integer, nil
|
440
|
+
else
|
441
|
+
str = check_string_type(sec) or
|
442
|
+
raise TypeError, "#{sec.class} is not an Integer or String"
|
443
|
+
sec = str.to_i
|
444
|
+
end
|
445
|
+
if @session = sec.nil?
|
446
|
+
@max_age = nil
|
447
|
+
else
|
448
|
+
@max_age = sec
|
339
449
|
end
|
340
450
|
end
|
341
451
|
|
@@ -347,17 +457,26 @@ class HTTP::Cookie
|
|
347
457
|
|
348
458
|
# Expires this cookie by setting the expires attribute value to a
|
349
459
|
# past date.
|
350
|
-
def expire
|
351
|
-
|
460
|
+
def expire!
|
461
|
+
self.expires = UNIX_EPOCH
|
352
462
|
self
|
353
463
|
end
|
354
464
|
|
355
|
-
|
356
|
-
|
357
|
-
|
465
|
+
# The version attribute. The only known version of the cookie
|
466
|
+
# format is 0.
|
467
|
+
attr_accessor :version
|
468
|
+
|
469
|
+
# The comment attribute.
|
470
|
+
attr_accessor :comment
|
471
|
+
|
472
|
+
# The time this cookie was created at.
|
473
|
+
attr_accessor :created_at
|
474
|
+
|
475
|
+
# The time this cookie was last accessed at.
|
476
|
+
attr_accessor :accessed_at
|
358
477
|
|
359
478
|
# Tests if it is OK to accept this cookie if it is sent from a given
|
360
|
-
#
|
479
|
+
# `uri`.
|
361
480
|
def acceptable_from_uri?(uri)
|
362
481
|
uri = URI(uri)
|
363
482
|
return false unless URI::HTTP === uri && uri.host
|
@@ -377,7 +496,7 @@ class HTTP::Cookie
|
|
377
496
|
end
|
378
497
|
end
|
379
498
|
|
380
|
-
# Tests if it is OK to send this cookie to a given
|
499
|
+
# Tests if it is OK to send this cookie to a given `uri`. A runtime
|
381
500
|
# error is raised if the cookie's domain is unknown.
|
382
501
|
def valid_for_uri?(uri)
|
383
502
|
if @domain.nil?
|
@@ -405,28 +524,37 @@ class HTTP::Cookie
|
|
405
524
|
origin = origin ? URI(origin) : @origin or
|
406
525
|
raise "origin must be specified to produce a value for Set-Cookie"
|
407
526
|
|
408
|
-
string =
|
527
|
+
string = "#{@name}=#{Scanner.quote(@value)}"
|
409
528
|
if @for_domain || @domain != DomainName.new(origin.host).hostname
|
410
|
-
string << ";
|
529
|
+
string << "; Domain=#{@domain}"
|
411
530
|
end
|
412
531
|
if (HTTP::Cookie.normalize_path(origin) + './').path != @path
|
413
|
-
string << ";
|
532
|
+
string << "; Path=#{@path}"
|
414
533
|
end
|
415
|
-
if @
|
416
|
-
string << ";
|
534
|
+
if @max_age
|
535
|
+
string << "; Max-Age=#{@max_age}"
|
536
|
+
elsif @expires
|
537
|
+
string << "; Expires=#{@expires.httpdate}"
|
417
538
|
end
|
418
539
|
if @comment
|
419
|
-
string << ";
|
540
|
+
string << "; Comment=#{Scanner.quote(@comment)}"
|
420
541
|
end
|
421
542
|
if @httponly
|
422
543
|
string << "; HttpOnly"
|
423
544
|
end
|
424
545
|
if @secure
|
425
|
-
string << ";
|
546
|
+
string << "; Secure"
|
426
547
|
end
|
427
548
|
string
|
428
549
|
end
|
429
550
|
|
551
|
+
def inspect
|
552
|
+
'#<%s:' % self.class << PERSISTENT_PROPERTIES.map { |key|
|
553
|
+
'%s=%s' % [key, instance_variable_get(:"@#{key}").inspect]
|
554
|
+
}.join(', ') << ' origin=%s>' % [@origin ? @origin.to_s : 'nil']
|
555
|
+
|
556
|
+
end
|
557
|
+
|
430
558
|
# Compares the cookie with another. When there are many cookies with
|
431
559
|
# the same name for a URL, the value of the smallest must be used.
|
432
560
|
def <=>(other)
|
@@ -461,7 +589,7 @@ class HTTP::Cookie
|
|
461
589
|
map.each { |key, value|
|
462
590
|
case key
|
463
591
|
when *PERSISTENT_PROPERTIES
|
464
|
-
|
592
|
+
__send__(:"#{key}=", value)
|
465
593
|
end
|
466
594
|
}
|
467
595
|
end
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'http/cookie'
|
2
|
+
require 'strscan'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
class HTTP::Cookie::Scanner < StringScanner
|
6
|
+
# Whitespace.
|
7
|
+
RE_WSP = /[ \t]+/
|
8
|
+
|
9
|
+
# A pattern that matches a cookie name or attribute name which may
|
10
|
+
# be empty, capturing trailing whitespace.
|
11
|
+
RE_NAME = /(?!#{RE_WSP})[^,;\\"=]*/
|
12
|
+
|
13
|
+
RE_BAD_CHAR = /([\x00-\x20\x7F",;\\])/
|
14
|
+
|
15
|
+
# A pattern that matches the comma in a (typically date) value.
|
16
|
+
RE_COOKIE_COMMA = /,(?=#{RE_WSP}?#{RE_NAME}=)/
|
17
|
+
|
18
|
+
def initialize(string, logger = nil)
|
19
|
+
@logger = logger
|
20
|
+
super(string)
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def quote(s)
|
25
|
+
return s unless s.match(RE_BAD_CHAR)
|
26
|
+
'"' << s.gsub(RE_BAD_CHAR, "\\\\\\1") << '"'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def skip_wsp
|
31
|
+
skip(RE_WSP)
|
32
|
+
end
|
33
|
+
|
34
|
+
def scan_dquoted
|
35
|
+
''.tap { |s|
|
36
|
+
case
|
37
|
+
when skip(/"/)
|
38
|
+
break
|
39
|
+
when skip(/\\/)
|
40
|
+
s << getch
|
41
|
+
when scan(/[^"\\]+/)
|
42
|
+
s << matched
|
43
|
+
end until eos?
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def scan_name
|
48
|
+
scan(RE_NAME).tap { |s|
|
49
|
+
s.rstrip! if s
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def scan_value
|
54
|
+
''.tap { |s|
|
55
|
+
case
|
56
|
+
when scan(/[^,;"]+/)
|
57
|
+
s << matched
|
58
|
+
when skip(/"/)
|
59
|
+
# RFC 6265 2.2
|
60
|
+
# A cookie-value may be DQUOTE'd.
|
61
|
+
s << scan_dquoted
|
62
|
+
when check(/;|#{RE_COOKIE_COMMA}/o)
|
63
|
+
break
|
64
|
+
else
|
65
|
+
s << getch
|
66
|
+
end until eos?
|
67
|
+
s.rstrip!
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def scan_name_value
|
72
|
+
name = scan_name
|
73
|
+
if skip(/\=/)
|
74
|
+
value = scan_value
|
75
|
+
else
|
76
|
+
scan_value
|
77
|
+
value = nil
|
78
|
+
end
|
79
|
+
[name, value]
|
80
|
+
end
|
81
|
+
|
82
|
+
if Time.respond_to?(:strptime)
|
83
|
+
def tuple_to_time(day_of_month, month, year, time)
|
84
|
+
Time.strptime(
|
85
|
+
'%02d %s %04d %02d:%02d:%02d UTC' % [day_of_month, month, year, *time],
|
86
|
+
'%d %b %Y %T %Z'
|
87
|
+
).tap { |date|
|
88
|
+
date.day == day_of_month or return nil
|
89
|
+
}
|
90
|
+
end
|
91
|
+
else
|
92
|
+
def tuple_to_time(day_of_month, month, year, time)
|
93
|
+
Time.parse(
|
94
|
+
'%02d %s %04d %02d:%02d:%02d UTC' % [day_of_month, month, year, *time]
|
95
|
+
).tap { |date|
|
96
|
+
date.day == day_of_month or return nil
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
private :tuple_to_time
|
101
|
+
|
102
|
+
def parse_cookie_date(s)
|
103
|
+
# RFC 6265 5.1.1
|
104
|
+
time = day_of_month = month = year = nil
|
105
|
+
|
106
|
+
s.split(/[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]+/).each { |token|
|
107
|
+
case
|
108
|
+
when time.nil? && token.match(/\A(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?(?=\D|\z)/)
|
109
|
+
sec =
|
110
|
+
if $3
|
111
|
+
$3.to_i
|
112
|
+
else
|
113
|
+
# violation of the RFC
|
114
|
+
@logger.warn("Time lacks the second part: #{token}") if @logger
|
115
|
+
0
|
116
|
+
end
|
117
|
+
time = [$1.to_i, $2.to_i, sec]
|
118
|
+
when day_of_month.nil? && token.match(/\A(\d{1,2})(?=\D|\z)/)
|
119
|
+
day_of_month = $1.to_i
|
120
|
+
when month.nil? && token.match(/\A(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i)
|
121
|
+
month = $1.capitalize
|
122
|
+
when year.nil? && token.match(/\A(\d{2,4})(?=\D|\z)/)
|
123
|
+
year = $1.to_i
|
124
|
+
end
|
125
|
+
}
|
126
|
+
|
127
|
+
if day_of_month.nil? || month.nil? || year.nil? || time.nil?
|
128
|
+
return nil
|
129
|
+
end
|
130
|
+
|
131
|
+
case day_of_month
|
132
|
+
when 1..31
|
133
|
+
else
|
134
|
+
return nil
|
135
|
+
end
|
136
|
+
|
137
|
+
case year
|
138
|
+
when 100..1600
|
139
|
+
return nil
|
140
|
+
when 70..99
|
141
|
+
year += 1900
|
142
|
+
when 0..69
|
143
|
+
year += 2000
|
144
|
+
end
|
145
|
+
|
146
|
+
if (time <=> [23,59,59]) > 0
|
147
|
+
return nil
|
148
|
+
end
|
149
|
+
|
150
|
+
tuple_to_time(day_of_month, month, year, time)
|
151
|
+
end
|
152
|
+
|
153
|
+
def scan_cookie
|
154
|
+
# cf. RFC 6265 5.2
|
155
|
+
until eos?
|
156
|
+
start = pos
|
157
|
+
len = nil
|
158
|
+
|
159
|
+
skip_wsp
|
160
|
+
|
161
|
+
name, value = scan_name_value
|
162
|
+
if name.nil?
|
163
|
+
break
|
164
|
+
elsif value.nil?
|
165
|
+
@logger.warn("Cookie definition lacks a name-value pair.") if @logger
|
166
|
+
elsif name.empty?
|
167
|
+
@logger.warn("Cookie definition has an empty name.") if @logger
|
168
|
+
value = nil
|
169
|
+
end
|
170
|
+
attrs = {}
|
171
|
+
|
172
|
+
case
|
173
|
+
when skip(/,/)
|
174
|
+
len = (pos - 1) - start
|
175
|
+
break
|
176
|
+
when skip(/;/)
|
177
|
+
skip_wsp
|
178
|
+
aname, avalue = scan_name_value
|
179
|
+
break if aname.nil?
|
180
|
+
next if aname.empty? || value.nil?
|
181
|
+
aname.downcase!
|
182
|
+
case aname
|
183
|
+
when 'expires'
|
184
|
+
# RFC 6265 5.2.1
|
185
|
+
avalue &&= parse_cookie_date(avalue) or next
|
186
|
+
when 'max-age'
|
187
|
+
# RFC 6265 5.2.2
|
188
|
+
next unless /\A-?\d+\z/.match(avalue)
|
189
|
+
when 'domain'
|
190
|
+
# RFC 6265 5.2.3
|
191
|
+
# An empty value SHOULD be ignored.
|
192
|
+
next if avalue.nil? || avalue.empty?
|
193
|
+
when 'path'
|
194
|
+
# RFC 6265 5.2.4
|
195
|
+
# A relative path must be ignored rather than normalizing it
|
196
|
+
# to "/".
|
197
|
+
next unless /\A\//.match(avalue)
|
198
|
+
when 'secure', 'httponly'
|
199
|
+
# RFC 6265 5.2.5, 5.2.6
|
200
|
+
avalue = true
|
201
|
+
end
|
202
|
+
attrs[aname] = avalue
|
203
|
+
end until eos?
|
204
|
+
|
205
|
+
len ||= pos - start
|
206
|
+
|
207
|
+
if len > HTTP::Cookie::MAX_LENGTH
|
208
|
+
@logger.warn("Cookie definition too long: #{name}") if @logger
|
209
|
+
next
|
210
|
+
end
|
211
|
+
|
212
|
+
return [name, value, attrs] if value
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
data/lib/http/cookie/version.rb
CHANGED
data/lib/http/cookie_jar.rb
CHANGED
@@ -114,7 +114,7 @@ class HTTP::CookieJar
|
|
114
114
|
if (debt = domain_cookies.size - HTTP::Cookie::MAX_COOKIES_PER_DOMAIN) > 0
|
115
115
|
domain_cookies.sort_by!(&:created_at)
|
116
116
|
domain_cookies.slice!(0, debt).each { |cookie|
|
117
|
-
add(cookie.expire)
|
117
|
+
add(cookie.expire!)
|
118
118
|
}
|
119
119
|
end
|
120
120
|
|
@@ -124,7 +124,7 @@ class HTTP::CookieJar
|
|
124
124
|
if (debt = all_cookies.size - HTTP::Cookie::MAX_COOKIES_TOTAL) > 0
|
125
125
|
all_cookies.sort_by!(&:created_at)
|
126
126
|
all_cookies.slice!(0, debt).each { |cookie|
|
127
|
-
add(cookie.expire)
|
127
|
+
add(cookie.expire!)
|
128
128
|
}
|
129
129
|
end
|
130
130
|
|
data/test/test_http_cookie.rb
CHANGED
@@ -33,7 +33,7 @@ class TestHTTPCookie < Test::Unit::TestCase
|
|
33
33
|
"Fri, 17 Mar 89 4:01:33",
|
34
34
|
"Fri, 17 Mar 89 4:01 GMT",
|
35
35
|
"Mon Jan 16 16:12 PDT 1989",
|
36
|
-
"Mon Jan 16 16:12 +0130 1989",
|
36
|
+
#"Mon Jan 16 16:12 +0130 1989",
|
37
37
|
"6 May 1992 16:41-JST (Wednesday)",
|
38
38
|
#"22-AUG-1993 10:59:12.82",
|
39
39
|
"22-AUG-1993 10:59pm",
|
@@ -42,7 +42,7 @@ class TestHTTPCookie < Test::Unit::TestCase
|
|
42
42
|
#"Friday, August 04, 1995 3:54 PM",
|
43
43
|
#"06/21/95 04:24:34 PM",
|
44
44
|
#"20/06/95 21:07",
|
45
|
-
"95-06-08 19:32:48 EDT",
|
45
|
+
#"95-06-08 19:32:48 EDT",
|
46
46
|
]
|
47
47
|
|
48
48
|
dates.each do |date|
|
@@ -83,7 +83,7 @@ class TestHTTPCookie < Test::Unit::TestCase
|
|
83
83
|
def test_parse_too_long_cookie
|
84
84
|
uri = URI.parse 'http://example'
|
85
85
|
|
86
|
-
cookie_str = "foo=#{'
|
86
|
+
cookie_str = "foo=#{'Cookie' * 680}; path=/ab/"
|
87
87
|
assert_equal(HTTP::Cookie::MAX_LENGTH - 1, cookie_str.bytesize)
|
88
88
|
|
89
89
|
assert_equal 1, HTTP::Cookie.parse(cookie_str, :origin => uri).size
|
@@ -101,7 +101,7 @@ class TestHTTPCookie < Test::Unit::TestCase
|
|
101
101
|
|
102
102
|
assert_equal 1, HTTP::Cookie.parse(cookie_str, :origin => uri) { |cookie|
|
103
103
|
assert_equal 'quoted', cookie.name
|
104
|
-
assert_equal '
|
104
|
+
assert_equal 'value', cookie.value
|
105
105
|
assert_equal 'comment is "comment"', cookie.comment
|
106
106
|
}.size
|
107
107
|
end
|
@@ -248,12 +248,14 @@ class TestHTTPCookie < Test::Unit::TestCase
|
|
248
248
|
"no_path1=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT, no_expires=nope; Path=/, " \
|
249
249
|
"no_path2=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path, " \
|
250
250
|
"no_path3=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=, " \
|
251
|
+
"rel_path1=rel_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=foo/bar, " \
|
252
|
+
"rel_path1=rel_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=foo, " \
|
251
253
|
"no_domain1=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope, " \
|
252
254
|
"no_domain2=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain, " \
|
253
255
|
"no_domain3=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain="
|
254
256
|
|
255
257
|
cookies = HTTP::Cookie.parse cookie_str, :origin => url
|
256
|
-
assert_equal
|
258
|
+
assert_equal 15, cookies.length
|
257
259
|
|
258
260
|
name = cookies.find { |c| c.name == 'name' }
|
259
261
|
assert_equal "Aaron", name.value
|
@@ -277,6 +279,13 @@ class TestHTTPCookie < Test::Unit::TestCase
|
|
277
279
|
assert_equal Time.at(1320539392), c.expires, c.name
|
278
280
|
}
|
279
281
|
|
282
|
+
rel_path_cookies = cookies.select { |c| c.value == 'rel_path' }
|
283
|
+
assert_equal 2, rel_path_cookies.size
|
284
|
+
rel_path_cookies.each { |c|
|
285
|
+
assert_equal "/", c.path, c.name
|
286
|
+
assert_equal Time.at(1320539392), c.expires, c.name
|
287
|
+
}
|
288
|
+
|
280
289
|
no_domain_cookies = cookies.select { |c| c.value == 'no_domain' }
|
281
290
|
assert_equal 3, no_domain_cookies.size
|
282
291
|
no_domain_cookies.each { |c|
|
@@ -364,23 +373,32 @@ class TestHTTPCookie < Test::Unit::TestCase
|
|
364
373
|
|
365
374
|
def test_set_cookie_value
|
366
375
|
url = URI.parse('http://rubyforge.org/')
|
367
|
-
cookie_params = @cookie_params.merge('secure' => 'secure')
|
368
|
-
cookie_value = 'foo=bar'
|
369
376
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
377
|
+
['foo=bar', 'foo="bar"', 'foo="ba\"r baz"'].each { |cookie_value|
|
378
|
+
cookie_params = @cookie_params.merge('secure' => 'secure', 'max-age' => 'Max-Age=1000')
|
379
|
+
date = Time.at(Time.now.to_i)
|
380
|
+
cookie_params.keys.combine.each do |keys|
|
381
|
+
cookie_text = [cookie_value, *keys.map { |key| cookie_params[key] }].join('; ')
|
382
|
+
cookie, = HTTP::Cookie.parse(cookie_text, :origin => url, :date => date)
|
383
|
+
cookie2, = HTTP::Cookie.parse(cookie.set_cookie_value, :origin => url, :date => date)
|
384
|
+
|
385
|
+
assert_equal(cookie.name, cookie2.name)
|
386
|
+
assert_equal(cookie.value, cookie2.value)
|
387
|
+
assert_equal(cookie.domain, cookie2.domain)
|
388
|
+
assert_equal(cookie.for_domain?, cookie2.for_domain?)
|
389
|
+
assert_equal(cookie.path, cookie2.path)
|
390
|
+
assert_equal(cookie.expires, cookie2.expires)
|
391
|
+
if keys.include?('max-age')
|
392
|
+
assert_equal(date + 1000, cookie2.expires)
|
393
|
+
elsif keys.include?('expires')
|
394
|
+
assert_equal(@expires, cookie2.expires)
|
395
|
+
else
|
396
|
+
assert_equal(nil, cookie2.expires)
|
397
|
+
end
|
398
|
+
assert_equal(cookie.secure?, cookie2.secure?)
|
399
|
+
assert_equal(cookie.httponly?, cookie2.httponly?)
|
400
|
+
end
|
401
|
+
}
|
384
402
|
end
|
385
403
|
|
386
404
|
def test_parse_cookie_no_spaces
|
@@ -445,6 +463,37 @@ class TestHTTPCookie < Test::Unit::TestCase
|
|
445
463
|
}.merge(options)
|
446
464
|
end
|
447
465
|
|
466
|
+
def test_bad_name
|
467
|
+
[
|
468
|
+
"a\tb", "a\vb", "a\rb", "a\nb", 'a b',
|
469
|
+
"a\\b", 'a"b', # 'a:b', 'a/b', 'a[b]',
|
470
|
+
'a=b', 'a,b', 'a;b',
|
471
|
+
].each { |name|
|
472
|
+
assert_raises(ArgumentError) {
|
473
|
+
HTTP::Cookie.new(cookie_values(:name => name))
|
474
|
+
}
|
475
|
+
cookie = HTTP::Cookie.new(cookie_values)
|
476
|
+
assert_raises(ArgumentError) {
|
477
|
+
cookie.name = name
|
478
|
+
}
|
479
|
+
}
|
480
|
+
end
|
481
|
+
|
482
|
+
def test_bad_value
|
483
|
+
[
|
484
|
+
"a\tb", "a\vb", "a\rb", "a\nb",
|
485
|
+
"a\\b", 'a"b', # 'a:b', 'a/b', 'a[b]',
|
486
|
+
].each { |name|
|
487
|
+
assert_raises(ArgumentError) {
|
488
|
+
HTTP::Cookie.new(cookie_values(:name => name))
|
489
|
+
}
|
490
|
+
cookie = HTTP::Cookie.new(cookie_values)
|
491
|
+
assert_raises(ArgumentError) {
|
492
|
+
cookie.name = name
|
493
|
+
}
|
494
|
+
}
|
495
|
+
end
|
496
|
+
|
448
497
|
def test_compare
|
449
498
|
time = Time.now
|
450
499
|
cookies = [
|
@@ -466,10 +515,33 @@ class TestHTTPCookie < Test::Unit::TestCase
|
|
466
515
|
assert_equal false, cookie.expired?
|
467
516
|
assert_equal true, cookie.expired?(cookie.expires + 1)
|
468
517
|
assert_equal false, cookie.expired?(cookie.expires - 1)
|
469
|
-
cookie.expire
|
518
|
+
cookie.expire!
|
470
519
|
assert_equal true, cookie.expired?
|
471
520
|
end
|
472
521
|
|
522
|
+
def test_session
|
523
|
+
cookie = HTTP::Cookie.new(cookie_values)
|
524
|
+
|
525
|
+
assert_equal false, cookie.session?
|
526
|
+
assert_equal nil, cookie.max_age
|
527
|
+
|
528
|
+
cookie.expires = nil
|
529
|
+
assert_equal true, cookie.session?
|
530
|
+
assert_equal nil, cookie.max_age
|
531
|
+
|
532
|
+
cookie.expires = Time.now + 3600
|
533
|
+
assert_equal false, cookie.session?
|
534
|
+
assert_equal nil, cookie.max_age
|
535
|
+
|
536
|
+
cookie.max_age = 3600
|
537
|
+
assert_equal false, cookie.session?
|
538
|
+
assert_equal nil, cookie.expires
|
539
|
+
|
540
|
+
cookie.max_age = nil
|
541
|
+
assert_equal true, cookie.session?
|
542
|
+
assert_equal nil, cookie.expires
|
543
|
+
end
|
544
|
+
|
473
545
|
def test_equal
|
474
546
|
assert_not_equal(HTTP::Cookie.new(cookie_values),
|
475
547
|
HTTP::Cookie.new(cookie_values(:value => 'bar')))
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: http-cookie
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0.pre1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Akinori MUSHA
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2013-03-
|
14
|
+
date: 2013-03-21 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: domain_name
|
@@ -116,6 +116,7 @@ files:
|
|
116
116
|
- http-cookie.gemspec
|
117
117
|
- lib/http-cookie.rb
|
118
118
|
- lib/http/cookie.rb
|
119
|
+
- lib/http/cookie/scanner.rb
|
119
120
|
- lib/http/cookie/version.rb
|
120
121
|
- lib/http/cookie_jar.rb
|
121
122
|
- lib/http/cookie_jar/abstract_saver.rb
|
@@ -141,9 +142,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
141
142
|
version: '0'
|
142
143
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
144
|
requirements:
|
144
|
-
- - '
|
145
|
+
- - '>'
|
145
146
|
- !ruby/object:Gem::Version
|
146
|
-
version:
|
147
|
+
version: 1.3.1
|
147
148
|
requirements: []
|
148
149
|
rubyforge_project:
|
149
150
|
rubygems_version: 2.0.3
|