http_monkey-cookie 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in http_monkey-cookie.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Roger Leite
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,54 @@
1
+ # HttpMonkey::Cookie
2
+
3
+ Rack middleware to use with [HttpMonkey](https://github.com/rogerleite/http_monkey) and support magic cookies on your requests.
4
+
5
+ TOC:
6
+
7
+ * [Installation](#installation)
8
+ * [Usage](#usage)
9
+ * [Notes](#notes)
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'http_monkey-cookie'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install http_monkey-cookie
24
+
25
+ ## Usage
26
+
27
+ ``` ruby
28
+ require "http_monkey"
29
+ require "http_monkey/cookie"
30
+
31
+ HttpMonkey.configure do
32
+ # Default HTTP Headers (to all requests)
33
+ middlewares.use HttpMonkey::M::Cookie
34
+ end
35
+
36
+ response = HttpMonkey.at("http://domain.com").get
37
+ # Returns Set-Cookie: token=magic;Version=1;Comment=;Domain=.domain.com.br;Path=/;Max-Age=999999999;httpOnly
38
+
39
+ HttpMonkey.at("http://domain.com/service").get
40
+ # Uses Cookie: token=magic etc.
41
+
42
+ HttpMonkey.at("http://example.com").get
43
+ # Don't send cookies
44
+ ```
45
+
46
+ ## Notes
47
+
48
+ This version is experimental and can explode in any moment.
49
+
50
+ Some resources to build a new cookie project:
51
+ * [Great doc about cookies specifications](http://hc.apache.org/httpclient-3.x/cookies.html)
52
+ * [Persistent Client State HTTP Cookies (netscape spec)](http://curl.haxx.se/rfc/cookie_spec.html)
53
+ * [HTTP State Management Mechanism RFC 2109](http://pretty-rfc.herokuapp.com/RFC2109)
54
+ * [HTTP State Management Mechanism RFC 2965](http://pretty-rfc.herokuapp.com/RFC2965)
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ ENV["RUBYOPT"] = "rubygems" if ENV["RUBYOPT"].nil?
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.test_files = FileList['test/**/*_test.rb']
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'http_monkey/cookie/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "http_monkey-cookie"
8
+ gem.version = HttpMonkey::Cookie::VERSION
9
+ gem.authors = ["Roger Leite"]
10
+ gem.email = ["roger.barreto@gmail.com"]
11
+ gem.description = %q{Rack middleware to support magic cookie on clients}
12
+ gem.summary = %q{Rack middleware to support magic cookie on clients}
13
+ gem.homepage = "https://github.com/rogerleite/http_monkey-cookie"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_runtime_dependency "http_monkey", "~> 0.0"
20
+ # https://github.com/rogerleite/cookiejar/tree/http_monkey, branch "http_monkey"
21
+ # Manually copied from git repository to lib folder here. It's a shame! I know.
22
+ # $ cp -R ../cookiejar/lib/* lib/
23
+
24
+ gem.add_development_dependency "rake"
25
+ gem.add_development_dependency "minitest", "~> 3"
26
+ gem.add_development_dependency "minitest-reporters", "~> 0.7.0"
27
+ gem.add_development_dependency "mocha"
28
+ gem.add_development_dependency "minion_server"
29
+ end
@@ -0,0 +1,2 @@
1
+ require 'cookiejar/cookie'
2
+ require 'cookiejar/jar'
@@ -0,0 +1,252 @@
1
+ require 'time'
2
+ require 'uri'
3
+ require 'cookiejar/cookie_validation'
4
+
5
+ module CookieJar
6
+
7
+ # Cookie is an immutable object which defines the data model of a HTTP Cookie.
8
+ # The data values within the cookie may be different from the
9
+ # values described in the literal cookie declaration.
10
+ # Specifically, the 'domain' and 'path' values may be set to defaults
11
+ # based on the requested resource that resulted in the cookie being set.
12
+ class Cookie
13
+
14
+ # [String] The name of the cookie.
15
+ attr_reader :name
16
+ # [String] The value of the cookie, without any attempts at decoding.
17
+ attr_reader :value
18
+
19
+ # [String] The domain scope of the cookie. Follows the RFC 2965
20
+ # 'effective host' rules. A 'dot' prefix indicates that it applies both
21
+ # to the non-dotted domain and child domains, while no prefix indicates
22
+ # that only exact matches of the domain are in scope.
23
+ attr_reader :domain
24
+
25
+ # [String] The path scope of the cookie. The cookie applies to URI paths
26
+ # that prefix match this value.
27
+ attr_reader :path
28
+
29
+ # [Boolean] The secure flag is set to indicate that the cookie should
30
+ # only be sent securely. Nearly all HTTP User Agent implementations assume
31
+ # this to mean that the cookie should only be sent over a
32
+ # SSL/TLS-protected connection
33
+ attr_reader :secure
34
+
35
+ # [Boolean] Popular browser extension to mark a cookie as invisible
36
+ # to code running within the browser, such as JavaScript
37
+ attr_reader :http_only
38
+
39
+ # [Fixnum] Version indicator, currently either
40
+ # * 0 for netscape cookies
41
+ # * 1 for RFC 2965 cookies
42
+ attr_reader :version
43
+ # [String] RFC 2965 field for indicating comment (or a location)
44
+ # describing the cookie to a usesr agent.
45
+ attr_reader :comment, :comment_url
46
+ # [Boolean] RFC 2965 field for indicating session lifetime for a cookie
47
+ attr_reader :discard
48
+ # [Array<FixNum>, nil] RFC 2965 port scope for the cookie. If not nil,
49
+ # indicates specific ports on the HTTP server which should receive this
50
+ # cookie if contacted.
51
+ attr_reader :ports
52
+ # [Time] Time when this cookie was first evaluated and created.
53
+ attr_reader :created_at
54
+
55
+ # Evaluate when this cookie will expire. Uses the original cookie fields
56
+ # for a max age or expires
57
+ #
58
+ # @return [Time, nil] Time of expiry, if this cookie has an expiry set
59
+ def expires_at
60
+ if @expiry.nil? || @expiry.is_a?(Time)
61
+ @expiry
62
+ else
63
+ @created_at + @expiry
64
+ end
65
+ end
66
+
67
+ # Indicates whether the cookie is currently considered valid
68
+ #
69
+ # @param [Time] time to compare against, or 'now' if omitted
70
+ # @return [Boolean]
71
+ def expired? (time = Time.now)
72
+ expires_at != nil && time > expires_at
73
+ end
74
+
75
+ # Indicates whether the cookie will be considered invalid after the end
76
+ # of the current user session
77
+ # @return [Boolean]
78
+ def session?
79
+ @expiry == nil || @discard
80
+ end
81
+
82
+ # Create a cookie based on an absolute URI and the string value of a
83
+ # 'Set-Cookie' header.
84
+ #
85
+ # @param request_uri [String, URI] HTTP/HTTPS absolute URI of request.
86
+ # This is used to fill in domain and port if missing from the cookie,
87
+ # and to perform appropriate validation.
88
+ # @param set_cookie_value [String] HTTP value for the Set-Cookie header.
89
+ # @return [Cookie] created from the header string and request URI
90
+ # @raise [InvalidCookieError] on validation failure(s)
91
+ def self.from_set_cookie request_uri, set_cookie_value
92
+ args = CookieJar::CookieValidation.parse_set_cookie set_cookie_value
93
+ args[:domain] = CookieJar::CookieValidation.determine_cookie_domain request_uri, args[:domain]
94
+ args[:path] = CookieJar::CookieValidation.determine_cookie_path request_uri, args[:path]
95
+ cookie = Cookie.new args
96
+ CookieJar::CookieValidation.validate_cookie request_uri, cookie
97
+ cookie
98
+ end
99
+
100
+ # Create a cookie based on an absolute URI and the string value of a
101
+ # 'Set-Cookie2' header.
102
+ #
103
+ # @param request_uri [String, URI] HTTP/HTTPS absolute URI of request.
104
+ # This is used to fill in domain and port if missing from the cookie,
105
+ # and to perform appropriate validation.
106
+ # @param set_cookie_value [String] HTTP value for the Set-Cookie2 header.
107
+ # @return [Cookie] created from the header string and request URI
108
+ # @raise [InvalidCookieError] on validation failure(s)
109
+ def self.from_set_cookie2 request_uri, set_cookie_value
110
+ args = CookieJar::CookieValidation.parse_set_cookie2 set_cookie_value
111
+ args[:domain] = CookieJar::CookieValidation.determine_cookie_domain request_uri, args[:domain]
112
+ args[:path] = CookieJar::CookieValidation.determine_cookie_path request_uri, args[:path]
113
+ cookie = Cookie.new args
114
+ CookieJar::CookieValidation.validate_cookie request_uri, cookie
115
+ cookie
116
+ end
117
+
118
+ # Returns cookie in a format appropriate to send to a server.
119
+ #
120
+ # @param [FixNum] 0 version, 0 for Netscape-style cookies, 1 for
121
+ # RFC2965-style.
122
+ # @param [Boolean] true prefix, for RFC2965, whether to prefix with
123
+ # "$Version=<version>;". Ignored for Netscape-style cookies
124
+ def to_s ver=0, prefix=true
125
+ case ver
126
+ when 0
127
+ "#{name}=#{value}"
128
+ when 1
129
+ # we do not need to encode path; the only characters required to be
130
+ # quoted must be escaped in URI
131
+ str = prefix ? "$Version=#{version};" : ""
132
+ str << "#{name}=#{value};$Path=\"#{path}\""
133
+ if domain.start_with? '.'
134
+ str << ";$Domain=#{domain}"
135
+ end
136
+ if ports
137
+ str << ";$Port=\"#{ports.join ','}\""
138
+ end
139
+ str
140
+ end
141
+ end
142
+
143
+ # Determine if a cookie should be sent given a request URI along with
144
+ # other options.
145
+ #
146
+ # This currently ignores domain.
147
+ #
148
+ # @param uri [String, URI] the requested page which may need to receive
149
+ # this cookie
150
+ # @param script [Boolean] indicates that cookies with the 'httponly'
151
+ # extension should be ignored
152
+ # @return [Boolean] whether this cookie should be sent to the server
153
+ def should_send? request_uri, script
154
+ uri = CookieJar::CookieValidation.to_uri request_uri
155
+ # cookie path must start with the uri, it must not be a secure cookie
156
+ # being sent over http, and it must not be a http_only cookie sent to
157
+ # a script
158
+ path_match = uri.path.start_with? @path
159
+ secure_match = !(@secure && uri.scheme == 'http')
160
+ script_match = !(script && @http_only)
161
+ expiry_match = !expired?
162
+ ports_match = ports.nil? || (ports.include? uri.port)
163
+ path_match && secure_match && script_match && expiry_match && ports_match
164
+ end
165
+
166
+ def decoded_value
167
+ CookieJar::CookieValidation::decode_value value
168
+ end
169
+
170
+ # Return a JSON 'object' for the various data values. Allows for
171
+ # persistence of the cookie information
172
+ #
173
+ # @param [Array] a options controlling output JSON text
174
+ # (usually a State and a depth)
175
+ # @return [String] JSON representation of object data
176
+ def to_json *a
177
+ result = {
178
+ :json_class => self.class.name,
179
+ :name => @name,
180
+ :value => @value,
181
+ :domain => @domain,
182
+ :path => @path,
183
+ :created_at => @created_at
184
+ }
185
+ {
186
+ :expiry => @expiry,
187
+ :secure => (true if @secure),
188
+ :http_only => (true if @http_only),
189
+ :version => (@version if version != 0),
190
+ :comment => @comment,
191
+ :comment_url => @comment_url,
192
+ :discard => (true if @discard),
193
+ :ports => @ports
194
+ }.each do |name, value|
195
+ result[name] = value if value
196
+ end
197
+ result.to_json(*a)
198
+ end
199
+
200
+ # Given a Hash representation of a JSON document, create a local cookie
201
+ # from the included data.
202
+ #
203
+ # @param [Hash] o JSON object of array data
204
+ # @return [Cookie] cookie formed from JSON data
205
+ def self.json_create o
206
+ params = o.inject({}) do |hash, (key, value)|
207
+ hash[key.to_sym] = value
208
+ hash
209
+ end
210
+ params[:version] ||= 0
211
+ params[:created_at] = Time.parse params[:created_at]
212
+ if params[:expiry].is_a? String
213
+ params[:expires_at] = Time.parse params[:expiry]
214
+ else
215
+ params[:max_age] = params[:expiry]
216
+ end
217
+ params.delete :expiry
218
+
219
+ self.new params
220
+ end
221
+
222
+ # Compute the cookie search domains for a given request URI
223
+ # This will be the effective host of the request uri, along with any
224
+ # possibly matching dot-prefixed domains
225
+ #
226
+ # @param request_uri [String, URI] address being requested
227
+ # @return [Array<String>] String domain matches
228
+ def self.compute_search_domains request_uri
229
+ CookieValidation.compute_search_domains request_uri
230
+ end
231
+ protected
232
+ # Call {from_set_cookie} to create a new Cookie instance
233
+ def initialize args
234
+
235
+ @created_at, @name, @value, @domain, @path, @secure,
236
+ @http_only, @version, @comment, @comment_url, @discard, @ports \
237
+ = args.values_at \
238
+ :created_at, :name, :value, :domain, :path, :secure,
239
+ :http_only, :version, :comment, :comment_url, :discard, :ports
240
+
241
+ @created_at ||= Time.now
242
+ @expiry = args[:max_age] || args[:expires_at]
243
+ @secure ||= false
244
+ @http_only ||= false
245
+ @discard ||= false
246
+
247
+ if @ports.is_a? Integer
248
+ @ports = [@ports]
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,390 @@
1
+ require 'cgi'
2
+ require 'uri'
3
+ module CookieJar
4
+ # Represents a set of cookie validation errors
5
+ class InvalidCookieError < StandardError
6
+ # [Array<String>] the specific validation issues encountered
7
+ attr_reader :messages
8
+
9
+ # Create a new instance
10
+ # @param [String, Array<String>] the validation issue(s) encountered
11
+ def initialize message
12
+ if message.is_a? Array
13
+ @messages = message
14
+ message = message.join ', '
15
+ else
16
+ @messages = [message]
17
+ end
18
+ super message
19
+ end
20
+ end
21
+
22
+ # Contains logic to parse and validate cookie headers
23
+ module CookieValidation
24
+ module PATTERN
25
+ include URI::REGEXP::PATTERN
26
+
27
+ TOKEN = '[^(),\/<>@;:\\\"\[\]?={}\s]+'
28
+ VALUE1 = "([^;]*)"
29
+ IPADDR = "#{IPV4ADDR}|#{IPV6ADDR}"
30
+ BASE_HOSTNAME = "(?:#{DOMLABEL}\\.)(?:((?:(?:#{DOMLABEL}\\.)+(?:#{TOPLABEL}\\.?))|local))"
31
+ BASE3_HOSTNAME = "(?:#{DOMLABEL}\\.)(?:#{DOMLABEL}\\.)(?:((?:(?:#{DOMLABEL}\\.)+(?:#{TOPLABEL}\\.?))|local))"
32
+
33
+ QUOTED_PAIR = "\\\\[\\x00-\\x7F]"
34
+ LWS = "\\r\\n(?:[ \\t]+)"
35
+ # TEXT="[\\t\\x20-\\x7E\\x80-\\xFF]|(?:#{LWS})"
36
+ QDTEXT="[\\t\\x20-\\x21\\x23-\\x7E\\x80-\\xFF]|(?:#{LWS})"
37
+ QUOTED_TEXT = "\\\"(?:#{QDTEXT}|#{QUOTED_PAIR})*\\\""
38
+ VALUE2 = "#{TOKEN}|#{QUOTED_TEXT}"
39
+
40
+ end
41
+ BASE_HOSTNAME = /#{PATTERN::BASE_HOSTNAME}/
42
+ BASE3_HOSTNAME = /#{PATTERN::BASE3_HOSTNAME}/ # Check 3 levels. Ex: dev.service.api.com.br
43
+ BASE_PATH = /\A((?:[^\/?#]*\/)*)/
44
+ IPADDR = /\A#{PATTERN::IPV4ADDR}\Z|\A#{PATTERN::IPV6ADDR}\Z/
45
+ HDN = /\A#{PATTERN::HOSTNAME}\Z/
46
+ TOKEN = /\A#{PATTERN::TOKEN}\Z/
47
+ PARAM1 = /\A(#{PATTERN::TOKEN})(?:=#{PATTERN::VALUE1})?\Z/
48
+ PARAM2 = Regexp.new "(#{PATTERN::TOKEN})(?:=(#{PATTERN::VALUE2}))?(?:\\Z|;)", '', 'n'
49
+ TWO_DOT_DOMAINS = /\A\.(com|edu|net|mil|gov|int|org)\Z/
50
+
51
+ # Converts the input object to a URI (if not already a URI)
52
+ #
53
+ # @param [String, URI] request_uri URI we are normalizing
54
+ # @param [URI] URI representation of input string, or original URI
55
+ def self.to_uri request_uri
56
+ (request_uri.is_a? URI)? request_uri : (URI.parse request_uri)
57
+ end
58
+
59
+ # Converts an input cookie or uri to a string representing the path.
60
+ # Assume strings are already paths
61
+ #
62
+ # @param [String, URI, Cookie] object containing the path
63
+ # @return [String] path information
64
+ def self.to_path uri_or_path
65
+ if (uri_or_path.is_a? URI) || (uri_or_path.is_a? Cookie)
66
+ uri_or_path.path
67
+ else
68
+ uri_or_path
69
+ end
70
+ end
71
+
72
+ # Converts an input cookie or uri to a string representing the domain.
73
+ # Assume strings are already domains. Value may not be an effective host.
74
+ #
75
+ # @param [String, URI, Cookie] object containing the domain
76
+ # @return [String] domain information.
77
+ def self.to_domain uri_or_domain
78
+ if uri_or_domain.is_a? URI
79
+ uri_or_domain.host
80
+ elsif uri_or_domain.is_a? Cookie
81
+ uri_or_domain.domain
82
+ else
83
+ uri_or_domain
84
+ end
85
+ end
86
+
87
+ # Compare a tested domain against the base domain to see if they match, or
88
+ # if the base domain is reachable.
89
+ #
90
+ # @param [String] tested_domain domain to be tested against
91
+ # @param [String] base_domain new domain being tested
92
+ # @return [String,nil] matching domain on success, nil on failure
93
+ def self.domains_match tested_domain, base_domain
94
+ base = effective_host base_domain
95
+ search_domains = compute_search_domains_for_host base
96
+ #puts "== #{tested_domain.inspect} vs #{search_domains.inspect}" # debug like js
97
+ result = search_domains.find do |domain|
98
+ domain == tested_domain
99
+ end
100
+ result
101
+ end
102
+
103
+ # Compute the base of a path, for default cookie path assignment
104
+ #
105
+ # @param [String, URI, Cookie] path, or object holding path
106
+ # @return base path (all characters up to final '/')
107
+ def self.cookie_base_path path
108
+ BASE_PATH.match(to_path path)[1]
109
+ end
110
+
111
+ # Processes cookie path data using the following rules:
112
+ # Paths are separated by '/' characters, and accepted values are truncated
113
+ # to the last '/' character. If no path is specified in the cookie, a path
114
+ # value will be taken from the request URI which was used for the site.
115
+ #
116
+ # Note that this will not attempt to detect a mismatch of the request uri domain
117
+ # and explicitly specified cookie path
118
+ #
119
+ # @param [String,URI] request URI yielding this cookie
120
+ # @param [String] path on cookie
121
+ def self.determine_cookie_path request_uri, cookie_path
122
+ uri = to_uri request_uri
123
+ cookie_path = to_path cookie_path
124
+
125
+ if cookie_path == nil || cookie_path.empty?
126
+ cookie_path = cookie_base_path uri.path
127
+ end
128
+ cookie_path
129
+ end
130
+
131
+ # Given a URI, compute the relevant search domains for pre-existing
132
+ # cookies. This includes all the valid dotted forms for a named or IP
133
+ # domains.
134
+ #
135
+ # @param [String, URI] request_uri requested uri
136
+ # @return [Array<String>] all cookie domain values which would match the
137
+ # requested uri
138
+ def self.compute_search_domains request_uri
139
+ uri = to_uri request_uri
140
+ host = uri.host
141
+ compute_search_domains_for_host host
142
+ end
143
+
144
+ # Given a host, compute the relevant search domains for pre-existing
145
+ # cookies
146
+ #
147
+ # @param [String] host host being requested
148
+ # @return [Array<String>] all cookie domain values which would match the
149
+ # requested uri
150
+ def self.compute_search_domains_for_host host
151
+ host = effective_host host
152
+ result = [host]
153
+ unless host =~ IPADDR
154
+ splited_host = host.split(/\./)
155
+ splited_host.each_with_index do |subdomain, index|
156
+ break if TWO_DOT_DOMAINS =~ ".#{subdomain}"
157
+ result << ".#{splited_host[index..-1].join('.')}"
158
+ end
159
+ end
160
+ result
161
+ end
162
+
163
+ # Processes cookie domain data using the following rules:
164
+ # Domains strings of the form .foo.com match 'foo.com' and all immediate
165
+ # subdomains of 'foo.com'. Domain strings specified of the form 'foo.com' are
166
+ # modified to '.foo.com', and as such will still apply to subdomains.
167
+ #
168
+ # Cookies without an explicit domain will have their domain value taken directly
169
+ # from the URL, and will _NOT_ have any leading dot applied. For example, a request
170
+ # to http://foo.com/ will cause an entry for 'foo.com' to be created - which applies
171
+ # to foo.com but no subdomain.
172
+ #
173
+ # Note that this will not attempt to detect a mismatch of the request uri domain
174
+ # and explicitly specified cookie domain
175
+ #
176
+ # @param [String, URI] request_uri originally requested URI
177
+ # @param [String] cookie domain value
178
+ # @return [String] effective host
179
+ def self.determine_cookie_domain request_uri, cookie_domain
180
+ uri = to_uri request_uri
181
+ domain = to_domain cookie_domain
182
+
183
+ if domain == nil || domain.empty?
184
+ domain = effective_host uri.host
185
+ else
186
+ domain = domain.downcase
187
+ if domain =~ IPADDR || domain.start_with?('.')
188
+ domain
189
+ else
190
+ ".#{domain}"
191
+ end
192
+ end
193
+ end
194
+
195
+ # Compute the effective host (RFC 2965, section 1)
196
+ #
197
+ # Has the added additional logic of searching for interior dots specifically, and
198
+ # matches colons to prevent .local being suffixed on IPv6 addresses
199
+ #
200
+ # @param [String, URI] host_or_uridomain name, or absolute URI
201
+ # @return [String] effective host per RFC rules
202
+ def self.effective_host host_or_uri
203
+ hostname = to_domain host_or_uri
204
+ hostname = hostname.downcase
205
+
206
+ if /.[\.:]./.match(hostname) || hostname == '.local'
207
+ hostname
208
+ else
209
+ hostname + '.local'
210
+ end
211
+ end
212
+
213
+ # Check whether a cookie meets all of the rules to be created, based on
214
+ # its internal settings and the URI it came from.
215
+ #
216
+ # @param [String,URI] request_uri originally requested URI
217
+ # @param [Cookie] cookie object
218
+ # @param [true] will always return true on success
219
+ # @raise [InvalidCookieError] on failures, containing all validation errors
220
+ def self.validate_cookie request_uri, cookie
221
+ uri = to_uri request_uri
222
+ request_host = effective_host uri.host
223
+ request_path = uri.path
224
+ request_secure = (uri.scheme == 'https')
225
+ cookie_host = cookie.domain
226
+ cookie_path = cookie.path
227
+
228
+ errors = []
229
+
230
+ # From RFC 2965, Section 3.3.2 Rejecting Cookies
231
+
232
+ # A user agent rejects (SHALL NOT store its information) if the
233
+ # Version attribute is missing. Note that the legacy Set-Cookie
234
+ # directive will result in an implicit version 0.
235
+ unless cookie.version
236
+ errors << "Version missing"
237
+ end
238
+
239
+ # The value for the Path attribute is not a prefix of the request-URI
240
+ unless request_path.start_with? cookie_path
241
+ errors << "Path is not a prefix of the request uri path"
242
+ end
243
+
244
+ unless cookie_host =~ IPADDR || #is an IPv4 or IPv6 address
245
+ cookie_host =~ /.\../ || #contains an embedded dot
246
+ cookie_host == '.local' #is the domain cookie for local addresses
247
+ errors << "Domain format is illegal"
248
+ end
249
+
250
+ # The effective host name that derives from the request-host does
251
+ # not domain-match the Domain attribute.
252
+ #
253
+ # The request-host is a HDN (not IP address) and has the form HD,
254
+ # where D is the value of the Domain attribute, and H is a string
255
+ # that contains one or more dots.
256
+ unless domains_match cookie_host, uri
257
+ errors << "Domain (#{cookie_host}) is inappropriate based on request URI hostname (#{uri.to_s})"
258
+ end
259
+
260
+ # The Port attribute has a "port-list", and the request-port was
261
+ # not in the list.
262
+ unless cookie.ports.nil? || cookie.ports.length != 0
263
+ unless cookie.ports.find_index uri.port
264
+ errors << "Ports list does not contain request URI port"
265
+ end
266
+ end
267
+
268
+ raise (InvalidCookieError.new errors) unless errors.empty?
269
+
270
+ # Note: 'secure' is not explicitly defined as an SSL channel, and no
271
+ # test is defined around validity and the 'secure' attribute
272
+ true
273
+ end
274
+
275
+ # Break apart a traditional (non RFC 2965) cookie value into its core
276
+ # components. This does not do any validation, or defaulting of values
277
+ # based on requested URI
278
+ #
279
+ # @param [String] set_cookie_value a Set-Cookie header formatted cookie
280
+ # definition
281
+ # @return [Hash] Contains the parsed values of the cookie
282
+ def self.parse_set_cookie set_cookie_value
283
+ args = { }
284
+ params=set_cookie_value.split /;\s*/
285
+
286
+ first=true
287
+ params.each do |param|
288
+ result = PARAM1.match param
289
+ if !result
290
+ raise InvalidCookieError.new "Invalid cookie parameter in cookie '#{set_cookie_value}'"
291
+ end
292
+ key = result[1].downcase.to_sym
293
+ keyvalue = result[2]
294
+ if first
295
+ args[:name] = result[1]
296
+ args[:value] = keyvalue
297
+ first = false
298
+ else
299
+ case key
300
+ when :expires
301
+ args[:expires_at] = Time.parse keyvalue
302
+ when *[:domain, :path, :version, :comment, :"max-age"]
303
+ args[key] = keyvalue
304
+ when :secure
305
+ args[:secure] = true
306
+ when :httponly
307
+ args[:http_only] = true
308
+ else
309
+ raise InvalidCookieError.new "Unknown cookie parameter '#{key}'"
310
+ end
311
+ end
312
+ end
313
+ args[:version] = 0
314
+ args
315
+ end
316
+
317
+ # Parse a RFC 2965 value and convert to a literal string
318
+ def self.value_to_string value
319
+ if /\A"(.*)"\Z/.match value
320
+ value = $1
321
+ value = value.gsub(/\\(.)/, '\1')
322
+ else
323
+ value
324
+ end
325
+ end
326
+
327
+ # Attempt to decipher a partially decoded version of text cookie values
328
+ def self.decode_value value
329
+ if /\A"(.*)"\Z/.match value
330
+ value_to_string value
331
+ else
332
+ CGI.unescape value
333
+ end
334
+ end
335
+
336
+ # Break apart a RFC 2965 cookie value into its core components.
337
+ # This does not do any validation, or defaulting of values
338
+ # based on requested URI
339
+ #
340
+ # @param [String] set_cookie_value a Set-Cookie2 header formatted cookie
341
+ # definition
342
+ # @return [Hash] Contains the parsed values of the cookie
343
+ def self.parse_set_cookie2 set_cookie_value
344
+ args = { }
345
+ first = true
346
+ index = 0
347
+ begin
348
+ md = PARAM2.match set_cookie_value[index..-1]
349
+ if md.nil? || md.offset(0).first != 0
350
+ raise InvalidCookieError.new "Invalid Set-Cookie2 header '#{set_cookie_value}'"
351
+ end
352
+ index+=md.offset(0)[1]
353
+
354
+ key = md[1].downcase.to_sym
355
+ keyvalue = md[2] || md[3]
356
+ if first
357
+ args[:name] = md[1]
358
+ args[:value] = keyvalue
359
+ first = false
360
+ else
361
+ keyvalue = value_to_string keyvalue
362
+ case key
363
+ when *[:comment,:commenturl,:domain,:path]
364
+ args[key] = keyvalue
365
+ when *[:discard,:secure]
366
+ args[key] = true
367
+ when :httponly
368
+ args[:http_only] = true
369
+ when :"max-age"
370
+ args[:max_age] = keyvalue.to_i
371
+ when :version
372
+ args[:version] = keyvalue.to_i
373
+ when :port
374
+ # must be in format '"port,port"'
375
+ ports = keyvalue.split /,\s*/
376
+ args[:ports] = ports.map do |portstr| portstr.to_i end
377
+ else
378
+ raise InvalidCookieError.new "Unknown cookie parameter '#{key}'"
379
+ end
380
+ end
381
+ end until md.post_match.empty?
382
+ # if our last match in the scan failed
383
+ if args[:version] != 1
384
+ raise InvalidCookieError.new "Set-Cookie2 declares a non RFC2965 version cookie"
385
+ end
386
+
387
+ args
388
+ end
389
+ end
390
+ end