barx 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/httpclient.rb ADDED
@@ -0,0 +1,2054 @@
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
+ dir = File.dirname(File.expand_path(__FILE__))
22
+ require "#{dir}/httpclient/http"
23
+ require "#{dir}/httpclient/cookie"
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
+ # ## modified by =vg to catch wildcard hostnames
964
+ # begin
965
+ # @ssl_socket.post_connection_check(hostname)
966
+ # rescue OpenSSL::SSL::SSLError
967
+ # @ssl_socket.post_connection_check(hostname.sub(/^[^.]+/, '*'))
968
+ # end
969
+ # else
970
+ @context.post_connection_check(@ssl_socket.peer_cert, hostname)
971
+ # end
972
+ end
973
+
974
+ def peer_cert
975
+ @ssl_socket.peer_cert
976
+ end
977
+
978
+ def addr
979
+ @socket.addr
980
+ end
981
+
982
+ def close
983
+ @ssl_socket.close
984
+ @socket.close
985
+ end
986
+
987
+ def closed?
988
+ @socket.closed?
989
+ end
990
+
991
+ def eof?
992
+ @ssl_socket.eof?
993
+ end
994
+
995
+ def gets(*args)
996
+ str = @ssl_socket.gets(*args)
997
+ @debug_dev << str if @debug_dev
998
+ str
999
+ end
1000
+
1001
+ def read(*args)
1002
+ str = @ssl_socket.read(*args)
1003
+ @debug_dev << str if @debug_dev
1004
+ str
1005
+ end
1006
+
1007
+ def <<(str)
1008
+ rv = @ssl_socket.write(str)
1009
+ @debug_dev << str if @debug_dev
1010
+ rv
1011
+ end
1012
+
1013
+ def flush
1014
+ @ssl_socket.flush
1015
+ end
1016
+
1017
+ def sync
1018
+ @ssl_socket.sync
1019
+ end
1020
+
1021
+ def sync=(sync)
1022
+ @ssl_socket.sync = sync
1023
+ end
1024
+
1025
+ private
1026
+
1027
+ def check_mask(value, mask)
1028
+ value & mask == mask
1029
+ end
1030
+
1031
+ def create_ssl_socket(socket)
1032
+ ssl_socket = nil
1033
+ if OpenSSL::SSL.const_defined?("SSLContext")
1034
+ ctx = OpenSSL::SSL::SSLContext.new
1035
+ @context.set_context(ctx)
1036
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
1037
+ else
1038
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket)
1039
+ @context.set_context(ssl_socket)
1040
+ end
1041
+ ssl_socket
1042
+ end
1043
+ end
1044
+
1045
+
1046
+ # HTTPClient::DebugSocket -- debugging support
1047
+ #
1048
+ class DebugSocket < TCPSocket
1049
+ attr_accessor :debug_dev # Device for logging.
1050
+
1051
+ class << self
1052
+ def create_socket(host, port, debug_dev)
1053
+ debug_dev << "! CONNECT TO #{host}:#{port}\n"
1054
+ socket = new(host, port)
1055
+ socket.debug_dev = debug_dev
1056
+ socket.log_connect
1057
+ socket
1058
+ end
1059
+
1060
+ private :new
1061
+ end
1062
+
1063
+ def initialize(*args)
1064
+ super
1065
+ @debug_dev = nil
1066
+ end
1067
+
1068
+ def log_connect
1069
+ @debug_dev << '! CONNECTION ESTABLISHED' << "\n"
1070
+ end
1071
+
1072
+ def close
1073
+ super
1074
+ @debug_dev << '! CONNECTION CLOSED' << "\n"
1075
+ end
1076
+
1077
+ def gets(*args)
1078
+ str = super
1079
+ @debug_dev << str if str
1080
+ str
1081
+ end
1082
+
1083
+ def read(*args)
1084
+ str = super
1085
+ @debug_dev << str if str
1086
+ str
1087
+ end
1088
+
1089
+ def <<(str)
1090
+ super
1091
+ @debug_dev << str
1092
+ end
1093
+ end
1094
+
1095
+
1096
+ # HTTPClient::Session -- manage http session with one site.
1097
+ # One or more TCP sessions with the site may be created.
1098
+ # Only 1 TCP session is live at the same time.
1099
+ #
1100
+ class Session # :nodoc:
1101
+
1102
+ class Error < StandardError # :nodoc:
1103
+ end
1104
+
1105
+ class InvalidState < Error # :nodoc:
1106
+ end
1107
+
1108
+ class BadResponse < Error # :nodoc:
1109
+ end
1110
+
1111
+ class KeepAliveDisconnected < Error # :nodoc:
1112
+ end
1113
+
1114
+ attr_reader :dest # Destination site
1115
+ attr_reader :src # Source site
1116
+ attr_accessor :proxy # Proxy site
1117
+ attr_accessor :socket_sync # Boolean value for Socket#sync
1118
+
1119
+ attr_accessor :requested_version # Requested protocol version
1120
+
1121
+ attr_accessor :debug_dev # Device for dumping log for debugging
1122
+
1123
+ # These session parameters are not used now...
1124
+ attr_accessor :connect_timeout
1125
+ attr_accessor :connect_retry
1126
+ attr_accessor :send_timeout
1127
+ attr_accessor :receive_timeout
1128
+ attr_accessor :read_block_size
1129
+
1130
+ attr_accessor :ssl_config
1131
+ attr_reader :ssl_peer_cert
1132
+
1133
+
1134
+ def initialize(dest, user_agent, from)
1135
+ @dest = dest
1136
+ @src = Site.new
1137
+ @proxy = nil
1138
+ @socket_sync = true
1139
+ @requested_version = nil
1140
+
1141
+ @debug_dev = nil
1142
+
1143
+ @connect_timeout = nil
1144
+ @connect_retry = 1
1145
+ @send_timeout = nil
1146
+ @receive_timeout = nil
1147
+ @read_block_size = nil
1148
+
1149
+ @ssl_config = nil
1150
+ @ssl_peer_cert = nil
1151
+
1152
+ @user_agent = user_agent
1153
+ @from = from
1154
+ @state = :INIT
1155
+
1156
+ @requests = []
1157
+
1158
+ @status = nil
1159
+ @reason = nil
1160
+ @headers = []
1161
+
1162
+ @socket = nil
1163
+
1164
+ end
1165
+
1166
+ # Send a request to the server
1167
+ def query(req)
1168
+ connect() if @state == :INIT
1169
+ begin
1170
+ timeout(@send_timeout) do
1171
+ set_header(req)
1172
+ req.dump(@socket)
1173
+ # flush the IO stream as IO::sync mode is false
1174
+ @socket.flush unless @socket_sync
1175
+ end
1176
+ rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE
1177
+ close
1178
+ raise KeepAliveDisconnected.new
1179
+ rescue
1180
+ if SSLEnabled and $!.is_a?(OpenSSL::SSL::SSLError)
1181
+ raise KeepAliveDisconnected.new
1182
+ elsif $!.is_a?(TimeoutError)
1183
+ close
1184
+ raise
1185
+ else
1186
+ raise
1187
+ end
1188
+ end
1189
+
1190
+ @state = :META if @state == :WAIT
1191
+ @next_connection = nil
1192
+ @requests.push(req)
1193
+ end
1194
+
1195
+ def close
1196
+ if !@socket.nil? and !@socket.closed?
1197
+ @socket.flush rescue nil # try to rescue OpenSSL::SSL::SSLError: cf. #120
1198
+ @socket.close
1199
+ end
1200
+ @state = :INIT
1201
+ end
1202
+
1203
+ def closed?
1204
+ @state == :INIT
1205
+ end
1206
+
1207
+ def get_status
1208
+ version = status = reason = nil
1209
+ begin
1210
+ if @state != :META
1211
+ raise RuntimeError.new("get_status must be called at the beginning of a session.")
1212
+ end
1213
+ version, status, reason = read_header()
1214
+ rescue
1215
+ close
1216
+ raise
1217
+ end
1218
+ return version, status, reason
1219
+ end
1220
+
1221
+ def get_header(&block)
1222
+ begin
1223
+ read_header() if @state == :META
1224
+ rescue
1225
+ close
1226
+ raise
1227
+ end
1228
+ if block
1229
+ @headers.each do |line|
1230
+ block.call(line)
1231
+ end
1232
+ else
1233
+ @headers
1234
+ end
1235
+ end
1236
+
1237
+ def eof?
1238
+ if !@content_length.nil?
1239
+ @content_length == 0
1240
+ elsif @readbuf.length > 0
1241
+ false
1242
+ else
1243
+ @socket.closed? or @socket.eof?
1244
+ end
1245
+ end
1246
+
1247
+ def get_data(&block)
1248
+ begin
1249
+ read_header() if @state == :META
1250
+ return nil if @state != :DATA
1251
+ unless @state == :DATA
1252
+ raise InvalidState.new('state != DATA')
1253
+ end
1254
+ data = nil
1255
+ if block
1256
+ while true
1257
+ begin
1258
+ timeout(@receive_timeout) do
1259
+ data = read_body()
1260
+ end
1261
+ rescue TimeoutError
1262
+ raise
1263
+ end
1264
+ block.call(data) if data
1265
+ break if eof?
1266
+ end
1267
+ data = nil # Calling with block returns nil.
1268
+ else
1269
+ begin
1270
+ timeout(@receive_timeout) do
1271
+ data = read_body()
1272
+ end
1273
+ rescue TimeoutError
1274
+ raise
1275
+ end
1276
+ end
1277
+ rescue
1278
+ close
1279
+ raise
1280
+ end
1281
+ if eof?
1282
+ if @next_connection
1283
+ @state = :WAIT
1284
+ else
1285
+ close
1286
+ end
1287
+ end
1288
+ data
1289
+ end
1290
+
1291
+ private
1292
+
1293
+ LibNames = "(#{RCS_FILE}/#{RCS_REVISION}, #{RUBY_VERSION_STRING})"
1294
+
1295
+ def set_header(req)
1296
+ req.version = @requested_version if @requested_version
1297
+ if @user_agent
1298
+ req.header.set('User-Agent', "#{@user_agent} #{LibNames}")
1299
+ end
1300
+ if @from
1301
+ req.header.set('From', @from)
1302
+ end
1303
+ req.header.set('Date', HTTP.http_date(Time.now))
1304
+
1305
+ end
1306
+
1307
+ # Connect to the server
1308
+ def connect
1309
+ site = @proxy || @dest
1310
+ begin
1311
+ retry_number = 0
1312
+ timeout(@connect_timeout) do
1313
+ @socket = create_socket(site)
1314
+ begin
1315
+ @src.host = @socket.addr[3]
1316
+ @src.port = @socket.addr[1]
1317
+ rescue SocketError
1318
+ # to avoid IPSocket#addr problem on Mac OS X 10.3 + ruby-1.8.1.
1319
+ # cf. [ruby-talk:84909], [ruby-talk:95827]
1320
+ end
1321
+ if @dest.scheme == 'https'
1322
+ @socket = create_ssl_socket(@socket)
1323
+ connect_ssl_proxy(@socket) if @proxy
1324
+ @socket.ssl_connect
1325
+ @socket.post_connection_check(@dest)
1326
+ @ssl_peer_cert = @socket.peer_cert
1327
+ end
1328
+ # Use Ruby internal buffering instead of passing data immediatly
1329
+ # to the underlying layer
1330
+ # => we need to to call explicitely flush on the socket
1331
+ @socket.sync = @socket_sync
1332
+ end
1333
+ rescue TimeoutError
1334
+ if @connect_retry == 0
1335
+ retry
1336
+ else
1337
+ retry_number += 1
1338
+ retry if retry_number < @connect_retry
1339
+ end
1340
+ close
1341
+ raise
1342
+ end
1343
+
1344
+ @state = :WAIT
1345
+ @readbuf = ''
1346
+ end
1347
+
1348
+ def create_socket(site)
1349
+ begin
1350
+ if @debug_dev
1351
+ DebugSocket.create_socket(site.host, site.port, @debug_dev)
1352
+ else
1353
+ TCPSocket.new(site.host, site.port)
1354
+ end
1355
+ rescue SystemCallError => e
1356
+ e.message << " (#{site})"
1357
+ raise
1358
+ rescue SocketError => e
1359
+ e.message << " (#{site})"
1360
+ raise
1361
+ end
1362
+ end
1363
+
1364
+ # wrap socket with OpenSSL.
1365
+ def create_ssl_socket(raw_socket)
1366
+ SSLSocketWrap.new(raw_socket, @ssl_config, (DEBUG_SSL ? @debug_dev : nil))
1367
+ end
1368
+
1369
+ def connect_ssl_proxy(socket)
1370
+ socket << sprintf("CONNECT %s:%s HTTP/1.1\r\n\r\n", @dest.host, @dest.port)
1371
+ parse_header(socket)
1372
+ unless @status == 200
1373
+ raise BadResponse.new(
1374
+ "connect to ssl proxy failed with status #{@status} #{@reason}")
1375
+ end
1376
+ end
1377
+
1378
+ # Read status block.
1379
+ def read_header
1380
+ if @state == :DATA
1381
+ get_data {}
1382
+ check_state()
1383
+ end
1384
+ unless @state == :META
1385
+ raise InvalidState, 'state != :META'
1386
+ end
1387
+ parse_header(@socket)
1388
+ @content_length = nil
1389
+ @chunked = false
1390
+ @headers.each do |line|
1391
+ case line
1392
+ when /^Content-Length:\s+(\d+)/i
1393
+ @content_length = $1.to_i
1394
+ when /^Transfer-Encoding:\s+chunked/i
1395
+ @chunked = true
1396
+ @content_length = true # how?
1397
+ @chunk_length = 0
1398
+ when /^Connection:\s+([\-\w]+)/i, /^Proxy-Connection:\s+([\-\w]+)/i
1399
+ case $1
1400
+ when /^Keep-Alive$/i
1401
+ @next_connection = true
1402
+ when /^close$/i
1403
+ @next_connection = false
1404
+ end
1405
+ else
1406
+ # Nothing to parse.
1407
+ end
1408
+ end
1409
+
1410
+ # Head of the request has been parsed.
1411
+ @state = :DATA
1412
+ req = @requests.shift
1413
+
1414
+ if req.header.request_method == 'HEAD'
1415
+ @content_length = 0
1416
+ if @next_connection
1417
+ @state = :WAIT
1418
+ else
1419
+ close
1420
+ end
1421
+ end
1422
+ @next_connection = false unless @content_length
1423
+ return [@version, @status, @reason]
1424
+ end
1425
+
1426
+ StatusParseRegexp = %r(\AHTTP/(\d+\.\d+)\s+(\d+)(?:\s+([^\r\n]+))?\r?\n\z)
1427
+ def parse_header(socket)
1428
+ begin
1429
+ timeout(@receive_timeout) do
1430
+ begin
1431
+ initial_line = socket.gets("\n")
1432
+ if initial_line.nil?
1433
+ raise KeepAliveDisconnected.new
1434
+ end
1435
+ if StatusParseRegexp =~ initial_line
1436
+ @version, @status, @reason = $1, $2.to_i, $3
1437
+ @next_connection = HTTP.keep_alive_enabled?(@version)
1438
+ else
1439
+ @version = '0.9'
1440
+ @status = nil
1441
+ @reason = nil
1442
+ @next_connection = false
1443
+ @readbuf = initial_line
1444
+ break
1445
+ end
1446
+ @headers = []
1447
+ while true
1448
+ line = socket.gets("\n")
1449
+ unless line
1450
+ raise BadResponse.new('Unexpected EOF.')
1451
+ end
1452
+ line.sub!(/\r?\n\z/, '')
1453
+ break if line.empty?
1454
+ if line.sub!(/^\t/, '')
1455
+ @headers[-1] << line
1456
+ else
1457
+ @headers.push(line)
1458
+ end
1459
+ end
1460
+ end while (@version == '1.1' && @status == 100)
1461
+ end
1462
+ rescue TimeoutError
1463
+ raise
1464
+ end
1465
+ end
1466
+
1467
+ def read_body
1468
+ if @chunked
1469
+ return read_body_chunked()
1470
+ elsif @content_length == 0
1471
+ return nil
1472
+ elsif @content_length
1473
+ return read_body_length()
1474
+ else
1475
+ if @readbuf.length > 0
1476
+ data = @readbuf
1477
+ @readbuf = ''
1478
+ return data
1479
+ else
1480
+ data = @socket.read(@read_block_size)
1481
+ data = nil if data and data.empty? # Absorbing interface mismatch.
1482
+ return data
1483
+ end
1484
+ end
1485
+ end
1486
+
1487
+ def read_body_length
1488
+ maxbytes = @read_block_size
1489
+ if @readbuf.length > 0
1490
+ data = @readbuf[0, @content_length]
1491
+ @readbuf[0, @content_length] = ''
1492
+ @content_length -= data.length
1493
+ return data
1494
+ end
1495
+ maxbytes = @content_length if maxbytes > @content_length
1496
+ data = @socket.read(maxbytes)
1497
+ if data
1498
+ @content_length -= data.length
1499
+ else
1500
+ @content_length = 0
1501
+ end
1502
+ return data
1503
+ end
1504
+
1505
+ RS = "\r\n"
1506
+ def read_body_chunked
1507
+ if @chunk_length == 0
1508
+ until (i = @readbuf.index(RS))
1509
+ @readbuf << @socket.gets(RS)
1510
+ end
1511
+ i += 2
1512
+ @chunk_length = @readbuf[0, i].hex
1513
+ @readbuf[0, i] = ''
1514
+ if @chunk_length == 0
1515
+ @content_length = 0
1516
+ @socket.gets(RS)
1517
+ return nil
1518
+ end
1519
+ end
1520
+ while @readbuf.length < @chunk_length + 2
1521
+ @readbuf << @socket.read(@chunk_length + 2 - @readbuf.length)
1522
+ end
1523
+ data = @readbuf[0, @chunk_length]
1524
+ @readbuf[0, @chunk_length + 2] = ''
1525
+ @chunk_length = 0
1526
+ return data
1527
+ end
1528
+
1529
+ def check_state
1530
+ if @state == :DATA
1531
+ if eof?
1532
+ if @next_connection
1533
+ if @requests.empty?
1534
+ @state = :WAIT
1535
+ else
1536
+ @state = :META
1537
+ end
1538
+ end
1539
+ end
1540
+ end
1541
+ end
1542
+ end
1543
+
1544
+ include Util
1545
+
1546
+ attr_reader :agent_name
1547
+ attr_reader :from
1548
+ attr_reader :ssl_config
1549
+ attr_accessor :cookie_manager
1550
+ attr_reader :test_loopback_response
1551
+ attr_reader :request_filter
1552
+ attr_reader :proxy_auth
1553
+ attr_reader :www_auth
1554
+
1555
+ class << self
1556
+ %w(get_content head get post put delete options trace).each do |name|
1557
+ eval <<-EOD
1558
+ def #{name}(*arg)
1559
+ new.#{name}(*arg)
1560
+ end
1561
+ EOD
1562
+ end
1563
+ end
1564
+
1565
+ class RetryableResponse < StandardError # :nodoc:
1566
+ end
1567
+
1568
+ # SYNOPSIS
1569
+ # Client.new(proxy = nil, agent_name = nil, from = nil)
1570
+ #
1571
+ # ARGS
1572
+ # proxy A String of HTTP proxy URL. ex. "http://proxy:8080".
1573
+ # agent_name A String for "User-Agent" HTTP request header.
1574
+ # from A String for "From" HTTP request header.
1575
+ #
1576
+ # DESCRIPTION
1577
+ # Create an instance.
1578
+ # SSLConfig cannot be re-initialized. Create new client.
1579
+ #
1580
+ def initialize(proxy = nil, agent_name = nil, from = nil)
1581
+ @proxy = nil # assigned later.
1582
+ @no_proxy = nil
1583
+ @agent_name = agent_name
1584
+ @from = from
1585
+ @www_auth = WWWAuth.new
1586
+ @proxy_auth = ProxyAuth.new
1587
+ @request_filter = [@proxy_auth, @www_auth]
1588
+ @debug_dev = nil
1589
+ @redirect_uri_callback = method(:default_redirect_uri_callback)
1590
+ @test_loopback_response = []
1591
+ @session_manager = SessionManager.new
1592
+ @session_manager.agent_name = @agent_name
1593
+ @session_manager.from = @from
1594
+ @session_manager.ssl_config = @ssl_config = SSLConfig.new(self)
1595
+ @cookie_manager = WebAgent::CookieManager.new
1596
+ load_environment
1597
+ self.proxy = proxy if proxy
1598
+ end
1599
+
1600
+ def debug_dev
1601
+ @debug_dev
1602
+ end
1603
+
1604
+ def debug_dev=(dev)
1605
+ @debug_dev = dev
1606
+ reset_all
1607
+ @session_manager.debug_dev = dev
1608
+ end
1609
+
1610
+ def protocol_version
1611
+ @session_manager.protocol_version
1612
+ end
1613
+
1614
+ def protocol_version=(protocol_version)
1615
+ reset_all
1616
+ @session_manager.protocol_version = protocol_version
1617
+ end
1618
+
1619
+ def connect_timeout
1620
+ @session_manager.connect_timeout
1621
+ end
1622
+
1623
+ def connect_timeout=(connect_timeout)
1624
+ reset_all
1625
+ @session_manager.connect_timeout = connect_timeout
1626
+ end
1627
+
1628
+ def send_timeout
1629
+ @session_manager.send_timeout
1630
+ end
1631
+
1632
+ def send_timeout=(send_timeout)
1633
+ reset_all
1634
+ @session_manager.send_timeout = send_timeout
1635
+ end
1636
+
1637
+ def receive_timeout
1638
+ @session_manager.receive_timeout
1639
+ end
1640
+
1641
+ def receive_timeout=(receive_timeout)
1642
+ reset_all
1643
+ @session_manager.receive_timeout = receive_timeout
1644
+ end
1645
+
1646
+ def proxy
1647
+ @proxy
1648
+ end
1649
+
1650
+ def proxy=(proxy)
1651
+ if proxy.nil?
1652
+ @proxy = nil
1653
+ @proxy_auth.reset_challenge
1654
+ else
1655
+ @proxy = urify(proxy)
1656
+ if @proxy.scheme == nil or @proxy.scheme.downcase != 'http' or
1657
+ @proxy.host == nil or @proxy.port == nil
1658
+ raise ArgumentError.new("unsupported proxy `#{proxy}'")
1659
+ end
1660
+ @proxy_auth.reset_challenge
1661
+ if @proxy.user || @proxy.password
1662
+ @proxy_auth.set_auth(@proxy.user, @proxy.password)
1663
+ end
1664
+ end
1665
+ reset_all
1666
+ @proxy
1667
+ end
1668
+
1669
+ def no_proxy
1670
+ @no_proxy
1671
+ end
1672
+
1673
+ def no_proxy=(no_proxy)
1674
+ @no_proxy = no_proxy
1675
+ reset_all
1676
+ end
1677
+
1678
+ # if your ruby is older than 2005-09-06, do not set socket_sync = false to
1679
+ # avoid an SSL socket blocking bug in openssl/buffering.rb.
1680
+ def socket_sync=(socket_sync)
1681
+ @session_manager.socket_sync = socket_sync
1682
+ end
1683
+
1684
+ def set_auth(uri, user, passwd)
1685
+ uri = urify(uri)
1686
+ @www_auth.set_auth(uri, user, passwd)
1687
+ reset_all
1688
+ end
1689
+
1690
+ # for backward compatibility
1691
+ def set_basic_auth(uri, user, passwd)
1692
+ uri = urify(uri)
1693
+ @www_auth.basic_auth.set(uri, user, passwd)
1694
+ reset_all
1695
+ end
1696
+
1697
+ def set_proxy_auth(user, passwd)
1698
+ uri = urify(uri)
1699
+ @proxy_auth.set_auth(user, passwd)
1700
+ reset_all
1701
+ end
1702
+
1703
+ def set_cookie_store(filename)
1704
+ if @cookie_manager.cookies_file
1705
+ raise RuntimeError.new("overriding cookie file location")
1706
+ end
1707
+ @cookie_manager.cookies_file = filename
1708
+ @cookie_manager.load_cookies if filename
1709
+ end
1710
+
1711
+ def save_cookie_store
1712
+ @cookie_manager.save_cookies
1713
+ end
1714
+
1715
+ def redirect_uri_callback=(redirect_uri_callback)
1716
+ @redirect_uri_callback = redirect_uri_callback
1717
+ end
1718
+
1719
+ # SYNOPSIS
1720
+ # Client#get_content(uri, query = nil, extheader = {}, &block = nil)
1721
+ #
1722
+ # ARGS
1723
+ # uri an_URI or a_string of uri to connect.
1724
+ # query a_hash or an_array of query part. e.g. { "a" => "b" }.
1725
+ # Give an array to pass multiple value like
1726
+ # [["a" => "b"], ["a" => "c"]].
1727
+ # extheader a_hash of extra headers like { "SOAPAction" => "urn:foo" }.
1728
+ # &block Give a block to get chunked message-body of response like
1729
+ # get_content(uri) { |chunked_body| ... }
1730
+ # Size of each chunk may not be the same.
1731
+ #
1732
+ # DESCRIPTION
1733
+ # Get a_sring of message-body of response.
1734
+ #
1735
+ def get_content(uri, query = nil, extheader = {}, &block)
1736
+ follow_redirect(uri, query) { |uri, query|
1737
+ get(uri, query, extheader, &block)
1738
+ }.content
1739
+ end
1740
+
1741
+ ## added by =vg so that we can follow redirects but preserve header expires info in result
1742
+ def get_with_redirect(uri, query = nil, extheader = {}, &block)
1743
+ follow_redirect(uri, query) { |uri, query|
1744
+ get(uri, query, extheader, &block)
1745
+ }
1746
+ end
1747
+ ##
1748
+
1749
+ def post_content(uri, body = nil, extheader = {}, &block)
1750
+ follow_redirect(uri, nil) { |uri, query|
1751
+ post(uri, body, extheader, &block)
1752
+ }.content
1753
+ end
1754
+
1755
+ def strict_redirect_uri_callback(uri, res)
1756
+ newuri = URI.parse(res.header['location'][0])
1757
+ puts "Redirect to: #{newuri}" if $DEBUG
1758
+ newuri
1759
+ end
1760
+
1761
+ def default_redirect_uri_callback(uri, res)
1762
+ newuri = URI.parse(res.header['location'][0])
1763
+ unless newuri.is_a?(URI::HTTP)
1764
+ newuri = uri + newuri
1765
+ STDERR.puts(
1766
+ "could be a relative URI in location header which is not recommended")
1767
+ STDERR.puts(
1768
+ "'The field value consists of a single absolute URI' in HTTP spec")
1769
+ end
1770
+ puts "Redirect to: #{newuri}" if $DEBUG
1771
+ newuri
1772
+ end
1773
+
1774
+ def head(uri, query = nil, extheader = {})
1775
+ request('HEAD', uri, query, nil, extheader)
1776
+ end
1777
+
1778
+ def get(uri, query = nil, extheader = {}, &block)
1779
+ request('GET', uri, query, nil, extheader, &block)
1780
+ end
1781
+
1782
+ def post(uri, body = nil, extheader = {}, &block)
1783
+ request('POST', uri, nil, body, extheader, &block)
1784
+ end
1785
+
1786
+ def put(uri, body = nil, extheader = {}, &block)
1787
+ request('PUT', uri, nil, body, extheader, &block)
1788
+ end
1789
+
1790
+ def delete(uri, extheader = {}, &block)
1791
+ request('DELETE', uri, nil, nil, extheader, &block)
1792
+ end
1793
+
1794
+ def options(uri, extheader = {}, &block)
1795
+ request('OPTIONS', uri, nil, nil, extheader, &block)
1796
+ end
1797
+
1798
+ def trace(uri, query = nil, body = nil, extheader = {}, &block)
1799
+ request('TRACE', uri, query, body, extheader, &block)
1800
+ end
1801
+
1802
+ def request(method, uri, query = nil, body = nil, extheader = {}, &block)
1803
+ uri = urify(uri)
1804
+ conn = Connection.new
1805
+ res = nil
1806
+ retry_count = 5
1807
+ while retry_count > 0
1808
+ begin
1809
+ prepare_request(method, uri, query, body, extheader) do |req, proxy|
1810
+ do_get_block(req, proxy, conn, &block)
1811
+ end
1812
+ res = conn.pop
1813
+ break
1814
+ rescue RetryableResponse
1815
+ res = conn.pop
1816
+ retry_count -= 1
1817
+ end
1818
+ end
1819
+ res
1820
+ end
1821
+
1822
+ # Async interface.
1823
+
1824
+ def head_async(uri, query = nil, extheader = {})
1825
+ request_async('HEAD', uri, query, nil, extheader)
1826
+ end
1827
+
1828
+ def get_async(uri, query = nil, extheader = {})
1829
+ request_async('GET', uri, query, nil, extheader)
1830
+ end
1831
+
1832
+ def post_async(uri, body = nil, extheader = {})
1833
+ request_async('POST', uri, nil, body, extheader)
1834
+ end
1835
+
1836
+ def put_async(uri, body = nil, extheader = {})
1837
+ request_async('PUT', uri, nil, body, extheader)
1838
+ end
1839
+
1840
+ def delete_async(uri, extheader = {})
1841
+ request_async('DELETE', uri, nil, nil, extheader)
1842
+ end
1843
+
1844
+ def options_async(uri, extheader = {})
1845
+ request_async('OPTIONS', uri, nil, nil, extheader)
1846
+ end
1847
+
1848
+ def trace_async(uri, query = nil, body = nil, extheader = {})
1849
+ request_async('TRACE', uri, query, body, extheader)
1850
+ end
1851
+
1852
+ def request_async(method, uri, query = nil, body = nil, extheader = {})
1853
+ uri = urify(uri)
1854
+ conn = Connection.new
1855
+ t = Thread.new(conn) { |tconn|
1856
+ prepare_request(method, uri, query, body, extheader) do |req, proxy|
1857
+ do_get_stream(req, proxy, tconn)
1858
+ end
1859
+ }
1860
+ conn.async_thread = t
1861
+ conn
1862
+ end
1863
+
1864
+ ##
1865
+ # Multiple call interface.
1866
+
1867
+ # ???
1868
+
1869
+ ##
1870
+ # Management interface.
1871
+
1872
+ def reset(uri)
1873
+ uri = urify(uri)
1874
+ @session_manager.reset(uri)
1875
+ end
1876
+
1877
+ def reset_all
1878
+ @session_manager.reset_all
1879
+ end
1880
+
1881
+ private
1882
+
1883
+ def load_environment
1884
+ # http_proxy
1885
+ if getenv('REQUEST_METHOD')
1886
+ # HTTP_PROXY conflicts with the environment variable usage in CGI where
1887
+ # HTTP_* is used for HTTP header information. Unlike open-uri, we
1888
+ # simpley ignore http_proxy in CGI env and use cgi_http_proxy instead.
1889
+ self.proxy = getenv('cgi_http_proxy')
1890
+ else
1891
+ self.proxy = getenv('http_proxy')
1892
+ end
1893
+ # no_proxy
1894
+ self.no_proxy = getenv('no_proxy')
1895
+ end
1896
+
1897
+ def getenv(name)
1898
+ ENV[name.downcase] || ENV[name.upcase]
1899
+ end
1900
+
1901
+ def follow_redirect(uri, query = nil)
1902
+ retry_number = 0
1903
+ while retry_number < 10
1904
+ res = yield(uri, query)
1905
+ if HTTP::Status.successful?(res.status)
1906
+ return res
1907
+ elsif HTTP::Status.redirect?(res.status)
1908
+ uri = @redirect_uri_callback.call(uri, res)
1909
+ query = nil
1910
+ retry_number += 1
1911
+ else
1912
+ #raise RuntimeError.new("Unexpected response: #{res.header.inspect}")
1913
+ #this seems more useful in production =vg
1914
+ raise RuntimeError.new("HTTP::Status #{res.status}")
1915
+ end
1916
+ end
1917
+ raise RuntimeError.new("Retry count exceeded.")
1918
+ end
1919
+
1920
+ def prepare_request(method, uri, query, body, extheader)
1921
+ proxy = no_proxy?(uri) ? nil : @proxy
1922
+ begin
1923
+ req = create_request(method, uri, query, body, extheader, !proxy.nil?)
1924
+ yield(req, proxy)
1925
+ rescue Session::KeepAliveDisconnected
1926
+ req = create_request(method, uri, query, body, extheader, !proxy.nil?)
1927
+ yield(req, proxy)
1928
+ end
1929
+ end
1930
+
1931
+ def create_request(method, uri, query, body, extheader, proxy)
1932
+ if extheader.is_a?(Hash)
1933
+ extheader = extheader.to_a
1934
+ end
1935
+ if cookies = @cookie_manager.find(uri)
1936
+ extheader << ['Cookie', cookies]
1937
+ end
1938
+ boundary = nil
1939
+ content_type = extheader.find { |key, value|
1940
+ key.downcase == 'content-type'
1941
+ }
1942
+ if content_type && content_type[1] =~ /boundary=(.+)\z/
1943
+ boundary = $1
1944
+ end
1945
+ req = HTTP::Message.new_request(method, uri, query, body, proxy, boundary)
1946
+ extheader.each do |key, value|
1947
+ req.header.set(key, value)
1948
+ end
1949
+ if content_type.nil? and !body.nil?
1950
+ req.header.set('content-type', 'application/x-www-form-urlencoded')
1951
+ end
1952
+ req
1953
+ end
1954
+
1955
+ NO_PROXY_HOSTS = ['localhost']
1956
+
1957
+ def no_proxy?(uri)
1958
+ if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
1959
+ return true
1960
+ end
1961
+ unless @no_proxy
1962
+ return false
1963
+ end
1964
+ @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
1965
+ if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
1966
+ (!port || uri.port == port.to_i)
1967
+ return true
1968
+ end
1969
+ end
1970
+ false
1971
+ end
1972
+
1973
+ # !! CAUTION !!
1974
+ # Method 'do_get*' runs under MT conditon. Be careful to change.
1975
+ def do_get_block(req, proxy, conn, &block)
1976
+ @request_filter.each do |filter|
1977
+ filter.filter_request(req)
1978
+ end
1979
+ if str = @test_loopback_response.shift
1980
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
1981
+ conn.push(HTTP::Message.new_response(str))
1982
+ return
1983
+ end
1984
+ content = ''
1985
+ res = HTTP::Message.new_response(content)
1986
+ @debug_dev << "= Request\n\n" if @debug_dev
1987
+ sess = @session_manager.query(req, proxy)
1988
+ res.peer_cert = sess.ssl_peer_cert
1989
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
1990
+ do_get_header(req, res, sess)
1991
+ conn.push(res)
1992
+ sess.get_data() do |str|
1993
+ block.call(str) if block
1994
+ content << str
1995
+ end
1996
+ @session_manager.keep(sess) unless sess.closed?
1997
+ commands = @request_filter.collect { |filter|
1998
+ filter.filter_response(req, res)
1999
+ }
2000
+ if commands.find { |command| command == :retry }
2001
+ raise RetryableResponse.new
2002
+ end
2003
+ end
2004
+
2005
+ def do_get_stream(req, proxy, conn)
2006
+ @request_filter.each do |filter|
2007
+ filter.filter_request(req)
2008
+ end
2009
+ if str = @test_loopback_response.shift
2010
+ dump_dummy_request_response(req.body.dump, str) if @debug_dev
2011
+ conn.push(HTTP::Message.new_response(str))
2012
+ return
2013
+ end
2014
+ piper, pipew = IO.pipe
2015
+ res = HTTP::Message.new_response(piper)
2016
+ @debug_dev << "= Request\n\n" if @debug_dev
2017
+ sess = @session_manager.query(req, proxy)
2018
+ res.peer_cert = sess.ssl_peer_cert
2019
+ @debug_dev << "\n\n= Response\n\n" if @debug_dev
2020
+ do_get_header(req, res, sess)
2021
+ conn.push(res)
2022
+ sess.get_data() do |str|
2023
+ pipew.syswrite(str)
2024
+ end
2025
+ pipew.close
2026
+ @session_manager.keep(sess) unless sess.closed?
2027
+ commands = @request_filter.collect { |filter|
2028
+ filter.filter_response(req, res)
2029
+ }
2030
+ # ignore commands (not retryable in async mode)
2031
+ end
2032
+
2033
+ def do_get_header(req, res, sess)
2034
+ res.version, res.status, res.reason = sess.get_status
2035
+ sess.get_header().each do |line|
2036
+ unless /^([^:]+)\s*:\s*(.*)$/ =~ line
2037
+ raise RuntimeError.new("Unparsable header: '#{line}'.") if $DEBUG
2038
+ end
2039
+ res.header.set($1, $2)
2040
+ end
2041
+ if res.header['set-cookie']
2042
+ res.header['set-cookie'].each do |cookie|
2043
+ @cookie_manager.parse(cookie, req.header.request_uri)
2044
+ end
2045
+ end
2046
+ end
2047
+
2048
+ def dump_dummy_request_response(req, res)
2049
+ @debug_dev << "= Dummy Request\n\n"
2050
+ @debug_dev << req
2051
+ @debug_dev << "\n\n= Dummy Response\n\n"
2052
+ @debug_dev << res
2053
+ end
2054
+ end