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

root/trunk/lib/http-access2.rb

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