http-cookie 0.1.5 → 1.0.0.pre1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|