http-cookie 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +24 -0
- data/README.md +139 -0
- data/Rakefile +26 -0
- data/http-cookie.gemspec +29 -0
- data/lib/http-cookie.rb +1 -0
- data/lib/http/cookie.rb +447 -0
- data/lib/http/cookie/version.rb +5 -0
- data/lib/http/cookie_jar.rb +233 -0
- data/lib/http/cookie_jar/abstract_saver.rb +53 -0
- data/lib/http/cookie_jar/abstract_store.rb +90 -0
- data/lib/http/cookie_jar/cookiestxt_saver.rb +74 -0
- data/lib/http/cookie_jar/hash_store.rb +141 -0
- data/lib/http/cookie_jar/yaml_saver.rb +37 -0
- data/test/helper.rb +33 -0
- data/test/simplecov_start.rb +2 -0
- data/test/test_http_cookie.rb +624 -0
- data/test/test_http_cookie_jar.rb +585 -0
- metadata +138 -0
@@ -0,0 +1,233 @@
|
|
1
|
+
require 'http/cookie'
|
2
|
+
|
3
|
+
##
|
4
|
+
# This class is used to manage the Cookies that have been returned from
|
5
|
+
# any particular website.
|
6
|
+
|
7
|
+
class HTTP::CookieJar
|
8
|
+
autoload :AbstractSaver, 'http/cookie_jar/abstract_saver'
|
9
|
+
autoload :AbstractStore, 'http/cookie_jar/abstract_store'
|
10
|
+
|
11
|
+
attr_reader :store
|
12
|
+
|
13
|
+
def initialize(store = :hash, options = nil)
|
14
|
+
case store
|
15
|
+
when Symbol
|
16
|
+
@store = AbstractStore.implementation(store).new(options)
|
17
|
+
when AbstractStore
|
18
|
+
options.empty? or
|
19
|
+
raise ArgumentError, 'wrong number of arguments (%d for 1)' % (1 + options.size)
|
20
|
+
@store = store
|
21
|
+
else
|
22
|
+
raise TypeError, 'wrong object given as cookie store: %s' % store.inspect
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize_copy(other)
|
27
|
+
@store = other.instance_eval { @store.dup }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Add a +cookie+ to the jar and return self.
|
31
|
+
def add(cookie, *_)
|
32
|
+
_.empty? or
|
33
|
+
raise ArgumentError, 'HTTP::Cookie equivalent for Mechanize::CookieJar#add(uri, cookie) is #add(cookie) after setting cookie.origin = uri.'
|
34
|
+
|
35
|
+
if cookie.domain.nil? || cookie.path.nil?
|
36
|
+
raise ArgumentError, "a cookie with unknown domain or path cannot be added"
|
37
|
+
end
|
38
|
+
|
39
|
+
@store.add(cookie)
|
40
|
+
self
|
41
|
+
end
|
42
|
+
alias << add
|
43
|
+
|
44
|
+
# Used to exist in Mechanize::CookieJar. Use #add().
|
45
|
+
def add!(cookie)
|
46
|
+
raise NoMethodError, 'HTTP::Cookie equivalent for Mechanize::CookieJar#add!() is #add().'
|
47
|
+
end
|
48
|
+
|
49
|
+
# Fetch the cookies that should be used for the URL/URI.
|
50
|
+
def cookies(url)
|
51
|
+
now = Time.now
|
52
|
+
each(url).select { |cookie|
|
53
|
+
!cookie.expired? && (cookie.accessed_at = now)
|
54
|
+
}.sort
|
55
|
+
end
|
56
|
+
|
57
|
+
# Tests if the jar is empty. If url is given, tests if there is no
|
58
|
+
# cookie for the URL.
|
59
|
+
def empty?(url = nil)
|
60
|
+
if url
|
61
|
+
each(url) { return false }
|
62
|
+
return true
|
63
|
+
else
|
64
|
+
@store.empty?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Iterates over all cookies that are not expired.
|
69
|
+
#
|
70
|
+
# Available option keywords are below:
|
71
|
+
#
|
72
|
+
# * +uri+
|
73
|
+
#
|
74
|
+
# Specify a URI/URL indicating the destination of the cookies
|
75
|
+
# being selected. Every cookie yielded should be good to send to
|
76
|
+
# the given URI, i.e. cookie.valid_for_uri?(uri) evaluates to
|
77
|
+
# true.
|
78
|
+
#
|
79
|
+
# If (and only if) this option is given, last access time of each
|
80
|
+
# cookie is updated to the current time.
|
81
|
+
def each(uri = nil, &block)
|
82
|
+
block_given? or return enum_for(__method__, uri)
|
83
|
+
|
84
|
+
if uri
|
85
|
+
block = proc { |cookie|
|
86
|
+
yield cookie if cookie.valid_for_uri?(uri)
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
@store.each(uri, &block)
|
91
|
+
self
|
92
|
+
end
|
93
|
+
include Enumerable
|
94
|
+
|
95
|
+
# call-seq:
|
96
|
+
# jar.save(filename_or_io, **options)
|
97
|
+
# jar.save(filename_or_io, format = :yaml, **options)
|
98
|
+
#
|
99
|
+
# Save the cookie jar into a file or an IO in the format specified
|
100
|
+
# and return self. If the given object responds to #write it is
|
101
|
+
# taken as an IO, or taken as a filename otherwise.
|
102
|
+
#
|
103
|
+
# Available option keywords are below:
|
104
|
+
#
|
105
|
+
# * +format+
|
106
|
+
# [<tt>:yaml</tt>]
|
107
|
+
# YAML structure (default)
|
108
|
+
# [<tt>:cookiestxt</tt>]
|
109
|
+
# Mozilla's cookies.txt format
|
110
|
+
# * +session+
|
111
|
+
# [+true+]
|
112
|
+
# Save session cookies as well.
|
113
|
+
# [+false+]
|
114
|
+
# Do not save session cookies. (default)
|
115
|
+
#
|
116
|
+
# All options given are passed through to the underlying cookie
|
117
|
+
# saver module.
|
118
|
+
def save(writable, *options)
|
119
|
+
opthash = {
|
120
|
+
:format => :yaml,
|
121
|
+
:session => false,
|
122
|
+
}
|
123
|
+
case options.size
|
124
|
+
when 0
|
125
|
+
when 1
|
126
|
+
case options = options.first
|
127
|
+
when Symbol
|
128
|
+
opthash[:format] = options
|
129
|
+
else
|
130
|
+
opthash.update(options) if options
|
131
|
+
end
|
132
|
+
when 2
|
133
|
+
opthash[:format], options = options
|
134
|
+
opthash.update(options) if options
|
135
|
+
else
|
136
|
+
raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size)
|
137
|
+
end
|
138
|
+
|
139
|
+
begin
|
140
|
+
saver = AbstractSaver.implementation(opthash[:format]).new(opthash)
|
141
|
+
rescue KeyError => e
|
142
|
+
raise ArgumentError, e.message
|
143
|
+
end
|
144
|
+
|
145
|
+
if writable.respond_to?(:write)
|
146
|
+
saver.save(writable, self)
|
147
|
+
else
|
148
|
+
File.open(writable, 'w') { |io|
|
149
|
+
saver.save(io, self)
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
self
|
154
|
+
end
|
155
|
+
|
156
|
+
# Used to exist in Mechanize::CookieJar. Use #save().
|
157
|
+
def save_as(*args)
|
158
|
+
raise NoMethodError, 'HTTP::Cookie equivalent for Mechanize::CookieJar#save_as() is #save().'
|
159
|
+
end
|
160
|
+
|
161
|
+
# call-seq:
|
162
|
+
# jar.load(filename_or_io, **options)
|
163
|
+
# jar.load(filename_or_io, format = :yaml, **options)
|
164
|
+
#
|
165
|
+
# Load cookies recorded in a file or an IO in the format specified
|
166
|
+
# into the jar and return self. If the given object responds to
|
167
|
+
# #read it is taken as an IO, or taken as a filename otherwise.
|
168
|
+
#
|
169
|
+
# Available option keywords are below:
|
170
|
+
#
|
171
|
+
# * +format+
|
172
|
+
# [<tt>:yaml</tt>]
|
173
|
+
# YAML structure (default)
|
174
|
+
# [<tt>:cookiestxt</tt>]
|
175
|
+
# Mozilla's cookies.txt format
|
176
|
+
#
|
177
|
+
# All options given are passed through to the underlying cookie
|
178
|
+
# saver module.
|
179
|
+
def load(readable, *options)
|
180
|
+
opthash = {
|
181
|
+
:format => :yaml,
|
182
|
+
:session => false,
|
183
|
+
}
|
184
|
+
case options.size
|
185
|
+
when 0
|
186
|
+
when 1
|
187
|
+
case options = options.first
|
188
|
+
when Symbol
|
189
|
+
opthash[:format] = options
|
190
|
+
else
|
191
|
+
opthash.update(options) if options
|
192
|
+
end
|
193
|
+
when 2
|
194
|
+
opthash[:format], options = options
|
195
|
+
opthash.update(options) if options
|
196
|
+
else
|
197
|
+
raise ArgumentError, 'wrong number of arguments (%d for 1-3)' % (1 + options.size)
|
198
|
+
end
|
199
|
+
|
200
|
+
begin
|
201
|
+
saver = AbstractSaver.implementation(opthash[:format]).new(opthash)
|
202
|
+
rescue KeyError => e
|
203
|
+
raise ArgumentError, e.message
|
204
|
+
end
|
205
|
+
|
206
|
+
if readable.respond_to?(:write)
|
207
|
+
saver.load(readable, self)
|
208
|
+
else
|
209
|
+
File.open(readable, 'r') { |io|
|
210
|
+
saver.load(io, self)
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
self
|
215
|
+
end
|
216
|
+
|
217
|
+
# Clear the cookie jar and return self.
|
218
|
+
def clear
|
219
|
+
@store.clear
|
220
|
+
self
|
221
|
+
end
|
222
|
+
|
223
|
+
# Used to exist in Mechanize::CookieJar. Use #clear().
|
224
|
+
def clear!(*args)
|
225
|
+
raise NoMethodError, 'HTTP::Cookie equivalent for Mechanize::CookieJar#clear!() is #clear().'
|
226
|
+
end
|
227
|
+
|
228
|
+
# Remove expired cookies and return self.
|
229
|
+
def cleanup(session = false)
|
230
|
+
@store.cleanup session
|
231
|
+
self
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'http/cookie_jar'
|
2
|
+
|
3
|
+
class HTTP::CookieJar::AbstractSaver
|
4
|
+
class << self
|
5
|
+
@@class_map = {}
|
6
|
+
|
7
|
+
# Gets an implementation class by the name, optionally trying to
|
8
|
+
# load "http/cookie_jar/*_saver" if not found. If loading fails,
|
9
|
+
# KeyError is raised.
|
10
|
+
def implementation(symbol)
|
11
|
+
@@class_map.fetch(symbol)
|
12
|
+
rescue KeyError
|
13
|
+
begin
|
14
|
+
require 'http/cookie_jar/%s_saver' % symbol
|
15
|
+
@@class_map.fetch(symbol)
|
16
|
+
rescue LoadError, KeyError => e
|
17
|
+
raise KeyError, 'cookie saver unavailable: %s' % symbol.inspect
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def inherited(subclass)
|
22
|
+
@@class_map[class_to_symbol(subclass)] = subclass
|
23
|
+
end
|
24
|
+
|
25
|
+
def class_to_symbol(klass)
|
26
|
+
klass.name[/[^:]+?(?=Saver$|$)/].downcase.to_sym
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_options
|
31
|
+
{}
|
32
|
+
end
|
33
|
+
private :default_options
|
34
|
+
|
35
|
+
def initialize(options = nil)
|
36
|
+
options ||= {}
|
37
|
+
@logger = options[:logger]
|
38
|
+
@session = options[:session]
|
39
|
+
# Initializes each instance variable of the same name as option
|
40
|
+
# keyword.
|
41
|
+
default_options.each_pair { |key, default|
|
42
|
+
instance_variable_set("@#{key}", options.key?(key) ? options[key] : default)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def save(io, jar)
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
|
50
|
+
def load(io, jar)
|
51
|
+
raise
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'http/cookie_jar'
|
2
|
+
|
3
|
+
class HTTP::CookieJar::AbstractStore
|
4
|
+
class << self
|
5
|
+
@@class_map = {}
|
6
|
+
|
7
|
+
# Gets an implementation class by the name, optionally trying to
|
8
|
+
# load "http/cookie_jar/*_store" if not found. If loading fails,
|
9
|
+
# KeyError is raised.
|
10
|
+
def implementation(symbol)
|
11
|
+
@@class_map.fetch(symbol)
|
12
|
+
rescue KeyError
|
13
|
+
begin
|
14
|
+
require 'http/cookie_jar/%s_store' % symbol
|
15
|
+
@@class_map.fetch(symbol)
|
16
|
+
rescue LoadError, KeyError => e
|
17
|
+
raise KeyError, 'cookie store unavailable: %s' % symbol.inspect
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def inherited(subclass)
|
22
|
+
@@class_map[class_to_symbol(subclass)] = subclass
|
23
|
+
end
|
24
|
+
|
25
|
+
def class_to_symbol(klass)
|
26
|
+
klass.name[/[^:]+?(?=Store$|$)/].downcase.to_sym
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_options
|
31
|
+
{}
|
32
|
+
end
|
33
|
+
private :default_options
|
34
|
+
|
35
|
+
def initialize(options = nil)
|
36
|
+
options ||= {}
|
37
|
+
@logger = options[:logger]
|
38
|
+
# Initializes each instance variable of the same name as option
|
39
|
+
# keyword.
|
40
|
+
default_options.each_pair { |key, default|
|
41
|
+
instance_variable_set("@#{key}", options.key?(key) ? options[key] : default)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize_copy(other)
|
46
|
+
raise
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def add(cookie)
|
51
|
+
raise
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
# Iterates over all cookies that are not expired.
|
56
|
+
#
|
57
|
+
# Available option keywords are below:
|
58
|
+
#
|
59
|
+
# * +uri+
|
60
|
+
#
|
61
|
+
# Specify a URI object indicating the destination of the cookies
|
62
|
+
# being selected. Every cookie yielded should be good to send to
|
63
|
+
# the given URI, i.e. cookie.valid_for_uri?(uri) evaluates to
|
64
|
+
# true.
|
65
|
+
#
|
66
|
+
# If (and only if) this option is given, last access time of each
|
67
|
+
# cookie is updated to the current time.
|
68
|
+
def each(options = nil, &block)
|
69
|
+
raise
|
70
|
+
self
|
71
|
+
end
|
72
|
+
include Enumerable
|
73
|
+
|
74
|
+
def clear
|
75
|
+
raise
|
76
|
+
self
|
77
|
+
end
|
78
|
+
|
79
|
+
def cleanup(session = false)
|
80
|
+
if session
|
81
|
+
select { |cookie| cookie.session? || cookie.expired? }
|
82
|
+
else
|
83
|
+
select(&:expired?)
|
84
|
+
end.each { |cookie|
|
85
|
+
add(cookie.expire)
|
86
|
+
}
|
87
|
+
# subclasses can optionally remove over-the-limit cookies.
|
88
|
+
self
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'http/cookie_jar'
|
2
|
+
|
3
|
+
# CookiestxtSaver saves and loads cookies in the cookies.txt format.
|
4
|
+
class HTTP::CookieJar::CookiestxtSaver < HTTP::CookieJar::AbstractSaver
|
5
|
+
True = "TRUE"
|
6
|
+
False = "FALSE"
|
7
|
+
|
8
|
+
def save(io, jar)
|
9
|
+
io.puts @header if @header
|
10
|
+
jar.each { |cookie|
|
11
|
+
next if !@session && cookie.session?
|
12
|
+
io.print cookie_to_record(cookie)
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def load(io, jar)
|
17
|
+
io.each_line { |line|
|
18
|
+
cookie = parse_record(line) and jar.add(cookie)
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def default_options
|
25
|
+
{
|
26
|
+
header: "# HTTP Cookie File",
|
27
|
+
linefeed: "\n",
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
# Serializes the cookie into a cookies.txt line.
|
32
|
+
def cookie_to_record(cookie)
|
33
|
+
cookie.instance_eval {
|
34
|
+
[
|
35
|
+
@domain,
|
36
|
+
@for_domain ? True : False,
|
37
|
+
@path,
|
38
|
+
@secure ? True : False,
|
39
|
+
@expires.to_i,
|
40
|
+
@name,
|
41
|
+
@value
|
42
|
+
]
|
43
|
+
}.join("\t") << @linefeed
|
44
|
+
end
|
45
|
+
|
46
|
+
# Parses a line from cookies.txt and returns a cookie object if the
|
47
|
+
# line represents a cookie record or returns nil otherwise.
|
48
|
+
def parse_record(line)
|
49
|
+
return nil if line.match(/^#/)
|
50
|
+
|
51
|
+
domain,
|
52
|
+
s_for_domain, # Whether this cookie is for domain
|
53
|
+
path, # Path for which the cookie is relevant
|
54
|
+
s_secure, # Requires a secure connection
|
55
|
+
s_expires, # Time the cookie expires (Unix epoch time)
|
56
|
+
name, value = line.split("\t", 7)
|
57
|
+
return nil if value.nil?
|
58
|
+
|
59
|
+
value.chomp!
|
60
|
+
|
61
|
+
if (expires_seconds = s_expires.to_i).nonzero?
|
62
|
+
expires = Time.at(expires_seconds)
|
63
|
+
return nil if expires < Time.now
|
64
|
+
end
|
65
|
+
|
66
|
+
HTTP::Cookie.new(name, value,
|
67
|
+
:domain => domain,
|
68
|
+
:for_domain => s_for_domain == True,
|
69
|
+
:path => path,
|
70
|
+
:secure => s_secure == True,
|
71
|
+
:expires => expires,
|
72
|
+
:version => 0)
|
73
|
+
end
|
74
|
+
end
|