rubyforge 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +125 -0
- data/bin/rubyforge +36 -30
- data/gemspec.rb +23 -0
- data/install.rb +174 -0
- data/lib/http-access2.rb +1588 -0
- data/lib/http-access2/cookie.rb +538 -0
- data/lib/http-access2/http.rb +542 -0
- data/rubyforge-0.1.1.gem +0 -0
- metadata +15 -4
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 }
|
64
|
-
#{ PROGRAM }
|
65
|
-
#{ PROGRAM }
|
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
|
+
|
data/lib/http-access2.rb
ADDED
@@ -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
|