httpclient 2.1.0

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