httpclient 2.1.2 → 2.1.3
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.
- data/lib/http-access2.rb +1 -2
- data/lib/httpclient.rb +631 -1832
- data/lib/httpclient/auth.rb +510 -0
- data/lib/httpclient/connection.rb +84 -0
- data/lib/httpclient/cookie.rb +82 -71
- data/lib/httpclient/http.rb +726 -484
- data/lib/httpclient/session.rb +855 -0
- data/lib/httpclient/ssl_config.rb +379 -0
- data/lib/httpclient/timeout.rb +122 -0
- data/lib/httpclient/util.rb +86 -0
- metadata +50 -37
data/lib/http-access2.rb
CHANGED
@@ -16,10 +16,9 @@ require 'httpclient'
|
|
16
16
|
module HTTPAccess2
|
17
17
|
VERSION = ::HTTPClient::VERSION
|
18
18
|
RUBY_VERSION_STRING = ::HTTPClient::RUBY_VERSION_STRING
|
19
|
-
RCS_FILE, RCS_REVISION = ::HTTPClient::RCS_FILE, ::HTTPClient::RCS_REVISION
|
20
19
|
SSLEnabled = ::HTTPClient::SSLEnabled
|
21
20
|
SSPIEnabled = ::HTTPClient::SSPIEnabled
|
22
|
-
DEBUG_SSL =
|
21
|
+
DEBUG_SSL = true
|
23
22
|
|
24
23
|
Util = ::HTTPClient::Util
|
25
24
|
|
data/lib/httpclient.rb
CHANGED
@@ -1,1802 +1,387 @@
|
|
1
1
|
# HTTPClient - HTTP client library.
|
2
|
-
# Copyright (C) 2000-
|
3
|
-
|
2
|
+
# Copyright (C) 2000-2008 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
|
3
|
+
#
|
4
4
|
# This program is copyrighted free software by NAKAMURA, Hiroshi. You can
|
5
5
|
# redistribute it and/or modify it under the same terms of Ruby's license;
|
6
6
|
# either the dual license version in 2003, or any later version.
|
7
7
|
|
8
|
-
# httpclient.rb is based on http-access.rb in http-access/0.0.4. Some part
|
9
|
-
# of code in http-access.rb was recycled in httpclient.rb. Those part is
|
10
|
-
# copyrighted by Maehashi-san.
|
11
8
|
|
12
|
-
|
13
|
-
# Ruby standard library
|
14
|
-
require 'timeout'
|
15
9
|
require 'uri'
|
16
|
-
require 'socket'
|
17
|
-
require 'thread'
|
18
10
|
require 'stringio'
|
19
|
-
require 'digest/
|
11
|
+
require 'digest/sha1'
|
20
12
|
|
21
13
|
# Extra library
|
14
|
+
require 'httpclient/util'
|
15
|
+
require 'httpclient/ssl_config'
|
16
|
+
require 'httpclient/connection'
|
17
|
+
require 'httpclient/session'
|
22
18
|
require 'httpclient/http'
|
19
|
+
require 'httpclient/auth'
|
23
20
|
require 'httpclient/cookie'
|
24
21
|
|
25
22
|
|
26
|
-
#
|
27
|
-
#
|
23
|
+
# The HTTPClient class provides several methods for accessing Web resources
|
24
|
+
# via HTTP.
|
25
|
+
#
|
26
|
+
# HTTPClient instance is designed to be MT-safe. You can call a HTTPClient
|
27
|
+
# instance from several threads without synchronization after setting up an
|
28
|
+
# instance.
|
29
|
+
#
|
30
|
+
# clnt = HTTPClient.new
|
31
|
+
# clnt.set_cookie_store('/home/nahi/cookie.dat')
|
32
|
+
# urls.each do |url|
|
33
|
+
# Thread.new(url) do |u|
|
34
|
+
# p clnt.head(u).status
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# == How to use
|
39
|
+
#
|
40
|
+
# At first, how to create your client. See initialize for more detail.
|
41
|
+
#
|
42
|
+
# 1. Create simple client.
|
28
43
|
#
|
29
|
-
# How to create your client.
|
30
|
-
# 1. Create simple client.
|
31
44
|
# clnt = HTTPClient.new
|
32
45
|
#
|
33
|
-
#
|
34
|
-
#
|
46
|
+
# 2. Accessing resources through HTTP proxy. You can use environment
|
47
|
+
# variable 'http_proxy' or 'HTTP_PROXY' instead.
|
48
|
+
#
|
49
|
+
# clnt = HTTPClient.new('http://myproxy:8080')
|
50
|
+
#
|
51
|
+
# === How to retrieve web resources
|
52
|
+
#
|
53
|
+
# See get_content.
|
54
|
+
#
|
55
|
+
# 1. Get content of specified URL. It returns a String of whole result.
|
56
|
+
#
|
57
|
+
# puts clnt.get_content('http://dev.ctor.org/')
|
58
|
+
#
|
59
|
+
# 2. Get content as chunks of String. It yields chunks of String.
|
60
|
+
#
|
61
|
+
# clnt.get_content('http://dev.ctor.org/') do |chunk|
|
62
|
+
# puts chunk
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# === Invoking other HTTP methods
|
35
66
|
#
|
36
|
-
#
|
37
|
-
#
|
67
|
+
# See head, get, post, put, delete, options, propfind, proppatch and trace.
|
68
|
+
# It returns a HTTP::Message instance as a response.
|
38
69
|
#
|
39
|
-
#
|
40
|
-
# 1. Get content of specified URL.
|
41
|
-
# puts clnt.get_content("http://www.ruby-lang.org/en/")
|
70
|
+
# 1. Do HEAD request.
|
42
71
|
#
|
43
|
-
# 2. Do HEAD request.
|
44
72
|
# res = clnt.head(uri)
|
73
|
+
# p res.header['Last-Modified'][0]
|
45
74
|
#
|
46
|
-
#
|
47
|
-
# res = clnt.get(uri)
|
75
|
+
# 2. Do GET request with query.
|
48
76
|
#
|
49
|
-
#
|
50
|
-
# res = clnt.
|
51
|
-
# res
|
77
|
+
# query = { 'keyword' => 'ruby', 'lang' => 'en' }
|
78
|
+
# res = clnt.get(uri, query)
|
79
|
+
# p res.status
|
80
|
+
# p res.contenttype
|
81
|
+
# p res.header['X-Custom']
|
82
|
+
# puts res.content
|
52
83
|
#
|
53
|
-
|
54
|
-
|
55
|
-
VERSION = '2.1.2'
|
56
|
-
RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
|
57
|
-
s = %w$Id: httpclient.rb 187 2007-09-22 14:15:42Z nahi $
|
58
|
-
RCS_FILE, RCS_REVISION = s[1][/.*(?=,v$)/], s[2]
|
59
|
-
|
60
|
-
SSLEnabled = begin
|
61
|
-
require 'openssl'
|
62
|
-
true
|
63
|
-
rescue LoadError
|
64
|
-
false
|
65
|
-
end
|
66
|
-
|
67
|
-
NTLMEnabled = begin
|
68
|
-
require 'net/ntlm'
|
69
|
-
true
|
70
|
-
rescue LoadError
|
71
|
-
false
|
72
|
-
end
|
73
|
-
|
74
|
-
SSPIEnabled = begin
|
75
|
-
require 'win32/sspi'
|
76
|
-
true
|
77
|
-
rescue LoadError
|
78
|
-
false
|
79
|
-
end
|
80
|
-
|
81
|
-
DEBUG_SSL = true
|
82
|
-
|
83
|
-
|
84
|
-
module Util
|
85
|
-
def urify(uri)
|
86
|
-
if uri.is_a?(URI)
|
87
|
-
uri
|
88
|
-
else
|
89
|
-
URI.parse(uri.to_s)
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def uri_part_of(uri, part)
|
94
|
-
((uri.scheme == part.scheme) and
|
95
|
-
(uri.host == part.host) and
|
96
|
-
(uri.port == part.port) and
|
97
|
-
uri.path.upcase.index(part.path.upcase) == 0)
|
98
|
-
end
|
99
|
-
module_function :uri_part_of
|
100
|
-
|
101
|
-
def uri_dirname(uri)
|
102
|
-
uri = uri.clone
|
103
|
-
uri.path = uri.path.sub(/\/[^\/]*\z/, '/')
|
104
|
-
uri
|
105
|
-
end
|
106
|
-
module_function :uri_dirname
|
107
|
-
|
108
|
-
def hash_find_value(hash)
|
109
|
-
hash.each do |k, v|
|
110
|
-
return v if yield(k, v)
|
111
|
-
end
|
112
|
-
nil
|
113
|
-
end
|
114
|
-
module_function :hash_find_value
|
115
|
-
|
116
|
-
def parse_challenge_param(param_str)
|
117
|
-
param = {}
|
118
|
-
param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
|
119
|
-
key, value = str[0].scan(/\A([^=]+)=(.*)\z/)[0]
|
120
|
-
if /\A"(.*)"\z/ =~ value
|
121
|
-
value = $1.gsub(/\\(.)/, '\1')
|
122
|
-
end
|
123
|
-
param[key] = value
|
124
|
-
end
|
125
|
-
param
|
126
|
-
end
|
127
|
-
module_function :parse_challenge_param
|
128
|
-
end
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
# HTTPClient::SSLConfig -- SSL configuration of a client.
|
84
|
+
# === How to POST
|
133
85
|
#
|
134
|
-
|
135
|
-
attr_reader :client_cert
|
136
|
-
attr_reader :client_key
|
137
|
-
attr_reader :client_ca
|
138
|
-
|
139
|
-
attr_reader :verify_mode
|
140
|
-
attr_reader :verify_depth
|
141
|
-
attr_reader :verify_callback
|
142
|
-
|
143
|
-
attr_reader :timeout
|
144
|
-
attr_reader :options
|
145
|
-
attr_reader :ciphers
|
146
|
-
|
147
|
-
attr_reader :cert_store # don't use if you don't know what it is.
|
148
|
-
|
149
|
-
def initialize(client)
|
150
|
-
return unless SSLEnabled
|
151
|
-
@client = client
|
152
|
-
@cert_store = OpenSSL::X509::Store.new
|
153
|
-
@client_cert = @client_key = @client_ca = nil
|
154
|
-
@verify_mode = OpenSSL::SSL::VERIFY_PEER |
|
155
|
-
OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
156
|
-
@verify_depth = nil
|
157
|
-
@verify_callback = nil
|
158
|
-
@dest = nil
|
159
|
-
@timeout = nil
|
160
|
-
@options = defined?(OpenSSL::SSL::OP_ALL) ?
|
161
|
-
OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_SSLv2 : nil
|
162
|
-
@ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:+SSLv2:@STRENGTH"
|
163
|
-
load_cacerts
|
164
|
-
end
|
165
|
-
|
166
|
-
def set_client_cert_file(cert_file, key_file)
|
167
|
-
@client_cert = OpenSSL::X509::Certificate.new(File.open(cert_file).read)
|
168
|
-
@client_key = OpenSSL::PKey::RSA.new(File.open(key_file).read)
|
169
|
-
change_notify
|
170
|
-
end
|
171
|
-
|
172
|
-
def clear_cert_store
|
173
|
-
@cert_store = OpenSSL::X509::Store.new
|
174
|
-
change_notify
|
175
|
-
end
|
176
|
-
|
177
|
-
def set_trust_ca(trust_ca_file_or_hashed_dir)
|
178
|
-
if FileTest.directory?(trust_ca_file_or_hashed_dir)
|
179
|
-
@cert_store.add_path(trust_ca_file_or_hashed_dir)
|
180
|
-
else
|
181
|
-
@cert_store.add_file(trust_ca_file_or_hashed_dir)
|
182
|
-
end
|
183
|
-
change_notify
|
184
|
-
end
|
185
|
-
|
186
|
-
def set_crl(crl_file)
|
187
|
-
crl = OpenSSL::X509::CRL.new(File.open(crl_file).read)
|
188
|
-
@cert_store.add_crl(crl)
|
189
|
-
@cert_store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
|
190
|
-
change_notify
|
191
|
-
end
|
192
|
-
|
193
|
-
def client_cert=(client_cert)
|
194
|
-
@client_cert = client_cert
|
195
|
-
change_notify
|
196
|
-
end
|
197
|
-
|
198
|
-
def client_key=(client_key)
|
199
|
-
@client_key = client_key
|
200
|
-
change_notify
|
201
|
-
end
|
202
|
-
|
203
|
-
def client_ca=(client_ca)
|
204
|
-
@client_ca = client_ca
|
205
|
-
change_notify
|
206
|
-
end
|
207
|
-
|
208
|
-
def verify_mode=(verify_mode)
|
209
|
-
@verify_mode = verify_mode
|
210
|
-
change_notify
|
211
|
-
end
|
212
|
-
|
213
|
-
def verify_depth=(verify_depth)
|
214
|
-
@verify_depth = verify_depth
|
215
|
-
change_notify
|
216
|
-
end
|
217
|
-
|
218
|
-
def verify_callback=(verify_callback)
|
219
|
-
@verify_callback = verify_callback
|
220
|
-
change_notify
|
221
|
-
end
|
222
|
-
|
223
|
-
def timeout=(timeout)
|
224
|
-
@timeout = timeout
|
225
|
-
change_notify
|
226
|
-
end
|
227
|
-
|
228
|
-
def options=(options)
|
229
|
-
@options = options
|
230
|
-
change_notify
|
231
|
-
end
|
232
|
-
|
233
|
-
def ciphers=(ciphers)
|
234
|
-
@ciphers = ciphers
|
235
|
-
change_notify
|
236
|
-
end
|
237
|
-
|
238
|
-
# don't use if you don't know what it is.
|
239
|
-
def cert_store=(cert_store)
|
240
|
-
@cert_store = cert_store
|
241
|
-
change_notify
|
242
|
-
end
|
243
|
-
|
244
|
-
# interfaces for SSLSocketWrap.
|
245
|
-
|
246
|
-
def set_context(ctx)
|
247
|
-
# Verification: Use Store#verify_callback instead of SSLContext#verify*?
|
248
|
-
ctx.cert_store = @cert_store
|
249
|
-
ctx.verify_mode = @verify_mode
|
250
|
-
ctx.verify_depth = @verify_depth if @verify_depth
|
251
|
-
ctx.verify_callback = @verify_callback || method(:default_verify_callback)
|
252
|
-
# SSL config
|
253
|
-
ctx.cert = @client_cert
|
254
|
-
ctx.key = @client_key
|
255
|
-
ctx.client_ca = @client_ca
|
256
|
-
ctx.timeout = @timeout
|
257
|
-
ctx.options = @options
|
258
|
-
ctx.ciphers = @ciphers
|
259
|
-
end
|
260
|
-
|
261
|
-
# this definition must match with the one in ext/openssl/lib/openssl/ssl.rb
|
262
|
-
def post_connection_check(peer_cert, hostname)
|
263
|
-
check_common_name = true
|
264
|
-
cert = peer_cert
|
265
|
-
cert.extensions.each{|ext|
|
266
|
-
next if ext.oid != "subjectAltName"
|
267
|
-
ext.value.split(/,\s+/).each{|general_name|
|
268
|
-
if /\ADNS:(.*)/ =~ general_name
|
269
|
-
check_common_name = false
|
270
|
-
reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+")
|
271
|
-
return true if /\A#{reg}\z/i =~ hostname
|
272
|
-
elsif /\AIP Address:(.*)/ =~ general_name
|
273
|
-
check_common_name = false
|
274
|
-
return true if $1 == hostname
|
275
|
-
end
|
276
|
-
}
|
277
|
-
}
|
278
|
-
if check_common_name
|
279
|
-
cert.subject.to_a.each{|oid, value|
|
280
|
-
if oid == "CN"
|
281
|
-
reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+")
|
282
|
-
return true if /\A#{reg}\z/i =~ hostname
|
283
|
-
end
|
284
|
-
}
|
285
|
-
end
|
286
|
-
raise OpenSSL::SSL::SSLError, "hostname not match"
|
287
|
-
end
|
288
|
-
|
289
|
-
# Default callback for verification: only dumps error.
|
290
|
-
def default_verify_callback(is_ok, ctx)
|
291
|
-
if $DEBUG
|
292
|
-
puts "#{ is_ok ? 'ok' : 'ng' }: #{ctx.current_cert.subject}"
|
293
|
-
end
|
294
|
-
if !is_ok
|
295
|
-
depth = ctx.error_depth
|
296
|
-
code = ctx.error
|
297
|
-
msg = ctx.error_string
|
298
|
-
STDERR.puts "at depth #{depth} - #{code}: #{msg}"
|
299
|
-
end
|
300
|
-
is_ok
|
301
|
-
end
|
302
|
-
|
303
|
-
# Sample callback method: CAUTION: does not check CRL/ARL.
|
304
|
-
def sample_verify_callback(is_ok, ctx)
|
305
|
-
unless is_ok
|
306
|
-
depth = ctx.error_depth
|
307
|
-
code = ctx.error
|
308
|
-
msg = ctx.error_string
|
309
|
-
STDERR.puts "at depth #{depth} - #{code}: #{msg}" if $DEBUG
|
310
|
-
return false
|
311
|
-
end
|
312
|
-
|
313
|
-
cert = ctx.current_cert
|
314
|
-
self_signed = false
|
315
|
-
ca = false
|
316
|
-
pathlen = nil
|
317
|
-
server_auth = true
|
318
|
-
self_signed = (cert.subject.cmp(cert.issuer) == 0)
|
319
|
-
|
320
|
-
# Check extensions whatever its criticality is. (sample)
|
321
|
-
cert.extensions.each do |ex|
|
322
|
-
case ex.oid
|
323
|
-
when 'basicConstraints'
|
324
|
-
/CA:(TRUE|FALSE), pathlen:(\d+)/ =~ ex.value
|
325
|
-
ca = ($1 == 'TRUE')
|
326
|
-
pathlen = $2.to_i
|
327
|
-
when 'keyUsage'
|
328
|
-
usage = ex.value.split(/\s*,\s*/)
|
329
|
-
ca = usage.include?('Certificate Sign')
|
330
|
-
server_auth = usage.include?('Key Encipherment')
|
331
|
-
when 'extendedKeyUsage'
|
332
|
-
usage = ex.value.split(/\s*,\s*/)
|
333
|
-
server_auth = usage.include?('Netscape Server Gated Crypto')
|
334
|
-
when 'nsCertType'
|
335
|
-
usage = ex.value.split(/\s*,\s*/)
|
336
|
-
ca = usage.include?('SSL CA')
|
337
|
-
server_auth = usage.include?('SSL Server')
|
338
|
-
end
|
339
|
-
end
|
340
|
-
|
341
|
-
if self_signed
|
342
|
-
STDERR.puts 'self signing CA' if $DEBUG
|
343
|
-
return true
|
344
|
-
elsif ca
|
345
|
-
STDERR.puts 'middle level CA' if $DEBUG
|
346
|
-
return true
|
347
|
-
elsif server_auth
|
348
|
-
STDERR.puts 'for server authentication' if $DEBUG
|
349
|
-
return true
|
350
|
-
end
|
351
|
-
|
352
|
-
return false
|
353
|
-
end
|
354
|
-
|
355
|
-
private
|
356
|
-
|
357
|
-
def change_notify
|
358
|
-
@client.reset_all
|
359
|
-
end
|
360
|
-
|
361
|
-
def load_cacerts
|
362
|
-
file = File.join(File.dirname(__FILE__), 'httpclient', 'cacert.p7s')
|
363
|
-
if File.exist?(file)
|
364
|
-
require 'openssl'
|
365
|
-
dist_cert =<<__DIST_CERT__
|
366
|
-
-----BEGIN CERTIFICATE-----
|
367
|
-
MIIC/jCCAmegAwIBAgIBATANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJKUDER
|
368
|
-
MA8GA1UECgwIY3Rvci5vcmcxFDASBgNVBAsMC0RldmVsb3BtZW50MRUwEwYDVQQD
|
369
|
-
DAxodHRwLWFjY2VzczIwHhcNMDcwOTExMTM1ODMxWhcNMDkwOTEwMTM1ODMxWjBN
|
370
|
-
MQswCQYDVQQGEwJKUDERMA8GA1UECgwIY3Rvci5vcmcxFDASBgNVBAsMC0RldmVs
|
371
|
-
b3BtZW50MRUwEwYDVQQDDAxodHRwLWFjY2VzczIwgZ8wDQYJKoZIhvcNAQEBBQAD
|
372
|
-
gY0AMIGJAoGBALi66ujWtUCQm5HpMSyr/AAIFYVXC/dmn7C8TR/HMiUuW3waY4uX
|
373
|
-
LFqCDAGOX4gf177pX+b99t3mpaiAjJuqc858D9xEECzhDWgXdLbhRqWhUOble4RY
|
374
|
-
c1yWYC990IgXJDMKx7VAuZ3cBhdBxtlE9sb1ZCzmHQsvTy/OoRzcJCrTAgMBAAGj
|
375
|
-
ge0wgeowDwYDVR0TAQH/BAUwAwEB/zAxBglghkgBhvhCAQ0EJBYiUnVieS9PcGVu
|
376
|
-
U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUJNE0GGaRKmN2qhnO
|
377
|
-
FyBWVl4Qj6owDgYDVR0PAQH/BAQDAgEGMHUGA1UdIwRuMGyAFCTRNBhmkSpjdqoZ
|
378
|
-
zhcgVlZeEI+qoVGkTzBNMQswCQYDVQQGEwJKUDERMA8GA1UECgwIY3Rvci5vcmcx
|
379
|
-
FDASBgNVBAsMC0RldmVsb3BtZW50MRUwEwYDVQQDDAxodHRwLWFjY2VzczKCAQEw
|
380
|
-
DQYJKoZIhvcNAQEFBQADgYEAH11tstSUuqFpMqoh/vM5l3Nqb8ygblbqEYQs/iG/
|
381
|
-
UeQkOZk/P1TxB6Ozn2htJ1srqDpUsncFVZ/ecP19GkeOZ6BmIhppcHhE5WyLBcPX
|
382
|
-
It5q1BW0PiAzT9LlEGoaiW0nw39so0Pr1whJDfc1t4fjdk+kSiMIzRHbTDvHWfpV
|
383
|
-
nTA=
|
384
|
-
-----END CERTIFICATE-----
|
385
|
-
__DIST_CERT__
|
386
|
-
p7 = OpenSSL::PKCS7.read_smime(File.open(file) { |f| f.read })
|
387
|
-
selfcert = OpenSSL::X509::Certificate.new(dist_cert)
|
388
|
-
store = OpenSSL::X509::Store.new
|
389
|
-
store.add_cert(selfcert)
|
390
|
-
if (p7.verify(nil, store, p7.data, 0))
|
391
|
-
set_trust_ca(file)
|
392
|
-
else
|
393
|
-
STDERR.puts("cacerts: #{file} loading failed")
|
394
|
-
end
|
395
|
-
end
|
396
|
-
end
|
397
|
-
end
|
398
|
-
|
399
|
-
|
400
|
-
# HTTPClient::BasicAuth -- BasicAuth repository.
|
86
|
+
# See post.
|
401
87
|
#
|
402
|
-
|
403
|
-
attr_reader :scheme
|
404
|
-
|
405
|
-
def initialize
|
406
|
-
@cred = nil
|
407
|
-
@auth = {}
|
408
|
-
@challengeable = {}
|
409
|
-
@scheme = "Basic"
|
410
|
-
end
|
411
|
-
|
412
|
-
def reset_challenge
|
413
|
-
@challengeable.clear
|
414
|
-
end
|
415
|
-
|
416
|
-
# uri == nil for generic purpose
|
417
|
-
def set(uri, user, passwd)
|
418
|
-
if uri.nil?
|
419
|
-
@cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
420
|
-
else
|
421
|
-
uri = Util.uri_dirname(uri)
|
422
|
-
@auth[uri] = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
423
|
-
end
|
424
|
-
end
|
425
|
-
|
426
|
-
# send cred only when a given uri is;
|
427
|
-
# - child page of challengeable(got WWW-Authenticate before) uri and,
|
428
|
-
# - child page of defined credential
|
429
|
-
def get(req)
|
430
|
-
target_uri = req.header.request_uri
|
431
|
-
return nil unless @challengeable.find { |uri, ok|
|
432
|
-
Util.uri_part_of(target_uri, uri) and ok
|
433
|
-
}
|
434
|
-
return @cred if @cred
|
435
|
-
Util.hash_find_value(@auth) { |uri, cred|
|
436
|
-
Util.uri_part_of(target_uri, uri)
|
437
|
-
}
|
438
|
-
end
|
439
|
-
|
440
|
-
def challenge(uri, param_str)
|
441
|
-
@challengeable[uri] = true
|
442
|
-
true
|
443
|
-
end
|
444
|
-
end
|
445
|
-
|
446
|
-
|
447
|
-
# HTTPClient::DigestAuth
|
88
|
+
# 1. Do POST a form data.
|
448
89
|
#
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
def initialize
|
453
|
-
@auth = {}
|
454
|
-
@challenge = {}
|
455
|
-
@nonce_count = 0
|
456
|
-
@scheme = "Digest"
|
457
|
-
end
|
458
|
-
|
459
|
-
def reset_challenge
|
460
|
-
@challenge.clear
|
461
|
-
end
|
462
|
-
|
463
|
-
def set(uri, user, passwd)
|
464
|
-
uri = Util.uri_dirname(uri)
|
465
|
-
@auth[uri] = [user, passwd]
|
466
|
-
end
|
467
|
-
|
468
|
-
# send cred only when a given uri is;
|
469
|
-
# - child page of challengeable(got WWW-Authenticate before) uri and,
|
470
|
-
# - child page of defined credential
|
471
|
-
def get(req)
|
472
|
-
target_uri = req.header.request_uri
|
473
|
-
param = Util.hash_find_value(@challenge) { |uri, param|
|
474
|
-
Util.uri_part_of(target_uri, uri)
|
475
|
-
}
|
476
|
-
return nil unless param
|
477
|
-
user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
|
478
|
-
Util.uri_part_of(target_uri, uri)
|
479
|
-
}
|
480
|
-
return nil unless user
|
481
|
-
uri = req.header.request_uri
|
482
|
-
calc_cred(req.header.request_method, uri, user, passwd, param)
|
483
|
-
end
|
484
|
-
|
485
|
-
def challenge(uri, param_str)
|
486
|
-
@challenge[uri] = Util.parse_challenge_param(param_str)
|
487
|
-
true
|
488
|
-
end
|
489
|
-
|
490
|
-
private
|
491
|
-
|
492
|
-
# this method is implemented by sromano and posted to
|
493
|
-
# http://tools.assembla.com/breakout/wiki/DigestForSoap
|
494
|
-
# Thanks!
|
495
|
-
# supported algorithm: MD5 only for now
|
496
|
-
def calc_cred(method, uri, user, passwd, param)
|
497
|
-
a_1 = "#{user}:#{param['realm']}:#{passwd}"
|
498
|
-
a_2 = "#{method}:#{uri.path}"
|
499
|
-
@nonce_count += 1
|
500
|
-
message_digest = []
|
501
|
-
message_digest << Digest::MD5.hexdigest(a_1)
|
502
|
-
message_digest << param['nonce']
|
503
|
-
message_digest << ('%08x' % @nonce_count)
|
504
|
-
message_digest << param['nonce']
|
505
|
-
message_digest << param['qop']
|
506
|
-
message_digest << Digest::MD5.hexdigest(a_2)
|
507
|
-
header = []
|
508
|
-
header << "username=\"#{user}\""
|
509
|
-
header << "realm=\"#{param['realm']}\""
|
510
|
-
header << "nonce=\"#{param['nonce']}\""
|
511
|
-
header << "uri=\"#{uri.path}\""
|
512
|
-
header << "cnonce=\"#{param['nonce']}\""
|
513
|
-
header << "nc=#{'%08x' % @nonce_count}"
|
514
|
-
header << "qop=\"#{param['qop']}\""
|
515
|
-
header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
|
516
|
-
header << "algorithm=\"MD5\""
|
517
|
-
header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
|
518
|
-
header.join(", ")
|
519
|
-
end
|
520
|
-
end
|
521
|
-
|
522
|
-
|
523
|
-
# HTTPClient::NegotiateAuth
|
90
|
+
# body = { 'keyword' => 'ruby', 'lang' => 'en' }
|
91
|
+
# res = clnt.post(uri, body)
|
524
92
|
#
|
525
|
-
|
526
|
-
|
527
|
-
attr_reader :ntlm_opt
|
528
|
-
|
529
|
-
def initialize
|
530
|
-
@auth = {}
|
531
|
-
@auth_default = nil
|
532
|
-
@challenge = {}
|
533
|
-
@scheme = "Negotiate"
|
534
|
-
@ntlm_opt = {
|
535
|
-
:ntlmv2 => true
|
536
|
-
}
|
537
|
-
end
|
538
|
-
|
539
|
-
def reset_challenge
|
540
|
-
@challenge.clear
|
541
|
-
end
|
542
|
-
|
543
|
-
def set(uri, user, passwd)
|
544
|
-
if uri
|
545
|
-
uri = Util.uri_dirname(uri)
|
546
|
-
@auth[uri] = [user, passwd]
|
547
|
-
else
|
548
|
-
@auth_default = [user, passwd]
|
549
|
-
end
|
550
|
-
end
|
551
|
-
|
552
|
-
def get(req)
|
553
|
-
return nil unless NTLMEnabled
|
554
|
-
target_uri = req.header.request_uri
|
555
|
-
param = Util.hash_find_value(@challenge) { |uri, param|
|
556
|
-
Util.uri_part_of(target_uri, uri)
|
557
|
-
}
|
558
|
-
return nil unless param
|
559
|
-
user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
|
560
|
-
Util.uri_part_of(target_uri, uri)
|
561
|
-
}
|
562
|
-
unless user
|
563
|
-
user, passwd = @auth_default
|
564
|
-
end
|
565
|
-
return nil unless user
|
566
|
-
state = param[:state]
|
567
|
-
authphrase = param[:authphrase]
|
568
|
-
case state
|
569
|
-
when :init
|
570
|
-
t1 = Net::NTLM::Message::Type1.new
|
571
|
-
return t1.encode64
|
572
|
-
when :response
|
573
|
-
t2 = Net::NTLM::Message.decode64(authphrase)
|
574
|
-
t3 = t2.response({:user => user, :password => passwd}, @ntlm_opt)
|
575
|
-
return t3.encode64
|
576
|
-
end
|
577
|
-
nil
|
578
|
-
end
|
579
|
-
|
580
|
-
def challenge(uri, param_str)
|
581
|
-
return false unless NTLMEnabled
|
582
|
-
if param_str.nil? or @challenge[uri].nil?
|
583
|
-
c = @challenge[uri] = {}
|
584
|
-
c[:state] = :init
|
585
|
-
c[:authphrase] = ""
|
586
|
-
else
|
587
|
-
c = @challenge[uri]
|
588
|
-
c[:state] = :response
|
589
|
-
c[:authphrase] = param_str
|
590
|
-
end
|
591
|
-
true
|
592
|
-
end
|
593
|
-
end
|
594
|
-
|
595
|
-
|
596
|
-
# HTTPClient::SSPINegotiateAuth
|
93
|
+
# 2. Do multipart file upload with POST. No need to set extra header by
|
94
|
+
# yourself from httpclient/2.1.3.
|
597
95
|
#
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
@challenge = {}
|
603
|
-
@scheme = "Negotiate"
|
604
|
-
end
|
605
|
-
|
606
|
-
def reset_challenge
|
607
|
-
@challenge.clear
|
608
|
-
end
|
609
|
-
|
610
|
-
def get(req)
|
611
|
-
return nil unless SSPIEnabled
|
612
|
-
target_uri = req.header.request_uri
|
613
|
-
param = Util.hash_find_value(@challenge) { |uri, param|
|
614
|
-
Util.uri_part_of(target_uri, uri)
|
615
|
-
}
|
616
|
-
return nil unless param
|
617
|
-
state = param[:state]
|
618
|
-
authenticator = param[:authenticator]
|
619
|
-
authphrase = param[:authphrase]
|
620
|
-
case state
|
621
|
-
when :init
|
622
|
-
authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
|
623
|
-
return authenticator.get_initial_token
|
624
|
-
when :response
|
625
|
-
return authenticator.complete_authentication(authphrase)
|
626
|
-
end
|
627
|
-
nil
|
628
|
-
end
|
629
|
-
|
630
|
-
def challenge(uri, param_str)
|
631
|
-
return false unless SSPIEnabled
|
632
|
-
if param_str.nil? or @challenge[uri].nil?
|
633
|
-
c = @challenge[uri] = {}
|
634
|
-
c[:state] = :init
|
635
|
-
c[:authenticator] = nil
|
636
|
-
c[:authphrase] = ""
|
637
|
-
else
|
638
|
-
c = @challenge[uri]
|
639
|
-
c[:state] = :response
|
640
|
-
c[:authphrase] = param_str
|
641
|
-
end
|
642
|
-
true
|
643
|
-
end
|
644
|
-
end
|
645
|
-
|
646
|
-
|
647
|
-
class AuthFilterBase # :nodoc:
|
648
|
-
private
|
649
|
-
|
650
|
-
def parse_authentication_header(res, tag)
|
651
|
-
challenge = res.header[tag]
|
652
|
-
unless challenge
|
653
|
-
raise RuntimeError.new("no #{tag} header exists: #{res}")
|
654
|
-
end
|
655
|
-
challenge.collect { |c| parse_challenge_header(c) }
|
656
|
-
end
|
657
|
-
|
658
|
-
def parse_challenge_header(challenge)
|
659
|
-
scheme, param_str = challenge.scan(/\A(\S+)(?:\s+(.*))?\z/)[0]
|
660
|
-
if scheme.nil?
|
661
|
-
raise RuntimeError.new("unsupported challenge: #{challenge}")
|
662
|
-
end
|
663
|
-
return scheme, param_str
|
664
|
-
end
|
665
|
-
end
|
666
|
-
|
667
|
-
|
668
|
-
class WWWAuth < AuthFilterBase # :nodoc:
|
669
|
-
attr_reader :basic_auth
|
670
|
-
attr_reader :digest_auth
|
671
|
-
attr_reader :negotiate_auth
|
672
|
-
|
673
|
-
def initialize
|
674
|
-
@basic_auth = BasicAuth.new
|
675
|
-
@digest_auth = DigestAuth.new
|
676
|
-
@negotiate_auth = NegotiateAuth.new
|
677
|
-
# sort authenticators by priority
|
678
|
-
@authenticator = [@negotiate_auth, @digest_auth, @basic_auth]
|
679
|
-
end
|
680
|
-
|
681
|
-
def reset_challenge
|
682
|
-
@authenticator.each do |auth|
|
683
|
-
auth.reset_challenge
|
684
|
-
end
|
685
|
-
end
|
686
|
-
|
687
|
-
def set_auth(uri, user, passwd)
|
688
|
-
@basic_auth.set(uri, user, passwd)
|
689
|
-
@digest_auth.set(uri, user, passwd)
|
690
|
-
@negotiate_auth.set(uri, user, passwd)
|
691
|
-
end
|
692
|
-
|
693
|
-
def filter_request(req)
|
694
|
-
@authenticator.each do |auth|
|
695
|
-
if cred = auth.get(req)
|
696
|
-
req.header.set('Authorization', auth.scheme + " " + cred)
|
697
|
-
return
|
698
|
-
end
|
699
|
-
end
|
700
|
-
end
|
701
|
-
|
702
|
-
def filter_response(req, res)
|
703
|
-
command = nil
|
704
|
-
uri = req.header.request_uri
|
705
|
-
if res.status == HTTP::Status::UNAUTHORIZED
|
706
|
-
if challenge = parse_authentication_header(res, 'www-authenticate')
|
707
|
-
challenge.each do |scheme, param_str|
|
708
|
-
@authenticator.each do |auth|
|
709
|
-
if scheme.downcase == auth.scheme.downcase
|
710
|
-
challengeable = auth.challenge(uri, param_str)
|
711
|
-
command = :retry if challengeable
|
712
|
-
end
|
713
|
-
end
|
714
|
-
end
|
715
|
-
# ignore unknown authentication scheme
|
716
|
-
end
|
717
|
-
end
|
718
|
-
command
|
719
|
-
end
|
720
|
-
end
|
721
|
-
|
722
|
-
|
723
|
-
class ProxyAuth < AuthFilterBase # :nodoc:
|
724
|
-
attr_reader :basic_auth
|
725
|
-
attr_reader :negotiate_auth
|
726
|
-
attr_reader :sspi_negotiate_auth
|
727
|
-
|
728
|
-
def initialize
|
729
|
-
@basic_auth = BasicAuth.new
|
730
|
-
@negotiate_auth = NegotiateAuth.new
|
731
|
-
@sspi_negotiate_auth = SSPINegotiateAuth.new
|
732
|
-
# sort authenticators by priority
|
733
|
-
@authenticator = [@negotiate_auth, @sspi_negotiate_auth, @basic_auth]
|
734
|
-
end
|
735
|
-
|
736
|
-
def reset_challenge
|
737
|
-
@authenticator.each do |auth|
|
738
|
-
auth.reset_challenge
|
739
|
-
end
|
740
|
-
end
|
741
|
-
|
742
|
-
def set_auth(user, passwd)
|
743
|
-
@basic_auth.set(nil, user, passwd)
|
744
|
-
@negotiate_auth.set(nil, user, passwd)
|
745
|
-
end
|
746
|
-
|
747
|
-
def filter_request(req)
|
748
|
-
@authenticator.each do |auth|
|
749
|
-
if cred = auth.get(req)
|
750
|
-
req.header.set('Proxy-Authorization', auth.scheme + " " + cred)
|
751
|
-
return
|
752
|
-
end
|
753
|
-
end
|
754
|
-
end
|
755
|
-
|
756
|
-
def filter_response(req, res)
|
757
|
-
command = nil
|
758
|
-
uri = req.header.request_uri
|
759
|
-
if res.status == HTTP::Status::PROXY_AUTHENTICATE_REQUIRED
|
760
|
-
if challenge = parse_authentication_header(res, 'proxy-authenticate')
|
761
|
-
challenge.each do |scheme, param_str|
|
762
|
-
@authenticator.each do |auth|
|
763
|
-
if scheme.downcase == auth.scheme.downcase
|
764
|
-
challengeable = auth.challenge(uri, param_str)
|
765
|
-
command = :retry if challengeable
|
766
|
-
end
|
767
|
-
end
|
768
|
-
end
|
769
|
-
# ignore unknown authentication scheme
|
770
|
-
end
|
771
|
-
end
|
772
|
-
command
|
773
|
-
end
|
774
|
-
end
|
775
|
-
|
776
|
-
|
777
|
-
# HTTPClient::Site -- manage a site(host and port)
|
96
|
+
# File.open('/tmp/post_data') do |file|
|
97
|
+
# body = { 'upload' => file, 'user' => 'nahi' }
|
98
|
+
# res = clnt.post(uri, body)
|
99
|
+
# end
|
778
100
|
#
|
779
|
-
|
780
|
-
attr_accessor :scheme
|
781
|
-
attr_accessor :host
|
782
|
-
attr_reader :port
|
783
|
-
|
784
|
-
def initialize(uri = nil)
|
785
|
-
if uri
|
786
|
-
@uri = uri
|
787
|
-
@scheme = uri.scheme
|
788
|
-
@host = uri.host
|
789
|
-
@port = uri.port.to_i
|
790
|
-
else
|
791
|
-
@uri = nil
|
792
|
-
@scheme = 'tcp'
|
793
|
-
@host = '0.0.0.0'
|
794
|
-
@port = 0
|
795
|
-
end
|
796
|
-
end
|
797
|
-
|
798
|
-
def addr
|
799
|
-
"#{@scheme}://#{@host}:#{@port.to_s}"
|
800
|
-
end
|
801
|
-
|
802
|
-
def port=(port)
|
803
|
-
@port = port.to_i
|
804
|
-
end
|
805
|
-
|
806
|
-
def ==(rhs)
|
807
|
-
if rhs.is_a?(Site)
|
808
|
-
((@scheme == rhs.scheme) and (@host == rhs.host) and (@port == rhs.port))
|
809
|
-
else
|
810
|
-
false
|
811
|
-
end
|
812
|
-
end
|
813
|
-
|
814
|
-
def to_s
|
815
|
-
addr
|
816
|
-
end
|
817
|
-
|
818
|
-
def inspect
|
819
|
-
sprintf("#<%s:0x%x %s>", self.class.name, __id__, @uri || addr)
|
820
|
-
end
|
821
|
-
end
|
822
|
-
|
823
|
-
|
824
|
-
# HTTPClient::Connection -- magage a connection(one request and response to it).
|
101
|
+
# === Accessing via SSL
|
825
102
|
#
|
826
|
-
|
827
|
-
attr_accessor :async_thread
|
828
|
-
|
829
|
-
def initialize(header_queue = [], body_queue = [])
|
830
|
-
@headers = header_queue
|
831
|
-
@body = body_queue
|
832
|
-
@async_thread = nil
|
833
|
-
@queue = Queue.new
|
834
|
-
end
|
835
|
-
|
836
|
-
def finished?
|
837
|
-
if !@async_thread
|
838
|
-
# Not in async mode.
|
839
|
-
true
|
840
|
-
elsif @async_thread.alive?
|
841
|
-
# Working...
|
842
|
-
false
|
843
|
-
else
|
844
|
-
# Async thread have been finished.
|
845
|
-
@async_thread.join
|
846
|
-
true
|
847
|
-
end
|
848
|
-
end
|
849
|
-
|
850
|
-
def pop
|
851
|
-
@queue.pop
|
852
|
-
end
|
853
|
-
|
854
|
-
def push(result)
|
855
|
-
@queue.push(result)
|
856
|
-
end
|
857
|
-
|
858
|
-
def join
|
859
|
-
unless @async_thread
|
860
|
-
false
|
861
|
-
else
|
862
|
-
@async_thread.join
|
863
|
-
end
|
864
|
-
end
|
865
|
-
end
|
866
|
-
|
867
|
-
|
868
|
-
# HTTPClient::SessionManager -- manage several sessions.
|
103
|
+
# Ruby needs to be compiled with OpenSSL.
|
869
104
|
#
|
870
|
-
|
871
|
-
|
872
|
-
attr_accessor :from # Owner of this client.
|
873
|
-
|
874
|
-
attr_accessor :protocol_version # Requested protocol version
|
875
|
-
attr_accessor :chunk_size # Chunk size for chunked request
|
876
|
-
attr_accessor :debug_dev # Device for dumping log for debugging
|
877
|
-
attr_accessor :socket_sync # Boolean value for Socket#sync
|
878
|
-
|
879
|
-
# These parameters are not used now...
|
880
|
-
attr_accessor :connect_timeout
|
881
|
-
attr_accessor :connect_retry # Maximum retry count. 0 for infinite.
|
882
|
-
attr_accessor :send_timeout
|
883
|
-
attr_accessor :receive_timeout
|
884
|
-
attr_accessor :read_block_size
|
885
|
-
|
886
|
-
attr_accessor :ssl_config
|
887
|
-
|
888
|
-
attr_reader :test_loopback_http_response
|
889
|
-
|
890
|
-
def initialize
|
891
|
-
@proxy = nil
|
892
|
-
|
893
|
-
@agent_name = nil
|
894
|
-
@from = nil
|
895
|
-
|
896
|
-
@protocol_version = nil
|
897
|
-
@debug_dev = nil
|
898
|
-
@socket_sync = true
|
899
|
-
@chunk_size = 4096
|
900
|
-
|
901
|
-
@connect_timeout = 60
|
902
|
-
@connect_retry = 1
|
903
|
-
@send_timeout = 120
|
904
|
-
@receive_timeout = 60 # For each read_block_size bytes
|
905
|
-
@read_block_size = 8192
|
906
|
-
|
907
|
-
@ssl_config = nil
|
908
|
-
@test_loopback_http_response = []
|
909
|
-
|
910
|
-
@sess_pool = []
|
911
|
-
@sess_pool_mutex = Mutex.new
|
912
|
-
end
|
913
|
-
|
914
|
-
def proxy=(proxy)
|
915
|
-
if proxy.nil?
|
916
|
-
@proxy = nil
|
917
|
-
else
|
918
|
-
@proxy = Site.new(proxy)
|
919
|
-
end
|
920
|
-
end
|
921
|
-
|
922
|
-
def query(req, proxy)
|
923
|
-
req.body.chunk_size = @chunk_size
|
924
|
-
dest_site = Site.new(req.header.request_uri)
|
925
|
-
proxy_site = if proxy
|
926
|
-
Site.new(proxy)
|
927
|
-
else
|
928
|
-
@proxy
|
929
|
-
end
|
930
|
-
sess = open(dest_site, proxy_site)
|
931
|
-
begin
|
932
|
-
sess.query(req)
|
933
|
-
rescue
|
934
|
-
sess.close
|
935
|
-
raise
|
936
|
-
end
|
937
|
-
sess
|
938
|
-
end
|
939
|
-
|
940
|
-
def reset(uri)
|
941
|
-
site = Site.new(uri)
|
942
|
-
close(site)
|
943
|
-
end
|
944
|
-
|
945
|
-
def reset_all
|
946
|
-
close_all
|
947
|
-
end
|
948
|
-
|
949
|
-
def keep(sess)
|
950
|
-
add_cached_session(sess)
|
951
|
-
end
|
952
|
-
|
953
|
-
private
|
954
|
-
|
955
|
-
def open(dest, proxy = nil)
|
956
|
-
sess = nil
|
957
|
-
if cached = get_cached_session(dest)
|
958
|
-
sess = cached
|
959
|
-
else
|
960
|
-
sess = Session.new(dest, @agent_name, @from)
|
961
|
-
sess.proxy = proxy
|
962
|
-
sess.socket_sync = @socket_sync
|
963
|
-
sess.requested_version = @protocol_version if @protocol_version
|
964
|
-
sess.connect_timeout = @connect_timeout
|
965
|
-
sess.connect_retry = @connect_retry
|
966
|
-
sess.send_timeout = @send_timeout
|
967
|
-
sess.receive_timeout = @receive_timeout
|
968
|
-
sess.read_block_size = @read_block_size
|
969
|
-
sess.ssl_config = @ssl_config
|
970
|
-
sess.debug_dev = @debug_dev
|
971
|
-
sess.test_loopback_http_response = @test_loopback_http_response
|
972
|
-
end
|
973
|
-
sess
|
974
|
-
end
|
975
|
-
|
976
|
-
def close_all
|
977
|
-
each_sess do |sess|
|
978
|
-
sess.close
|
979
|
-
end
|
980
|
-
@sess_pool.clear
|
981
|
-
end
|
982
|
-
|
983
|
-
def close(dest)
|
984
|
-
if cached = get_cached_session(dest)
|
985
|
-
cached.close
|
986
|
-
true
|
987
|
-
else
|
988
|
-
false
|
989
|
-
end
|
990
|
-
end
|
991
|
-
|
992
|
-
def get_cached_session(dest)
|
993
|
-
cached = nil
|
994
|
-
@sess_pool_mutex.synchronize do
|
995
|
-
new_pool = []
|
996
|
-
@sess_pool.each do |s|
|
997
|
-
if s.dest == dest
|
998
|
-
cached = s
|
999
|
-
else
|
1000
|
-
new_pool << s
|
1001
|
-
end
|
1002
|
-
end
|
1003
|
-
@sess_pool = new_pool
|
1004
|
-
end
|
1005
|
-
cached
|
1006
|
-
end
|
1007
|
-
|
1008
|
-
def add_cached_session(sess)
|
1009
|
-
@sess_pool_mutex.synchronize do
|
1010
|
-
@sess_pool << sess
|
1011
|
-
end
|
1012
|
-
end
|
1013
|
-
|
1014
|
-
def each_sess
|
1015
|
-
@sess_pool_mutex.synchronize do
|
1016
|
-
@sess_pool.each do |sess|
|
1017
|
-
yield(sess)
|
1018
|
-
end
|
1019
|
-
end
|
1020
|
-
end
|
1021
|
-
end
|
1022
|
-
|
1023
|
-
|
1024
|
-
# HTTPClient::SSLSocketWrap
|
105
|
+
# 1. Get content of specified URL via SSL.
|
106
|
+
# Just pass an URL which starts with 'https://'.
|
1025
107
|
#
|
1026
|
-
|
1027
|
-
|
1028
|
-
unless SSLEnabled
|
1029
|
-
raise RuntimeError.new(
|
1030
|
-
"Ruby/OpenSSL module is required for https access.")
|
1031
|
-
end
|
1032
|
-
@context = context
|
1033
|
-
@socket = socket
|
1034
|
-
@ssl_socket = create_ssl_socket(@socket)
|
1035
|
-
@debug_dev = debug_dev
|
1036
|
-
end
|
1037
|
-
|
1038
|
-
def ssl_connect
|
1039
|
-
@ssl_socket.connect
|
1040
|
-
end
|
1041
|
-
|
1042
|
-
def post_connection_check(host)
|
1043
|
-
verify_mode = @context.verify_mode || OpenSSL::SSL::VERIFY_NONE
|
1044
|
-
if verify_mode == OpenSSL::SSL::VERIFY_NONE
|
1045
|
-
return
|
1046
|
-
elsif @ssl_socket.peer_cert.nil? and
|
1047
|
-
check_mask(verify_mode, OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT)
|
1048
|
-
raise OpenSSL::SSL::SSLError, "no peer cert"
|
1049
|
-
end
|
1050
|
-
hostname = host.host
|
1051
|
-
if @ssl_socket.respond_to?(:post_connection_check) and RUBY_VERSION > "1.8.4"
|
1052
|
-
@ssl_socket.post_connection_check(hostname)
|
1053
|
-
else
|
1054
|
-
@context.post_connection_check(@ssl_socket.peer_cert, hostname)
|
1055
|
-
end
|
1056
|
-
end
|
1057
|
-
|
1058
|
-
def peer_cert
|
1059
|
-
@ssl_socket.peer_cert
|
1060
|
-
end
|
1061
|
-
|
1062
|
-
def addr
|
1063
|
-
@socket.addr
|
1064
|
-
end
|
1065
|
-
|
1066
|
-
def close
|
1067
|
-
@ssl_socket.close
|
1068
|
-
@socket.close
|
1069
|
-
end
|
1070
|
-
|
1071
|
-
def closed?
|
1072
|
-
@socket.closed?
|
1073
|
-
end
|
1074
|
-
|
1075
|
-
def eof?
|
1076
|
-
@ssl_socket.eof?
|
1077
|
-
end
|
1078
|
-
|
1079
|
-
def gets(*args)
|
1080
|
-
str = @ssl_socket.gets(*args)
|
1081
|
-
@debug_dev << str if @debug_dev
|
1082
|
-
str
|
1083
|
-
end
|
1084
|
-
|
1085
|
-
def read(*args)
|
1086
|
-
str = @ssl_socket.read(*args)
|
1087
|
-
@debug_dev << str if @debug_dev
|
1088
|
-
str
|
1089
|
-
end
|
1090
|
-
|
1091
|
-
def <<(str)
|
1092
|
-
rv = @ssl_socket.write(str)
|
1093
|
-
@debug_dev << str if @debug_dev
|
1094
|
-
rv
|
1095
|
-
end
|
1096
|
-
|
1097
|
-
def flush
|
1098
|
-
@ssl_socket.flush
|
1099
|
-
end
|
1100
|
-
|
1101
|
-
def sync
|
1102
|
-
@ssl_socket.sync
|
1103
|
-
end
|
1104
|
-
|
1105
|
-
def sync=(sync)
|
1106
|
-
@ssl_socket.sync = sync
|
1107
|
-
end
|
1108
|
-
|
1109
|
-
private
|
1110
|
-
|
1111
|
-
def check_mask(value, mask)
|
1112
|
-
value & mask == mask
|
1113
|
-
end
|
1114
|
-
|
1115
|
-
def create_ssl_socket(socket)
|
1116
|
-
ssl_socket = nil
|
1117
|
-
if OpenSSL::SSL.const_defined?("SSLContext")
|
1118
|
-
ctx = OpenSSL::SSL::SSLContext.new
|
1119
|
-
@context.set_context(ctx)
|
1120
|
-
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
|
1121
|
-
else
|
1122
|
-
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket)
|
1123
|
-
@context.set_context(ssl_socket)
|
1124
|
-
end
|
1125
|
-
ssl_socket
|
1126
|
-
end
|
1127
|
-
end
|
1128
|
-
|
1129
|
-
|
1130
|
-
module SocketWrap
|
1131
|
-
def initialize(socket, *args)
|
1132
|
-
super(*args)
|
1133
|
-
@socket = socket
|
1134
|
-
end
|
1135
|
-
|
1136
|
-
def addr
|
1137
|
-
@socket.addr
|
1138
|
-
end
|
1139
|
-
|
1140
|
-
def close
|
1141
|
-
@socket.close
|
1142
|
-
end
|
1143
|
-
|
1144
|
-
def closed?
|
1145
|
-
@socket.closed?
|
1146
|
-
end
|
1147
|
-
|
1148
|
-
def eof?
|
1149
|
-
@socket.eof?
|
1150
|
-
end
|
1151
|
-
|
1152
|
-
def gets(*args)
|
1153
|
-
@socket.gets(*args)
|
1154
|
-
end
|
1155
|
-
|
1156
|
-
def read(*args)
|
1157
|
-
@socket.read(*args)
|
1158
|
-
end
|
1159
|
-
|
1160
|
-
def <<(str)
|
1161
|
-
@socket << str
|
1162
|
-
end
|
1163
|
-
|
1164
|
-
def flush
|
1165
|
-
@socket.flush
|
1166
|
-
end
|
1167
|
-
|
1168
|
-
def sync
|
1169
|
-
@socket.sync
|
1170
|
-
end
|
1171
|
-
|
1172
|
-
def sync=(sync)
|
1173
|
-
@socket.sync = sync
|
1174
|
-
end
|
1175
|
-
end
|
1176
|
-
|
1177
|
-
|
1178
|
-
# HTTPClient::DebugSocket -- debugging support
|
108
|
+
# https_url = 'https://www.rsa.com'
|
109
|
+
# clnt.get_content(https_url)
|
1179
110
|
#
|
1180
|
-
|
1181
|
-
extend SocketWrap
|
1182
|
-
|
1183
|
-
def debug_dev=(debug_dev)
|
1184
|
-
@debug_dev = debug_dev
|
1185
|
-
end
|
1186
|
-
|
1187
|
-
def close
|
1188
|
-
super
|
1189
|
-
debug("! CONNECTION CLOSED\n")
|
1190
|
-
end
|
1191
|
-
|
1192
|
-
def gets(*args)
|
1193
|
-
str = super
|
1194
|
-
debug(str)
|
1195
|
-
str
|
1196
|
-
end
|
1197
|
-
|
1198
|
-
def read(*args)
|
1199
|
-
str = super
|
1200
|
-
debug(str)
|
1201
|
-
str
|
1202
|
-
end
|
1203
|
-
|
1204
|
-
def <<(str)
|
1205
|
-
super
|
1206
|
-
debug(str)
|
1207
|
-
end
|
1208
|
-
|
1209
|
-
private
|
1210
|
-
|
1211
|
-
def debug(str)
|
1212
|
-
@debug_dev << str if @debug_dev
|
1213
|
-
end
|
1214
|
-
end
|
1215
|
-
|
1216
|
-
|
1217
|
-
# HTTPClient::LoopBackSocket -- dummy socket for dummy response
|
111
|
+
# 2. Getting peer certificate from response.
|
1218
112
|
#
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
def initialize(host, port, response)
|
1223
|
-
super(StringIO.new(response))
|
1224
|
-
@host = host
|
1225
|
-
@port = port
|
1226
|
-
end
|
1227
|
-
|
1228
|
-
def addr
|
1229
|
-
[nil, @port, @host, @host]
|
1230
|
-
end
|
1231
|
-
|
1232
|
-
def <<(str)
|
1233
|
-
# ignored
|
1234
|
-
end
|
1235
|
-
end
|
1236
|
-
|
1237
|
-
|
1238
|
-
# HTTPClient::Session -- manage http session with one site.
|
1239
|
-
# One or more TCP sessions with the site may be created.
|
1240
|
-
# Only 1 TCP session is live at the same time.
|
113
|
+
# res = clnt.get(https_url)
|
114
|
+
# p res.peer_cert #=> returns OpenSSL::X509::Certificate
|
1241
115
|
#
|
1242
|
-
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
@state = :META if @state == :WAIT
|
1334
|
-
@next_connection = nil
|
1335
|
-
@requests.push(req)
|
1336
|
-
end
|
1337
|
-
|
1338
|
-
def close
|
1339
|
-
if !@socket.nil? and !@socket.closed?
|
1340
|
-
@socket.flush rescue nil # try to rescue OpenSSL::SSL::SSLError: cf. #120
|
1341
|
-
@socket.close
|
1342
|
-
end
|
1343
|
-
@state = :INIT
|
1344
|
-
end
|
1345
|
-
|
1346
|
-
def closed?
|
1347
|
-
@state == :INIT
|
1348
|
-
end
|
116
|
+
# 3. Configuring OpenSSL options. See HTTPClient::SSLConfig for more details.
|
117
|
+
#
|
118
|
+
# user_cert_file = 'cert.pem'
|
119
|
+
# user_key_file = 'privkey.pem'
|
120
|
+
# clnt.ssl_config.set_client_cert_file(user_cert_file, user_key_file)
|
121
|
+
# clnt.get_content(https_url)
|
122
|
+
#
|
123
|
+
# === Handling Cookies
|
124
|
+
#
|
125
|
+
# 1. Using volatile Cookies. Nothing to do. HTTPClient handles Cookies.
|
126
|
+
#
|
127
|
+
# clnt = HTTPClient.new
|
128
|
+
# clnt.get_content(url1) # receives Cookies.
|
129
|
+
# clnt.get_content(url2) # sends Cookies if needed.
|
130
|
+
#
|
131
|
+
# 2. Saving non volatile Cookies to a specified file. Need to set a file at
|
132
|
+
# first and invoke save method at last.
|
133
|
+
#
|
134
|
+
# clnt = HTTPClient.new
|
135
|
+
# clnt.set_cookie_store('/home/nahi/cookie.dat')
|
136
|
+
# clnt.get_content(url)
|
137
|
+
# ...
|
138
|
+
# clnt.save_cookie_store
|
139
|
+
#
|
140
|
+
# 3. Disabling Cookies.
|
141
|
+
#
|
142
|
+
# clnt = HTTPClient.new
|
143
|
+
# clnt.cookie_manager = nil
|
144
|
+
#
|
145
|
+
# === Configuring authentication credentials
|
146
|
+
#
|
147
|
+
# 1. Authentication with Web server. Supports BasicAuth, DigestAuth, and
|
148
|
+
# Negotiate/NTLM (requires ruby/ntlm module).
|
149
|
+
#
|
150
|
+
# clnt = HTTPClient.new
|
151
|
+
# domain = 'http://dev.ctor.org/http-access2/'
|
152
|
+
# user = 'user'
|
153
|
+
# password = 'user'
|
154
|
+
# clnt.set_auth(domain, user, password)
|
155
|
+
# p clnt.get_content('http://dev.ctor.org/http-access2/login').status
|
156
|
+
#
|
157
|
+
# 2. Authentication with Proxy server. Supports BasicAuth and NTLM
|
158
|
+
# (requires win32/sspi)
|
159
|
+
#
|
160
|
+
# clnt = HTTPClient.new(proxy)
|
161
|
+
# user = 'proxy'
|
162
|
+
# password = 'proxy'
|
163
|
+
# clnt.set_proxy_auth(user, password)
|
164
|
+
# p clnt.get_content(url)
|
165
|
+
#
|
166
|
+
# === Invoking HTTP methods with custom header
|
167
|
+
#
|
168
|
+
# Pass a Hash or an Array for extheader argument.
|
169
|
+
#
|
170
|
+
# extheader = { 'Accept' => '*/*' }
|
171
|
+
# clnt.get_content(uri, query, extheader)
|
172
|
+
#
|
173
|
+
# extheader = [['Accept', 'image/jpeg'], ['Accept', 'image/png']]
|
174
|
+
# clnt.get_content(uri, query, extheader)
|
175
|
+
#
|
176
|
+
# === Invoking HTTP methods asynchronously
|
177
|
+
#
|
178
|
+
# See head_async, get_async, post_async, put_async, delete_async,
|
179
|
+
# options_async, propfind_async, proppatch_async, and trace_async.
|
180
|
+
# It immediately returns a HTTPClient::Connection instance as a returning value.
|
181
|
+
#
|
182
|
+
# connection = clnt.post_async(url, body)
|
183
|
+
# print 'posting.'
|
184
|
+
# while true
|
185
|
+
# break if connection.finished?
|
186
|
+
# print '.'
|
187
|
+
# sleep 1
|
188
|
+
# end
|
189
|
+
# puts '.'
|
190
|
+
# res = connection.pop
|
191
|
+
# p res.status
|
192
|
+
# p res.content.read # res.content is an IO for the res of async method.
|
193
|
+
#
|
194
|
+
# === Shortcut methods
|
195
|
+
#
|
196
|
+
# You can invoke get_content, get, etc. without creating HTTPClient instance.
|
197
|
+
#
|
198
|
+
# ruby -rhttpclient -e 'puts HTTPClient.get_content(ARGV.shift)' http://dev.ctor.org/
|
199
|
+
# ruby -rhttpclient -e 'p HTTPClient.head(ARGV.shift).header["last-modified"]' http://dev.ctor.org/
|
200
|
+
#
|
201
|
+
class HTTPClient
|
202
|
+
VERSION = '2.1.3'
|
203
|
+
RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
|
204
|
+
/: (\S+) (\S+)/ =~ %q$Id: httpclient.rb 256 2008-12-29 14:40:49Z nahi $
|
205
|
+
LIB_NAME = "(#{$1}/#{$2}, #{RUBY_VERSION_STRING})"
|
1349
206
|
|
1350
|
-
|
1351
|
-
version = status = reason = nil
|
1352
|
-
begin
|
1353
|
-
if @state != :META
|
1354
|
-
raise RuntimeError.new("get_status must be called at the beginning of a session.")
|
1355
|
-
end
|
1356
|
-
version, status, reason = read_header()
|
1357
|
-
rescue
|
1358
|
-
close
|
1359
|
-
raise
|
1360
|
-
end
|
1361
|
-
return version, status, reason
|
1362
|
-
end
|
207
|
+
include Util
|
1363
208
|
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
rescue
|
1368
|
-
close
|
1369
|
-
raise
|
1370
|
-
end
|
1371
|
-
if block
|
1372
|
-
@headers.each do |line|
|
1373
|
-
block.call(line)
|
1374
|
-
end
|
1375
|
-
else
|
1376
|
-
@headers
|
1377
|
-
end
|
209
|
+
# Raised for indicating running environment configuration error for example
|
210
|
+
# accessing via SSL under the ruby which is not compiled with OpenSSL.
|
211
|
+
class ConfigurationError < StandardError
|
1378
212
|
end
|
1379
213
|
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
false
|
1385
|
-
else
|
1386
|
-
@socket.closed? or @socket.eof?
|
1387
|
-
end
|
1388
|
-
end
|
214
|
+
# Raised for indicating HTTP response error.
|
215
|
+
class BadResponseError < RuntimeError
|
216
|
+
# HTTP::Message:: a response
|
217
|
+
attr_reader :res
|
1389
218
|
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
return nil if @state != :DATA
|
1394
|
-
unless @state == :DATA
|
1395
|
-
raise InvalidState.new('state != DATA')
|
1396
|
-
end
|
1397
|
-
data = nil
|
1398
|
-
if block
|
1399
|
-
while true
|
1400
|
-
begin
|
1401
|
-
timeout(@receive_timeout) do
|
1402
|
-
data = read_body()
|
1403
|
-
end
|
1404
|
-
rescue TimeoutError
|
1405
|
-
raise
|
1406
|
-
end
|
1407
|
-
block.call(data) if data
|
1408
|
-
break if eof?
|
1409
|
-
end
|
1410
|
-
data = nil # Calling with block returns nil.
|
1411
|
-
else
|
1412
|
-
begin
|
1413
|
-
timeout(@receive_timeout) do
|
1414
|
-
data = read_body()
|
1415
|
-
end
|
1416
|
-
rescue TimeoutError
|
1417
|
-
raise
|
1418
|
-
end
|
1419
|
-
end
|
1420
|
-
rescue
|
1421
|
-
close
|
1422
|
-
raise
|
1423
|
-
end
|
1424
|
-
if eof?
|
1425
|
-
if @next_connection
|
1426
|
-
@state = :WAIT
|
1427
|
-
else
|
1428
|
-
close
|
1429
|
-
end
|
219
|
+
def initialize(msg, res = nil) # :nodoc:
|
220
|
+
super(msg)
|
221
|
+
@res = res
|
1430
222
|
end
|
1431
|
-
data
|
1432
223
|
end
|
1433
224
|
|
1434
|
-
|
1435
|
-
|
1436
|
-
LibNames = "(#{RCS_FILE}/#{RCS_REVISION}, #{RUBY_VERSION_STRING})"
|
1437
|
-
|
1438
|
-
def set_header(req)
|
1439
|
-
req.version = @requested_version if @requested_version
|
1440
|
-
if @user_agent
|
1441
|
-
req.header.set('User-Agent', "#{@user_agent} #{LibNames}")
|
1442
|
-
end
|
1443
|
-
if @from
|
1444
|
-
req.header.set('From', @from)
|
1445
|
-
end
|
1446
|
-
req.header.set('Date', HTTP.http_date(Time.now))
|
225
|
+
# Raised for indicating a timeout error.
|
226
|
+
class TimeoutError < RuntimeError
|
1447
227
|
end
|
1448
228
|
|
1449
|
-
#
|
1450
|
-
|
1451
|
-
|
1452
|
-
begin
|
1453
|
-
retry_number = 0
|
1454
|
-
timeout(@connect_timeout) do
|
1455
|
-
@socket = create_socket(site)
|
1456
|
-
begin
|
1457
|
-
@src.host = @socket.addr[3]
|
1458
|
-
@src.port = @socket.addr[1]
|
1459
|
-
rescue SocketError
|
1460
|
-
# to avoid IPSocket#addr problem on Mac OS X 10.3 + ruby-1.8.1.
|
1461
|
-
# cf. [ruby-talk:84909], [ruby-talk:95827]
|
1462
|
-
end
|
1463
|
-
if @dest.scheme == 'https'
|
1464
|
-
@socket = create_ssl_socket(@socket)
|
1465
|
-
connect_ssl_proxy(@socket) if @proxy
|
1466
|
-
@socket.ssl_connect
|
1467
|
-
@socket.post_connection_check(@dest)
|
1468
|
-
@ssl_peer_cert = @socket.peer_cert
|
1469
|
-
end
|
1470
|
-
# Use Ruby internal buffering instead of passing data immediatly
|
1471
|
-
# to the underlying layer
|
1472
|
-
# => we need to to call explicitely flush on the socket
|
1473
|
-
@socket.sync = @socket_sync
|
1474
|
-
end
|
1475
|
-
rescue TimeoutError
|
1476
|
-
if @connect_retry == 0
|
1477
|
-
retry
|
1478
|
-
else
|
1479
|
-
retry_number += 1
|
1480
|
-
retry if retry_number < @connect_retry
|
1481
|
-
end
|
1482
|
-
close
|
1483
|
-
raise
|
1484
|
-
end
|
1485
|
-
|
1486
|
-
@state = :WAIT
|
1487
|
-
@readbuf = ''
|
229
|
+
# Raised for indicating a connection timeout error.
|
230
|
+
# You can configure connection timeout via HTTPClient#connect_timeout=.
|
231
|
+
class ConnectTimeoutError < TimeoutError
|
1488
232
|
end
|
1489
233
|
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1493
|
-
@debug_dev << "! CONNECT TO #{site.host}:#{site.port}\n" if @debug_dev
|
1494
|
-
if str = @test_loopback_http_response.shift
|
1495
|
-
socket = LoopBackSocket.new(site.host, site.port, str)
|
1496
|
-
else
|
1497
|
-
socket = TCPSocket.new(site.host, site.port)
|
1498
|
-
end
|
1499
|
-
if @debug_dev
|
1500
|
-
@debug_dev << "! CONNECTION ESTABLISHED\n"
|
1501
|
-
socket.extend(DebugSocket)
|
1502
|
-
socket.debug_dev = @debug_dev
|
1503
|
-
end
|
1504
|
-
rescue SystemCallError => e
|
1505
|
-
e.message << " (#{site})"
|
1506
|
-
raise
|
1507
|
-
rescue SocketError => e
|
1508
|
-
e.message << " (#{site})"
|
1509
|
-
raise
|
1510
|
-
end
|
1511
|
-
socket
|
234
|
+
# Raised for indicating a request sending timeout error.
|
235
|
+
# You can configure request sending timeout via HTTPClient#send_timeout=.
|
236
|
+
class SendTimeoutError < TimeoutError
|
1512
237
|
end
|
1513
238
|
|
1514
|
-
#
|
1515
|
-
|
1516
|
-
|
239
|
+
# Raised for indicating a response receiving timeout error.
|
240
|
+
# You can configure response receiving timeout via
|
241
|
+
# HTTPClient#receive_timeout=.
|
242
|
+
class ReceiveTimeoutError < TimeoutError
|
1517
243
|
end
|
1518
244
|
|
1519
|
-
|
1520
|
-
|
1521
|
-
|
1522
|
-
unless @status == 200
|
1523
|
-
raise BadResponse.new(
|
1524
|
-
"connect to ssl proxy failed with status #{@status} #{@reason}")
|
1525
|
-
end
|
245
|
+
# Deprecated. just for backward compatibility
|
246
|
+
class Session
|
247
|
+
BadResponse = ::HTTPClient::BadResponseError
|
1526
248
|
end
|
1527
249
|
|
1528
|
-
|
1529
|
-
|
1530
|
-
|
1531
|
-
|
1532
|
-
|
1533
|
-
end
|
1534
|
-
unless @state == :META
|
1535
|
-
raise InvalidState, 'state != :META'
|
1536
|
-
end
|
1537
|
-
parse_header(@socket)
|
1538
|
-
@content_length = nil
|
1539
|
-
@chunked = false
|
1540
|
-
@headers.each do |line|
|
1541
|
-
case line
|
1542
|
-
when /^Content-Length:\s+(\d+)/i
|
1543
|
-
@content_length = $1.to_i
|
1544
|
-
when /^Transfer-Encoding:\s+chunked/i
|
1545
|
-
@chunked = true
|
1546
|
-
@content_length = true # how?
|
1547
|
-
@chunk_length = 0
|
1548
|
-
when /^Connection:\s+([\-\w]+)/i, /^Proxy-Connection:\s+([\-\w]+)/i
|
1549
|
-
case $1
|
1550
|
-
when /^Keep-Alive$/i
|
1551
|
-
@next_connection = true
|
1552
|
-
when /^close$/i
|
1553
|
-
@next_connection = false
|
250
|
+
class << self
|
251
|
+
%w(get_content post_content head get post put delete options propfind proppatch trace).each do |name|
|
252
|
+
eval <<-EOD
|
253
|
+
def #{name}(*arg)
|
254
|
+
new.#{name}(*arg)
|
1554
255
|
end
|
1555
|
-
|
1556
|
-
# Nothing to parse.
|
1557
|
-
end
|
1558
|
-
end
|
1559
|
-
|
1560
|
-
# Head of the request has been parsed.
|
1561
|
-
@state = :DATA
|
1562
|
-
req = @requests.shift
|
1563
|
-
|
1564
|
-
if req.header.request_method == 'HEAD'
|
1565
|
-
@content_length = 0
|
1566
|
-
if @next_connection
|
1567
|
-
@state = :WAIT
|
1568
|
-
else
|
1569
|
-
close
|
1570
|
-
end
|
1571
|
-
end
|
1572
|
-
@next_connection = false unless @content_length
|
1573
|
-
return [@version, @status, @reason]
|
1574
|
-
end
|
1575
|
-
|
1576
|
-
StatusParseRegexp = %r(\AHTTP/(\d+\.\d+)\s+(\d\d\d)\s*([^\r\n]+)?\r?\n\z)
|
1577
|
-
def parse_header(socket)
|
1578
|
-
begin
|
1579
|
-
timeout(@receive_timeout) do
|
1580
|
-
begin
|
1581
|
-
initial_line = socket.gets("\n")
|
1582
|
-
if initial_line.nil?
|
1583
|
-
raise KeepAliveDisconnected.new
|
1584
|
-
end
|
1585
|
-
if StatusParseRegexp =~ initial_line
|
1586
|
-
@version, @status, @reason = $1, $2.to_i, $3
|
1587
|
-
@next_connection = HTTP.keep_alive_enabled?(@version)
|
1588
|
-
else
|
1589
|
-
@version = '0.9'
|
1590
|
-
@status = nil
|
1591
|
-
@reason = nil
|
1592
|
-
@next_connection = false
|
1593
|
-
@readbuf = initial_line
|
1594
|
-
break
|
1595
|
-
end
|
1596
|
-
@headers = []
|
1597
|
-
while true
|
1598
|
-
line = socket.gets("\n")
|
1599
|
-
unless line
|
1600
|
-
raise BadResponse.new('Unexpected EOF.')
|
1601
|
-
end
|
1602
|
-
line.sub!(/\r?\n\z/, '')
|
1603
|
-
break if line.empty?
|
1604
|
-
if line.sub!(/^\t/, '')
|
1605
|
-
@headers[-1] << line
|
1606
|
-
else
|
1607
|
-
@headers.push(line)
|
1608
|
-
end
|
1609
|
-
end
|
1610
|
-
end while (@version == '1.1' && @status == 100)
|
1611
|
-
end
|
1612
|
-
rescue TimeoutError
|
1613
|
-
raise
|
1614
|
-
end
|
1615
|
-
end
|
1616
|
-
|
1617
|
-
def read_body
|
1618
|
-
if @chunked
|
1619
|
-
return read_body_chunked()
|
1620
|
-
elsif @content_length == 0
|
1621
|
-
return nil
|
1622
|
-
elsif @content_length
|
1623
|
-
return read_body_length()
|
1624
|
-
else
|
1625
|
-
if @readbuf.length > 0
|
1626
|
-
data = @readbuf
|
1627
|
-
@readbuf = ''
|
1628
|
-
return data
|
1629
|
-
else
|
1630
|
-
data = @socket.read(@read_block_size)
|
1631
|
-
data = nil if data and data.empty? # Absorbing interface mismatch.
|
1632
|
-
return data
|
1633
|
-
end
|
256
|
+
EOD
|
1634
257
|
end
|
1635
|
-
end
|
1636
258
|
|
1637
|
-
|
1638
|
-
maxbytes = @read_block_size
|
1639
|
-
if @readbuf.length > 0
|
1640
|
-
data = @readbuf[0, @content_length]
|
1641
|
-
@readbuf[0, @content_length] = ''
|
1642
|
-
@content_length -= data.length
|
1643
|
-
return data
|
1644
|
-
end
|
1645
|
-
maxbytes = @content_length if maxbytes > @content_length
|
1646
|
-
data = @socket.read(maxbytes)
|
1647
|
-
if data
|
1648
|
-
@content_length -= data.length
|
1649
|
-
else
|
1650
|
-
@content_length = 0
|
1651
|
-
end
|
1652
|
-
return data
|
1653
|
-
end
|
259
|
+
private
|
1654
260
|
|
1655
|
-
|
1656
|
-
|
1657
|
-
|
1658
|
-
|
1659
|
-
|
1660
|
-
|
1661
|
-
|
1662
|
-
|
1663
|
-
|
1664
|
-
|
1665
|
-
|
1666
|
-
@socket.gets(RS)
|
1667
|
-
return nil
|
1668
|
-
end
|
1669
|
-
end
|
1670
|
-
while @readbuf.length < @chunk_length + 2
|
1671
|
-
@readbuf << @socket.read(@chunk_length + 2 - @readbuf.length)
|
1672
|
-
end
|
1673
|
-
data = @readbuf[0, @chunk_length]
|
1674
|
-
@readbuf[0, @chunk_length + 2] = ''
|
1675
|
-
@chunk_length = 0
|
1676
|
-
return data
|
1677
|
-
end
|
1678
|
-
|
1679
|
-
def check_state
|
1680
|
-
if @state == :DATA
|
1681
|
-
if eof?
|
1682
|
-
if @next_connection
|
1683
|
-
if @requests.empty?
|
1684
|
-
@state = :WAIT
|
1685
|
-
else
|
1686
|
-
@state = :META
|
1687
|
-
end
|
1688
|
-
end
|
261
|
+
def attr_proxy(symbol, assignable = false)
|
262
|
+
name = symbol.to_s
|
263
|
+
define_method(name) {
|
264
|
+
@session_manager.__send__(name)
|
265
|
+
}
|
266
|
+
if assignable
|
267
|
+
aname = name + '='
|
268
|
+
define_method(aname) { |rhs|
|
269
|
+
reset_all
|
270
|
+
@session_manager.__send__(aname, rhs)
|
271
|
+
}
|
1689
272
|
end
|
1690
273
|
end
|
1691
274
|
end
|
1692
|
-
end
|
1693
|
-
|
1694
|
-
include Util
|
1695
275
|
|
1696
|
-
|
1697
|
-
attr_reader :from
|
276
|
+
# HTTPClient::SSLConfig:: SSL configurator.
|
1698
277
|
attr_reader :ssl_config
|
278
|
+
# WebAgent::CookieManager:: Cookies configurator.
|
1699
279
|
attr_accessor :cookie_manager
|
280
|
+
# An array of response HTTP message body String which is used for loop-back
|
281
|
+
# test. See test/* to see how to use it. If you want to do loop-back test
|
282
|
+
# of HTTP header, use test_loopback_http_response instead.
|
1700
283
|
attr_reader :test_loopback_response
|
284
|
+
# An array of request filter which can trap HTTP request/response.
|
285
|
+
# See HTTPClient::WWWAuth to see how to use it.
|
1701
286
|
attr_reader :request_filter
|
287
|
+
# HTTPClient::ProxyAuth:: Proxy authentication handler.
|
1702
288
|
attr_reader :proxy_auth
|
289
|
+
# HTTPClient::WWWAuth:: WWW authentication handler.
|
1703
290
|
attr_reader :www_auth
|
1704
|
-
|
1705
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1712
|
-
|
1713
|
-
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1717
|
-
|
1718
|
-
#
|
1719
|
-
#
|
291
|
+
# How many times get_content and post_content follows HTTP redirect.
|
292
|
+
# 10 by default.
|
293
|
+
attr_accessor :follow_redirect_count
|
294
|
+
|
295
|
+
# Set HTTP version as a String:: 'HTTP/1.0' or 'HTTP/1.1'
|
296
|
+
attr_proxy(:protocol_version, true)
|
297
|
+
# Connect timeout in sec.
|
298
|
+
attr_proxy(:connect_timeout, true)
|
299
|
+
# Request sending timeout in sec.
|
300
|
+
attr_proxy(:send_timeout, true)
|
301
|
+
# Response receiving timeout in sec.
|
302
|
+
attr_proxy(:receive_timeout, true)
|
303
|
+
# Negotiation retry count for authentication. 5 by default.
|
304
|
+
attr_proxy(:protocol_retry_count, true)
|
305
|
+
# if your ruby is older than 2005-09-06, do not set socket_sync = false to
|
306
|
+
# avoid an SSL socket blocking bug in openssl/buffering.rb.
|
307
|
+
attr_proxy(:socket_sync, true)
|
308
|
+
# User-Agent header in HTTP request.
|
309
|
+
attr_proxy(:agent_name, true)
|
310
|
+
# From header in HTTP request.
|
311
|
+
attr_proxy(:from, true)
|
312
|
+
# An array of response HTTP String (not a HTTP message body) which is used
|
313
|
+
# for loopback test. See test/* to see how to use it.
|
314
|
+
attr_proxy(:test_loopback_http_response)
|
315
|
+
|
316
|
+
# Default extheader for PROPFIND request.
|
317
|
+
PROPFIND_DEFAULT_EXTHEADER = { 'Depth' => '0' }
|
318
|
+
|
319
|
+
# Creates a HTTPClient instance which manages sessions, cookies, etc.
|
320
|
+
#
|
321
|
+
# HTTPClient.new takes 3 optional arguments for proxy url string,
|
322
|
+
# User-Agent String and From header String. User-Agent and From are embedded
|
323
|
+
# in HTTP request Header if given. No User-Agent and From header added
|
324
|
+
# without setting it explicitly.
|
1720
325
|
#
|
1721
|
-
#
|
1722
|
-
#
|
1723
|
-
#
|
1724
|
-
#
|
326
|
+
# proxy = 'http://myproxy:8080'
|
327
|
+
# agent_name = 'MyAgent/0.1'
|
328
|
+
# from = 'from@example.com'
|
329
|
+
# HTTPClient.new(proxy, agent_name, from)
|
1725
330
|
#
|
1726
|
-
#
|
1727
|
-
#
|
1728
|
-
# SSLConfig cannot be re-initialized. Create new client.
|
331
|
+
# You can use a keyword argument style Hash. Keys are :proxy, :agent_name
|
332
|
+
# and :from.
|
1729
333
|
#
|
1730
|
-
|
334
|
+
# HTTPClient.new(:agent_name = 'MyAgent/0.1')
|
335
|
+
def initialize(*args)
|
336
|
+
proxy, agent_name, from = keyword_argument(args, :proxy, :agent_name, :from)
|
1731
337
|
@proxy = nil # assigned later.
|
1732
338
|
@no_proxy = nil
|
1733
|
-
@agent_name = agent_name
|
1734
|
-
@from = from
|
1735
339
|
@www_auth = WWWAuth.new
|
1736
340
|
@proxy_auth = ProxyAuth.new
|
1737
341
|
@request_filter = [@proxy_auth, @www_auth]
|
1738
342
|
@debug_dev = nil
|
1739
343
|
@redirect_uri_callback = method(:default_redirect_uri_callback)
|
1740
344
|
@test_loopback_response = []
|
1741
|
-
@session_manager = SessionManager.new
|
1742
|
-
@session_manager.agent_name =
|
1743
|
-
@session_manager.from =
|
345
|
+
@session_manager = SessionManager.new(self)
|
346
|
+
@session_manager.agent_name = agent_name
|
347
|
+
@session_manager.from = from
|
1744
348
|
@session_manager.ssl_config = @ssl_config = SSLConfig.new(self)
|
1745
349
|
@cookie_manager = WebAgent::CookieManager.new
|
350
|
+
@follow_redirect_count = 10
|
1746
351
|
load_environment
|
1747
352
|
self.proxy = proxy if proxy
|
1748
353
|
end
|
1749
354
|
|
355
|
+
# Returns debug device if exists. See debug_dev=.
|
1750
356
|
def debug_dev
|
1751
357
|
@debug_dev
|
1752
358
|
end
|
1753
359
|
|
360
|
+
# Sets debug device. Once debug device is set, all HTTP requests and
|
361
|
+
# responses are dumped to given device. dev must respond to << for dump.
|
362
|
+
#
|
363
|
+
# Calling this method resets all existing sessions.
|
1754
364
|
def debug_dev=(dev)
|
1755
365
|
@debug_dev = dev
|
1756
366
|
reset_all
|
1757
367
|
@session_manager.debug_dev = dev
|
1758
368
|
end
|
1759
369
|
|
1760
|
-
|
1761
|
-
@session_manager.protocol_version
|
1762
|
-
end
|
1763
|
-
|
1764
|
-
def protocol_version=(protocol_version)
|
1765
|
-
reset_all
|
1766
|
-
@session_manager.protocol_version = protocol_version
|
1767
|
-
end
|
1768
|
-
|
1769
|
-
def connect_timeout
|
1770
|
-
@session_manager.connect_timeout
|
1771
|
-
end
|
1772
|
-
|
1773
|
-
def connect_timeout=(connect_timeout)
|
1774
|
-
reset_all
|
1775
|
-
@session_manager.connect_timeout = connect_timeout
|
1776
|
-
end
|
1777
|
-
|
1778
|
-
def send_timeout
|
1779
|
-
@session_manager.send_timeout
|
1780
|
-
end
|
1781
|
-
|
1782
|
-
def send_timeout=(send_timeout)
|
1783
|
-
reset_all
|
1784
|
-
@session_manager.send_timeout = send_timeout
|
1785
|
-
end
|
1786
|
-
|
1787
|
-
def receive_timeout
|
1788
|
-
@session_manager.receive_timeout
|
1789
|
-
end
|
1790
|
-
|
1791
|
-
def receive_timeout=(receive_timeout)
|
1792
|
-
reset_all
|
1793
|
-
@session_manager.receive_timeout = receive_timeout
|
1794
|
-
end
|
1795
|
-
|
370
|
+
# Returns URI object of HTTP proxy if exists.
|
1796
371
|
def proxy
|
1797
372
|
@proxy
|
1798
373
|
end
|
1799
374
|
|
375
|
+
# Sets HTTP proxy used for HTTP connection. Given proxy can be an URI,
|
376
|
+
# a String or nil. You can set user/password for proxy authentication like
|
377
|
+
# HTTPClient#proxy = 'http://user:passwd@myproxy:8080'
|
378
|
+
#
|
379
|
+
# You can use environment variable 'http_proxy' or 'HTTP_PROXY' for it.
|
380
|
+
# You need to use 'cgi_http_proxy' or 'CGI_HTTP_PROXY' instead if you run
|
381
|
+
# HTTPClient from CGI environment from security reason. (HTTPClient checks
|
382
|
+
# 'REQUEST_METHOD' environment variable whether it's CGI or not)
|
383
|
+
#
|
384
|
+
# Calling this method resets all existing sessions.
|
1800
385
|
def proxy=(proxy)
|
1801
386
|
if proxy.nil?
|
1802
387
|
@proxy = nil
|
@@ -1805,7 +390,7 @@ end
|
|
1805
390
|
@proxy = urify(proxy)
|
1806
391
|
if @proxy.scheme == nil or @proxy.scheme.downcase != 'http' or
|
1807
392
|
@proxy.host == nil or @proxy.port == nil
|
1808
|
-
raise ArgumentError.new("unsupported proxy
|
393
|
+
raise ArgumentError.new("unsupported proxy #{proxy}")
|
1809
394
|
end
|
1810
395
|
@proxy_auth.reset_challenge
|
1811
396
|
if @proxy.user || @proxy.password
|
@@ -1813,225 +398,398 @@ end
|
|
1813
398
|
end
|
1814
399
|
end
|
1815
400
|
reset_all
|
401
|
+
@session_manager.proxy = @proxy
|
1816
402
|
@proxy
|
1817
403
|
end
|
1818
404
|
|
405
|
+
# Returns NO_PROXY setting String if given.
|
1819
406
|
def no_proxy
|
1820
407
|
@no_proxy
|
1821
408
|
end
|
1822
409
|
|
410
|
+
# Sets NO_PROXY setting String. no_proxy must be a comma separated String.
|
411
|
+
# Each entry must be 'host' or 'host:port' such as;
|
412
|
+
# HTTPClient#no_proxy = 'example.com,example.co.jp:443'
|
413
|
+
#
|
414
|
+
# 'localhost' is treated as a no_proxy site regardless of explicitly listed.
|
415
|
+
# HTTPClient checks given URI objects before accessing it.
|
416
|
+
# 'host' is tail string match. No IP-addr conversion.
|
417
|
+
#
|
418
|
+
# You can use environment variable 'no_proxy' or 'NO_PROXY' for it.
|
419
|
+
#
|
420
|
+
# Calling this method resets all existing sessions.
|
1823
421
|
def no_proxy=(no_proxy)
|
1824
422
|
@no_proxy = no_proxy
|
1825
423
|
reset_all
|
1826
424
|
end
|
1827
425
|
|
1828
|
-
#
|
1829
|
-
#
|
1830
|
-
|
1831
|
-
|
1832
|
-
|
1833
|
-
|
1834
|
-
|
1835
|
-
|
426
|
+
# Sets credential for Web server authentication.
|
427
|
+
# domain:: a String or an URI to specify where HTTPClient should use this
|
428
|
+
# credential. If you set uri to nil, HTTPClient uses this credential
|
429
|
+
# wherever a server requires it.
|
430
|
+
# user:: username String.
|
431
|
+
# passwd:: password String.
|
432
|
+
#
|
433
|
+
# You can set multiple credentials for each uri.
|
434
|
+
#
|
435
|
+
# clnt.set_auth('http://www.example.com/foo/', 'foo_user', 'passwd')
|
436
|
+
# clnt.set_auth('http://www.example.com/bar/', 'bar_user', 'passwd')
|
437
|
+
#
|
438
|
+
# Calling this method resets all existing sessions.
|
439
|
+
def set_auth(domain, user, passwd)
|
440
|
+
uri = urify(domain)
|
1836
441
|
@www_auth.set_auth(uri, user, passwd)
|
1837
442
|
reset_all
|
1838
443
|
end
|
1839
444
|
|
1840
|
-
#
|
1841
|
-
def set_basic_auth(
|
1842
|
-
uri = urify(
|
445
|
+
# Deprecated. Use set_auth instead.
|
446
|
+
def set_basic_auth(domain, user, passwd)
|
447
|
+
uri = urify(domain)
|
1843
448
|
@www_auth.basic_auth.set(uri, user, passwd)
|
1844
449
|
reset_all
|
1845
450
|
end
|
1846
451
|
|
452
|
+
# Sets credential for Proxy authentication.
|
453
|
+
# user:: username String.
|
454
|
+
# passwd:: password String.
|
455
|
+
#
|
456
|
+
# Calling this method resets all existing sessions.
|
1847
457
|
def set_proxy_auth(user, passwd)
|
1848
|
-
uri = urify(uri)
|
1849
458
|
@proxy_auth.set_auth(user, passwd)
|
1850
459
|
reset_all
|
1851
460
|
end
|
1852
461
|
|
462
|
+
# Sets the filename where non-volatile Cookies be saved by calling
|
463
|
+
# save_cookie_store.
|
464
|
+
# This method tries to load and managing Cookies from the specified file.
|
465
|
+
#
|
466
|
+
# Calling this method resets all existing sessions.
|
1853
467
|
def set_cookie_store(filename)
|
1854
|
-
if @cookie_manager.cookies_file
|
1855
|
-
raise RuntimeError.new("overriding cookie file location")
|
1856
|
-
end
|
1857
468
|
@cookie_manager.cookies_file = filename
|
1858
469
|
@cookie_manager.load_cookies if filename
|
470
|
+
reset_all
|
1859
471
|
end
|
1860
472
|
|
473
|
+
# Try to save Cookies to the file specified in set_cookie_store. Unexpected
|
474
|
+
# error will be raised if you don't call set_cookie_store first.
|
475
|
+
# (interface mismatch between WebAgent::CookieManager implementation)
|
1861
476
|
def save_cookie_store
|
1862
477
|
@cookie_manager.save_cookies
|
1863
478
|
end
|
1864
479
|
|
480
|
+
# Sets callback proc when HTTP redirect status is returned for get_content
|
481
|
+
# and post_content. default_redirect_uri_callback is used by default.
|
482
|
+
#
|
483
|
+
# If you need strict implementation which does not allow relative URI
|
484
|
+
# redirection, set strict_redirect_uri_callback instead.
|
485
|
+
#
|
486
|
+
# clnt.redirect_uri_callback = clnt.method(:strict_redirect_uri_callback)
|
487
|
+
#
|
1865
488
|
def redirect_uri_callback=(redirect_uri_callback)
|
1866
489
|
@redirect_uri_callback = redirect_uri_callback
|
1867
490
|
end
|
1868
491
|
|
1869
|
-
|
1870
|
-
@session_manager.test_loopback_http_response
|
1871
|
-
end
|
1872
|
-
|
1873
|
-
# SYNOPSIS
|
1874
|
-
# Client#get_content(uri, query = nil, extheader = {}, &block = nil)
|
492
|
+
# Retrieves a web resource.
|
1875
493
|
#
|
1876
|
-
#
|
1877
|
-
#
|
1878
|
-
#
|
1879
|
-
#
|
1880
|
-
#
|
1881
|
-
#
|
1882
|
-
#
|
1883
|
-
#
|
1884
|
-
#
|
494
|
+
# uri:: a String or an URI object which represents an URL of web resource.
|
495
|
+
# query:: a Hash or an Array of query part of URL.
|
496
|
+
# e.g. { "a" => "b" } => 'http://host/part?a=b'.
|
497
|
+
# Give an array to pass multiple value like
|
498
|
+
# [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'.
|
499
|
+
# extheader:: a Hash or an Array of extra headers. e.g.
|
500
|
+
# { 'Accept' => '*/*' } or
|
501
|
+
# [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
|
502
|
+
# &block:: Give a block to get chunked message-body of response like
|
503
|
+
# get_content(uri) { |chunked_body| ... }.
|
504
|
+
# Size of each chunk may not be the same.
|
1885
505
|
#
|
1886
|
-
#
|
1887
|
-
#
|
506
|
+
# get_content follows HTTP redirect status (see HTTP::Status.redirect?)
|
507
|
+
# internally and try to retrieve content from redirected URL. See
|
508
|
+
# redirect_uri_callback= how HTTP redirection is handled.
|
1888
509
|
#
|
510
|
+
# If you need to get full HTTP response including HTTP status and headers,
|
511
|
+
# use get method. get returns HTTP::Message as a response and you need to
|
512
|
+
# follow HTTP redirect by yourself if you need.
|
1889
513
|
def get_content(uri, query = nil, extheader = {}, &block)
|
1890
|
-
|
1891
|
-
|
1892
|
-
|
514
|
+
uri = urify(uri)
|
515
|
+
req = create_request('GET', uri, query, nil, extheader)
|
516
|
+
follow_redirect(req, &block).content
|
1893
517
|
end
|
1894
518
|
|
519
|
+
# Posts a content.
|
520
|
+
#
|
521
|
+
# uri:: a String or an URI object which represents an URL of web resource.
|
522
|
+
# body:: a Hash or an Array of body part.
|
523
|
+
# e.g. { "a" => "b" } => 'a=b'.
|
524
|
+
# Give an array to pass multiple value like
|
525
|
+
# [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
|
526
|
+
# When you pass a File as a value, it will be posted as a
|
527
|
+
# multipart/form-data. e.g. { 'upload' => file }
|
528
|
+
# extheader:: a Hash or an Array of extra headers. e.g.
|
529
|
+
# { 'Accept' => '*/*' } or
|
530
|
+
# [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
|
531
|
+
# &block:: Give a block to get chunked message-body of response like
|
532
|
+
# post_content(uri) { |chunked_body| ... }.
|
533
|
+
# Size of each chunk may not be the same.
|
534
|
+
#
|
535
|
+
# post_content follows HTTP redirect status (see HTTP::Status.redirect?)
|
536
|
+
# internally and try to post the content to redirected URL. See
|
537
|
+
# redirect_uri_callback= how HTTP redirection is handled.
|
538
|
+
#
|
539
|
+
# If you need to get full HTTP response including HTTP status and headers,
|
540
|
+
# use post method.
|
1895
541
|
def post_content(uri, body = nil, extheader = {}, &block)
|
1896
|
-
|
1897
|
-
|
1898
|
-
|
542
|
+
uri = urify(uri)
|
543
|
+
req = create_request('POST', uri, nil, body, extheader)
|
544
|
+
follow_redirect(req, &block).content
|
1899
545
|
end
|
1900
546
|
|
547
|
+
# A method for redirect uri callback. How to use:
|
548
|
+
# clnt.redirect_uri_callback = clnt.method(:strict_redirect_uri_callback)
|
549
|
+
# This callback does not allow relative redirect such as
|
550
|
+
# Location: ../foo/
|
551
|
+
# in HTTP header. (raises BadResponseError instead)
|
1901
552
|
def strict_redirect_uri_callback(uri, res)
|
1902
553
|
newuri = URI.parse(res.header['location'][0])
|
1903
|
-
|
554
|
+
unless newuri.is_a?(URI::HTTP)
|
555
|
+
raise BadResponseError.new("unexpected location: #{newuri}", res)
|
556
|
+
end
|
557
|
+
puts "redirect to: #{newuri}" if $DEBUG
|
1904
558
|
newuri
|
1905
559
|
end
|
1906
560
|
|
561
|
+
# A default method for redirect uri callback. This method is used by
|
562
|
+
# HTTPClient instance by default.
|
563
|
+
# This callback allows relative redirect such as
|
564
|
+
# Location: ../foo/
|
565
|
+
# in HTTP header.
|
1907
566
|
def default_redirect_uri_callback(uri, res)
|
1908
567
|
newuri = URI.parse(res.header['location'][0])
|
1909
568
|
unless newuri.is_a?(URI::HTTP)
|
1910
569
|
newuri = uri + newuri
|
1911
|
-
STDERR.puts(
|
1912
|
-
|
1913
|
-
STDERR.puts(
|
1914
|
-
"'The field value consists of a single absolute URI' in HTTP spec")
|
570
|
+
STDERR.puts("could be a relative URI in location header which is not recommended")
|
571
|
+
STDERR.puts("'The field value consists of a single absolute URI' in HTTP spec")
|
1915
572
|
end
|
1916
|
-
puts "
|
573
|
+
puts "redirect to: #{newuri}" if $DEBUG
|
1917
574
|
newuri
|
1918
575
|
end
|
1919
576
|
|
577
|
+
# Sends HEAD request to the specified URL. See request for arguments.
|
1920
578
|
def head(uri, query = nil, extheader = {})
|
1921
|
-
request(
|
579
|
+
request(:head, uri, query, nil, extheader)
|
1922
580
|
end
|
1923
581
|
|
582
|
+
# Sends GET request to the specified URL. See request for arguments.
|
1924
583
|
def get(uri, query = nil, extheader = {}, &block)
|
1925
|
-
request(
|
584
|
+
request(:get, uri, query, nil, extheader, &block)
|
1926
585
|
end
|
1927
586
|
|
587
|
+
# Sends POST request to the specified URL. See request for arguments.
|
1928
588
|
def post(uri, body = nil, extheader = {}, &block)
|
1929
|
-
request(
|
589
|
+
request(:post, uri, nil, body, extheader, &block)
|
1930
590
|
end
|
1931
591
|
|
592
|
+
# Sends PUT request to the specified URL. See request for arguments.
|
1932
593
|
def put(uri, body = nil, extheader = {}, &block)
|
1933
|
-
request(
|
594
|
+
request(:put, uri, nil, body, extheader, &block)
|
1934
595
|
end
|
1935
596
|
|
597
|
+
# Sends DELETE request to the specified URL. See request for arguments.
|
1936
598
|
def delete(uri, extheader = {}, &block)
|
1937
|
-
request(
|
599
|
+
request(:delete, uri, nil, nil, extheader, &block)
|
1938
600
|
end
|
1939
601
|
|
602
|
+
# Sends OPTIONS request to the specified URL. See request for arguments.
|
1940
603
|
def options(uri, extheader = {}, &block)
|
1941
|
-
request(
|
604
|
+
request(:options, uri, nil, nil, extheader, &block)
|
1942
605
|
end
|
1943
606
|
|
607
|
+
# Sends PROPFIND request to the specified URL. See request for arguments.
|
608
|
+
def propfind(uri, extheader = PROPFIND_DEFAULT_EXTHEADER, &block)
|
609
|
+
request(:propfind, uri, nil, nil, extheader, &block)
|
610
|
+
end
|
611
|
+
|
612
|
+
# Sends PROPPATCH request to the specified URL. See request for arguments.
|
613
|
+
def proppatch(uri, body = nil, extheader = {}, &block)
|
614
|
+
request(:proppatch, uri, nil, body, extheader, &block)
|
615
|
+
end
|
616
|
+
|
617
|
+
# Sends TRACE request to the specified URL. See request for arguments.
|
1944
618
|
def trace(uri, query = nil, body = nil, extheader = {}, &block)
|
1945
619
|
request('TRACE', uri, query, body, extheader, &block)
|
1946
620
|
end
|
1947
621
|
|
622
|
+
# Sends a request to the specified URL.
|
623
|
+
#
|
624
|
+
# method:: HTTP method to be sent. method.to_s.upcase is used.
|
625
|
+
# uri:: a String or an URI object which represents an URL of web resource.
|
626
|
+
# query:: a Hash or an Array of query part of URL.
|
627
|
+
# e.g. { "a" => "b" } => 'http://host/part?a=b'
|
628
|
+
# Give an array to pass multiple value like
|
629
|
+
# [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'
|
630
|
+
# body:: a Hash or an Array of body part.
|
631
|
+
# e.g. { "a" => "b" } => 'a=b'.
|
632
|
+
# Give an array to pass multiple value like
|
633
|
+
# [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
|
634
|
+
# When the given method is 'POST' and the given body contains a file
|
635
|
+
# as a value, it will be posted as a multipart/form-data.
|
636
|
+
# e.g. { 'upload' => file }
|
637
|
+
# See HTTP::Message.file? for actual condition of 'a file'.
|
638
|
+
# extheader:: a Hash or an Array of extra headers. e.g.
|
639
|
+
# { 'Accept' => '*/*' } or
|
640
|
+
# [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
|
641
|
+
# &block:: Give a block to get chunked message-body of response like
|
642
|
+
# get(uri) { |chunked_body| ... }.
|
643
|
+
# Size of each chunk may not be the same.
|
644
|
+
#
|
645
|
+
# You can also pass a String as a body. HTTPClient just sends a String as
|
646
|
+
# a HTTP request message body.
|
647
|
+
#
|
648
|
+
# When you pass an IO as a body, HTTPClient sends it as a HTTP request with
|
649
|
+
# chunked encoding (Transfer-Encoding: chunked in HTTP header). Bear in mind
|
650
|
+
# that some server application does not support chunked request. At least
|
651
|
+
# cgi.rb does not support it.
|
1948
652
|
def request(method, uri, query = nil, body = nil, extheader = {}, &block)
|
1949
653
|
uri = urify(uri)
|
1950
|
-
|
1951
|
-
|
1952
|
-
|
1953
|
-
|
1954
|
-
|
1955
|
-
|
1956
|
-
do_get_block(req, proxy, conn, &block)
|
1957
|
-
end
|
1958
|
-
res = conn.pop
|
1959
|
-
break
|
1960
|
-
rescue RetryableResponse
|
1961
|
-
res = conn.pop
|
1962
|
-
retry_count -= 1
|
1963
|
-
end
|
654
|
+
proxy = no_proxy?(uri) ? nil : @proxy
|
655
|
+
req = create_request(method.to_s.upcase, uri, query, body, extheader)
|
656
|
+
if block
|
657
|
+
filtered_block = proc { |res, str|
|
658
|
+
block.call(str)
|
659
|
+
}
|
1964
660
|
end
|
1965
|
-
|
661
|
+
do_request(req, proxy, &filtered_block)
|
1966
662
|
end
|
1967
663
|
|
1968
|
-
#
|
1969
|
-
|
664
|
+
# Sends HEAD request in async style. See request_async for arguments.
|
665
|
+
# It immediately returns a HTTPClient::Connection instance as a result.
|
1970
666
|
def head_async(uri, query = nil, extheader = {})
|
1971
|
-
request_async(
|
667
|
+
request_async(:head, uri, query, nil, extheader)
|
1972
668
|
end
|
1973
669
|
|
670
|
+
# Sends GET request in async style. See request_async for arguments.
|
671
|
+
# It immediately returns a HTTPClient::Connection instance as a result.
|
1974
672
|
def get_async(uri, query = nil, extheader = {})
|
1975
|
-
request_async(
|
673
|
+
request_async(:get, uri, query, nil, extheader)
|
1976
674
|
end
|
1977
675
|
|
676
|
+
# Sends POST request in async style. See request_async for arguments.
|
677
|
+
# It immediately returns a HTTPClient::Connection instance as a result.
|
1978
678
|
def post_async(uri, body = nil, extheader = {})
|
1979
|
-
request_async(
|
679
|
+
request_async(:post, uri, nil, body, extheader)
|
1980
680
|
end
|
1981
681
|
|
682
|
+
# Sends PUT request in async style. See request_async for arguments.
|
683
|
+
# It immediately returns a HTTPClient::Connection instance as a result.
|
1982
684
|
def put_async(uri, body = nil, extheader = {})
|
1983
|
-
request_async(
|
685
|
+
request_async(:put, uri, nil, body, extheader)
|
1984
686
|
end
|
1985
687
|
|
688
|
+
# Sends DELETE request in async style. See request_async for arguments.
|
689
|
+
# It immediately returns a HTTPClient::Connection instance as a result.
|
1986
690
|
def delete_async(uri, extheader = {})
|
1987
|
-
request_async(
|
691
|
+
request_async(:delete, uri, nil, nil, extheader)
|
1988
692
|
end
|
1989
693
|
|
694
|
+
# Sends OPTIONS request in async style. See request_async for arguments.
|
695
|
+
# It immediately returns a HTTPClient::Connection instance as a result.
|
1990
696
|
def options_async(uri, extheader = {})
|
1991
|
-
request_async(
|
697
|
+
request_async(:options, uri, nil, nil, extheader)
|
1992
698
|
end
|
1993
699
|
|
700
|
+
# Sends PROPFIND request in async style. See request_async for arguments.
|
701
|
+
# It immediately returns a HTTPClient::Connection instance as a result.
|
702
|
+
def propfind_async(uri, extheader = PROPFIND_DEFAULT_EXTHEADER)
|
703
|
+
request_async(:propfind, uri, nil, nil, extheader)
|
704
|
+
end
|
705
|
+
|
706
|
+
# Sends PROPPATCH request in async style. See request_async for arguments.
|
707
|
+
# It immediately returns a HTTPClient::Connection instance as a result.
|
708
|
+
def proppatch_async(uri, body = nil, extheader = {})
|
709
|
+
request_async(:proppatch, uri, nil, body, extheader)
|
710
|
+
end
|
711
|
+
|
712
|
+
# Sends TRACE request in async style. See request_async for arguments.
|
713
|
+
# It immediately returns a HTTPClient::Connection instance as a result.
|
1994
714
|
def trace_async(uri, query = nil, body = nil, extheader = {})
|
1995
|
-
request_async(
|
715
|
+
request_async(:trace, uri, query, body, extheader)
|
1996
716
|
end
|
1997
717
|
|
718
|
+
# Sends a request in async style. request method creates new Thread for
|
719
|
+
# HTTP connection and returns a HTTPClient::Connection instance immediately.
|
720
|
+
#
|
721
|
+
# Arguments definition is the same as request.
|
1998
722
|
def request_async(method, uri, query = nil, body = nil, extheader = {})
|
1999
723
|
uri = urify(uri)
|
2000
|
-
|
2001
|
-
|
2002
|
-
|
2003
|
-
do_get_stream(req, proxy, tconn)
|
2004
|
-
end
|
2005
|
-
}
|
2006
|
-
conn.async_thread = t
|
2007
|
-
conn
|
724
|
+
proxy = no_proxy?(uri) ? nil : @proxy
|
725
|
+
req = create_request(method.to_s.upcase, uri, query, body, extheader)
|
726
|
+
do_request_async(req, proxy)
|
2008
727
|
end
|
2009
728
|
|
2010
|
-
|
2011
|
-
#
|
2012
|
-
|
2013
|
-
# ???
|
2014
|
-
|
2015
|
-
##
|
2016
|
-
# Management interface.
|
2017
|
-
|
729
|
+
# Resets internal session for the given URL. Keep-alive connection for the
|
730
|
+
# site (host-port pair) is disconnected if exists.
|
2018
731
|
def reset(uri)
|
2019
732
|
uri = urify(uri)
|
2020
733
|
@session_manager.reset(uri)
|
2021
734
|
end
|
2022
735
|
|
736
|
+
# Resets all of internal sessions. Keep-alive connections are disconnected.
|
2023
737
|
def reset_all
|
2024
738
|
@session_manager.reset_all
|
2025
739
|
end
|
2026
740
|
|
2027
741
|
private
|
2028
742
|
|
743
|
+
class RetryableResponse < StandardError # :nodoc:
|
744
|
+
end
|
745
|
+
|
746
|
+
class KeepAliveDisconnected < StandardError # :nodoc:
|
747
|
+
end
|
748
|
+
|
749
|
+
def do_request(req, proxy, &block)
|
750
|
+
conn = Connection.new
|
751
|
+
res = nil
|
752
|
+
retry_count = @session_manager.protocol_retry_count
|
753
|
+
while retry_count > 0
|
754
|
+
begin
|
755
|
+
protect_keep_alive_disconnected do
|
756
|
+
do_get_block(req, proxy, conn, &block)
|
757
|
+
end
|
758
|
+
res = conn.pop
|
759
|
+
break
|
760
|
+
rescue RetryableResponse
|
761
|
+
res = conn.pop
|
762
|
+
retry_count -= 1
|
763
|
+
end
|
764
|
+
end
|
765
|
+
res
|
766
|
+
end
|
767
|
+
|
768
|
+
def do_request_async(req, proxy)
|
769
|
+
conn = Connection.new
|
770
|
+
t = Thread.new(conn) { |tconn|
|
771
|
+
retry_count = @session_manager.protocol_retry_count
|
772
|
+
while retry_count > 0
|
773
|
+
begin
|
774
|
+
protect_keep_alive_disconnected do
|
775
|
+
do_get_stream(req, proxy, tconn)
|
776
|
+
end
|
777
|
+
break
|
778
|
+
rescue RetryableResponse
|
779
|
+
retry_count -= 1
|
780
|
+
end
|
781
|
+
end
|
782
|
+
}
|
783
|
+
conn.async_thread = t
|
784
|
+
conn
|
785
|
+
end
|
786
|
+
|
2029
787
|
def load_environment
|
2030
788
|
# http_proxy
|
2031
789
|
if getenv('REQUEST_METHOD')
|
2032
790
|
# HTTP_PROXY conflicts with the environment variable usage in CGI where
|
2033
791
|
# HTTP_* is used for HTTP header information. Unlike open-uri, we
|
2034
|
-
#
|
792
|
+
# simply ignore http_proxy in CGI env and use cgi_http_proxy instead.
|
2035
793
|
self.proxy = getenv('cgi_http_proxy')
|
2036
794
|
else
|
2037
795
|
self.proxy = getenv('http_proxy')
|
@@ -2044,58 +802,99 @@ private
|
|
2044
802
|
ENV[name.downcase] || ENV[name.upcase]
|
2045
803
|
end
|
2046
804
|
|
2047
|
-
def follow_redirect(
|
805
|
+
def follow_redirect(req, &block)
|
2048
806
|
retry_number = 0
|
2049
|
-
|
2050
|
-
|
807
|
+
if block
|
808
|
+
filtered_block = proc { |r, str|
|
809
|
+
block.call(str) if HTTP::Status.successful?(r.status)
|
810
|
+
}
|
811
|
+
end
|
812
|
+
while retry_number < @follow_redirect_count
|
813
|
+
proxy = no_proxy?(req.header.request_uri) ? nil : @proxy
|
814
|
+
res = do_request(req, proxy, &filtered_block)
|
2051
815
|
if HTTP::Status.successful?(res.status)
|
2052
816
|
return res
|
2053
817
|
elsif HTTP::Status.redirect?(res.status)
|
2054
|
-
uri = @redirect_uri_callback.call(
|
2055
|
-
|
818
|
+
uri = urify(@redirect_uri_callback.call(req.header.request_uri, res))
|
819
|
+
req.header.request_uri = uri
|
2056
820
|
retry_number += 1
|
2057
821
|
else
|
2058
|
-
raise
|
822
|
+
raise BadResponseError.new("unexpected response: #{res.header.inspect}", res)
|
2059
823
|
end
|
2060
824
|
end
|
2061
|
-
raise
|
825
|
+
raise BadResponseError.new("retry count exceeded", res)
|
2062
826
|
end
|
2063
827
|
|
2064
|
-
def
|
2065
|
-
proxy = no_proxy?(uri) ? nil : @proxy
|
828
|
+
def protect_keep_alive_disconnected
|
2066
829
|
begin
|
2067
|
-
|
2068
|
-
|
2069
|
-
|
2070
|
-
req = create_request(method, uri, query, body, extheader, !proxy.nil?)
|
2071
|
-
yield(req, proxy)
|
830
|
+
yield
|
831
|
+
rescue KeepAliveDisconnected
|
832
|
+
yield
|
2072
833
|
end
|
2073
834
|
end
|
2074
835
|
|
2075
|
-
def create_request(method, uri, query, body, extheader
|
836
|
+
def create_request(method, uri, query, body, extheader)
|
2076
837
|
if extheader.is_a?(Hash)
|
2077
838
|
extheader = extheader.to_a
|
2078
|
-
|
2079
|
-
|
2080
|
-
extheader << ['Cookie', cookies]
|
839
|
+
else
|
840
|
+
extheader = extheader.dup
|
2081
841
|
end
|
2082
842
|
boundary = nil
|
2083
|
-
|
2084
|
-
|
2085
|
-
|
2086
|
-
|
2087
|
-
|
843
|
+
if body
|
844
|
+
dummy, content_type = extheader.find { |key, value|
|
845
|
+
key.downcase == 'content-type'
|
846
|
+
}
|
847
|
+
if content_type
|
848
|
+
if /\Amultipart/ =~ content_type
|
849
|
+
if content_type =~ /boundary=(.+)\z/
|
850
|
+
boundary = $1
|
851
|
+
else
|
852
|
+
boundary = create_boundary
|
853
|
+
content_type = "#{content_type}; boundary=#{boundary}"
|
854
|
+
extheader = override_header(extheader, 'Content-Type', content_type)
|
855
|
+
end
|
856
|
+
end
|
857
|
+
elsif method == 'POST'
|
858
|
+
if file_in_form_data?(body)
|
859
|
+
boundary = create_boundary
|
860
|
+
content_type = "multipart/form-data; boundary=#{boundary}"
|
861
|
+
else
|
862
|
+
content_type = 'application/x-www-form-urlencoded'
|
863
|
+
end
|
864
|
+
extheader << ['Content-Type', content_type]
|
865
|
+
end
|
2088
866
|
end
|
2089
|
-
req = HTTP::Message.new_request(method, uri, query, body,
|
867
|
+
req = HTTP::Message.new_request(method, uri, query, body, boundary)
|
2090
868
|
extheader.each do |key, value|
|
2091
869
|
req.header.set(key, value)
|
2092
870
|
end
|
2093
|
-
if
|
2094
|
-
req.header.set('
|
871
|
+
if @cookie_manager && cookies = @cookie_manager.find(uri)
|
872
|
+
req.header.set('Cookie', cookies)
|
2095
873
|
end
|
2096
874
|
req
|
2097
875
|
end
|
2098
876
|
|
877
|
+
def create_boundary
|
878
|
+
Digest::SHA1.hexdigest(Time.now.to_s)
|
879
|
+
end
|
880
|
+
|
881
|
+
def file_in_form_data?(body)
|
882
|
+
HTTP::Message.multiparam_query?(body) &&
|
883
|
+
body.any? { |k, v| HTTP::Message.file?(v) }
|
884
|
+
end
|
885
|
+
|
886
|
+
def override_header(extheader, key, value)
|
887
|
+
result = []
|
888
|
+
extheader.each do |k, v|
|
889
|
+
if k.downcase == key.downcase
|
890
|
+
result << [key, value]
|
891
|
+
else
|
892
|
+
result << [k, v]
|
893
|
+
end
|
894
|
+
end
|
895
|
+
result
|
896
|
+
end
|
897
|
+
|
2099
898
|
NO_PROXY_HOSTS = ['localhost']
|
2100
899
|
|
2101
900
|
def no_proxy?(uri)
|
@@ -2125,7 +924,7 @@ private
|
|
2125
924
|
conn.push(HTTP::Message.new_response(str))
|
2126
925
|
return
|
2127
926
|
end
|
2128
|
-
content = ''
|
927
|
+
content = block ? nil : ''
|
2129
928
|
res = HTTP::Message.new_response(content)
|
2130
929
|
@debug_dev << "= Request\n\n" if @debug_dev
|
2131
930
|
sess = @session_manager.query(req, proxy)
|
@@ -2133,9 +932,12 @@ private
|
|
2133
932
|
@debug_dev << "\n\n= Response\n\n" if @debug_dev
|
2134
933
|
do_get_header(req, res, sess)
|
2135
934
|
conn.push(res)
|
2136
|
-
sess.
|
2137
|
-
|
2138
|
-
|
935
|
+
sess.get_body do |part|
|
936
|
+
if block
|
937
|
+
block.call(res, part)
|
938
|
+
else
|
939
|
+
content << part
|
940
|
+
end
|
2139
941
|
end
|
2140
942
|
@session_manager.keep(sess) unless sess.closed?
|
2141
943
|
commands = @request_filter.collect { |filter|
|
@@ -2152,7 +954,7 @@ private
|
|
2152
954
|
end
|
2153
955
|
if str = @test_loopback_response.shift
|
2154
956
|
dump_dummy_request_response(req.body.dump, str) if @debug_dev
|
2155
|
-
conn.push(HTTP::Message.new_response(str))
|
957
|
+
conn.push(HTTP::Message.new_response(StringIO.new(str)))
|
2156
958
|
return
|
2157
959
|
end
|
2158
960
|
piper, pipew = IO.pipe
|
@@ -2163,8 +965,8 @@ private
|
|
2163
965
|
@debug_dev << "\n\n= Response\n\n" if @debug_dev
|
2164
966
|
do_get_header(req, res, sess)
|
2165
967
|
conn.push(res)
|
2166
|
-
sess.
|
2167
|
-
pipew.syswrite(
|
968
|
+
sess.get_body do |part|
|
969
|
+
pipew.syswrite(part)
|
2168
970
|
end
|
2169
971
|
pipew.close
|
2170
972
|
@session_manager.keep(sess) unless sess.closed?
|
@@ -2175,14 +977,11 @@ private
|
|
2175
977
|
end
|
2176
978
|
|
2177
979
|
def do_get_header(req, res, sess)
|
2178
|
-
res.version, res.status, res.reason = sess.
|
2179
|
-
|
2180
|
-
|
2181
|
-
raise RuntimeError.new("Unparsable header: '#{line}'.") if $DEBUG
|
2182
|
-
end
|
2183
|
-
res.header.set($1, $2)
|
980
|
+
res.version, res.status, res.reason, headers = sess.get_header
|
981
|
+
headers.each do |key, value|
|
982
|
+
res.header.set(key, value)
|
2184
983
|
end
|
2185
|
-
if
|
984
|
+
if @cookie_manager
|
2186
985
|
res.header['set-cookie'].each do |cookie|
|
2187
986
|
@cookie_manager.parse(cookie, req.header.request_uri)
|
2188
987
|
end
|