Welcome to the "trac"-ing site of http-access2!
[soap4r] [httpclient] [openpgp4u] [pkcs1] [logger] [csv] [vtr]

root/trunk/lib/http-access2.rb

Revision 127, 36.8 kB (checked in by nahi, 4 years ago)

Version 2.0.7

  • Property svn:eol-style set to native
  • Property svn:keywords set to author date id revision
Line 
1 # HTTPAccess2 - HTTP accessing library.
2 # Copyright (C) 2000-2005  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 # Ruby standard library
14 require 'timeout'
15 require 'uri'
16 require 'socket'
17 require 'thread'
18
19 # Extra library
20 require 'http-access2/http'
21 require 'http-access2/cookie'
22
23
24 module HTTPAccess2
25   VERSION = '2.0.7'
26   RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
27   s = %w$Id$
28   RCS_FILE, RCS_REVISION = s[1][/.*(?=,v$)/], s[2]
29
30   SSLEnabled = begin
31       require 'openssl'
32       true
33     rescue LoadError
34       false
35     end
36
37   DEBUG_SSL = true
38
39
40 module Util
41   def urify(uri)
42     if uri.is_a?(URI)
43       uri
44     else
45       URI.parse(uri.to_s)
46     end
47   end
48 end
49
50
51 # DESCRIPTION
52 #   HTTPAccess2::Client -- Client to retrieve web resources via HTTP.
53 #
54 # How to create your client.
55 #   1. Create simple client.
56 #     clnt = HTTPAccess2::Client.new
57 #
58 #   2. Accessing resources through HTTP proxy.
59 #     clnt = HTTPAccess2::Client.new("http://myproxy:8080")
60 #
61 #   3. Set User-Agent and From in HTTP request header.(nil means "No proxy")
62 #     clnt = HTTPAccess2::Client.new(nil, "MyAgent", "nahi@keynauts.com")
63 #
64 # How to retrieve web resources.
65 #   1. Get content of specified URL.
66 #     puts clnt.get_content("http://www.ruby-lang.org/en/")
67 #
68 #   2. Do HEAD request.
69 #     res = clnt.head(uri)
70 #
71 #   3. Do GET request with query.
72 #     res = clnt.get(uri)
73 #
74 #   4. Do POST request.
75 #     res = clnt.post(uri)
76 #     res = clnt.get|post|head(uri, proxy)
77 #
78 class Client
79   include Util
80
81   attr_reader :agent_name
82   attr_reader :from
83   attr_reader :ssl_config
84   attr_accessor :cookie_manager
85   attr_reader :test_loopback_response
86
87   class << self
88     %w(get_content head get post put delete options trace).each do |name|
89       eval <<-EOD
90         def #{name}(*arg)
91           new.#{name}(*arg)
92         end
93       EOD
94     end
95   end
96
97   # SYNOPSIS
98   #   Client.new(proxy = nil, agent_name = nil, from = nil)
99   #
100   # ARGS
101   #   proxy             A String of HTTP proxy URL. ex. "http://proxy:8080".
102   #   agent_name        A String for "User-Agent" HTTP request header.
103   #   from              A String for "From" HTTP request header.
104   #
105   # DESCRIPTION
106   #   Create an instance.
107   #   SSLConfig cannot be re-initialized.  Create new client.
108   #
109   def initialize(proxy = nil, agent_name = nil, from = nil)
110     @proxy = nil        # assigned later.
111     @no_proxy = nil
112     @agent_name = agent_name
113     @from = from
114     @basic_auth = BasicAuth.new(self)
115     @debug_dev = nil
116     @ssl_config = SSLConfig.new(self)
117     @redirect_uri_callback = method(:default_redirect_uri_callback)
118     @test_loopback_response = []
119     @session_manager = SessionManager.new
120     @session_manager.agent_name = @agent_name
121     @session_manager.from = @from
122     @session_manager.ssl_config = @ssl_config
123     @cookie_manager = WebAgent::CookieManager.new
124     self.proxy = proxy
125   end
126
127   def debug_dev
128     @debug_dev
129   end
130
131   def debug_dev=(dev)
132     @debug_dev = dev
133     reset_all
134     @session_manager.debug_dev = dev
135   end
136
137   def protocol_version
138     @session_manager.protocol_version
139   end
140
141   def protocol_version=(protocol_version)
142     reset_all
143     @session_manager.protocol_version = protocol_version
144   end
145
146   def connect_timeout
147     @session_manager.connect_timeout
148   end
149
150   def connect_timeout=(connect_timeout)
151     reset_all
152     @session_manager.connect_timeout = connect_timeout
153   end
154
155   def send_timeout
156     @session_manager.send_timeout
157   end
158
159   def send_timeout=(send_timeout)
160     reset_all
161     @session_manager.send_timeout = send_timeout
162   end
163
164   def receive_timeout
165     @session_manager.receive_timeout
166   end
167
168   def receive_timeout=(receive_timeout)
169     reset_all
170     @session_manager.receive_timeout = receive_timeout
171   end
172
173   def proxy
174     @proxy
175   end
176
177   def proxy=(proxy)
178     if proxy.nil?
179       @proxy = nil
180     else
181       @proxy = urify(proxy)
182       if @proxy.scheme == nil or @proxy.scheme.downcase != 'http' or
183           @proxy.host == nil or @proxy.port == nil
184         raise ArgumentError.new("unsupported proxy `#{proxy}'")
185       end
186     end
187     reset_all
188     @proxy
189   end
190
191   def no_proxy
192     @no_proxy
193   end
194
195   def no_proxy=(no_proxy)
196     @no_proxy = no_proxy
197     reset_all
198   end
199
200   # if your ruby is older than 2005-09-06, do not set socket_sync = false to
201   # avoid an SSL socket blocking bug in openssl/buffering.rb.
202   def socket_sync=(socket_sync)
203     @session_manager.socket_sync = socket_sync
204   end
205
206   def set_basic_auth(uri, user_id, passwd)
207     uri = urify(uri)
208     @basic_auth.set(uri, user_id, passwd)
209   end
210
211   def set_cookie_store(filename)
212     if @cookie_manager.cookies_file
213       raise RuntimeError.new("overriding cookie file location")
214     end
215     @cookie_manager.cookies_file = filename
216     @cookie_manager.load_cookies if filename
217   end
218
219   def save_cookie_store
220     @cookie_manager.save_cookies
221   end
222
223   def redirect_uri_callback=(redirect_uri_callback)
224     @redirect_uri_callback = redirect_uri_callback
225   end
226
227   # SYNOPSIS
228   #   Client#get_content(uri, query = nil, extheader = {}, &block = nil)
229   #
230   # ARGS
231   #   uri       an_URI or a_string of uri to connect.
232   #   query     a_hash or an_array of query part.  e.g. { "a" => "b" }.
233   #             Give an array to pass multiple value like
234   #             [["a" => "b"], ["a" => "c"]].
235   #   extheader a_hash of extra headers like { "SOAPAction" => "urn:foo" }.
236   #   &block    Give a block to get chunked message-body of response like
237   #             get_content(uri) { |chunked_body| ... }
238   #             Size of each chunk may not be the same.
239   #
240   # DESCRIPTION
241   #   Get a_sring of message-body of response.
242   #
243   def get_content(uri, query = nil, extheader = {}, &block)
244     retry_connect(uri, query) { |uri, query|
245       get(uri, query, extheader, &block)
246     }.content
247   end
248
249   def post_content(uri, body = nil, extheader = {}, &block)
250     retry_connect(uri, nil) { |uri, query|
251       post(uri, body, extheader, &block)
252     }.content
253   end
254
255   def strict_redirect_uri_callback(uri, res)
256     newuri = URI.parse(res.header['location'][0])
257     puts "Redirect to: #{newuri}" if $DEBUG
258     newuri
259   end
260
261   def default_redirect_uri_callback(uri, res)
262     newuri = URI.parse(res.header['location'][0])
263     unless newuri.is_a?(URI::HTTP)
264       newuri = uri + newuri
265       STDERR.puts(
266         "could be a relative URI in location header which is not recommended")
267       STDERR.puts(
268         "'The field value consists of a single absolute URI' in HTTP spec")
269     end
270     puts "Redirect to: #{newuri}" if $DEBUG
271     newuri
272   end
273
274   def head(uri, query = nil, extheader = {})
275     request('HEAD', uri, query, nil, extheader)
276   end
277
278   def get(uri, query = nil, extheader = {}, &block)
279     request('GET', uri, query, nil, extheader, &block)
280   end
281
282   def post(uri, body = nil, extheader = {}, &block)
283     request('POST', uri, nil, body, extheader, &block)
284   end
285
286   def put(uri, body = nil, extheader = {}, &block)
287     request('PUT', uri, nil, body, extheader, &block)
288   end
289
290   def delete(uri, extheader = {}, &block)
291     request('DELETE', uri, nil, nil, extheader, &block)
292   end
293
294   def options(uri, extheader = {}, &block)
295     request('OPTIONS', uri, nil, nil, extheader, &block)
296   end
297
298   def trace(uri, query = nil, body = nil, extheader = {}, &block)
299     request('TRACE', uri, query, body, extheader, &block)
300   end
301
302   def request(method, uri, query = nil, body = nil, extheader = {}, &block)
303     conn = Connection.new
304     conn_request(conn, method, uri, query, body, extheader, &block)
305     conn.pop
306   end
307
308   # Async interface.
309
310   def head_async(uri, query = nil, extheader = {})
311     request_async('HEAD', uri, query, nil, extheader)
312   end
313
314   def get_async(uri, query = nil, extheader = {})
315     request_async('GET', uri, query, nil, extheader)
316   end
317
318   def post_async(uri, body = nil, extheader = {})
319     request_async('POST', uri, nil, body, extheader)
320   end
321
322   def put_async(uri, body = nil, extheader = {})
323     request_async('PUT', uri, nil, body, extheader)
324   end
325
326   def delete_async(uri, extheader = {})
327     request_async('DELETE', uri, nil, nil, extheader)
328   end
329
330   def options_async(uri, extheader = {})
331     request_async('OPTIONS', uri, nil, nil, extheader)
332   end
333
334   def trace_async(uri, query = nil, body = nil, extheader = {})
335     request_async('TRACE', uri, query, body, extheader)
336   end
337
338   def request_async(method, uri, query = nil, body = nil, extheader = {})
339     conn = Connection.new
340     t = Thread.new(conn) { |tconn|
341       conn_request(tconn, method, uri, query, body, extheader)
342     }
343     conn.async_thread = t
344     conn
345   end
346
347   ##
348   # Multiple call interface.
349
350   # ???
351
352   ##
353   # Management interface.
354
355   def reset(uri)
356     uri = urify(uri)
357     @session_manager.reset(uri)
358   end
359
360   def reset_all
361     @session_manager.reset_all
362   end
363
364 private
365
366   def retry_connect(uri, query = nil)
367     retry_number = 0
368     while retry_number < 10
369       res = yield(uri, query)
370       if res.status == HTTP::Status::OK
371         return res
372       elsif HTTP::Status.redirect?(res.status)
373         uri = @redirect_uri_callback.call(uri, res)
374         query = nil
375         retry_number += 1
376       else
377         raise RuntimeError.new("Unexpected response: #{res.header.inspect}")
378       end
379     end
380     raise RuntimeError.new("Retry count exceeded.")
381   end
382
383   def conn_request(conn, method, uri, query, body, extheader, &block)
384     uri = urify(uri)
385     proxy = no_proxy?(uri) ? nil : @proxy
386     begin
387       req = create_request(method, uri, query, body, extheader, !proxy.nil?)
388       do_get_block(req, proxy, conn, &block)
389     rescue Session::KeepAliveDisconnected
390       req = create_request(method, uri, query, body, extheader, !proxy.nil?)
391       do_get_block(req, proxy, conn, &block)
392     end
393   end
394
395   def create_request(method, uri, query, body, extheader, proxy)
396     if extheader.is_a?(Hash)
397       extheader = extheader.to_a
398     end
399     cred = @basic_auth.get(uri)
400     if cred
401       extheader << ['Authorization', "Basic " << cred]
402     end
403     if cookies = @cookie_manager.find(uri)
404       extheader << ['Cookie', cookies]
405     end
406     boundary = nil
407     content_type = extheader.find { |key, value|
408       key.downcase == 'content-type'
409     }
410     if content_type && content_type[1] =~ /boundary=(.+)\z/
411       boundary = $1
412     end
413     req = HTTP::Message.new_request(method, uri, query, body, proxy, boundary)
414     extheader.each do |key, value|
415       req.header.set(key, value)
416     end
417     if content_type.nil? and !body.nil?
418       req.header.set('content-type', 'application/x-www-form-urlencoded')
419     end
420     req
421   end
422
423   NO_PROXY_HOSTS = ['localhost']
424
425   def no_proxy?(uri)
426     if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
427       return true
428     end
429     unless @no_proxy
430       return false
431     end
432     @no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
433       if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
434           (!port || uri.port == port.to_i)
435         return true
436       end
437     end
438     false
439   end
440
441   # !! CAUTION !!
442   #   Method 'do_get*' runs under MT conditon. Be careful to change.
443   def do_get_block(req, proxy, conn, &block)
444     if str = @test_loopback_response.shift
445       dump_dummy_request_response(req.body.dump, str) if @debug_dev
446       conn.push(HTTP::Message.new_response(str))
447       return
448     end
449     content = ''
450     res = HTTP::Message.new_response(content)
451     @debug_dev << "= Request\n\n" if @debug_dev
452     sess = @session_manager.query(req, proxy)
453     @debug_dev << "\n\n= Response\n\n" if @debug_dev
454     do_get_header(req, res, sess)
455     conn.push(res)
456     sess.get_data() do |str|
457       block.call(str) if block
458       content << str
459     end
460     @session_manager.keep(sess) unless sess.closed?
461   end
462
463   def do_get_stream(req, proxy, conn)
464     if str = @test_loopback_response.shift
465       dump_dummy_request_response(req.body.dump, str) if @debug_dev
466       conn.push(HTTP::Message.new_response(str))
467       return
468     end
469     piper, pipew = IO.pipe
470     res = HTTP::Message.new_response(piper)
471     @debug_dev << "= Request\n\n" if @debug_dev
472     sess = @session_manager.query(req, proxy)
473     @debug_dev << "\n\n= Response\n\n" if @debug_dev
474     do_get_header(req, res, sess)
475     conn.push(res)
476     sess.get_data() do |str|
477       pipew.syswrite(str)
478     end
479     pipew.close
480     @session_manager.keep(sess) unless sess.closed?
481   end
482
483   def do_get_header(req, res, sess)
484     res.version, res.status, res.reason = sess.get_status
485     sess.get_header().each do |line|
486       unless /^([^:]+)\s*:\s*(.*)$/ =~ line
487         raise RuntimeError.new("Unparsable header: '#{line}'.") if $DEBUG
488       end
489       res.header.set($1, $2)
490     end
491     if res.header['set-cookie']
492       res.header['set-cookie'].each do |cookie|
493         @cookie_manager.parse(cookie, req.header.request_uri)
494       end
495     end
496   end
497
498   def dump_dummy_request_response(req, res)
499     @debug_dev << "= Dummy Request\n\n"
500     @debug_dev << req
501     @debug_dev << "\n\n= Dummy Response\n\n"
502     @debug_dev << res
503   end
504 end
505
506
507 # HTTPAccess2::SSLConfig -- SSL configuration of a client.
508 #
509 class SSLConfig # :nodoc:
510   attr_reader :client_cert
511   attr_reader :client_key
512   attr_reader :client_ca
513
514   attr_reader :verify_mode
515   attr_reader :verify_depth
516   attr_reader :verify_callback
517
518   attr_reader :timeout
519   attr_reader :options
520   attr_reader :ciphers
521
522   attr_reader :cert_store       # don't use if you don't know what it is.
523
524   def initialize(client)
525     return unless SSLEnabled
526     @client = client
527     @cert_store = OpenSSL::X509::Store.new
528     @client_cert = @client_key = @client_ca = nil
529     @verify_mode = OpenSSL::SSL::VERIFY_PEER |
530       OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
531     @verify_depth = nil
532     @verify_callback = nil
533     @dest = nil
534     @timeout = nil
535     @options = defined?(OpenSSL::SSL::OP_ALL) ?
536       OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_SSLv2 : nil
537     @ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"
538   end
539
540   def set_client_cert_file(cert_file, key_file)
541     @client_cert = OpenSSL::X509::Certificate.new(File.open(cert_file).read)
542     @client_key = OpenSSL::PKey::RSA.new(File.open(key_file).read)
543     change_notify
544   end
545
546   def set_trust_ca(trust_ca_file_or_hashed_dir)
547     if FileTest.directory?(trust_ca_file_or_hashed_dir)
548       @cert_store.add_path(trust_ca_file_or_hashed_dir)
549     else
550       @cert_store.add_file(trust_ca_file_or_hashed_dir)
551     end
552     change_notify
553   end
554
555   def set_crl(crl_file)
556     crl = OpenSSL::X509::CRL.new(File.open(crl_file).read)
557     @cert_store.add_crl(crl)
558     @cert_store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
559     change_notify
560   end
561
562   def client_cert=(client_cert)
563     @client_cert = client_cert
564     change_notify
565   end
566
567   def client_key=(client_key)
568     @client_key = client_key
569     change_notify
570   end
571
572   def client_ca=(client_ca)
573     @client_ca = client_ca
574     change_notify
575   end
576
577   def verify_mode=(verify_mode)
578     @verify_mode = verify_mode
579     change_notify
580   end
581
582   def verify_depth=(verify_depth)
583     @verify_depth = verify_depth
584     change_notify
585   end
586
587   def verify_callback=(verify_callback)
588     @verify_callback = verify_callback
589     change_notify
590   end
591
592   def timeout=(timeout)
593     @timeout = timeout
594     change_notify
595   end
596
597   def options=(options)
598     @options = options
599     change_notify
600   end
601
602   def ciphers=(ciphers)
603     @ciphers = ciphers
604     change_notify
605   end
606
607   # don't use if you don't know what it is.
608   def cert_store=(cert_store)
609     @cert_store = cert_store
610     change_notify
611   end
612
613   # interfaces for SSLSocketWrap.
614
615   def set_context(ctx)
616     # Verification: Use Store#verify_callback instead of SSLContext#verify*?
617     ctx.cert_store = @cert_store
618     ctx.verify_mode = @verify_mode
619     ctx.verify_depth = @verify_depth if @verify_depth
620     ctx.verify_callback = @verify_callback || method(:default_verify_callback)
621     # SSL config
622     ctx.cert = @client_cert
623     ctx.key = @client_key
624     ctx.client_ca = @client_ca
625     ctx.timeout = @timeout
626     ctx.options = @options
627     ctx.ciphers = @ciphers
628   end
629
630   # this definition must match with the one in ext/openssl/lib/openssl/ssl.rb
631   def post_connection_check(peer_cert, hostname)
632     check_common_name = true
633     cert = peer_cert
634     cert.extensions.each{|ext|
635       next if ext.oid != "subjectAltName"
636       ext.value.split(/,\s+/).each{|general_name|
637         if /\ADNS:(.*)/ =~ general_name
638           check_common_name = false
639           reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+")
640           return true if /\A#{reg}\z/i =~ hostname
641         elsif /\AIP Address:(.*)/ =~ general_name
642           check_common_name = false
643           return true if $1 == hostname
644         end
645       }
646     }
647     if check_common_name
648       cert.subject.to_a.each{|oid, value|
649         if oid == "CN" && value.casecmp(hostname) == 0
650           return true
651         end
652       }
653     end
654     raise OpenSSL::SSL::SSLError, "hostname not match"
655   end
656
657   # Default callback for verification: only dumps error.
658   def default_verify_callback(is_ok, ctx)
659     if $DEBUG
660       puts "#{ is_ok ? 'ok' : 'ng' }: #{ctx.current_cert.subject}"
661     end
662     if !is_ok
663       depth = ctx.error_depth
664       code = ctx.error
665       msg = ctx.error_string
666       STDERR.puts "at depth #{depth} - #{code}: #{msg}"
667     end
668     is_ok
669   end
670
671   # Sample callback method:  CAUTION: does not check CRL/ARL.
672   def sample_verify_callback(is_ok, ctx)
673     unless is_ok
674       depth = ctx.error_depth
675       code = ctx.error
676       msg = ctx.error_string
677       STDERR.puts "at depth #{depth} - #{code}: #{msg}" if $DEBUG
678       return false
679     end
680
681     cert = ctx.current_cert
682     self_signed = false
683     ca = false
684     pathlen = nil
685     server_auth = true
686     self_signed = (cert.subject.cmp(cert.issuer) == 0)
687
688     # Check extensions whatever its criticality is. (sample)
689     cert.extensions.each do |ex|
690       case ex.oid
691       when 'basicConstraints'
692         /CA:(TRUE|FALSE), pathlen:(\d+)/ =~ ex.value
693         ca = ($1 == 'TRUE')
694         pathlen = $2.to_i
695       when 'keyUsage'
696         usage = ex.value.split(/\s*,\s*/)
697         ca = usage.include?('Certificate Sign')
698         server_auth = usage.include?('Key Encipherment')
699       when 'extendedKeyUsage'
700         usage = ex.value.split(/\s*,\s*/)
701         server_auth = usage.include?('Netscape Server Gated Crypto')
702       when 'nsCertType'
703         usage = ex.value.split(/\s*,\s*/)
704         ca = usage.include?('SSL CA')
705         server_auth = usage.include?('SSL Server')
706       end
707     end
708
709     if self_signed
710       STDERR.puts 'self signing CA' if $DEBUG
711       return true
712     elsif ca
713       STDERR.puts 'middle level CA' if $DEBUG
714       return true
715     elsif server_auth
716       STDERR.puts 'for server authentication' if $DEBUG
717       return true
718     end
719
720     return false
721   end
722
723 private
724
725   def change_notify
726     @client.reset_all
727   end
728 end
729
730
731 # HTTPAccess2::BasicAuth -- BasicAuth repository.
732 #
733 class BasicAuth # :nodoc:
734   def initialize(client)
735     @client = client
736     @auth = {}
737   end
738
739   def set(uri, user_id, passwd)
740     uri = uri.clone
741     uri.path = uri.path.sub(/\/[^\/]*$/, '/')
742     @auth[uri] = ["#{user_id}:#{passwd}"].pack('m').strip
743     @client.reset_all
744   end
745
746   def get(uri)
747     @auth.each do |realm_uri, cred|
748       if ((realm_uri.host == uri.host) and
749           (realm_uri.scheme == uri.scheme) and
750           (realm_uri.port == uri.port) and
751           uri.path.upcase.index(realm_uri.path.upcase) == 0)
752         return cred
753       end
754     end
755     nil
756   end
757 end
758
759
760 # HTTPAccess2::Site -- manage a site(host and port)
761 #
762 class Site      # :nodoc:
763   attr_accessor :scheme
764   attr_accessor :host
765   attr_reader :port
766
767   def initialize(uri = nil)
768     if uri
769       @scheme = uri.scheme
770       @host = uri.host
771       @port = uri.port.to_i
772     else
773       @scheme = 'tcp'
774       @host = '0.0.0.0'
775       @port = 0
776     end
777   end
778
779   def addr
780     "#{@scheme}://#{@host}:#{@port.to_s}"
781   end
782
783   def port=(port)
784     @port = port.to_i
785   end
786
787   def ==(rhs)
788     if rhs.is_a?(Site)
789       ((@scheme == rhs.scheme) and (@host == rhs.host) and (@port == rhs.port))
790     else
791       false
792     end
793   end
794
795   def to_s
796     addr
797   end
798
799   def inspect
800     sprintf("#<%s:0x%x %s>", self.class.name, __id__, addr)
801   end
802 end
803
804
805 # HTTPAccess2::Connection -- magage a connection(one request and response to it).
806 #
807 class Connection        # :nodoc:
808   attr_accessor :async_thread
809
810   def initialize(header_queue = [], body_queue = [])
811     @headers = header_queue
812     @body = body_queue
813     @async_thread = nil
814     @queue = Queue.new
815   end
816
817   def finished?
818     if !@async_thread
819       # Not in async mode.
820       true
821     elsif @async_thread.alive?
822       # Working...
823       false
824     else
825       # Async thread have been finished.
826       @async_thread.join
827       true
828     end
829   end
830
831   def pop
832     @queue.pop
833   end
834
835   def push(result)
836     @queue.push(result)
837   end
838
839   def join
840     unless @async_thread
841       false
842     else
843       @async_thread.join
844     end
845   end
846 end
847
848
849 # HTTPAccess2::SessionManager -- manage several sessions.
850 #
851 class SessionManager    # :nodoc:
852   attr_accessor :agent_name     # Name of this client.
853   attr_accessor :from           # Owner of this client.
854
855   attr_accessor :protocol_version       # Requested protocol version
856   attr_accessor :chunk_size             # Chunk size for chunked request
857   attr_accessor :debug_dev              # Device for dumping log for debugging
858   attr_accessor :socket_sync            # Boolean value for Socket#sync
859
860   # These parameters are not used now...
861   attr_accessor :connect_timeout
862   attr_accessor :connect_retry          # Maximum retry count.  0 for infinite.
863   attr_accessor :send_timeout
864   attr_accessor :receive_timeout
865   attr_accessor :read_block_size
866
867   attr_accessor :ssl_config
868
869   def initialize
870     @proxy = nil
871
872     @agent_name = nil
873     @from = nil
874
875     @protocol_version = nil
876     @debug_dev = nil
877     @socket_sync = true
878     @chunk_size = 4096
879
880     @connect_timeout = 60
881     @connect_retry = 1
882     @send_timeout = 120
883     @receive_timeout = 60       # For each read_block_size bytes
884     @read_block_size = 8192
885
886     @ssl_config = nil
887
888     @sess_pool = []
889     @sess_pool_mutex = Mutex.new
890   end
891
892   def proxy=(proxy)
893     if proxy.nil?
894       @proxy = nil
895     else
896       @proxy = Site.new(proxy)
897     end
898   end
899
900   def query(req, proxy)
901     req.body.chunk_size = @chunk_size
902     dest_site = Site.new(req.header.request_uri)
903     proxy_site = if proxy
904         Site.new(proxy)
905       else
906         @proxy
907       end
908     sess = open(dest_site, proxy_site)
909     begin
910       sess.query(req)
911     rescue
912       sess.close
913       raise
914     end
915     sess
916   end
917
918   def reset(uri)
919     site = Site.new(uri)
920     close(site)
921   end
922
923   def reset_all
924     close_all
925   end
926
927   def keep(sess)
928     add_cached_session(sess)
929   end
930
931 private
932
933   def open(dest, proxy = nil)
934     sess = nil
935     if cached = get_cached_session(dest)
936       sess = cached
937     else
938       sess = Session.new(dest, @agent_name, @from)
939       sess.proxy = proxy
940       sess.socket_sync = @socket_sync
941       sess.requested_version = @protocol_version if @protocol_version
942       sess.connect_timeout = @connect_timeout
943       sess.connect_retry = @connect_retry
944       sess.send_timeout = @send_timeout
945       sess.receive_timeout = @receive_timeout
946       sess.read_block_size = @read_block_size
947       sess.ssl_config = @ssl_config
948       sess.debug_dev = @debug_dev
949     end
950     sess
951   end
952
953   def close_all
954     each_sess do |sess|
955       sess.close
956     end
957     @sess_pool.clear
958   end
959
960   def close(dest)
961     if cached = get_cached_session(dest)
962       cached.close
963       true
964     else
965       false
966     end
967   end
968
969   def get_cached_session(dest)
970     cached = nil
971     @sess_pool_mutex.synchronize do
972       new_pool = []
973       @sess_pool.each do |s|
974         if s.dest == dest
975           cached = s
976         else
977           new_pool << s
978         end
979       end
980       @sess_pool = new_pool
981     end
982     cached
983   end
984
985   def add_cached_session(sess)
986     @sess_pool_mutex.synchronize do
987       @sess_pool << sess
988     end
989   end
990
991   def each_sess
992     @sess_pool_mutex.synchronize do
993       @sess_pool.each do |sess|
994         yield(sess)
995       end
996     end
997   end
998 end
999
1000
1001 # HTTPAccess2::SSLSocketWrap
1002 #
1003 class SSLSocketWrap
1004   def initialize(socket, context, debug_dev = nil)
1005     unless SSLEnabled
1006       raise RuntimeError.new(
1007         "Ruby/OpenSSL module is required for https access.")
1008     end
1009     @context = context
1010     @socket = socket
1011     @ssl_socket = create_ssl_socket(@socket)
1012     @debug_dev = debug_dev
1013   end
1014
1015   def ssl_connect
1016     @ssl_socket.connect
1017   end
1018
1019   def post_connection_check(host)
1020     verify_mode = @context.verify_mode || OpenSSL::SSL::VERIFY_NONE
1021     if verify_mode == OpenSSL::SSL::VERIFY_NONE
1022       return
1023     elsif @ssl_socket.peer_cert.nil? and
1024         check_mask(verify_mode, OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT)
1025       raise OpenSSL::SSL::SSLError, "no peer cert"
1026     end
1027     hostname = host.host
1028     if @ssl_socket.respond_to?(:post_connection_check)
1029       @ssl_socket.post_connection_check(hostname)
1030     end
1031     @context.post_connection_check(@ssl_socket.peer_cert, hostname)
1032   end
1033
1034   def peer_cert
1035     @ssl_socket.peer_cert
1036   end
1037
1038   def addr
1039     @socket.addr
1040   end
1041
1042   def close
1043     @ssl_socket.close
1044     @socket.close
1045   end
1046
1047   def closed?
1048     @socket.closed?
1049   end
1050
1051   def eof?
1052     @ssl_socket.eof?
1053   end
1054
1055   def gets(*args)
1056     str = @ssl_socket.gets(*args)
1057     @debug_dev << str if @debug_dev
1058     str
1059   end
1060
1061   def read(*args)
1062     str = @ssl_socket.read(*args)
1063     @debug_dev << str if @debug_dev
1064     str
1065   end
1066
1067   def <<(str)
1068     rv = @ssl_socket.write(str)
1069     @debug_dev << str if @debug_dev
1070     rv
1071   end
1072
1073   def flush
1074     @ssl_socket.flush
1075   end
1076
1077   def sync
1078     @ssl_socket.sync
1079   end
1080
1081   def sync=(sync)
1082     @ssl_socket.sync = sync
1083   end
1084
1085 private
1086
1087   def check_mask(value, mask)
1088     value & mask == mask
1089   end
1090
1091   def create_ssl_socket(socket)
1092     ssl_socket = nil
1093     if OpenSSL::SSL.const_defined?("SSLContext")
1094       ctx = OpenSSL::SSL::SSLContext.new
1095       @context.set_context(ctx)
1096       ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
1097     else
1098       ssl_socket = OpenSSL::SSL::SSLSocket.new(socket)
1099       @context.set_context(ssl_socket)
1100     end
1101     ssl_socket
1102   end
1103 end
1104
1105
1106 # HTTPAccess2::DebugSocket -- debugging support
1107 #
1108 class DebugSocket < TCPSocket
1109   attr_accessor :debug_dev     # Device for logging.
1110
1111   class << self
1112     def create_socket(host, port, debug_dev)
1113       debug_dev << "! CONNECT TO #{host}:#{port}\n"
1114       socket = new(host, port)
1115       socket.debug_dev = debug_dev
1116       socket.log_connect
1117       socket
1118     end
1119
1120     private :new
1121   end
1122
1123   def initialize(*args)
1124     super
1125     @debug_dev = nil
1126   end
1127
1128   def log_connect
1129     @debug_dev << '! CONNECTION ESTABLISHED' << "\n"
1130   end
1131
1132   def close
1133     super
1134     @debug_dev << '! CONNECTION CLOSED' << "\n"
1135   end
1136
1137   def gets(*args)
1138     str = super
1139     @debug_dev << str if str
1140     str
1141   end
1142
1143   def read(*args)
1144     str = super
1145     @debug_dev << str if str
1146     str
1147   end
1148
1149   def <<(str)
1150     super
1151     @debug_dev << str
1152   end
1153 end
1154
1155
1156 # HTTPAccess2::Session -- manage http session with one site.
1157 #   One or more TCP sessions with the site may be created.
1158 #   Only 1 TCP session is live at the same time.
1159 #
1160 class Session   # :nodoc:
1161
1162   class Error < StandardError   # :nodoc:
1163   end
1164
1165   class InvalidState < Error    # :nodoc:
1166   end
1167
1168   class BadResponse < Error     # :nodoc:
1169   end
1170
1171   class KeepAliveDisconnected < Error   # :nodoc:
1172   end
1173
1174   attr_reader :dest                     # Destination site
1175   attr_reader :src                      # Source site
1176   attr_accessor :proxy                  # Proxy site
1177   attr_accessor :socket_sync            # Boolean value for Socket#sync
1178
1179   attr_accessor :requested_version      # Requested protocol version
1180
1181   attr_accessor :debug_dev              # Device for dumping log for debugging
1182
1183   # These session parameters are not used now...
1184   attr_accessor :connect_timeout
1185   attr_accessor :connect_retry
1186   attr_accessor :send_timeout
1187   attr_accessor :receive_timeout
1188   attr_accessor :read_block_size
1189
1190   attr_accessor :ssl_config
1191
1192   def initialize(dest, user_agent, from)
1193     @dest = dest
1194     @src = Site.new
1195     @proxy = nil
1196     @socket_sync = true
1197     @requested_version = nil
1198
1199     @debug_dev = nil
1200
1201     @connect_timeout = nil
1202     @connect_retry = 1
1203     @send_timeout = nil
1204     @receive_timeout = nil
1205     @read_block_size = nil
1206
1207     @ssl_config = nil
1208
1209     @user_agent = user_agent
1210     @from = from
1211     @state = :INIT
1212
1213     @requests = []
1214
1215     @status = nil
1216     @reason = nil
1217     @headers = []
1218
1219     @socket = nil
1220   end
1221
1222   # Send a request to the server
1223   def query(req)
1224     connect() if @state == :INIT
1225     begin
1226       timeout(@send_timeout) do
1227         set_header(req)
1228         req.dump(@socket)
1229         # flush the IO stream as IO::sync mode is false
1230         @socket.flush unless @socket_sync
1231       end
1232     rescue Errno::ECONNABORTED, Errno::ECONNRESET
1233       close
1234       raise KeepAliveDisconnected.new
1235     rescue
1236       if SSLEnabled and $!.is_a?(OpenSSL::SSL::SSLError)
1237         raise KeepAliveDisconnected.new
1238       elsif $!.is_a?(TimeoutError)
1239         close
1240         raise
1241       else
1242         raise
1243       end
1244     end
1245
1246     @state = :META if @state == :WAIT
1247     @next_connection = nil
1248     @requests.push(req)
1249   end
1250
1251   def close
1252     if !@socket.nil? and !@socket.closed?
1253       @socket.flush
1254       @socket.close
1255     end
1256     @state = :INIT
1257   end
1258
1259   def closed?
1260     @state == :INIT
1261   end
1262
1263   def get_status
1264     version = status = reason = nil
1265     begin
1266       if @state != :META
1267         raise RuntimeError.new("get_status must be called at the beginning of a session.")
1268       end
1269       version, status, reason = read_header()
1270     rescue
1271       close
1272       raise
1273     end
1274     return version, status, reason
1275   end
1276
1277   def get_header(&block)
1278     begin
1279       read_header() if @state == :META
1280     rescue
1281       close
1282       raise
1283     end
1284     if block
1285       @headers.each do |line|
1286         block.call(line)
1287       end
1288     else
1289       @headers
1290     end
1291   end
1292
1293   def eof?
1294     if !@content_length.nil?
1295       @content_length == 0
1296     elsif @readbuf.length > 0
1297       false
1298     else
1299       @socket.closed? or @socket.eof?
1300     end
1301   end
1302
1303   def get_data(&block)
1304     begin
1305       read_header() if @state == :META
1306       return nil if @state != :DATA
1307       unless @state == :DATA
1308         raise InvalidState.new('state != DATA')
1309       end
1310       data = nil
1311       if block
1312         until eof?
1313           begin
1314             timeout(@receive_timeout) do
1315               data = read_body()
1316             end
1317           rescue TimeoutError
1318             raise
1319           end
1320           block.call(data) if data
1321         end
1322         data = nil      # Calling with block returns nil.
1323       else
1324         begin
1325           timeout(@receive_timeout) do
1326             data = read_body()
1327           end
1328         rescue TimeoutError
1329           raise
1330         end
1331       end
1332     rescue
1333       close
1334       raise
1335     end
1336     if eof?
1337       if @next_connection
1338         @state = :WAIT
1339       else
1340         close
1341       end
1342     end
1343     data
1344   end
1345
1346 private
1347
1348   LibNames = "(#{RCS_FILE}/#{RCS_REVISION}, #{RUBY_VERSION_STRING})"
1349
1350   def set_header(req)
1351     req.version = @requested_version if @requested_version
1352     if @user_agent
1353       req.header.set('User-Agent', "#{@user_agent} #{LibNames}")
1354     end
1355     if @from
1356       req.header.set('From', @from)
1357     end
1358     req.header.set('Date', HTTP.http_date(Time.now))
1359   end
1360
1361   # Connect to the server
1362   def connect
1363     site = @proxy || @dest
1364     begin
1365       retry_number = 0
1366       timeout(@connect_timeout) do
1367         @socket = create_socket(site)
1368         begin
1369           @src.host = @socket.addr[3]
1370           @src.port = @socket.addr[1]
1371         rescue SocketError
1372           # to avoid IPSocket#addr problem on Mac OS X 10.3 + ruby-1.8.1.
1373           # cf. [ruby-talk:84909], [ruby-talk:95827]
1374         end
1375         if @dest.scheme == 'https'
1376           @socket = create_ssl_socket(@socket)
1377           connect_ssl_proxy(@socket) if @proxy
1378           @socket.ssl_connect
1379           @socket.post_connection_check(@dest)
1380         end
1381         # Use Ruby internal buffering instead of passing data immediatly
1382         # to the underlying layer
1383         # => we need to to call explicitely flush on the socket
1384         @socket.sync = @socket_sync
1385       end
1386     rescue TimeoutError
1387       if @connect_retry == 0
1388         retry
1389       else
1390         retry_number += 1
1391         retry if retry_number < @connect_retry
1392       end
1393       close
1394       raise
1395     end
1396
1397     @state = :WAIT
1398     @readbuf = ''
1399   end
1400
1401   def create_socket(site)
1402     begin
1403       if @debug_dev
1404         DebugSocket.create_socket(site.host, site.port, @debug_dev)
1405       else
1406         TCPSocket.new(site.host, site.port)
1407       end
1408     rescue SystemCallError => e
1409       e.message << " (#{site.host}, ##{site.port})"
1410       raise
1411     end
1412   end
1413
1414   # wrap socket with OpenSSL.
1415   def create_ssl_socket(raw_socket)
1416     SSLSocketWrap.new(raw_socket, @ssl_config, (DEBUG_SSL ? @debug_dev : nil))
1417   end
1418
1419   def connect_ssl_proxy(socket)
1420     socket << sprintf("CONNECT %s:%s HTTP/1.1\r\n\r\n", @dest.host, @dest.port)
1421     parse_header(socket)
1422     unless @status == 200
1423       raise BadResponse.new(
1424         "connect to ssl proxy failed with status #{@status} #{@reason}")
1425     end
1426   end
1427
1428   # Read status block.
1429   def read_header
1430     if @state == :DATA
1431       get_data {}
1432       check_state()
1433     end
1434     unless @state == :META
1435       raise InvalidState, 'state != :META'
1436     end
1437     parse_header(@socket)
1438     @content_length = nil
1439     @chunked = false
1440     @headers.each do |line|
1441       case line
1442       when /^Content-Length:\s+(\d+)/i
1443         @content_length = $1.to_i
1444       when /^Transfer-Encoding:\s+chunked/i
1445         @chunked = true
1446         @content_length = true  # how?
1447         @chunk_length = 0
1448       when /^Connection:\s+([\-\w]+)/i, /^Proxy-Connection:\s+([\-\w]+)/i
1449         case $1
1450         when /^Keep-Alive$/i
1451           @next_connection = true
1452         when /^close$/i
1453           @next_connection = false
1454         end
1455       else
1456         # Nothing to parse.
1457       end
1458     end
1459
1460     # Head of the request has been parsed.
1461     @state = :DATA
1462     req = @requests.shift
1463
1464     if req.header.request_method == 'HEAD'
1465       @content_length = 0
1466       if @next_connection
1467         @state = :WAIT
1468       else
1469         close
1470       end
1471     end
1472     @next_connection = false unless @content_length
1473     return [@version, @status, @reason]
1474   end
1475
1476   StatusParseRegexp = %r(\AHTTP/(\d+\.\d+)\s+(\d+)(?:\s+([^\r\n]+))?\r?\n\z)
1477   def parse_header(socket)
1478     begin
1479       timeout(@receive_timeout) do
1480         begin
1481           initial_line = socket.gets("\n")
1482           if initial_line.nil?
1483             raise KeepAliveDisconnected.new
1484           end
1485           if StatusParseRegexp =~ initial_line
1486             @version, @status, @reason = $1, $2.to_i, $3
1487             @next_connection = HTTP.keep_alive_enabled?(@version)
1488           else
1489             @version = '0.9'
1490             @status = nil
1491             @reason = nil
1492             @next_connection = false
1493             @readbuf = initial_line
1494             break
1495           end
1496           @headers = []
1497           while true
1498             line = socket.gets("\n")
1499             unless line
1500               raise BadResponse.new('Unexpected EOF.')
1501             end
1502             line.sub!(/\r?\n\z/, '')
1503             break if line.empty?
1504             if line.sub!(/^\t/, '')
1505               @headers[-1] << line
1506             else
1507               @headers.push(line)
1508             end
1509           end
1510         end while (@version == '1.1' && @status == 100)
1511       end
1512     rescue TimeoutError
1513       raise
1514     end
1515   end
1516
1517   def read_body
1518     if @chunked
1519       return read_body_chunked()
1520     elsif @content_length == 0
1521       return nil
1522     elsif @content_length
1523       return read_body_length()
1524     else
1525       if @readbuf.length > 0
1526         data = @readbuf
1527         @readbuf = ''
1528         return data
1529       else
1530         data = @socket.read(@read_block_size)
1531         data = nil if data.empty?       # Absorbing interface mismatch.
1532         return data
1533       end
1534     end
1535   end
1536
1537   def read_body_length
1538     maxbytes = @read_block_size
1539     if @readbuf.length > 0
1540       data = @readbuf[0, @content_length]
1541       @readbuf[0, @content_length] = ''
1542       @content_length -= data.length
1543       return data
1544     end
1545     maxbytes = @content_length if maxbytes > @content_length
1546     data = @socket.read(maxbytes)
1547     if data
1548       @content_length -= data.length
1549     else
1550       @content_length = 0
1551     end
1552     return data
1553   end
1554
1555   RS = "\r\n"
1556   ChunkDelimiter = "0#{RS}"
1557   ChunkTrailer = "0#{RS}#{RS}"
1558   def read_body_chunked
1559     if @chunk_length == 0
1560       until (i = @readbuf.index(RS))
1561         @readbuf << @socket.gets(RS)
1562       end
1563       i += 2
1564       if @readbuf[0, i] == ChunkDelimiter
1565         @content_length = 0
1566         unless @readbuf[0, 5] == ChunkTrailer
1567           @readbuf << @socket.gets(RS)
1568         end
1569         @readbuf[0, 5] = ''
1570         return nil
1571       end
1572       @chunk_length = @readbuf[0, i].hex
1573       @readbuf[0, i] = ''
1574     end
1575     while @readbuf.length < @chunk_length + 2
1576       @readbuf << @socket.read(@chunk_length + 2 - @readbuf.length)
1577     end
1578     data = @readbuf[0, @chunk_length]
1579     @readbuf[0, @chunk_length + 2] = ''
1580     @chunk_length = 0
1581     return data
1582   end
1583
1584   def check_state
1585     if @state == :DATA
1586       if eof?
1587         if @next_connection
1588           if @requests.empty?
1589             @state = :WAIT
1590           else
1591             @state = :META
1592           end
1593         end
1594       end
1595     end
1596   end
1597 end
1598
1599
1600 end
1601
1602
1603 HTTPClient = HTTPAccess2::Client
Note: See TracBrowser for help on using the browser.