rubyforge 0.0.1 → 0.1.1

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