reap 4.3.2 → 4.3.3

Sign up to get free protection for your applications and to get access to all the features.
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