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

root/trunk/lib/http-access2.rb

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