http_monkey-cookie 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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