reap 4.3.2 → 4.3.3

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