http_mimic 0.3.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +263 -0
- data/http_mimic.gemspec +31 -0
- data/lib/http_mimic/client.rb +72 -0
- data/lib/http_mimic/command_builder.rb +333 -0
- data/lib/http_mimic/configuration.rb +41 -0
- data/lib/http_mimic/cookies.rb +111 -0
- data/lib/http_mimic/downloader.rb +272 -0
- data/lib/http_mimic/exceptions.rb +34 -0
- data/lib/http_mimic/headers.rb +101 -0
- data/lib/http_mimic/module_methods.rb +101 -0
- data/lib/http_mimic/request.rb +88 -0
- data/lib/http_mimic/response.rb +99 -0
- data/lib/http_mimic/response_parser.rb +148 -0
- data/lib/http_mimic/version.rb +5 -0
- data/lib/http_mimic.rb +85 -0
- metadata +102 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module HttpMimic
|
|
7
|
+
class CommandBuilder
|
|
8
|
+
COMMON_SEARCH_PATHS = [
|
|
9
|
+
'/opt/homebrew/bin',
|
|
10
|
+
'/usr/local/bin',
|
|
11
|
+
'/usr/bin',
|
|
12
|
+
'/bin',
|
|
13
|
+
File.expand_path('~/.local/bin')
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
TARGET_BINARY_MAP = {
|
|
17
|
+
'chrome' => %w[curl_chrome131 curl_chrome124 curl_chrome120 curl_chrome116 curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
18
|
+
'chrome116' => %w[curl_chrome116 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
19
|
+
'chrome120' => %w[curl_chrome120 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
20
|
+
'chrome123' => %w[curl_chrome123 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
21
|
+
'chrome124' => %w[curl_chrome124 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
22
|
+
'chrome131' => %w[curl_chrome131 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
23
|
+
'chrome133a' => %w[curl_chrome133a curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
24
|
+
'chrome136' => %w[curl_chrome136 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
25
|
+
'chrome142' => %w[curl_chrome142 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
26
|
+
'chrome110' => %w[curl_chrome110 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
27
|
+
'chrome107' => %w[curl_chrome107 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
28
|
+
'chrome104' => %w[curl_chrome104 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
29
|
+
'chrome101' => %w[curl_chrome101 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
30
|
+
'chrome100' => %w[curl_chrome100 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
31
|
+
'chrome99' => %w[curl_chrome99 curl-impersonate-chrome curl_chrome curl-impersonate],
|
|
32
|
+
'firefox' => %w[curl_firefox135 curl_firefox133 curl_ff117 curl_firefox117 curl_ff109 curl-impersonate-ff curl_firefox curl-impersonate],
|
|
33
|
+
'firefox135' => %w[curl_firefox135 curl-impersonate-ff curl_firefox curl-impersonate],
|
|
34
|
+
'firefox133' => %w[curl_firefox133 curl-impersonate-ff curl_firefox curl-impersonate],
|
|
35
|
+
'firefox117' => %w[curl_ff117 curl_firefox117 curl-impersonate-ff curl_firefox curl-impersonate],
|
|
36
|
+
'firefox109' => %w[curl_ff109 curl_firefox109 curl-impersonate-ff curl_firefox curl-impersonate],
|
|
37
|
+
'firefox102' => %w[curl_ff102 curl_firefox102 curl-impersonate-ff curl_firefox curl-impersonate],
|
|
38
|
+
'firefox98' => %w[curl_ff98 curl_firefox98 curl-impersonate-ff curl_firefox curl-impersonate],
|
|
39
|
+
'safari' => %w[curl_safari180 curl_safari170 curl_safari155 curl_safari153 curl-impersonate-safari curl_safari curl-impersonate],
|
|
40
|
+
'safari180' => %w[curl_safari180 curl_safari18_0 curl-impersonate-safari curl_safari curl-impersonate],
|
|
41
|
+
'safari170' => %w[curl_safari170 curl_safari17_0 curl-impersonate-safari curl_safari curl-impersonate],
|
|
42
|
+
'safari155' => %w[curl_safari155 curl_safari15_5 curl-impersonate-safari curl_safari curl-impersonate],
|
|
43
|
+
'safari15_5' => %w[curl_safari155 curl_safari15_5 curl-impersonate-safari curl_safari curl-impersonate],
|
|
44
|
+
'safari153' => %w[curl_safari153 curl_safari15_3 curl-impersonate-safari curl_safari curl-impersonate],
|
|
45
|
+
'safari15_3' => %w[curl_safari153 curl_safari15_3 curl-impersonate-safari curl_safari curl-impersonate],
|
|
46
|
+
'edge' => %w[curl_edge101 curl_edge99 curl-impersonate-edge curl_edge curl-impersonate],
|
|
47
|
+
'edge101' => %w[curl_edge101 curl-impersonate-edge curl_edge curl-impersonate],
|
|
48
|
+
'edge99' => %w[curl_edge99 curl-impersonate-edge curl_edge curl-impersonate],
|
|
49
|
+
'tor' => %w[curl_tor145 curl_tor curl-impersonate],
|
|
50
|
+
'tor145' => %w[curl_tor145 curl_tor curl-impersonate]
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
attr_reader :method, :url, :options, :config
|
|
54
|
+
|
|
55
|
+
def initialize(method, url, options = {}, config = nil)
|
|
56
|
+
@method = method.to_s.upcase
|
|
57
|
+
@url = url.to_s
|
|
58
|
+
@options = options.dup
|
|
59
|
+
@config = config || HttpMimic.configuration
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build
|
|
63
|
+
binary = resolve_binary
|
|
64
|
+
args = []
|
|
65
|
+
stdin_data = nil
|
|
66
|
+
|
|
67
|
+
# Base flags: silent mode, include HTTP headers
|
|
68
|
+
args << '-s'
|
|
69
|
+
args << '-i'
|
|
70
|
+
|
|
71
|
+
# HTTP Method
|
|
72
|
+
if @method == 'HEAD'
|
|
73
|
+
args << '-I'
|
|
74
|
+
else
|
|
75
|
+
args << '-X' << @method
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Process URL and query parameters
|
|
79
|
+
final_url = build_url
|
|
80
|
+
|
|
81
|
+
# Redirects
|
|
82
|
+
follow = options.fetch(:follow_redirects, config.follow_redirects)
|
|
83
|
+
if follow
|
|
84
|
+
args << '-L'
|
|
85
|
+
max_redirs = options[:max_redirects] || config.max_redirects
|
|
86
|
+
args << '--max-redirs' << max_redirs.to_s if max_redirs
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Timeouts
|
|
90
|
+
timeout = options[:timeout] || options[:read_timeout] || config.default_timeout
|
|
91
|
+
args << '--max-time' << timeout.to_s if timeout
|
|
92
|
+
|
|
93
|
+
connect_timeout = options[:connect_timeout] || config.default_connect_timeout
|
|
94
|
+
args << '--connect-timeout' << connect_timeout.to_s if connect_timeout
|
|
95
|
+
|
|
96
|
+
# SSL / Insecure
|
|
97
|
+
insecure = options[:insecure] || (options.key?(:verify_ssl) && !options[:verify_ssl])
|
|
98
|
+
args << '-k' if insecure
|
|
99
|
+
args << '--cacert' << options[:ssl_ca_file] if options[:ssl_ca_file]
|
|
100
|
+
args << '--cert' << options[:ssl_cert] if options[:ssl_cert]
|
|
101
|
+
args << '--key' << options[:ssl_key] if options[:ssl_key]
|
|
102
|
+
|
|
103
|
+
# Proxy
|
|
104
|
+
if options[:proxy]
|
|
105
|
+
args << '-x' << options[:proxy].to_s
|
|
106
|
+
if options[:proxy_auth]
|
|
107
|
+
u = options[:proxy_auth][:username]
|
|
108
|
+
p = options[:proxy_auth][:password]
|
|
109
|
+
args << '--proxy-user' << "#{u}:#{p}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Basic / Digest Auth
|
|
114
|
+
if options[:basic_auth]
|
|
115
|
+
u = options[:basic_auth][:username]
|
|
116
|
+
p = options[:basic_auth][:password]
|
|
117
|
+
args << '-u' << "#{u}:#{p}"
|
|
118
|
+
elsif options[:digest_auth]
|
|
119
|
+
u = options[:digest_auth][:username]
|
|
120
|
+
p = options[:digest_auth][:password]
|
|
121
|
+
args << '--digest' << '-u' << "#{u}:#{p}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Headers
|
|
125
|
+
headers_to_send = Headers.new(options[:headers] || {})
|
|
126
|
+
|
|
127
|
+
# Bearer Token
|
|
128
|
+
if options[:bearer_token]
|
|
129
|
+
headers_to_send['Authorization'] = "Bearer #{options[:bearer_token]}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Body / JSON / Form
|
|
133
|
+
if options.key?(:json)
|
|
134
|
+
headers_to_send['Content-Type'] ||= 'application/json'
|
|
135
|
+
headers_to_send['Accept'] ||= 'application/json'
|
|
136
|
+
|
|
137
|
+
json_data = options[:json]
|
|
138
|
+
stdin_data = json_data.is_a?(String) ? json_data : JSON.dump(json_data)
|
|
139
|
+
args << '-d' << '@-'
|
|
140
|
+
elsif options.key?(:body)
|
|
141
|
+
body_data = options[:body]
|
|
142
|
+
if body_data.is_a?(Hash)
|
|
143
|
+
headers_to_send['Content-Type'] ||= 'application/x-www-form-urlencoded'
|
|
144
|
+
stdin_data = encode_params(body_data)
|
|
145
|
+
else
|
|
146
|
+
stdin_data = body_data.to_s
|
|
147
|
+
end
|
|
148
|
+
args << '-d' << '@-'
|
|
149
|
+
elsif options[:form_data] || options[:form] || options[:multipart]
|
|
150
|
+
form_hash = options[:form_data] || options[:form] || options[:multipart]
|
|
151
|
+
if form_hash.is_a?(Hash)
|
|
152
|
+
form_hash.each do |k, v|
|
|
153
|
+
args << '-F' << "#{k}=#{v}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Cookies
|
|
159
|
+
if options[:cookies]
|
|
160
|
+
args << '-b' << Cookies.format(options[:cookies])
|
|
161
|
+
elsif options[:cookie_file]
|
|
162
|
+
args << '-b' << options[:cookie_file].to_s
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
if options[:cookie_jar]
|
|
166
|
+
args << '-c' << options[:cookie_jar].to_s
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# User-Agent
|
|
170
|
+
if options[:user_agent]
|
|
171
|
+
args << '-A' << options[:user_agent].to_s
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Append headers
|
|
175
|
+
headers_to_send.each do |k, v|
|
|
176
|
+
if v.is_a?(Array)
|
|
177
|
+
v.each { |item| args << '-H' << "#{k}: #{item}" }
|
|
178
|
+
else
|
|
179
|
+
args << '-H' << "#{k}: #{v}"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Custom extra curl options
|
|
184
|
+
if options[:curl_options]
|
|
185
|
+
extra_opts = options[:curl_options]
|
|
186
|
+
extra_opts = extra_opts.split if extra_opts.is_a?(String)
|
|
187
|
+
args.concat(Array(extra_opts))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Append final URL
|
|
191
|
+
args << final_url
|
|
192
|
+
|
|
193
|
+
[binary, args, stdin_data, final_url]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def resolve_binary
|
|
197
|
+
# 1. Use explicit binary_path from options or config if provided
|
|
198
|
+
explicit = options[:binary] || config.binary_path
|
|
199
|
+
if explicit
|
|
200
|
+
path = find_executable(explicit) || explicit
|
|
201
|
+
return path if executable?(path)
|
|
202
|
+
raise BinaryNotFoundError, "Specified executable does not exist or is not executable: #{explicit}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
target = (options[:impersonate] || config.default_impersonate).to_s.downcase
|
|
206
|
+
candidate_names = TARGET_BINARY_MAP[target] || ["curl_#{target}", target]
|
|
207
|
+
|
|
208
|
+
# 2. Check local installation directory (~/.http_mimic/bin)
|
|
209
|
+
if binary = find_in_download_dir(candidate_names)
|
|
210
|
+
return binary
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# 3. Auto-download driver if not installed locally and auto_download is enabled
|
|
214
|
+
if config.auto_download
|
|
215
|
+
begin
|
|
216
|
+
Downloader.download!(version: config.driver_version, install_dir: config.install_dir)
|
|
217
|
+
if binary = find_in_download_dir(candidate_names)
|
|
218
|
+
return binary
|
|
219
|
+
end
|
|
220
|
+
rescue StandardError => e
|
|
221
|
+
if config.logger
|
|
222
|
+
config.logger.warn("[HttpMimic] Failed to auto-download curl-impersonate: #{e.message}")
|
|
223
|
+
elsif config.debug
|
|
224
|
+
puts "[HttpMimic WARN] Failed to auto-download curl-impersonate: #{e.message}"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# 4. Search system PATH for target impersonate binary
|
|
230
|
+
candidate_names.each do |name|
|
|
231
|
+
path = find_executable(name)
|
|
232
|
+
return path if path
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# 5. Fall back to general curl-impersonate binary if specific version is not found
|
|
236
|
+
%w[curl-impersonate-chrome curl-impersonate-ff curl-impersonate].each do |name|
|
|
237
|
+
path = find_executable(name)
|
|
238
|
+
return path if path
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# 6. Fallback to system curl if enabled
|
|
242
|
+
if config.fallback_to_curl
|
|
243
|
+
curl_path = find_executable('curl') || 'curl'
|
|
244
|
+
return curl_path if executable?(curl_path)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
raise BinaryNotFoundError, "Could not find a curl-impersonate binary for '#{target}', and no fallback curl executable was found."
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
private
|
|
251
|
+
|
|
252
|
+
def find_in_download_dir(candidates)
|
|
253
|
+
target_dir = File.expand_path(config.install_dir)
|
|
254
|
+
return nil unless Dir.exist?(target_dir)
|
|
255
|
+
|
|
256
|
+
candidates.each do |name|
|
|
257
|
+
path = File.join(target_dir, name)
|
|
258
|
+
return path if executable?(path)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Try generic curl-impersonate binary
|
|
262
|
+
gen_path = File.join(target_dir, 'curl-impersonate')
|
|
263
|
+
return gen_path if executable?(gen_path)
|
|
264
|
+
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def build_url
|
|
269
|
+
base = options[:base_uri] || ''
|
|
270
|
+
full = if @url.start_with?('http://', 'https://')
|
|
271
|
+
@url
|
|
272
|
+
elsif base.empty?
|
|
273
|
+
@url
|
|
274
|
+
else
|
|
275
|
+
URI.join(base.end_with?('/') ? base : "#{base}/", @url.sub(%r{^/}, '')).to_s
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
query_params = options[:query] || options[:params]
|
|
279
|
+
if query_params && !query_params.empty?
|
|
280
|
+
uri = URI.parse(full)
|
|
281
|
+
existing_query = uri.query
|
|
282
|
+
new_query = encode_params(query_params)
|
|
283
|
+
uri.query = existing_query && !existing_query.empty? ? "#{existing_query}&#{new_query}" : new_query
|
|
284
|
+
uri.to_s
|
|
285
|
+
else
|
|
286
|
+
full
|
|
287
|
+
end
|
|
288
|
+
rescue URI::InvalidURIError
|
|
289
|
+
full
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def encode_params(params)
|
|
293
|
+
if defined?(Rack::Utils) && Rack::Utils.respond_to?(:build_nested_query)
|
|
294
|
+
Rack::Utils.build_nested_query(params)
|
|
295
|
+
else
|
|
296
|
+
URI.encode_www_form(flatten_params(params))
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def flatten_params(params, parent_key = nil)
|
|
301
|
+
params.flat_map do |k, v|
|
|
302
|
+
full_key = parent_key ? "#{parent_key}[#{k}]" : k.to_s
|
|
303
|
+
if v.is_a?(Hash)
|
|
304
|
+
flatten_params(v, full_key)
|
|
305
|
+
elsif v.is_a?(Array)
|
|
306
|
+
v.map { |item| ["#{full_key}[]", item] }
|
|
307
|
+
else
|
|
308
|
+
[[full_key, v]]
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def find_executable(name)
|
|
314
|
+
return name if name.include?(File::SEPARATOR) && executable?(name)
|
|
315
|
+
|
|
316
|
+
# Search in system PATH
|
|
317
|
+
paths = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) + COMMON_SEARCH_PATHS
|
|
318
|
+
paths.uniq.each do |dir|
|
|
319
|
+
next unless dir && Dir.exist?(dir)
|
|
320
|
+
full_path = File.join(dir, name)
|
|
321
|
+
return full_path if executable?(full_path)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
nil
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def executable?(path)
|
|
328
|
+
File.file?(path) && File.executable?(path)
|
|
329
|
+
rescue StandardError
|
|
330
|
+
false
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HttpMimic
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :default_impersonate
|
|
6
|
+
attr_accessor :binary_path
|
|
7
|
+
attr_accessor :fallback_to_curl
|
|
8
|
+
attr_accessor :default_timeout
|
|
9
|
+
attr_accessor :default_connect_timeout
|
|
10
|
+
attr_accessor :follow_redirects
|
|
11
|
+
attr_accessor :max_redirects
|
|
12
|
+
attr_accessor :raise_on_error
|
|
13
|
+
attr_accessor :logger
|
|
14
|
+
attr_accessor :debug
|
|
15
|
+
|
|
16
|
+
# Webdrivers-like auto download and driver management settings
|
|
17
|
+
attr_accessor :auto_download
|
|
18
|
+
attr_accessor :driver_version
|
|
19
|
+
attr_accessor :install_dir
|
|
20
|
+
attr_accessor :github_repo
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@default_impersonate = 'chrome131'
|
|
24
|
+
@binary_path = nil
|
|
25
|
+
@fallback_to_curl = true
|
|
26
|
+
@default_timeout = 30
|
|
27
|
+
@default_connect_timeout = 10
|
|
28
|
+
@follow_redirects = true
|
|
29
|
+
@max_redirects = 10
|
|
30
|
+
@raise_on_error = false
|
|
31
|
+
@logger = nil
|
|
32
|
+
@debug = false
|
|
33
|
+
|
|
34
|
+
# Enable automatic downloading of curl-impersonate driver (lexiforest) by default
|
|
35
|
+
@auto_download = true
|
|
36
|
+
@driver_version = 'v2.1.1'
|
|
37
|
+
@install_dir = File.expand_path('~/.http_mimic/bin')
|
|
38
|
+
@github_repo = 'lexiforest/curl-impersonate'
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HttpMimic
|
|
4
|
+
class Cookies
|
|
5
|
+
include Enumerable
|
|
6
|
+
|
|
7
|
+
CookieItem = Struct.new(:name, :value, :attributes)
|
|
8
|
+
|
|
9
|
+
attr_reader :store
|
|
10
|
+
|
|
11
|
+
def initialize(cookie_hash = {})
|
|
12
|
+
@store = {}
|
|
13
|
+
@cookie_items = {}
|
|
14
|
+
|
|
15
|
+
cookie_hash.each do |k, v|
|
|
16
|
+
self[k] = v
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def [](name)
|
|
21
|
+
@store[name.to_s]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def []=(name, value)
|
|
25
|
+
@store[name.to_s] = value.to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def item(name)
|
|
29
|
+
@cookie_items[name.to_s]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def key?(name)
|
|
33
|
+
@store.key?(name.to_s)
|
|
34
|
+
end
|
|
35
|
+
alias has_key? key?
|
|
36
|
+
alias include? key?
|
|
37
|
+
|
|
38
|
+
def each(&block)
|
|
39
|
+
@store.each(&block)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def keys
|
|
43
|
+
@store.keys
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def values
|
|
47
|
+
@store.values
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_h
|
|
51
|
+
@store.dup
|
|
52
|
+
end
|
|
53
|
+
alias to_hash to_h
|
|
54
|
+
|
|
55
|
+
def to_cookie_string
|
|
56
|
+
@store.map { |k, v| "#{k}=#{v}" }.join('; ')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def inspect
|
|
60
|
+
"#<#{self.class.name} #{@store.inspect}>"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_s
|
|
64
|
+
to_cookie_string
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Parse a Cookies object from Set-Cookie headers
|
|
68
|
+
def self.parse_set_cookie(header_value_or_array)
|
|
69
|
+
cookies = new
|
|
70
|
+
headers = Array(header_value_or_array).flatten.compact
|
|
71
|
+
|
|
72
|
+
headers.each do |header_str|
|
|
73
|
+
parts = header_str.split(';').map(&:strip)
|
|
74
|
+
next if parts.empty?
|
|
75
|
+
|
|
76
|
+
name_val = parts.first
|
|
77
|
+
key, val = name_val.split('=', 2)
|
|
78
|
+
next unless key
|
|
79
|
+
|
|
80
|
+
key = key.strip
|
|
81
|
+
val = val ? val.strip : ''
|
|
82
|
+
|
|
83
|
+
attributes = {}
|
|
84
|
+
parts[1..-1].each do |attr_str|
|
|
85
|
+
attr_k, attr_v = attr_str.split('=', 2)
|
|
86
|
+
attr_k = attr_k.strip.downcase
|
|
87
|
+
attributes[attr_k] = attr_v ? attr_v.strip : true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
cookies[key] = val
|
|
91
|
+
cookies.instance_variable_get(:@cookie_items)[key] = CookieItem.new(key, val, attributes)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
cookies
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Format Hash, Cookies, or String into a curl -b cookie string
|
|
98
|
+
def self.format(cookie_input)
|
|
99
|
+
case cookie_input
|
|
100
|
+
when Hash
|
|
101
|
+
cookie_input.map { |k, v| "#{k}=#{v}" }.join('; ')
|
|
102
|
+
when Cookies
|
|
103
|
+
cookie_input.to_cookie_string
|
|
104
|
+
when String
|
|
105
|
+
cookie_input
|
|
106
|
+
else
|
|
107
|
+
cookie_input.to_s
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|