cookiejar_of_greed 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c7fed7210e10cc3b9c36b720a812aebf97c55a45c5a270dfd9d2e66ae9f4f1b3
4
+ data.tar.gz: 441f3d672b874199e0e13ab9e6c5b45ec9d967983cf3bd8d5c974dd882246e3e
5
+ SHA512:
6
+ metadata.gz: c17fa636869e11c82b67473268e7945519c454fb1342696a02b9780d3c7b49cd0f7ae31e9fa7695aae2c71c39ad9a26f992390569df9bfa770f1ca2c48caef18
7
+ data.tar.gz: d19d1243dcf32fc5e427ed606517ead882177a5a5035c77146469cfe903d673e2e1108d564f3a08f1415003a525be0d2287e45a2ee83530840af9b750a47f5d0
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'greed'
data/lib/greed.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/dependencies/autoload'
3
+
4
+ module Greed
5
+ extend ::ActiveSupport::Autoload
6
+ autoload :Cookie
7
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/dependencies/autoload'
3
+ require_relative './cookie/version'
4
+
5
+ module Greed
6
+ module Cookie
7
+ extend ::ActiveSupport::Autoload
8
+
9
+ autoload :Error
10
+ autoload :Iterator
11
+ autoload :Jar
12
+ autoload :Parser
13
+
14
+ 'greed/cookie/expiration_handler'.tap do |load_path|
15
+ autoload :ExpirationHandler
16
+ autoload :ExpirationError, load_path
17
+ autoload :Expired, load_path
18
+ end
19
+
20
+ 'greed/cookie/domain_handler'.tap do |load_path|
21
+ autoload :DomainHandler
22
+ autoload :DomainError, load_path
23
+ autoload :CrossDomainViolation, load_path
24
+ autoload :MalformedCookieDomain, load_path
25
+ end
26
+
27
+ 'greed/cookie/path_handler'.tap do |load_path|
28
+ autoload :PathHandler
29
+ autoload :PathError, load_path
30
+ autoload :PathViolation, load_path
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'ipaddr'
4
+ require 'public_suffix'
5
+
6
+ module Greed
7
+ module Cookie
8
+ class DomainError < Error
9
+ end
10
+ class CrossDomainViolation < DomainError
11
+ end
12
+ class MalformedCookieDomain < DomainError
13
+ end
14
+
15
+ class DomainHandler
16
+ def determine_domain(document_domain, cookie_domain)
17
+ document_domain = document_domain.downcase
18
+ unless cookie_domain.present?
19
+ return {
20
+ domain: document_domain, # cookie domain not present
21
+ include_subdomains: false
22
+ }
23
+ end
24
+ document_ip_address = begin
25
+ ::IPAddr.new(document_domain)
26
+ rescue ::IPAddr::Error
27
+ nil
28
+ end
29
+ if document_ip_address
30
+ # handles IP Addresses
31
+ cookie_ip_address = begin
32
+ ::IPAddr.new(cookie_domain)
33
+ rescue ::IPAddr::Error
34
+ raise CrossDomainViolation
35
+ end
36
+ raise CrossDomainViolation unless cookie_ip_address == document_ip_address
37
+ return {
38
+ domain: cookie_ip_address.to_s, # normalized
39
+ include_subdomains: false
40
+ }
41
+ end
42
+ cookie_domain = cookie_domain.downcase
43
+ # ignore leading dot
44
+ matched_data = /\A\s*\.?(?!\.)(\S+)\s*\z/.match(cookie_domain)
45
+ raise MalformedCookieDomain unless matched_data
46
+ cookie_domain = matched_data[1]
47
+ if document_domain == cookie_domain
48
+ # exact domain matched
49
+ return {
50
+ domain: document_domain,
51
+ include_subdomains: true
52
+ }
53
+ end
54
+ # prevent setting cookie on a top level domain
55
+ # "localhost" use cases should already ruled out with the exact domain match condition
56
+ raise CrossDomainViolation unless ::PublicSuffix.valid?(cookie_domain, ignore_private: true)
57
+ # prevent parent domain from setting cookie of a subdomain
58
+ raise CrossDomainViolation unless (document_domain[
59
+ document_domain.size - cookie_domain.size,
60
+ cookie_domain.size
61
+ ] == cookie_domain) && \
62
+ (document_domain[
63
+ document_domain.size - cookie_domain.size - 1
64
+ ] == ?.)
65
+ {
66
+ domain: cookie_domain, # set cookie for its parent domain
67
+ include_subdomains: true
68
+ }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+ module Greed
3
+ module Cookie
4
+ class Error < StandardError
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/time'
3
+ require 'time'
4
+
5
+ module Greed
6
+ module Cookie
7
+ class ExpirationError < Error
8
+ end
9
+ class Expired < ExpirationError
10
+ end
11
+
12
+ class ExpirationHandler
13
+ def calculate_expiration(current_time, max_age, expires)
14
+ {
15
+ expires: if max_age
16
+ current_time + max_age.seconds
17
+ else
18
+ expires
19
+ end,
20
+ retrieved_at: current_time,
21
+ }.tap do |tapped|
22
+ tapped_expires = tapped[:expires]
23
+ return tapped unless tapped_expires # keep session cookie
24
+ raise Expired unless (current_time < tapped_expires) # reject expired cookie
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'public_suffix'
4
+ require 'strscan'
5
+
6
+ module Greed
7
+ module Cookie
8
+ module Iterator
9
+ private
10
+
11
+ def iterate_cookie_domain(domain_name)
12
+ chunk_scanner = /\A[-_\w\d]+\./
13
+ ::Enumerator.new do |yielder|
14
+ scanner = ::StringScanner.new(domain_name.downcase)
15
+ until scanner.eos?
16
+ removed_part = scanner.scan(chunk_scanner)
17
+ break unless removed_part
18
+ yielder << scanner.rest
19
+ end
20
+ end.lazy.take_while do |parent_domain|
21
+ ::PublicSuffix.valid?(parent_domain, ignore_private: true)
22
+ end.yield_self do |parent_domains|
23
+ [domain_name].chain(parent_domains)
24
+ end
25
+ end
26
+
27
+ def iterate_cookie_path(path)
28
+ ensured_absolute = path.sub(/\A\/*/, ?/)
29
+ ::Enumerator.new do |yielder|
30
+ scanner = ::File.expand_path(?., ensured_absolute)
31
+ loop do
32
+ yielder << scanner
33
+ break if (scanner.blank?) || (?/ == scanner)
34
+ scanner = ::File.expand_path('..', scanner)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'active_support/core_ext/object/try'
4
+ require 'active_support/core_ext/time'
5
+ require 'ipaddr'
6
+ require 'time'
7
+ require 'uri'
8
+
9
+ module Greed
10
+ module Cookie
11
+ class Jar
12
+ include Iterator
13
+
14
+ def initialize(\
15
+ state = nil,
16
+ set_cookie_parser: nil,
17
+ get_current_time: nil,
18
+ calculate_expiration: nil,
19
+ determine_domain: nil,
20
+ determine_path: nil
21
+ )
22
+ @set_cookie_parser = set_cookie_parser || Parser.new.method(:parse)
23
+ @get_current_time = get_current_time || ::Time.method(:current)
24
+ @calculate_expiration = calculate_expiration || ExpirationHandler.new.method(:calculate_expiration)
25
+ @determine_domain = determine_domain || DomainHandler.new.method(:determine_domain)
26
+ @determine_path = determine_path || PathHandler.new.method(:determine_path)
27
+ @cookie_map = state || {}
28
+ end
29
+
30
+ def append_cookie(document_uri, cookie_hash)
31
+ return false unless cookie_hash.present?
32
+ current_time = @get_current_time.call
33
+ parsed_document_uri = ::URI.parse(document_uri)
34
+ base = cookie_hash.slice(:name, :value, :secure)
35
+ domain_attributes = @determine_domain.call(
36
+ parsed_document_uri.hostname,
37
+ cookie_hash[:domain]
38
+ )
39
+ path_attributes = @determine_path.call(
40
+ parsed_document_uri.path,
41
+ cookie_hash[:path]
42
+ )
43
+ final_domain = domain_attributes[:domain]
44
+ final_path = path_attributes[:path]
45
+ domain_holder = @cookie_map[final_domain].try(:clone) || {}
46
+ path_holder = domain_holder[final_path].try(:clone) || {}
47
+ expires_attributes = @calculate_expiration.call(
48
+ current_time,
49
+ cookie_hash[:'max-age'],
50
+ cookie_hash[:expires]
51
+ )
52
+ path_holder[base[:name]] = base.merge(
53
+ domain_attributes,
54
+ expires_attributes,
55
+ path_attributes,
56
+ )
57
+ domain_holder[final_path] = path_holder
58
+ @cookie_map[final_domain] = domain_holder
59
+ true
60
+ rescue DomainError, PathError
61
+ return false
62
+ rescue Expired
63
+ removed_cookie = path_holder.delete(base[:name])
64
+ return false unless removed_cookie
65
+ domain_holder[final_path] = path_holder
66
+ @cookie_map[final_domain] = domain_holder
67
+ return true
68
+ end
69
+
70
+ def dump
71
+ garbage_collect
72
+ @cookie_map.clone
73
+ end
74
+
75
+ def cookie_record_for(document_uri)
76
+ parsed_document_uri = begin
77
+ ::URI.parse(document_uri)
78
+ rescue ::URI::Error
79
+ return []
80
+ end
81
+ is_secure = case parsed_document_uri
82
+ when ::URI::HTTPS
83
+ true
84
+ when ::URI::HTTP
85
+ false
86
+ else
87
+ return []
88
+ end
89
+ domain_name = parsed_document_uri.hostname
90
+ ip_address = begin
91
+ ::IPAddr.new(domain_name)
92
+ rescue ::IPAddr::Error
93
+ nil
94
+ end
95
+ return cookie_for_ip_address(parsed_document_uri.path, is_secure, ip_address) if ip_address
96
+ return [] unless domain_name.present?
97
+ cookie_for_domain(parsed_document_uri.path, is_secure, domain_name)
98
+ end
99
+
100
+ def cookie_header_for(document_uri)
101
+ cookie_record_for(document_uri).map do |cookie_record|
102
+ "#{cookie_record[:name]}=#{cookie_record[:value]}"
103
+ end.to_a.join('; ')
104
+ end
105
+
106
+ def parse_set_cookie(document_uri, header)
107
+ append_cookie(document_uri, @set_cookie_parser.call(header))
108
+ end
109
+
110
+ def garbage_collect
111
+ current_time = @get_current_time.call
112
+ @cookie_map = @cookie_map.map do |domain_name, domain_holder|
113
+ domain_holder.map do |path_name, path_holder|
114
+ path_holder.select do |_cookie_name, cookie_record|
115
+ !cookie_record[:expires] || current_time < cookie_record[:expires]
116
+ end.yield_self do |filtered_result|
117
+ break nil unless filtered_result.present?
118
+ [path_name, filtered_result]
119
+ end
120
+ end.compact.to_h.yield_self do |filtered_result|
121
+ break nil unless filtered_result.present?
122
+ [domain_name, filtered_result]
123
+ end
124
+ end.compact.to_h
125
+ nil
126
+ end
127
+
128
+ private
129
+
130
+ def compile_effective_cookies(domain_candidates, document_path, &cookie_selector)
131
+ path_candidates = iterate_cookie_path(document_path)
132
+ effective_cookies = {}
133
+ domain_candidates.each do |domain_candidate|
134
+ domain_holder = @cookie_map[domain_candidate]
135
+ next if domain_holder.blank?
136
+ path_candidates.each do |path_candidate|
137
+ path_holder = domain_holder[path_candidate]
138
+ next unless path_holder.present? && path_holder.respond_to?(:select)
139
+ path_holder = path_holder.select(&cookie_selector)
140
+ effective_cookies = path_holder.merge(effective_cookies)
141
+ end
142
+ end
143
+ effective_cookies
144
+ end
145
+
146
+ def cookie_for_domain(document_path, is_document_secure, domain_name)
147
+ current_time = @get_current_time.call
148
+ domain_candidates = iterate_cookie_domain(domain_name)
149
+ compile_effective_cookies(domain_candidates, document_path) do |_cookie_name, cookie_record|
150
+ filter_cookie(cookie_record, is_document_secure, current_time) &&
151
+ ((cookie_record[:domain] == domain_name) || cookie_record[:include_subdomains])
152
+ end.values
153
+ end
154
+
155
+ def cookie_for_ip_address(document_path, is_document_secure, ip_address)
156
+ current_time = @get_current_time.call
157
+ compile_effective_cookies([ip_address.to_s], document_path) do |_cookie_name, cookie_record|
158
+ filter_cookie(cookie_record, is_document_secure, current_time)
159
+ end.values
160
+ end
161
+
162
+ def filter_cookie(cookie_record, is_document_secure, current_time)
163
+ (!cookie_record[:expires] || current_time < cookie_record[:expires]) &&
164
+ (is_document_secure || !cookie_record[:secure])
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'active_support/core_ext/object/try'
4
+ require 'active_support/core_ext/time'
5
+ require 'memoist'
6
+ require 'strscan'
7
+ require 'time'
8
+
9
+ module Greed
10
+ module Cookie
11
+ class Parser
12
+ class << self
13
+ extend ::Memoist
14
+
15
+ memoize def _default_kv_matcher
16
+ /\A\s*([-_.+%\d\w]+)=\s*([^;]*)\s*(?:;\s*|\z)/
17
+ end
18
+
19
+ memoize def _default_flag_matcher
20
+ /\A\s*([-_\w\d]+)\s*(?:;\s*|\z)/
21
+ end
22
+ end
23
+
24
+ def initialize(\
25
+ cookie_matcher: nil,
26
+ attribute_matcher: nil,
27
+ flag_matcher: nil
28
+ )
29
+ @cookie_matcher = cookie_matcher || self.class._default_kv_matcher
30
+ @attribute_matcher = attribute_matcher || self.class._default_kv_matcher
31
+ @flag_matcher = flag_matcher || self.class._default_flag_matcher
32
+ freeze
33
+ end
34
+
35
+ def parse(set_cookie_header)
36
+ scanner = ::StringScanner.new(set_cookie_header)
37
+ matched = scanner.scan(@cookie_matcher)
38
+ return nil unless matched
39
+ captured = scanner.captures
40
+ mandatory_parsed = {
41
+ name: captured[0].tap do |cookie_name|
42
+ return nil unless cookie_name.present?
43
+ end,
44
+ value: captured[1],
45
+ }
46
+ flags_parsed = {}
47
+ attributes_parsed = {}
48
+ until scanner.eos? do
49
+ matched = scanner.scan(@flag_matcher)
50
+ if matched
51
+ captured = scanner.captures
52
+ flags_parsed.merge!(
53
+ "#{captured[0].downcase}": true,
54
+ )
55
+ next
56
+ end
57
+ matched = scanner.scan(@attribute_matcher)
58
+ if matched
59
+ captured = scanner.captures
60
+ attributes_parsed.merge!(
61
+ "#{captured[0].downcase}": captured[1],
62
+ )
63
+ next
64
+ end
65
+ return nil
66
+ end
67
+ combine_fragments(
68
+ mandatory_parsed,
69
+ attributes_parsed,
70
+ flags_parsed
71
+ )
72
+ end
73
+
74
+ private
75
+
76
+ def combine_fragments(\
77
+ mandatory_parsed,
78
+ attributes_parsed,
79
+ flags_parsed
80
+ )
81
+ mandatory_parsed.merge(
82
+ expires: attributes_parsed[:expires].yield_self do |expires|
83
+ ::Time.parse(expires)
84
+ rescue ArgumentError, TypeError
85
+ nil
86
+ end,
87
+ 'max-age': attributes_parsed[:'max-age'].yield_self do |max_age|
88
+ Integer(max_age)
89
+ rescue ArgumentError, TypeError
90
+ nil
91
+ end,
92
+ domain: attributes_parsed[:domain].presence.try(:strip),
93
+ path: attributes_parsed[:path].presence.try(:strip),
94
+ samesite: attributes_parsed[:samesite].yield_self do |same_site|
95
+ break 'Lax' unless same_site.present?
96
+ %w[Strict Lax None]
97
+ .lazy
98
+ .select { |enum_value| enum_value.casecmp?(same_site) }
99
+ .first || 'Lax'
100
+ end,
101
+ secure: !!flags_parsed[:secure],
102
+ httponly: !!flags_parsed[:httponly],
103
+ )
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'active_support/core_ext/time'
4
+ require 'time'
5
+
6
+ module Greed
7
+ module Cookie
8
+ class PathError < Error
9
+ end
10
+ class PathViolation < PathError
11
+ end
12
+
13
+ class PathHandler
14
+ include Iterator
15
+
16
+ def determine_path(document_path, cookie_path)
17
+ return generate_default_path(document_path) if cookie_path.blank?
18
+ # speed optimization for the common use case
19
+ if cookie_path == ?/
20
+ return {
21
+ path: ?/,
22
+ }
23
+ end
24
+ normalized_cookie_path = ::File.expand_path(?., cookie_path)
25
+ iterate_cookie_path(document_path).each do |path_candidate|
26
+ next unless path_candidate == normalized_cookie_path
27
+ return {
28
+ path: normalized_cookie_path,
29
+ }
30
+ end
31
+ raise PathViolation
32
+ end
33
+
34
+ private
35
+
36
+ def generate_default_path(document_path)
37
+ # RFC 6265 5.1.4
38
+ if (document_path.blank?) ||
39
+ (!document_path.start_with?(?/))
40
+ return {
41
+ path: ?/,
42
+ }
43
+ end
44
+ {
45
+ path: ::File.expand_path('..', document_path),
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Greed
4
+ module Cookie
5
+ VERSION = '0.0.1'
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cookiejar_of_greed
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sarun Rattanasiri
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: memoist
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: public_suffix
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Cookiejar of greed is an implementation of cookiejar focused on browser
56
+ compatibility and loosely based on the standard.
57
+ email: midnight_w@gmx.tw
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - lib/cookiejar_of_greed.rb
63
+ - lib/greed.rb
64
+ - lib/greed/cookie.rb
65
+ - lib/greed/cookie/domain_handler.rb
66
+ - lib/greed/cookie/error.rb
67
+ - lib/greed/cookie/expiration_handler.rb
68
+ - lib/greed/cookie/iterator.rb
69
+ - lib/greed/cookie/jar.rb
70
+ - lib/greed/cookie/parser.rb
71
+ - lib/greed/cookie/path_handler.rb
72
+ - lib/greed/cookie/version.rb
73
+ homepage: https://github.com/the-cave/cookiejar-of-greed
74
+ licenses:
75
+ - BSD-3-Clause
76
+ metadata:
77
+ source_code_uri: https://github.com/the-cave/cookiejar-of-greed/tree/v0.0.1
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 2.5.0
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubygems_version: 3.0.3
94
+ signing_key:
95
+ specification_version: 4
96
+ summary: A compatibility-first cookiejar implementation
97
+ test_files: []