dwaite-cookiejar 0.1.0
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.
- data/lib/cookiejar/cookie.rb +95 -0
- data/lib/cookiejar/cookie_common.rb +4 -0
- data/lib/cookiejar/cookie_logic.rb +202 -0
- data/lib/cookiejar/jar.rb +125 -0
- data/lib/cookiejar.rb +2 -0
- data/test/cookie_logic_test.rb +236 -0
- data/test/cookie_test.rb +24 -0
- data/test/jar_test.rb +54 -0
- metadata +61 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'cookiejar/cookie_logic'
|
3
|
+
module CookieJar
|
4
|
+
class Cookie
|
5
|
+
include CookieLogic
|
6
|
+
|
7
|
+
attr_reader :name, :value
|
8
|
+
attr_reader :created_at
|
9
|
+
attr_reader :expiry
|
10
|
+
attr_reader :domain, :path
|
11
|
+
attr_reader :secure, :http_only
|
12
|
+
attr_reader :version
|
13
|
+
# Currently unused attributes for RFC 2965 cookies
|
14
|
+
# attr_reader :comment, :comment_url
|
15
|
+
# attr_reader :discard
|
16
|
+
# attr_reader :ports
|
17
|
+
|
18
|
+
def expires_at
|
19
|
+
if expiry.nil?
|
20
|
+
nil
|
21
|
+
elsif expiry.is_a? Time
|
22
|
+
expiry
|
23
|
+
else
|
24
|
+
Time.now + expiry
|
25
|
+
end
|
26
|
+
end
|
27
|
+
def max_age
|
28
|
+
if expiry.is_a? Integer
|
29
|
+
expiry
|
30
|
+
else
|
31
|
+
expiry - Time.now
|
32
|
+
end
|
33
|
+
end
|
34
|
+
def port
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
def initialize(uri, *params)
|
38
|
+
case params.length
|
39
|
+
when 1
|
40
|
+
args = params[0]
|
41
|
+
when 2
|
42
|
+
args = {:name => args[0], :value => args[1]}
|
43
|
+
else
|
44
|
+
raise ArgumentError.new "wrong number of arguments (expected 1 or 2)"
|
45
|
+
end
|
46
|
+
|
47
|
+
@domain = determine_cookie_domain(uri, args[:domain])
|
48
|
+
@expiry = args[:max_age] || args[:expires_at] || nil
|
49
|
+
@path = determine_cookie_path(uri, args[:path])
|
50
|
+
@secure = args[:secure] || false
|
51
|
+
@http_only = args[:http_only] ||false
|
52
|
+
@name = args[:name]
|
53
|
+
@value = args[:value]
|
54
|
+
@version = args[:version]
|
55
|
+
@created_at = DateTime.now
|
56
|
+
end
|
57
|
+
|
58
|
+
PARAM1 = /\A(#{PATTERN::TOKEN})(?:=#{PATTERN::VALUE1})?\Z/
|
59
|
+
# PARAM2 = /\A(#{PATTERN::TOKEN})(?:=#{PATTERN::VALUE2})?\Z/
|
60
|
+
|
61
|
+
def self.from_set_cookie(request_uri, set_cookie_value)
|
62
|
+
args = {}
|
63
|
+
params=set_cookie_value.split(/;\s*/)
|
64
|
+
params.each do |param|
|
65
|
+
result = PARAM1.match param
|
66
|
+
if (!result)
|
67
|
+
raise InvalidCookieError.new("Invalid cookie parameter in cookie '#{set_cookie_value}'")
|
68
|
+
end
|
69
|
+
key = result[1].upcase
|
70
|
+
keyvalue = result[2] || result[3]
|
71
|
+
case key
|
72
|
+
when 'EXPIRES'
|
73
|
+
args[:expires_at] = DateTime.parse(keyvalue)
|
74
|
+
when 'DOMAIN'
|
75
|
+
args[:domain] = keyvalue.downcase
|
76
|
+
when 'PATH'
|
77
|
+
args[:path] = keyvalue
|
78
|
+
when 'SECURE'
|
79
|
+
args[:secure] = true
|
80
|
+
when 'HTTPONLY'
|
81
|
+
args[:http_only] = true
|
82
|
+
else
|
83
|
+
args[:name] = result[1]
|
84
|
+
args[:value] = keyvalue
|
85
|
+
end
|
86
|
+
end
|
87
|
+
args[:version] = 0
|
88
|
+
Cookie.new(request_uri, args)
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s
|
92
|
+
%Q^#{name}=#{value}#{if(domain) then "; domain=#{domain}" end}#{if (expiry) then "; expiry=#{expiry}" end}#{if (path) then "; path=#{path}" end}#{if (secure) then "; secure" end }#{if (http_only) then "; HTTPOnly" end}^
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'cookiejar/cookie_common'
|
3
|
+
|
4
|
+
module CookieJar
|
5
|
+
module CookieLogic
|
6
|
+
module PATTERN
|
7
|
+
include URI::REGEXP::PATTERN
|
8
|
+
|
9
|
+
TOKEN = '[^(),\/<>@;:\\\"\[\]?={}\s]*'
|
10
|
+
VALUE1 = "([^;]*)"
|
11
|
+
IPADDR = "#{IPV4ADDR}|#{IPV6ADDR}"
|
12
|
+
BASE_HOSTNAME = "(?:#{DOMLABEL}\\.)((?:(?:#{DOMLABEL}\\.)+(?:#{TOPLABEL}\\.?)|local))"
|
13
|
+
|
14
|
+
# QUOTED_PAIR = "\\\\[\\x00-\\x7F]"
|
15
|
+
# LWS = "\\r\\n(?:[ \\t]+)"
|
16
|
+
# TEXT="[\\t\\x20-\\x7E\\x80-\\xFF]|(?:#{LWS})"
|
17
|
+
# QDTEXT="[\\t\\x20-\\x21\\x23-\\x7E\\x80-\\xFF]|(?:#{LWS})"
|
18
|
+
# QUOTED_TEXT = "\\\"((?:#{QDTEXT}|#{QUOTED_PAIR})*)\\\""
|
19
|
+
# VALUE2 = "(#{TOKEN})|#{QUOTED_TEXT}"
|
20
|
+
|
21
|
+
end
|
22
|
+
BASE_HOSTNAME = /#{PATTERN::BASE_HOSTNAME}/
|
23
|
+
BASE_PATH = /\A((?:[^\/?#]*\/)*)/
|
24
|
+
IPADDR = /\A#{PATTERN::IPADDR}\Z/
|
25
|
+
# HDN = /\A#{PATTERN::HOSTNAME}\Z/
|
26
|
+
# TOKEN = /\A#{PATTERN::TOKEN}\Z/
|
27
|
+
# TWO_DOT_DOMAINS = /\A\.(com|edu|net|mil|gov|int|org)\Z/
|
28
|
+
|
29
|
+
# Compute the effective host (RFC 2965, section 1)
|
30
|
+
# [host] a string or URI.
|
31
|
+
#
|
32
|
+
# Has the added additional logic of searching for interior dots specifically, and
|
33
|
+
# matches colons to prevent .local being suffixed on IPv6 addresses
|
34
|
+
def effective_host(host)
|
35
|
+
hostname = host.is_a?(URI) ? host.host : host
|
36
|
+
hostname = hostname.downcase
|
37
|
+
|
38
|
+
if /.(?:[\.:])./.match(hostname)
|
39
|
+
hostname
|
40
|
+
else
|
41
|
+
hostname + '.local'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Processes cookie domain data using the following rules:
|
46
|
+
# Domains strings of the form .foo.com match 'foo.com' and all immediate
|
47
|
+
# subdomains of 'foo.com'. Domain strings specified of the form 'foo.com' are
|
48
|
+
# modified to '.foo.com', and as such will still apply to subdomains.
|
49
|
+
#
|
50
|
+
# Cookies without an explicit domain will have their domain value taken directly
|
51
|
+
# from the URL, and will _NOT_ have any leading dot applied. For example, a request
|
52
|
+
# to http://foo.com/ will cause an entry for 'foo.com' to be created - which applies
|
53
|
+
# to foo.com but no subdomain.
|
54
|
+
#
|
55
|
+
# Note that this will not attempt to detect a mismatch of the request uri domain
|
56
|
+
# and explicitly specified cookie domain
|
57
|
+
def determine_cookie_domain (request_uri, cookie_domain)
|
58
|
+
uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
|
59
|
+
domain = cookie_domain.is_a?(Cookie) ? cookie_domain.domain : cookie_domain
|
60
|
+
|
61
|
+
if domain == nil || domain.empty?
|
62
|
+
domain = effective_host(uri.host)
|
63
|
+
else
|
64
|
+
domain = domain.downcase
|
65
|
+
if (domain =~ IPADDR || domain.start_with?('.'))
|
66
|
+
domain
|
67
|
+
else
|
68
|
+
".#{domain}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Processes cookie path data using the following rules:
|
74
|
+
# Paths are separated by '/' characters, and accepted values are truncated
|
75
|
+
# to the last '/' character. If no path is specified in the cookie, a path
|
76
|
+
# value will be taken from the request URI which was used for the site.
|
77
|
+
#
|
78
|
+
# Note that this will not attempt to detect a mismatch of the request uri domain
|
79
|
+
# and explicitly specified cookie path
|
80
|
+
def determine_cookie_path(request_uri, cookie_path)
|
81
|
+
uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
|
82
|
+
cookie_path = cookie_path.is_a?(Cookie) ? cookie_path.path : cookie_path
|
83
|
+
|
84
|
+
if (cookie_path == nil || cookie_path.empty?)
|
85
|
+
cookie_path = cookie_base_path uri.path
|
86
|
+
end
|
87
|
+
cookie_path
|
88
|
+
end
|
89
|
+
|
90
|
+
# Compute the reach of a hostname (RFC 2965, section 1)
|
91
|
+
# Determines the next highest superdomain, or nil if none valid
|
92
|
+
def hostname_reach hostname
|
93
|
+
host = hostname.is_a?(URI) ? hostname.host : hostname
|
94
|
+
BASE_HOSTNAME.match(host) && $~[1] || nil
|
95
|
+
end
|
96
|
+
|
97
|
+
# Compute the base of a path.
|
98
|
+
def cookie_base_path(path)
|
99
|
+
path = path.is_a?(URI) ? path.path : path
|
100
|
+
BASE_PATH.match(path)[1]
|
101
|
+
end
|
102
|
+
|
103
|
+
# Compare a base domain against the base domain to see if they match, or
|
104
|
+
# if the base domain is reachable
|
105
|
+
def domains_match tested_domain,base_domain
|
106
|
+
return true if (tested_domain == base_domain || ".#{tested_domain}" == base_domain)
|
107
|
+
lhs = effective_host tested_domain
|
108
|
+
rhs = effective_host base_domain
|
109
|
+
lhs == rhs || ".#{lhs}" == rhs || hostname_reach(lhs) == rhs || ".#{hostname_reach lhs}" == rhs
|
110
|
+
end
|
111
|
+
|
112
|
+
# Check whether a cookie meets all of the rules to be created, based on
|
113
|
+
# its internal settings and the URI it came from.
|
114
|
+
#
|
115
|
+
# returns true on success, but will raise an InvalidCookieError on failure
|
116
|
+
# with an appropriate error message
|
117
|
+
def validate_cookie request_uri, cookie
|
118
|
+
uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
|
119
|
+
|
120
|
+
request_host = effective_host uri.host
|
121
|
+
request_path = uri.path
|
122
|
+
request_secure = (uri.scheme == 'https')
|
123
|
+
cookie_host = cookie.domain
|
124
|
+
cookie_path = cookie.path
|
125
|
+
|
126
|
+
# From RFC 2965, Section 3.3.2 Rejecting Cookies
|
127
|
+
|
128
|
+
# A user agent rejects (SHALL NOT store its information) if the
|
129
|
+
# Version attribute is missing. Note that the legacy Set-Cookie
|
130
|
+
# directive will result in an implicit version 0.
|
131
|
+
unless cookie.version
|
132
|
+
raise InvalidCookieError, "Cookie version not supplied (or implicit with Set-Cookie)"
|
133
|
+
end
|
134
|
+
|
135
|
+
# The value for the Path attribute is not a prefix of the request-URI
|
136
|
+
unless request_path.start_with? cookie_path
|
137
|
+
raise InvalidCookieError, "Cookie path should match or be a subset of the request path"
|
138
|
+
end
|
139
|
+
|
140
|
+
# The value for the Domain attribute contains no embedded dots, and the value is not .local
|
141
|
+
# Note: we also allow IPv4 and IPv6 addresses
|
142
|
+
unless cookie_host =~ IPADDR || cookie_host =~ /.\../ || cookie_host == '.local'
|
143
|
+
raise InvalidCookieError, "Cookie domain format is not legal"
|
144
|
+
end
|
145
|
+
|
146
|
+
# The effective host name that derives from the request-host does
|
147
|
+
# not domain-match the Domain attribute.
|
148
|
+
#
|
149
|
+
# The request-host is a HDN (not IP address) and has the form HD,
|
150
|
+
# where D is the value of the Domain attribute, and H is a string
|
151
|
+
# that contains one or more dots.
|
152
|
+
effective_host = effective_host uri
|
153
|
+
unless domains_match effective_host, cookie_host
|
154
|
+
raise InvalidCookieError, "Cookie domain is inappropriate based on request hostname"
|
155
|
+
end
|
156
|
+
|
157
|
+
# The Port attribute has a "port-list", and the request-port was
|
158
|
+
# not in the list.
|
159
|
+
if cookie.port.to_a.length != 0
|
160
|
+
unless cookie.port.to_a.find_index uri.port
|
161
|
+
raise InvalidCookieError, "incoming request port does not match cookie port(s)"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Note: 'secure' is not explicitly defined as an SSL channel, and no
|
166
|
+
# test is defined around validity and the 'secure' attribute
|
167
|
+
true
|
168
|
+
end
|
169
|
+
|
170
|
+
# Given a URI, compute the relevant search domains for pre-existing
|
171
|
+
# cookies. This includes all the valid dotted forms for a named or IP
|
172
|
+
# domains.
|
173
|
+
def compute_search_domains request_uri
|
174
|
+
uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
|
175
|
+
host = effective_host uri
|
176
|
+
result = [host]
|
177
|
+
if (host !~ IPADDR)
|
178
|
+
result << ".#{host}"
|
179
|
+
end
|
180
|
+
base = hostname_reach host
|
181
|
+
if (base)
|
182
|
+
result << ".#{base}"
|
183
|
+
end
|
184
|
+
result
|
185
|
+
end
|
186
|
+
|
187
|
+
# Return true if (given a URI, a cookie object and other options) a cookie
|
188
|
+
# should be sent to a host. Note that this currently ignores domain.
|
189
|
+
#
|
190
|
+
# The third option, 'script', indicates that cookies with the 'http only'
|
191
|
+
# extension should be ignored
|
192
|
+
def send_cookie? uri, cookie, script
|
193
|
+
# cookie path must start with the uri, it must not be a secure cookie being sent over http,
|
194
|
+
# and it must not be a http_only cookie sent to a script
|
195
|
+
path_match = uri.path.start_with? cookie.path
|
196
|
+
secure_match = !(cookie.secure && uri.scheme == 'http')
|
197
|
+
script_match = !(script && cookie.http_only)
|
198
|
+
expiry_match = cookie.expires_at.nil? || cookie.expires_at > Time.now
|
199
|
+
path_match && secure_match && script_match && expiry_match
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'cookiejar/cookie_logic'
|
2
|
+
|
3
|
+
# A cookie store for client side usage.
|
4
|
+
# - Enforces cookie validity rules
|
5
|
+
# - Returns just the cookies valid for a given URI
|
6
|
+
# - Handles expiration of cookies
|
7
|
+
# - Allows for persistence of cookie data (with or without sessoin)
|
8
|
+
#
|
9
|
+
#--
|
10
|
+
# Internal format:
|
11
|
+
#
|
12
|
+
# Internally, the data structure is a set of nested hashes.
|
13
|
+
# Domain Level:
|
14
|
+
# At the domain level, the hashes are of individual domains,
|
15
|
+
# down-cased and without any leading period. For instance, imagine cookies
|
16
|
+
# for .foo.com, .bar.com, and .auth.bar.com:
|
17
|
+
#
|
18
|
+
# {
|
19
|
+
# "foo.com" : <host data>,
|
20
|
+
# "bar.com" : <host data>,
|
21
|
+
# "auth.bar.com" : <host data>
|
22
|
+
# }
|
23
|
+
# Lookups are done both for the matching entry, and for an entry without
|
24
|
+
# the first segment up to the dot, ie. for /^\.?[^\.]+\.(.*)$/.
|
25
|
+
# A lookup of auth.bar.com would match both bar.com and
|
26
|
+
# auth.bar.com, but not entries for com or www.auth.bar.com.
|
27
|
+
#
|
28
|
+
# Host Level:
|
29
|
+
# Entries are in an hash, with keys of the path and values of a hash of
|
30
|
+
# cookie names to cookie object
|
31
|
+
#
|
32
|
+
# {
|
33
|
+
# "/" : {"session" : <Cookie>, "cart_id" : <Cookie>}
|
34
|
+
# "/protected" : {"authentication" : <Cookie>}
|
35
|
+
# }
|
36
|
+
#
|
37
|
+
# Paths are given a straight prefix string comparison to match.
|
38
|
+
# Further filters <secure, http only, ports> are not represented in this
|
39
|
+
# heirarchy.
|
40
|
+
#
|
41
|
+
# Cookies returned are ordered solely by specificity (length) of the
|
42
|
+
# path.
|
43
|
+
module CookieJar
|
44
|
+
class Jar
|
45
|
+
include CookieLogic
|
46
|
+
def initialize
|
47
|
+
@domains = {}
|
48
|
+
end
|
49
|
+
|
50
|
+
# Given a request URI and a literal Set-Cookie header value, attempt to
|
51
|
+
# add the cookie to the cookie store.
|
52
|
+
#
|
53
|
+
# returns the Cookie object on success, otherwise raises an
|
54
|
+
# InvalidCookieError
|
55
|
+
def set_cookie request_uri, cookie_header_value
|
56
|
+
uri = request_uri.is_a?(URI) ? request_uri : URI.parse(request_uri)
|
57
|
+
host = effective_host uri
|
58
|
+
cookie = Cookie.from_set_cookie(uri, cookie_header_value)
|
59
|
+
if (validate_cookie uri, cookie)
|
60
|
+
domain_paths = find_or_add_domain_for_cookie(cookie.domain)
|
61
|
+
add_cookie_to_path(domain_paths,cookie)
|
62
|
+
cookie
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Given a request URI, return a sorted list of Cookie objects. Cookies
|
67
|
+
# will be in order per RFC 2965 - sorted by longest path length, but
|
68
|
+
# otherwise unordered.
|
69
|
+
#
|
70
|
+
# optional arguments are
|
71
|
+
# - :script - if set, cookies set to be HTTP-only will be ignored
|
72
|
+
def get_cookies request_uri, args = {}
|
73
|
+
uri = request_uri.is_a?(URI)? request_uri : URI.parse(request_uri)
|
74
|
+
hosts = compute_search_domains uri
|
75
|
+
|
76
|
+
results = []
|
77
|
+
hosts.each do |host|
|
78
|
+
domain = find_domain_for_cookie host
|
79
|
+
domain.each do |path, cookies|
|
80
|
+
if uri.path.start_with? path
|
81
|
+
results += cookies.select do |name, cookie|
|
82
|
+
send_cookie? uri, cookie, args[:script]
|
83
|
+
end.collect do |name, cookie|
|
84
|
+
cookie
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
#Sort by path length, longest first
|
90
|
+
results.sort do |lhs, rhs|
|
91
|
+
rhs.path.length <=> lhs.path.length
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Given a request URI, return a sorted array of Cookie headers, in the
|
96
|
+
# format ['Cookie', '<Header Value>']. Cookies will be in order per
|
97
|
+
# RFC 2965 - sorted by longest path length, but otherwise unordered.
|
98
|
+
#
|
99
|
+
# optional arguments are
|
100
|
+
# - :script - if set, cookies set to be HTTP-only will be ignored
|
101
|
+
def get_cookie_headers request_uri, args = {}
|
102
|
+
cookies = get_cookies request_uri, args
|
103
|
+
cookies.map do |cookie|
|
104
|
+
['Cookie', "#{cookie.name}=#{cookie.value}"]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
protected
|
109
|
+
|
110
|
+
def find_domain_for_cookie domain
|
111
|
+
domain = effective_host domain
|
112
|
+
@domains[domain] || {}
|
113
|
+
end
|
114
|
+
|
115
|
+
def find_or_add_domain_for_cookie(domain)
|
116
|
+
domain = effective_host domain
|
117
|
+
@domains[domain] ||= {}
|
118
|
+
end
|
119
|
+
|
120
|
+
def add_cookie_to_path (paths, cookie)
|
121
|
+
path_entry = (paths[cookie.path] ||= {})
|
122
|
+
path_entry[cookie.name] = cookie
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/cookiejar.rb
ADDED
@@ -0,0 +1,236 @@
|
|
1
|
+
require 'cookiejar'
|
2
|
+
require 'cookiejar/cookie_logic'
|
3
|
+
include CookieJar
|
4
|
+
|
5
|
+
describe CookieLogic do
|
6
|
+
include CookieLogic
|
7
|
+
|
8
|
+
describe ".effective_host" do
|
9
|
+
it "should leave proper domains the same" do
|
10
|
+
['google.com', 'www.google.com', 'google.com.'].each do |value|
|
11
|
+
effective_host(value).should == value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
it "should handle a URI object" do
|
15
|
+
effective_host(URI.parse('http://example.com/')).should == 'example.com'
|
16
|
+
end
|
17
|
+
it "should add a local suffix on unqualified hosts" do
|
18
|
+
effective_host('localhost').should == 'localhost.local'
|
19
|
+
end
|
20
|
+
it "should leave IPv4 addresses alone" do
|
21
|
+
effective_host('127.0.0.1').should == '127.0.0.1'
|
22
|
+
end
|
23
|
+
it "should leave IPv6 addresses alone" do
|
24
|
+
['2001:db8:85a3::8a2e:370:7334', ':ffff:192.0.2.128'].each do |value|
|
25
|
+
effective_host(value).should == value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
it "should lowercase addresses" do
|
29
|
+
effective_host('FOO.COM').should == 'foo.com'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '.hostname_reach' do
|
34
|
+
it "should find the next highest subdomain" do
|
35
|
+
{'www.google.com' => 'google.com', 'auth.corp.companyx.com' => 'corp.companyx.com'}.each do |entry|
|
36
|
+
hostname_reach(entry[0]).should == entry[1]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
it "should handle domains with suffixed dots" do
|
40
|
+
hostname_reach('www.google.com.').should == 'google.com.'
|
41
|
+
end
|
42
|
+
it "should return nil for a root domain" do
|
43
|
+
hostname_reach('github.com').should be_nil
|
44
|
+
end
|
45
|
+
it "should return 'local' for a local domain" do
|
46
|
+
['foo.local', 'foo.local.'].each do |hostname|
|
47
|
+
hostname_reach(hostname).should == 'local'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
it "should return nil for an IPv4 address" do
|
51
|
+
hostname_reach('127.0.0.1').should be_nil
|
52
|
+
end
|
53
|
+
it "should return nil for IPv6 addresses" do
|
54
|
+
['2001:db8:85a3::8a2e:370:7334', '::ffff:192.0.2.128'].each do |value|
|
55
|
+
hostname_reach(value).should be_nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '.cookie_base_path' do
|
61
|
+
it "should leave '/' alone" do
|
62
|
+
cookie_base_path('/').should == '/'
|
63
|
+
end
|
64
|
+
it "should strip off everything after the last '/'" do
|
65
|
+
cookie_base_path('/foo/bar/baz').should == '/foo/bar/'
|
66
|
+
end
|
67
|
+
it "should handle query parameters and fragments with slashes" do
|
68
|
+
cookie_base_path('/foo/bar?query=a/b/c#fragment/b/c').should == '/foo/'
|
69
|
+
end
|
70
|
+
it "should handle URI objects" do
|
71
|
+
cookie_base_path(URI.parse('http://www.foo.com/bar/')).should == '/bar/'
|
72
|
+
end
|
73
|
+
it "should preserve case" do
|
74
|
+
cookie_base_path("/BaR/").should == '/BaR/'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '.determine_cookie_domain' do
|
79
|
+
it "should add a dot to the front of domains" do
|
80
|
+
determine_cookie_domain('http://foo.com/', 'foo.com').should == '.foo.com'
|
81
|
+
end
|
82
|
+
it "should not add a second dot if one present" do
|
83
|
+
determine_cookie_domain('http://foo.com/', '.foo.com').should == '.foo.com'
|
84
|
+
end
|
85
|
+
it "should handle Cookie objects" do
|
86
|
+
c = Cookie.from_set_cookie('http://foo.com/', "foo=bar;domain=foo.com")
|
87
|
+
determine_cookie_domain('http://foo.com/', c).should == '.foo.com'
|
88
|
+
end
|
89
|
+
it "should handle URI objects" do
|
90
|
+
determine_cookie_domain(URI.parse('http://foo.com/'), '.foo.com').should == '.foo.com'
|
91
|
+
end
|
92
|
+
it "should use an exact hostname when no domain specified" do
|
93
|
+
determine_cookie_domain('http://foo.com/', '').should == 'foo.com'
|
94
|
+
end
|
95
|
+
it "should leave IPv4 addresses alone" do
|
96
|
+
determine_cookie_domain('http://127.0.0.1/', '127.0.0.1').should == '127.0.0.1'
|
97
|
+
end
|
98
|
+
it "should leave IPv6 addresses alone" do
|
99
|
+
['2001:db8:85a3::8a2e:370:7334', '::ffff:192.0.2.128'].each do |value|
|
100
|
+
determine_cookie_domain("http://[#{value}]/", value).should == value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
describe '.determine_cookie_path' do
|
105
|
+
it "should use the requested path when none is specified for the cookie" do
|
106
|
+
determine_cookie_path('http://foo.com/', nil).should == '/'
|
107
|
+
determine_cookie_path('http://foo.com/bar/baz', '').should == '/bar/'
|
108
|
+
end
|
109
|
+
it "should handle URI objects" do
|
110
|
+
determine_cookie_path(URI.parse('http://foo.com/bar/'), '').should == '/bar/'
|
111
|
+
end
|
112
|
+
it "should handle Cookie objects" do
|
113
|
+
cookie = Cookie.from_set_cookie('http://foo.com/', "name=value;path=/")
|
114
|
+
determine_cookie_path('http://foo.com/', cookie).should == '/'
|
115
|
+
end
|
116
|
+
it "should ignore the request when a path is specified" do
|
117
|
+
determine_cookie_path('http://foo.com/ignorable/path', '/path/').should == '/path/'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
describe '.domains_match' do
|
121
|
+
it "should handle exact matches" do
|
122
|
+
domains_match('foo', 'foo').should be_true
|
123
|
+
domains_match('foo.com', 'foo.com').should be_true
|
124
|
+
domains_match('127.0.0.1', '127.0.0.1').should be_true
|
125
|
+
domains_match('::ffff:192.0.2.128', '::ffff:192.0.2.128').should be_true
|
126
|
+
end
|
127
|
+
it "should handle matching a superdomain" do
|
128
|
+
domains_match('auth.foo.com', 'foo.com').should be_true
|
129
|
+
domains_match('x.y.z.foo.com', 'y.z.foo.com').should be_true
|
130
|
+
end
|
131
|
+
it "should not match superdomains, or illegal domains" do
|
132
|
+
domains_match('x.y.z.foo.com', 'z.foo.com').should be_false
|
133
|
+
domains_match('foo.com', 'com').should be_false
|
134
|
+
end
|
135
|
+
it "should not match domains with and without a dot suffix together" do
|
136
|
+
domains_match('foo.com.', 'foo.com').should be_false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
describe '.validate_cookie' do
|
140
|
+
localaddr = 'http://localhost/foo/bar/'
|
141
|
+
it "should fail if version unset" do
|
142
|
+
unversioned = Cookie.new localaddr, :name => 'foo', :value => 'bar', :version => nil
|
143
|
+
lambda do
|
144
|
+
validate_cookie localaddr, unversioned
|
145
|
+
end.should raise_error InvalidCookieError
|
146
|
+
end
|
147
|
+
it "should fail if the path is more specific" do
|
148
|
+
subdirred = Cookie.new localaddr, :name => 'foo', :value => 'bar', :version => 0, :path => '/foo/bar/baz'
|
149
|
+
lambda do
|
150
|
+
validate_cookie localaddr, subdirred
|
151
|
+
end.should raise_error InvalidCookieError
|
152
|
+
end
|
153
|
+
it "should fail if the path is different than the request" do
|
154
|
+
difdirred = Cookie.new localaddr, :name => 'foo', :value => 'bar', :version => 0, :path => '/baz/'
|
155
|
+
lambda do
|
156
|
+
validate_cookie localaddr, difdirred
|
157
|
+
end.should raise_error InvalidCookieError
|
158
|
+
end
|
159
|
+
it "should fail if the domain has no dots" do
|
160
|
+
nodot = Cookie.new 'http://zero/', :name => 'foo', :value => 'bar', :version => 0, :domain => 'zero'
|
161
|
+
lambda do
|
162
|
+
validate_cookie 'http://zero/', nodot
|
163
|
+
end.should raise_error InvalidCookieError
|
164
|
+
end
|
165
|
+
it "should fail for explicit localhost" do
|
166
|
+
localhost = Cookie.new localaddr, :name => 'foo', :value => 'bar', :version => 0, :domain => 'localhost'
|
167
|
+
lambda do
|
168
|
+
validate_cookie localaddr, localhost
|
169
|
+
end.should raise_error InvalidCookieError
|
170
|
+
end
|
171
|
+
it "should fail for mismatched domains" do
|
172
|
+
foobar = Cookie.new 'http://www.foo.com/', :name => 'foo', :value => 'bar', :version => 0, :domain => 'bar.com'
|
173
|
+
lambda do
|
174
|
+
validate_cookie 'http://www.foo.com/', foobar
|
175
|
+
end.should raise_error InvalidCookieError
|
176
|
+
end
|
177
|
+
it "should fail for domains more than one level up" do
|
178
|
+
xyz = Cookie.new 'http://x.y.z.com/', :name => 'foo', :value => 'bar', :version => 0, :domain => 'z.com'
|
179
|
+
lambda do
|
180
|
+
validate_cookie 'http://x.y.z.com/', xyz
|
181
|
+
end.should raise_error InvalidCookieError
|
182
|
+
end
|
183
|
+
it "should fail for setting subdomain cookies" do
|
184
|
+
subdomain = Cookie.new 'http://foo.com/', :name => 'foo', :value => 'bar', :version => 0, :domain => 'auth.foo.com'
|
185
|
+
lambda do
|
186
|
+
validate_cookie 'http://foo.com/', subdomain
|
187
|
+
end.should raise_error InvalidCookieError
|
188
|
+
end
|
189
|
+
it "should handle a normal implicit internet cookie" do
|
190
|
+
normal = Cookie.new 'http://foo.com/', :name => 'foo', :value => 'bar', :version => 0
|
191
|
+
validate_cookie('http://foo.com/', normal).should be_true
|
192
|
+
end
|
193
|
+
it "should handle a normal implicit localhost cookie" do
|
194
|
+
localhost = Cookie.new 'http://localhost/', :name => 'foo', :value => 'bar', :version => 0
|
195
|
+
validate_cookie('http://localhost/', localhost).should be_true
|
196
|
+
end
|
197
|
+
it "should handle an implicit IP address cookie" do
|
198
|
+
ipaddr = Cookie.new 'http://127.0.0.1/', :name => 'foo', :value => 'bar', :version => 0
|
199
|
+
validate_cookie('http://127.0.0.1/', ipaddr).should be_true
|
200
|
+
end
|
201
|
+
it "should handle an explicit domain on an internet site" do
|
202
|
+
explicit = Cookie.new 'http://foo.com/', :name => 'foo', :value => 'bar', :version => 0, :domain => '.foo.com'
|
203
|
+
validate_cookie('http://foo.com/', explicit).should be_true
|
204
|
+
end
|
205
|
+
it "should handle setting a cookie explicitly on a superdomain" do
|
206
|
+
superdomain = Cookie.new 'http://auth.foo.com/', :name => 'foo', :value => 'bar', :version => 0, :domain => '.foo.com'
|
207
|
+
validate_cookie('http://foo.com/', superdomain).should be_true
|
208
|
+
end
|
209
|
+
it "should handle explicitly setting a cookie" do
|
210
|
+
explicit = Cookie.new 'http://foo.com/bar/', :name => 'foo', :value => 'bar', :version => 0, :path => '/bar/'
|
211
|
+
validate_cookie('http://foo.com/bar/', explicit)
|
212
|
+
end
|
213
|
+
it "should handle setting a cookie on a higher path" do
|
214
|
+
higher = Cookie.new 'http://foo.com/bar/baz/', :name => 'foo', :value => 'bar', :version => 0, :path => '/bar/'
|
215
|
+
validate_cookie('http://foo.com/bar/baz/', higher)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
describe '.compute_search_domains' do
|
219
|
+
it "should handle subdomains" do
|
220
|
+
compute_search_domains('http://www.auth.foo.com/').should ==
|
221
|
+
['www.auth.foo.com', '.www.auth.foo.com', '.auth.foo.com']
|
222
|
+
end
|
223
|
+
it "should handle root domains" do
|
224
|
+
compute_search_domains('http://foo.com/').should ==
|
225
|
+
['foo.com', '.foo.com']
|
226
|
+
end
|
227
|
+
it "should handle IP addresses" do
|
228
|
+
compute_search_domains('http://127.0.0.1/').should ==
|
229
|
+
['127.0.0.1']
|
230
|
+
end
|
231
|
+
it "should handle local addresses" do
|
232
|
+
compute_search_domains('http://zero/').should ==
|
233
|
+
['zero.local', '.zero.local', '.local']
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
data/test/cookie_test.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'cookiejar'
|
2
|
+
include CookieJar
|
3
|
+
|
4
|
+
NETSCAPE_SPEC_SET_COOKIE_HEADERS =
|
5
|
+
['CUSTOMER=WILE_E_COYOTE; path=/; expires=Wednesday, 09-Nov-99 23:12:40 GMT',
|
6
|
+
'PART_NUMBER=ROCKET_LAUNCHER_0001; path=/',
|
7
|
+
'SHIPPING=FEDEX; path=/foo',
|
8
|
+
'PART_NUMBER=ROCKET_LAUNCHER_0001; path=/',
|
9
|
+
'PART_NUMBER=RIDING_ROCKET_0023; path=/ammo']
|
10
|
+
|
11
|
+
describe Cookie do
|
12
|
+
describe "#from_set_cookie" do
|
13
|
+
it "should handle cookies from the netscape spec" do
|
14
|
+
NETSCAPE_SPEC_SET_COOKIE_HEADERS.each do |header|
|
15
|
+
cookie = Cookie.from_set_cookie 'http://localhost', header
|
16
|
+
end
|
17
|
+
end
|
18
|
+
it "should give back the input names and values" do
|
19
|
+
cookie = Cookie.from_set_cookie 'http://localhost/', 'foo=bar'
|
20
|
+
cookie.name.should == 'foo'
|
21
|
+
cookie.value.should == 'bar'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/test/jar_test.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'cookiejar'
|
2
|
+
require 'yaml'
|
3
|
+
include CookieJar
|
4
|
+
|
5
|
+
describe Jar do
|
6
|
+
describe '.setCookie' do
|
7
|
+
it "should allow me to set a cookie" do
|
8
|
+
jar = Jar.new
|
9
|
+
jar.set_cookie 'http://foo.com/', 'foo=bar'
|
10
|
+
end
|
11
|
+
it "should allow me to set multiple cookies" do
|
12
|
+
jar = Jar.new
|
13
|
+
jar.set_cookie 'http://foo.com/', 'foo=bar'
|
14
|
+
jar.set_cookie 'http://foo.com/', 'bar=baz'
|
15
|
+
jar.set_cookie 'http://auth.foo.com/', 'foo=bar'
|
16
|
+
jar.set_cookie 'http://auth.foo.com/', 'auth=135121...;domain=foo.com'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
describe '.get_cookies' do
|
20
|
+
it "should let me read back cookies which are set" do
|
21
|
+
jar = Jar.new
|
22
|
+
jar.set_cookie 'http://foo.com/', 'foo=bar'
|
23
|
+
jar.set_cookie 'http://foo.com/', 'bar=baz'
|
24
|
+
jar.set_cookie 'http://auth.foo.com/', 'foo=bar'
|
25
|
+
jar.set_cookie 'http://auth.foo.com/', 'auth=135121...;domain=foo.com'
|
26
|
+
jar.get_cookies('http://foo.com/').should have(3).items
|
27
|
+
end
|
28
|
+
it "should return cookies longest path first" do
|
29
|
+
jar = Jar.new
|
30
|
+
uri = 'http://foo.com/a/b/c/d'
|
31
|
+
jar.set_cookie uri, 'a=bar'
|
32
|
+
jar.set_cookie uri, 'b=baz;path=/a/b/c/d'
|
33
|
+
jar.set_cookie uri, 'c=bar;path=/a/b'
|
34
|
+
jar.set_cookie uri, 'd=bar;path=/a/'
|
35
|
+
cookies = jar.get_cookies(uri)
|
36
|
+
cookies.should have(4).items
|
37
|
+
cookies[0].name.should == 'b'
|
38
|
+
cookies[1].name.should == 'a'
|
39
|
+
cookies[2].name.should == 'c'
|
40
|
+
cookies[3].name.should == 'd'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
describe '.get_cookie_headers' do
|
44
|
+
it "should return cookie headers" do
|
45
|
+
jar = Jar.new
|
46
|
+
uri = 'http://foo.com/a/b/c/d'
|
47
|
+
jar.set_cookie uri, 'a=bar'
|
48
|
+
jar.set_cookie uri, 'b=baz;path=/a/b/c/d'
|
49
|
+
cookie_headers = jar.get_cookie_headers uri
|
50
|
+
cookie_headers.should have(2).items
|
51
|
+
cookie_headers.should == [['Cookie', 'b=baz'],['Cookie', 'a=bar']]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dwaite-cookiejar
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Waite
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-09-07 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Allows for parsing and returning cookies in Ruby HTTP client code
|
17
|
+
email: david@alkaline-solutions.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- lib/cookiejar/cookie.rb
|
26
|
+
- lib/cookiejar/cookie_common.rb
|
27
|
+
- lib/cookiejar/cookie_logic.rb
|
28
|
+
- lib/cookiejar/jar.rb
|
29
|
+
- lib/cookiejar.rb
|
30
|
+
- test/cookie_logic_test.rb
|
31
|
+
- test/cookie_test.rb
|
32
|
+
- test/jar_test.rb
|
33
|
+
has_rdoc: false
|
34
|
+
homepage: http://alkaline-solutions.com
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options:
|
37
|
+
- --title
|
38
|
+
- CookieJar -- Client-side HTTP Cookies
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.2.0
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: Client-side HTTP Cookie library
|
60
|
+
test_files: []
|
61
|
+
|