rss-client 2.0.9
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/MIT-LICENSE +21 -0
- data/README +79 -0
- data/Rakefile +83 -0
- data/bin/rssclient +206 -0
- data/install.rb +84 -0
- data/lib/rss-client.rb +109 -0
- data/lib/rss-client/http-access2.rb +2023 -0
- data/lib/rss-client/http-access2/cookie.rb +541 -0
- data/lib/rss-client/http-access2/http.rb +602 -0
- data/rake_tasks/annotations.rake +84 -0
- data/spec/helper.rb +10 -0
- data/spec/spec_http_access2.rb +13 -0
- data/spec/spec_rss_client.rb +22 -0
- metadata +88 -0
data/lib/rss-client.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'rss-client/http-access2'
|
4
|
+
require 'feed-normalizer'
|
5
|
+
require 'digest/sha1'
|
6
|
+
|
7
|
+
module RSSClient
|
8
|
+
|
9
|
+
attr_reader :rssc_error # save the last error message
|
10
|
+
attr_reader :rssc_raw # the raw RSS saved for additional processing
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def guid_for(rss_entry)
|
15
|
+
guid = rss_entry.urls.first
|
16
|
+
guid = rss_entry.id.to_s if rss_entry.id
|
17
|
+
return Digest::SHA1.hexdigest("--#{guid}--myBIGsecret")
|
18
|
+
end
|
19
|
+
|
20
|
+
def fix_content(content, site_link)
|
21
|
+
content = CGI.unescapeHTML(content) unless /</ =~ content
|
22
|
+
correct_urls(content, site_link)
|
23
|
+
end
|
24
|
+
|
25
|
+
def correct_urls(text, site_link)
|
26
|
+
site_link += '/' unless site_link[-1..-1] == '/'
|
27
|
+
text.gsub(%r{(src|href)=(['"])(?!http)([^'"]*?)}) do
|
28
|
+
first_part = "#{$1}=#{$2}"
|
29
|
+
url = $3
|
30
|
+
url = url[1..-1] if url[0..0] == '/'
|
31
|
+
"#{first_part}#{site_link}#{url}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# opts is an OpenStruct
|
36
|
+
# set last_fetch_time to nill to force fresh fetch
|
37
|
+
def get_feed(url, opts)
|
38
|
+
|
39
|
+
opts.extra = Hash.new
|
40
|
+
opts.extra["Connection"] = "close"
|
41
|
+
# some sites need User-Agent field
|
42
|
+
opts.extra["User-Agent"] = "RSSClient/2.0.9"
|
43
|
+
|
44
|
+
# Ask for changes (get 304 code in response)
|
45
|
+
# opts.since is Time::
|
46
|
+
if not opts.forceUpdate and opts.since
|
47
|
+
time = Time.parse(opts.since.to_s)
|
48
|
+
opts.extra["If-Modified-Since"] = time.httpdate() if time
|
49
|
+
end
|
50
|
+
|
51
|
+
begin
|
52
|
+
@rssc_raw = get_url(url, opts)
|
53
|
+
return nil unless @rssc_raw
|
54
|
+
|
55
|
+
case @rssc_raw.status
|
56
|
+
when 200
|
57
|
+
# good
|
58
|
+
when 301, 302
|
59
|
+
@rssc_raw = get_url(@rssc_raw.header["Location"], opts)
|
60
|
+
return nil unless @rssc_raw
|
61
|
+
|
62
|
+
when 304
|
63
|
+
# Not modified - nothing to do
|
64
|
+
return nil
|
65
|
+
|
66
|
+
when 401
|
67
|
+
raise RuntimeError, "access denied, " + @rssc_raw.header['WWW-Authenticate'].to_s
|
68
|
+
when 404
|
69
|
+
raise RuntimeError, "feed [ #{url} ] not found"
|
70
|
+
else
|
71
|
+
raise RuntimeError, "can't fetch feed (unknown response code: #{@rssc_raw.status})"
|
72
|
+
end
|
73
|
+
# Parse the raw RSS
|
74
|
+
return @rssc_raw.content ? FeedNormalizer::FeedNormalizer.parse(@rssc_raw.content) : nil
|
75
|
+
rescue RuntimeError => error
|
76
|
+
@rssc_error = error
|
77
|
+
return nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# opts is an OpenStruct
|
82
|
+
def get_url(url, opts)
|
83
|
+
begin
|
84
|
+
Timeout::timeout(opts.giveup) do
|
85
|
+
client = HTTPAccess2::Client.new
|
86
|
+
# FIXME: set additional client options here
|
87
|
+
client.ssl_config.verify_mode = nil
|
88
|
+
client.proxy = opts.proxy
|
89
|
+
uri = URI.parse(url.to_s)
|
90
|
+
client.set_basic_auth(url, uri.user, uri.password ) if uri.user and uri.password
|
91
|
+
return client.get(url, nil, opts.extra)
|
92
|
+
end
|
93
|
+
rescue URI::InvalidURIError => error
|
94
|
+
raise RuntimeError, "Invalid URL (#{error})"
|
95
|
+
|
96
|
+
rescue TimeoutError => error
|
97
|
+
raise RuntimeError, "Connection timeout (#{error})"
|
98
|
+
|
99
|
+
rescue SocketError => error
|
100
|
+
raise RuntimeError, "Socket error (#{error})"
|
101
|
+
|
102
|
+
rescue
|
103
|
+
raise RuntimeError, "can't fetch feed (#{$!})"
|
104
|
+
else
|
105
|
+
return nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,2023 @@
|
|
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
|
+
require 'digest/md5'
|
19
|
+
|
20
|
+
# Extra library
|
21
|
+
require 'rss-client/http-access2/http'
|
22
|
+
require 'rss-client/http-access2/cookie'
|
23
|
+
|
24
|
+
|
25
|
+
module HTTPAccess2
|
26
|
+
VERSION = '2.0.9'
|
27
|
+
RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
|
28
|
+
s = %w$Id: http-access2.rb 162 2007-07-04 07:28:33Z nahi $
|
29
|
+
RCS_FILE, RCS_REVISION = s[1][/.*(?=,v$)/], s[2]
|
30
|
+
|
31
|
+
SSLEnabled = begin
|
32
|
+
require 'openssl'
|
33
|
+
true
|
34
|
+
rescue LoadError
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
SSPIEnabled = begin
|
39
|
+
require 'win32/sspi'
|
40
|
+
true
|
41
|
+
rescue LoadError
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
DEBUG_SSL = true
|
46
|
+
|
47
|
+
|
48
|
+
module Util
|
49
|
+
def urify(uri)
|
50
|
+
if uri.is_a?(URI)
|
51
|
+
uri
|
52
|
+
else
|
53
|
+
URI.parse(uri.to_s)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def uri_part_of(uri, part)
|
58
|
+
((uri.scheme == part.scheme) and
|
59
|
+
(uri.host == part.host) and
|
60
|
+
(uri.port == part.port) and
|
61
|
+
uri.path.upcase.index(part.path.upcase) == 0)
|
62
|
+
end
|
63
|
+
module_function :uri_part_of
|
64
|
+
|
65
|
+
def uri_dirname(uri)
|
66
|
+
uri = uri.clone
|
67
|
+
uri.path = uri.path.sub(/\/[^\/]*\z/, '/')
|
68
|
+
uri
|
69
|
+
end
|
70
|
+
module_function :uri_dirname
|
71
|
+
|
72
|
+
def hash_find_value(hash)
|
73
|
+
hash.each do |k, v|
|
74
|
+
return v if yield(k, v)
|
75
|
+
end
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
module_function :hash_find_value
|
79
|
+
|
80
|
+
def parse_challenge_param(param_str)
|
81
|
+
param = {}
|
82
|
+
param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
|
83
|
+
key, value = str[0].scan(/\A([^=]+)=(.*)\z/)[0]
|
84
|
+
if /\A"(.*)"\z/ =~ value
|
85
|
+
value = $1.gsub(/\\(.)/, '\1')
|
86
|
+
end
|
87
|
+
param[key] = value
|
88
|
+
end
|
89
|
+
param
|
90
|
+
end
|
91
|
+
module_function :parse_challenge_param
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# DESCRIPTION
|
96
|
+
# HTTPAccess2::Client -- Client to retrieve web resources via HTTP.
|
97
|
+
#
|
98
|
+
# How to create your client.
|
99
|
+
# 1. Create simple client.
|
100
|
+
# clnt = HTTPAccess2::Client.new
|
101
|
+
#
|
102
|
+
# 2. Accessing resources through HTTP proxy.
|
103
|
+
# clnt = HTTPAccess2::Client.new("http://myproxy:8080")
|
104
|
+
#
|
105
|
+
# 3. Set User-Agent and From in HTTP request header.(nil means "No proxy")
|
106
|
+
# clnt = HTTPAccess2::Client.new(nil, "MyAgent", "nahi@keynauts.com")
|
107
|
+
#
|
108
|
+
# How to retrieve web resources.
|
109
|
+
# 1. Get content of specified URL.
|
110
|
+
# puts clnt.get_content("http://www.ruby-lang.org/en/")
|
111
|
+
#
|
112
|
+
# 2. Do HEAD request.
|
113
|
+
# res = clnt.head(uri)
|
114
|
+
#
|
115
|
+
# 3. Do GET request with query.
|
116
|
+
# res = clnt.get(uri)
|
117
|
+
#
|
118
|
+
# 4. Do POST request.
|
119
|
+
# res = clnt.post(uri)
|
120
|
+
# res = clnt.get|post|head(uri, proxy)
|
121
|
+
#
|
122
|
+
class Client
|
123
|
+
include Util
|
124
|
+
|
125
|
+
attr_reader :agent_name
|
126
|
+
attr_reader :from
|
127
|
+
attr_reader :ssl_config
|
128
|
+
attr_accessor :cookie_manager
|
129
|
+
attr_reader :test_loopback_response
|
130
|
+
attr_reader :request_filter
|
131
|
+
attr_reader :proxy_auth
|
132
|
+
attr_reader :www_auth
|
133
|
+
|
134
|
+
class << self
|
135
|
+
%w(get_content head get post put delete options trace).each do |name|
|
136
|
+
eval <<-EOD
|
137
|
+
def #{name}(*arg)
|
138
|
+
new.#{name}(*arg)
|
139
|
+
end
|
140
|
+
EOD
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class RetryableResponse < StandardError # :nodoc:
|
145
|
+
end
|
146
|
+
|
147
|
+
# SYNOPSIS
|
148
|
+
# Client.new(proxy = nil, agent_name = nil, from = nil)
|
149
|
+
#
|
150
|
+
# ARGS
|
151
|
+
# proxy A String of HTTP proxy URL. ex. "http://proxy:8080".
|
152
|
+
# agent_name A String for "User-Agent" HTTP request header.
|
153
|
+
# from A String for "From" HTTP request header.
|
154
|
+
#
|
155
|
+
# DESCRIPTION
|
156
|
+
# Create an instance.
|
157
|
+
# SSLConfig cannot be re-initialized. Create new client.
|
158
|
+
#
|
159
|
+
def initialize(proxy = nil, agent_name = nil, from = nil)
|
160
|
+
@proxy = nil # assigned later.
|
161
|
+
@no_proxy = nil
|
162
|
+
@agent_name = agent_name
|
163
|
+
@from = from
|
164
|
+
@www_auth = WWWAuth.new
|
165
|
+
@proxy_auth = ProxyAuth.new
|
166
|
+
@request_filter = [@proxy_auth, @www_auth]
|
167
|
+
@debug_dev = nil
|
168
|
+
@redirect_uri_callback = method(:default_redirect_uri_callback)
|
169
|
+
@test_loopback_response = []
|
170
|
+
@session_manager = SessionManager.new
|
171
|
+
@session_manager.agent_name = @agent_name
|
172
|
+
@session_manager.from = @from
|
173
|
+
@session_manager.ssl_config = @ssl_config = SSLConfig.new(self)
|
174
|
+
@cookie_manager = WebAgent::CookieManager.new
|
175
|
+
self.proxy = proxy
|
176
|
+
end
|
177
|
+
|
178
|
+
def debug_dev
|
179
|
+
@debug_dev
|
180
|
+
end
|
181
|
+
|
182
|
+
def debug_dev=(dev)
|
183
|
+
@debug_dev = dev
|
184
|
+
reset_all
|
185
|
+
@session_manager.debug_dev = dev
|
186
|
+
end
|
187
|
+
|
188
|
+
def protocol_version
|
189
|
+
@session_manager.protocol_version
|
190
|
+
end
|
191
|
+
|
192
|
+
def protocol_version=(protocol_version)
|
193
|
+
reset_all
|
194
|
+
@session_manager.protocol_version = protocol_version
|
195
|
+
end
|
196
|
+
|
197
|
+
def connect_timeout
|
198
|
+
@session_manager.connect_timeout
|
199
|
+
end
|
200
|
+
|
201
|
+
def connect_timeout=(connect_timeout)
|
202
|
+
reset_all
|
203
|
+
@session_manager.connect_timeout = connect_timeout
|
204
|
+
end
|
205
|
+
|
206
|
+
def send_timeout
|
207
|
+
@session_manager.send_timeout
|
208
|
+
end
|
209
|
+
|
210
|
+
def send_timeout=(send_timeout)
|
211
|
+
reset_all
|
212
|
+
@session_manager.send_timeout = send_timeout
|
213
|
+
end
|
214
|
+
|
215
|
+
def receive_timeout
|
216
|
+
@session_manager.receive_timeout
|
217
|
+
end
|
218
|
+
|
219
|
+
def receive_timeout=(receive_timeout)
|
220
|
+
reset_all
|
221
|
+
@session_manager.receive_timeout = receive_timeout
|
222
|
+
end
|
223
|
+
|
224
|
+
def proxy
|
225
|
+
@proxy
|
226
|
+
end
|
227
|
+
|
228
|
+
def proxy=(proxy)
|
229
|
+
if proxy.nil?
|
230
|
+
@proxy = nil
|
231
|
+
@proxy_auth.reset_challenge
|
232
|
+
else
|
233
|
+
@proxy = urify(proxy)
|
234
|
+
if @proxy.scheme == nil or @proxy.scheme.downcase != 'http' or
|
235
|
+
@proxy.host == nil or @proxy.port == nil
|
236
|
+
raise ArgumentError.new("unsupported proxy `#{proxy}'")
|
237
|
+
end
|
238
|
+
@proxy_auth.reset_challenge
|
239
|
+
if @proxy.user || @proxy.password
|
240
|
+
@proxy_auth.set_auth(@proxy.user, @proxy.password)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
reset_all
|
244
|
+
@proxy
|
245
|
+
end
|
246
|
+
|
247
|
+
def no_proxy
|
248
|
+
@no_proxy
|
249
|
+
end
|
250
|
+
|
251
|
+
def no_proxy=(no_proxy)
|
252
|
+
@no_proxy = no_proxy
|
253
|
+
reset_all
|
254
|
+
end
|
255
|
+
|
256
|
+
# if your ruby is older than 2005-09-06, do not set socket_sync = false to
|
257
|
+
# avoid an SSL socket blocking bug in openssl/buffering.rb.
|
258
|
+
def socket_sync=(socket_sync)
|
259
|
+
@session_manager.socket_sync = socket_sync
|
260
|
+
end
|
261
|
+
|
262
|
+
def set_auth(uri, user, passwd)
|
263
|
+
uri = urify(uri)
|
264
|
+
@www_auth.set_auth(uri, user, passwd)
|
265
|
+
reset_all
|
266
|
+
end
|
267
|
+
|
268
|
+
# for backward compatibility
|
269
|
+
def set_basic_auth(uri, user, passwd)
|
270
|
+
uri = urify(uri)
|
271
|
+
@www_auth.basic_auth.set(uri, user, passwd)
|
272
|
+
reset_all
|
273
|
+
end
|
274
|
+
|
275
|
+
def set_proxy_auth(user, passwd)
|
276
|
+
uri = urify(uri)
|
277
|
+
@proxy_auth.set_auth(user, passwd)
|
278
|
+
reset_all
|
279
|
+
end
|
280
|
+
|
281
|
+
def set_cookie_store(filename)
|
282
|
+
if @cookie_manager.cookies_file
|
283
|
+
raise RuntimeError.new("overriding cookie file location")
|
284
|
+
end
|
285
|
+
@cookie_manager.cookies_file = filename
|
286
|
+
@cookie_manager.load_cookies if filename
|
287
|
+
end
|
288
|
+
|
289
|
+
def save_cookie_store
|
290
|
+
@cookie_manager.save_cookies
|
291
|
+
end
|
292
|
+
|
293
|
+
def redirect_uri_callback=(redirect_uri_callback)
|
294
|
+
@redirect_uri_callback = redirect_uri_callback
|
295
|
+
end
|
296
|
+
|
297
|
+
# SYNOPSIS
|
298
|
+
# Client#get_content(uri, query = nil, extheader = {}, &block = nil)
|
299
|
+
#
|
300
|
+
# ARGS
|
301
|
+
# uri an_URI or a_string of uri to connect.
|
302
|
+
# query a_hash or an_array of query part. e.g. { "a" => "b" }.
|
303
|
+
# Give an array to pass multiple value like
|
304
|
+
# [["a" => "b"], ["a" => "c"]].
|
305
|
+
# extheader a_hash of extra headers like { "SOAPAction" => "urn:foo" }.
|
306
|
+
# &block Give a block to get chunked message-body of response like
|
307
|
+
# get_content(uri) { |chunked_body| ... }
|
308
|
+
# Size of each chunk may not be the same.
|
309
|
+
#
|
310
|
+
# DESCRIPTION
|
311
|
+
# Get a_sring of message-body of response.
|
312
|
+
#
|
313
|
+
def get_content(uri, query = nil, extheader = {}, &block)
|
314
|
+
follow_redirect(uri, query) { |uri, query|
|
315
|
+
get(uri, query, extheader, &block)
|
316
|
+
}.content
|
317
|
+
end
|
318
|
+
|
319
|
+
def post_content(uri, body = nil, extheader = {}, &block)
|
320
|
+
follow_redirect(uri, nil) { |uri, query|
|
321
|
+
post(uri, body, extheader, &block)
|
322
|
+
}.content
|
323
|
+
end
|
324
|
+
|
325
|
+
def strict_redirect_uri_callback(uri, res)
|
326
|
+
newuri = URI.parse(res.header['location'][0])
|
327
|
+
puts "Redirect to: #{newuri}" if $DEBUG
|
328
|
+
newuri
|
329
|
+
end
|
330
|
+
|
331
|
+
def default_redirect_uri_callback(uri, res)
|
332
|
+
newuri = URI.parse(res.header['location'][0])
|
333
|
+
unless newuri.is_a?(URI::HTTP)
|
334
|
+
newuri = uri + newuri
|
335
|
+
STDERR.puts(
|
336
|
+
"could be a relative URI in location header which is not recommended")
|
337
|
+
STDERR.puts(
|
338
|
+
"'The field value consists of a single absolute URI' in HTTP spec")
|
339
|
+
end
|
340
|
+
puts "Redirect to: #{newuri}" if $DEBUG
|
341
|
+
newuri
|
342
|
+
end
|
343
|
+
|
344
|
+
def head(uri, query = nil, extheader = {})
|
345
|
+
request('HEAD', uri, query, nil, extheader)
|
346
|
+
end
|
347
|
+
|
348
|
+
def get(uri, query = nil, extheader = {}, &block)
|
349
|
+
request('GET', uri, query, nil, extheader, &block)
|
350
|
+
end
|
351
|
+
|
352
|
+
def post(uri, body = nil, extheader = {}, &block)
|
353
|
+
request('POST', uri, nil, body, extheader, &block)
|
354
|
+
end
|
355
|
+
|
356
|
+
def put(uri, body = nil, extheader = {}, &block)
|
357
|
+
request('PUT', uri, nil, body, extheader, &block)
|
358
|
+
end
|
359
|
+
|
360
|
+
def delete(uri, extheader = {}, &block)
|
361
|
+
request('DELETE', uri, nil, nil, extheader, &block)
|
362
|
+
end
|
363
|
+
|
364
|
+
def options(uri, extheader = {}, &block)
|
365
|
+
request('OPTIONS', uri, nil, nil, extheader, &block)
|
366
|
+
end
|
367
|
+
|
368
|
+
def trace(uri, query = nil, body = nil, extheader = {}, &block)
|
369
|
+
request('TRACE', uri, query, body, extheader, &block)
|
370
|
+
end
|
371
|
+
|
372
|
+
def request(method, uri, query = nil, body = nil, extheader = {}, &block)
|
373
|
+
uri = urify(uri)
|
374
|
+
conn = Connection.new
|
375
|
+
res = nil
|
376
|
+
retry_count = 5
|
377
|
+
while retry_count > 0
|
378
|
+
begin
|
379
|
+
prepare_request(method, uri, query, body, extheader) do |req, proxy|
|
380
|
+
do_get_block(req, proxy, conn, &block)
|
381
|
+
end
|
382
|
+
res = conn.pop
|
383
|
+
break
|
384
|
+
rescue Client::RetryableResponse
|
385
|
+
res = conn.pop
|
386
|
+
retry_count -= 1
|
387
|
+
end
|
388
|
+
end
|
389
|
+
res
|
390
|
+
end
|
391
|
+
|
392
|
+
# Async interface.
|
393
|
+
|
394
|
+
def head_async(uri, query = nil, extheader = {})
|
395
|
+
request_async('HEAD', uri, query, nil, extheader)
|
396
|
+
end
|
397
|
+
|
398
|
+
def get_async(uri, query = nil, extheader = {})
|
399
|
+
request_async('GET', uri, query, nil, extheader)
|
400
|
+
end
|
401
|
+
|
402
|
+
def post_async(uri, body = nil, extheader = {})
|
403
|
+
request_async('POST', uri, nil, body, extheader)
|
404
|
+
end
|
405
|
+
|
406
|
+
def put_async(uri, body = nil, extheader = {})
|
407
|
+
request_async('PUT', uri, nil, body, extheader)
|
408
|
+
end
|
409
|
+
|
410
|
+
def delete_async(uri, extheader = {})
|
411
|
+
request_async('DELETE', uri, nil, nil, extheader)
|
412
|
+
end
|
413
|
+
|
414
|
+
def options_async(uri, extheader = {})
|
415
|
+
request_async('OPTIONS', uri, nil, nil, extheader)
|
416
|
+
end
|
417
|
+
|
418
|
+
def trace_async(uri, query = nil, body = nil, extheader = {})
|
419
|
+
request_async('TRACE', uri, query, body, extheader)
|
420
|
+
end
|
421
|
+
|
422
|
+
def request_async(method, uri, query = nil, body = nil, extheader = {})
|
423
|
+
uri = urify(uri)
|
424
|
+
conn = Connection.new
|
425
|
+
t = Thread.new(conn) { |tconn|
|
426
|
+
prepare_request(method, uri, query, body, extheader) do |req, proxy|
|
427
|
+
do_get_stream(req, proxy, tconn)
|
428
|
+
end
|
429
|
+
}
|
430
|
+
conn.async_thread = t
|
431
|
+
conn
|
432
|
+
end
|
433
|
+
|
434
|
+
##
|
435
|
+
# Multiple call interface.
|
436
|
+
|
437
|
+
# ???
|
438
|
+
|
439
|
+
##
|
440
|
+
# Management interface.
|
441
|
+
|
442
|
+
def reset(uri)
|
443
|
+
uri = urify(uri)
|
444
|
+
@session_manager.reset(uri)
|
445
|
+
end
|
446
|
+
|
447
|
+
def reset_all
|
448
|
+
@session_manager.reset_all
|
449
|
+
end
|
450
|
+
|
451
|
+
private
|
452
|
+
|
453
|
+
def follow_redirect(uri, query = nil)
|
454
|
+
retry_number = 0
|
455
|
+
while retry_number < 10
|
456
|
+
res = yield(uri, query)
|
457
|
+
if HTTP::Status.successful?(res.status)
|
458
|
+
return res
|
459
|
+
elsif HTTP::Status.redirect?(res.status)
|
460
|
+
uri = @redirect_uri_callback.call(uri, res)
|
461
|
+
query = nil
|
462
|
+
retry_number += 1
|
463
|
+
else
|
464
|
+
raise RuntimeError.new("Unexpected response: #{res.header.inspect}")
|
465
|
+
end
|
466
|
+
end
|
467
|
+
raise RuntimeError.new("Retry count exceeded.")
|
468
|
+
end
|
469
|
+
|
470
|
+
def prepare_request(method, uri, query, body, extheader)
|
471
|
+
proxy = no_proxy?(uri) ? nil : @proxy
|
472
|
+
begin
|
473
|
+
req = create_request(method, uri, query, body, extheader, !proxy.nil?)
|
474
|
+
yield(req, proxy)
|
475
|
+
rescue Session::KeepAliveDisconnected
|
476
|
+
req = create_request(method, uri, query, body, extheader, !proxy.nil?)
|
477
|
+
yield(req, proxy)
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
def create_request(method, uri, query, body, extheader, proxy)
|
482
|
+
if extheader.is_a?(Hash)
|
483
|
+
extheader = extheader.to_a
|
484
|
+
end
|
485
|
+
if cookies = @cookie_manager.find(uri)
|
486
|
+
extheader << ['Cookie', cookies]
|
487
|
+
end
|
488
|
+
boundary = nil
|
489
|
+
content_type = extheader.find { |key, value|
|
490
|
+
key.downcase == 'content-type'
|
491
|
+
}
|
492
|
+
if content_type && content_type[1] =~ /boundary=(.+)\z/
|
493
|
+
boundary = $1
|
494
|
+
end
|
495
|
+
req = HTTP::Message.new_request(method, uri, query, body, proxy, boundary)
|
496
|
+
extheader.each do |key, value|
|
497
|
+
req.header.set(key, value)
|
498
|
+
end
|
499
|
+
if content_type.nil? and !body.nil?
|
500
|
+
req.header.set('content-type', 'application/x-www-form-urlencoded')
|
501
|
+
end
|
502
|
+
req
|
503
|
+
end
|
504
|
+
|
505
|
+
NO_PROXY_HOSTS = ['localhost']
|
506
|
+
|
507
|
+
def no_proxy?(uri)
|
508
|
+
if !@proxy or NO_PROXY_HOSTS.include?(uri.host)
|
509
|
+
return true
|
510
|
+
end
|
511
|
+
unless @no_proxy
|
512
|
+
return false
|
513
|
+
end
|
514
|
+
@no_proxy.scan(/([^:,]+)(?::(\d+))?/) do |host, port|
|
515
|
+
if /(\A|\.)#{Regexp.quote(host)}\z/i =~ uri.host &&
|
516
|
+
(!port || uri.port == port.to_i)
|
517
|
+
return true
|
518
|
+
end
|
519
|
+
end
|
520
|
+
false
|
521
|
+
end
|
522
|
+
|
523
|
+
# !! CAUTION !!
|
524
|
+
# Method 'do_get*' runs under MT conditon. Be careful to change.
|
525
|
+
def do_get_block(req, proxy, conn, &block)
|
526
|
+
@request_filter.each do |filter|
|
527
|
+
filter.filter_request(req)
|
528
|
+
end
|
529
|
+
if str = @test_loopback_response.shift
|
530
|
+
dump_dummy_request_response(req.body.dump, str) if @debug_dev
|
531
|
+
conn.push(HTTP::Message.new_response(str))
|
532
|
+
return
|
533
|
+
end
|
534
|
+
content = ''
|
535
|
+
res = HTTP::Message.new_response(content)
|
536
|
+
@debug_dev << "= Request\n\n" if @debug_dev
|
537
|
+
sess = @session_manager.query(req, proxy)
|
538
|
+
res.peer_cert = sess.ssl_peer_cert
|
539
|
+
@debug_dev << "\n\n= Response\n\n" if @debug_dev
|
540
|
+
do_get_header(req, res, sess)
|
541
|
+
conn.push(res)
|
542
|
+
sess.get_data() do |str|
|
543
|
+
block.call(str) if block
|
544
|
+
content << str
|
545
|
+
end
|
546
|
+
@session_manager.keep(sess) unless sess.closed?
|
547
|
+
commands = @request_filter.collect { |filter|
|
548
|
+
filter.filter_response(req, res)
|
549
|
+
}
|
550
|
+
if commands.find { |command| command == :retry }
|
551
|
+
raise Client::RetryableResponse.new
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
def do_get_stream(req, proxy, conn)
|
556
|
+
@request_filter.each do |filter|
|
557
|
+
filter.filter_request(req)
|
558
|
+
end
|
559
|
+
if str = @test_loopback_response.shift
|
560
|
+
dump_dummy_request_response(req.body.dump, str) if @debug_dev
|
561
|
+
conn.push(HTTP::Message.new_response(str))
|
562
|
+
return
|
563
|
+
end
|
564
|
+
piper, pipew = IO.pipe
|
565
|
+
res = HTTP::Message.new_response(piper)
|
566
|
+
@debug_dev << "= Request\n\n" if @debug_dev
|
567
|
+
sess = @session_manager.query(req, proxy)
|
568
|
+
res.peer_cert = sess.ssl_peer_cert
|
569
|
+
@debug_dev << "\n\n= Response\n\n" if @debug_dev
|
570
|
+
do_get_header(req, res, sess)
|
571
|
+
conn.push(res)
|
572
|
+
sess.get_data() do |str|
|
573
|
+
pipew.syswrite(str)
|
574
|
+
end
|
575
|
+
pipew.close
|
576
|
+
@session_manager.keep(sess) unless sess.closed?
|
577
|
+
commands = @request_filter.collect { |filter|
|
578
|
+
filter.filter_response(req, res)
|
579
|
+
}
|
580
|
+
# ignore commands (not retryable in async mode)
|
581
|
+
end
|
582
|
+
|
583
|
+
def do_get_header(req, res, sess)
|
584
|
+
res.version, res.status, res.reason = sess.get_status
|
585
|
+
sess.get_header().each do |line|
|
586
|
+
unless /^([^:]+)\s*:\s*(.*)$/ =~ line
|
587
|
+
raise RuntimeError.new("Unparsable header: '#{line}'.") if $DEBUG
|
588
|
+
end
|
589
|
+
res.header.set($1, $2)
|
590
|
+
end
|
591
|
+
if res.header['set-cookie']
|
592
|
+
res.header['set-cookie'].each do |cookie|
|
593
|
+
@cookie_manager.parse(cookie, req.header.request_uri)
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
def dump_dummy_request_response(req, res)
|
599
|
+
@debug_dev << "= Dummy Request\n\n"
|
600
|
+
@debug_dev << req
|
601
|
+
@debug_dev << "\n\n= Dummy Response\n\n"
|
602
|
+
@debug_dev << res
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
|
607
|
+
# HTTPAccess2::SSLConfig -- SSL configuration of a client.
|
608
|
+
#
|
609
|
+
class SSLConfig # :nodoc:
|
610
|
+
attr_reader :client_cert
|
611
|
+
attr_reader :client_key
|
612
|
+
attr_reader :client_ca
|
613
|
+
|
614
|
+
attr_reader :verify_mode
|
615
|
+
attr_reader :verify_depth
|
616
|
+
attr_reader :verify_callback
|
617
|
+
|
618
|
+
attr_reader :timeout
|
619
|
+
attr_reader :options
|
620
|
+
attr_reader :ciphers
|
621
|
+
|
622
|
+
attr_reader :cert_store # don't use if you don't know what it is.
|
623
|
+
|
624
|
+
def initialize(client)
|
625
|
+
return unless SSLEnabled
|
626
|
+
@client = client
|
627
|
+
@cert_store = OpenSSL::X509::Store.new
|
628
|
+
@client_cert = @client_key = @client_ca = nil
|
629
|
+
@verify_mode = OpenSSL::SSL::VERIFY_PEER |
|
630
|
+
OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
631
|
+
@verify_depth = nil
|
632
|
+
@verify_callback = nil
|
633
|
+
@dest = nil
|
634
|
+
@timeout = nil
|
635
|
+
@options = defined?(OpenSSL::SSL::OP_ALL) ?
|
636
|
+
OpenSSL::SSL::OP_ALL | OpenSSL::SSL::OP_NO_SSLv2 : nil
|
637
|
+
@ciphers = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"
|
638
|
+
load_cacerts
|
639
|
+
end
|
640
|
+
|
641
|
+
def set_client_cert_file(cert_file, key_file)
|
642
|
+
@client_cert = OpenSSL::X509::Certificate.new(File.open(cert_file).read)
|
643
|
+
@client_key = OpenSSL::PKey::RSA.new(File.open(key_file).read)
|
644
|
+
change_notify
|
645
|
+
end
|
646
|
+
|
647
|
+
def clear_cert_store
|
648
|
+
@cert_store = OpenSSL::X509::Store.new
|
649
|
+
change_notify
|
650
|
+
end
|
651
|
+
|
652
|
+
def set_trust_ca(trust_ca_file_or_hashed_dir)
|
653
|
+
if FileTest.directory?(trust_ca_file_or_hashed_dir)
|
654
|
+
@cert_store.add_path(trust_ca_file_or_hashed_dir)
|
655
|
+
else
|
656
|
+
@cert_store.add_file(trust_ca_file_or_hashed_dir)
|
657
|
+
end
|
658
|
+
change_notify
|
659
|
+
end
|
660
|
+
|
661
|
+
def set_crl(crl_file)
|
662
|
+
crl = OpenSSL::X509::CRL.new(File.open(crl_file).read)
|
663
|
+
@cert_store.add_crl(crl)
|
664
|
+
@cert_store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
|
665
|
+
change_notify
|
666
|
+
end
|
667
|
+
|
668
|
+
def client_cert=(client_cert)
|
669
|
+
@client_cert = client_cert
|
670
|
+
change_notify
|
671
|
+
end
|
672
|
+
|
673
|
+
def client_key=(client_key)
|
674
|
+
@client_key = client_key
|
675
|
+
change_notify
|
676
|
+
end
|
677
|
+
|
678
|
+
def client_ca=(client_ca)
|
679
|
+
@client_ca = client_ca
|
680
|
+
change_notify
|
681
|
+
end
|
682
|
+
|
683
|
+
def verify_mode=(verify_mode)
|
684
|
+
@verify_mode = verify_mode
|
685
|
+
change_notify
|
686
|
+
end
|
687
|
+
|
688
|
+
def verify_depth=(verify_depth)
|
689
|
+
@verify_depth = verify_depth
|
690
|
+
change_notify
|
691
|
+
end
|
692
|
+
|
693
|
+
def verify_callback=(verify_callback)
|
694
|
+
@verify_callback = verify_callback
|
695
|
+
change_notify
|
696
|
+
end
|
697
|
+
|
698
|
+
def timeout=(timeout)
|
699
|
+
@timeout = timeout
|
700
|
+
change_notify
|
701
|
+
end
|
702
|
+
|
703
|
+
def options=(options)
|
704
|
+
@options = options
|
705
|
+
change_notify
|
706
|
+
end
|
707
|
+
|
708
|
+
def ciphers=(ciphers)
|
709
|
+
@ciphers = ciphers
|
710
|
+
change_notify
|
711
|
+
end
|
712
|
+
|
713
|
+
# don't use if you don't know what it is.
|
714
|
+
def cert_store=(cert_store)
|
715
|
+
@cert_store = cert_store
|
716
|
+
change_notify
|
717
|
+
end
|
718
|
+
|
719
|
+
# interfaces for SSLSocketWrap.
|
720
|
+
|
721
|
+
def set_context(ctx)
|
722
|
+
# Verification: Use Store#verify_callback instead of SSLContext#verify*?
|
723
|
+
ctx.cert_store = @cert_store
|
724
|
+
ctx.verify_mode = @verify_mode
|
725
|
+
ctx.verify_depth = @verify_depth if @verify_depth
|
726
|
+
ctx.verify_callback = @verify_callback || method(:default_verify_callback)
|
727
|
+
# SSL config
|
728
|
+
ctx.cert = @client_cert
|
729
|
+
ctx.key = @client_key
|
730
|
+
ctx.client_ca = @client_ca
|
731
|
+
ctx.timeout = @timeout
|
732
|
+
ctx.options = @options
|
733
|
+
ctx.ciphers = @ciphers
|
734
|
+
end
|
735
|
+
|
736
|
+
# this definition must match with the one in ext/openssl/lib/openssl/ssl.rb
|
737
|
+
def post_connection_check(peer_cert, hostname)
|
738
|
+
check_common_name = true
|
739
|
+
cert = peer_cert
|
740
|
+
cert.extensions.each{|ext|
|
741
|
+
next if ext.oid != "subjectAltName"
|
742
|
+
ext.value.split(/,\s+/).each{|general_name|
|
743
|
+
if /\ADNS:(.*)/ =~ general_name
|
744
|
+
check_common_name = false
|
745
|
+
reg = Regexp.escape($1).gsub(/\\\*/, "[^.]+")
|
746
|
+
return true if /\A#{reg}\z/i =~ hostname
|
747
|
+
elsif /\AIP Address:(.*)/ =~ general_name
|
748
|
+
check_common_name = false
|
749
|
+
return true if $1 == hostname
|
750
|
+
end
|
751
|
+
}
|
752
|
+
}
|
753
|
+
if check_common_name
|
754
|
+
cert.subject.to_a.each{|oid, value|
|
755
|
+
if oid == "CN"
|
756
|
+
reg = Regexp.escape(value).gsub(/\\\*/, "[^.]+")
|
757
|
+
return true if /\A#{reg}\z/i =~ hostname
|
758
|
+
end
|
759
|
+
}
|
760
|
+
end
|
761
|
+
raise OpenSSL::SSL::SSLError, "hostname not match"
|
762
|
+
end
|
763
|
+
|
764
|
+
# Default callback for verification: only dumps error.
|
765
|
+
def default_verify_callback(is_ok, ctx)
|
766
|
+
if $DEBUG
|
767
|
+
puts "#{ is_ok ? 'ok' : 'ng' }: #{ctx.current_cert.subject}"
|
768
|
+
end
|
769
|
+
if !is_ok
|
770
|
+
depth = ctx.error_depth
|
771
|
+
code = ctx.error
|
772
|
+
msg = ctx.error_string
|
773
|
+
STDERR.puts "at depth #{depth} - #{code}: #{msg}"
|
774
|
+
end
|
775
|
+
is_ok
|
776
|
+
end
|
777
|
+
|
778
|
+
# Sample callback method: CAUTION: does not check CRL/ARL.
|
779
|
+
def sample_verify_callback(is_ok, ctx)
|
780
|
+
unless is_ok
|
781
|
+
depth = ctx.error_depth
|
782
|
+
code = ctx.error
|
783
|
+
msg = ctx.error_string
|
784
|
+
STDERR.puts "at depth #{depth} - #{code}: #{msg}" if $DEBUG
|
785
|
+
return false
|
786
|
+
end
|
787
|
+
|
788
|
+
cert = ctx.current_cert
|
789
|
+
self_signed = false
|
790
|
+
ca = false
|
791
|
+
pathlen = nil
|
792
|
+
server_auth = true
|
793
|
+
self_signed = (cert.subject.cmp(cert.issuer) == 0)
|
794
|
+
|
795
|
+
# Check extensions whatever its criticality is. (sample)
|
796
|
+
cert.extensions.each do |ex|
|
797
|
+
case ex.oid
|
798
|
+
when 'basicConstraints'
|
799
|
+
/CA:(TRUE|FALSE), pathlen:(\d+)/ =~ ex.value
|
800
|
+
ca = ($1 == 'TRUE')
|
801
|
+
pathlen = $2.to_i
|
802
|
+
when 'keyUsage'
|
803
|
+
usage = ex.value.split(/\s*,\s*/)
|
804
|
+
ca = usage.include?('Certificate Sign')
|
805
|
+
server_auth = usage.include?('Key Encipherment')
|
806
|
+
when 'extendedKeyUsage'
|
807
|
+
usage = ex.value.split(/\s*,\s*/)
|
808
|
+
server_auth = usage.include?('Netscape Server Gated Crypto')
|
809
|
+
when 'nsCertType'
|
810
|
+
usage = ex.value.split(/\s*,\s*/)
|
811
|
+
ca = usage.include?('SSL CA')
|
812
|
+
server_auth = usage.include?('SSL Server')
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
if self_signed
|
817
|
+
STDERR.puts 'self signing CA' if $DEBUG
|
818
|
+
return true
|
819
|
+
elsif ca
|
820
|
+
STDERR.puts 'middle level CA' if $DEBUG
|
821
|
+
return true
|
822
|
+
elsif server_auth
|
823
|
+
STDERR.puts 'for server authentication' if $DEBUG
|
824
|
+
return true
|
825
|
+
end
|
826
|
+
|
827
|
+
return false
|
828
|
+
end
|
829
|
+
|
830
|
+
private
|
831
|
+
|
832
|
+
def change_notify
|
833
|
+
@client.reset_all
|
834
|
+
end
|
835
|
+
|
836
|
+
def load_cacerts
|
837
|
+
file = File.join(File.dirname(__FILE__), 'http-access2', 'cacert.p7s')
|
838
|
+
if File.exist?(file)
|
839
|
+
require 'openssl'
|
840
|
+
dist_cert =<<__DIST_CERT__
|
841
|
+
-----BEGIN CERTIFICATE-----
|
842
|
+
MIIC/jCCAmegAwIBAgIBADANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJKUDER
|
843
|
+
MA8GA1UECgwIY3Rvci5vcmcxFDASBgNVBAsMC0RldmVsb3BtZW50MRUwEwYDVQQD
|
844
|
+
DAxodHRwLWFjY2VzczIwHhcNMDcwNDI4MjM1NTI0WhcNMDkwNDI3MjM1NTI0WjBN
|
845
|
+
MQswCQYDVQQGEwJKUDERMA8GA1UECgwIY3Rvci5vcmcxFDASBgNVBAsMC0RldmVs
|
846
|
+
b3BtZW50MRUwEwYDVQQDDAxodHRwLWFjY2VzczIwgZ8wDQYJKoZIhvcNAQEBBQAD
|
847
|
+
gY0AMIGJAoGBALi66ujWtUCQm5HpMSyr/AAIFYVXC/dmn7C8TR/HMiUuW3waY4uX
|
848
|
+
LFqCDAGOX4gf177pX+b99t3mpaiAjJuqc858D9xEECzhDWgXdLbhRqWhUOble4RY
|
849
|
+
c1yWYC990IgXJDMKx7VAuZ3cBhdBxtlE9sb1ZCzmHQsvTy/OoRzcJCrTAgMBAAGj
|
850
|
+
ge0wgeowDwYDVR0TAQH/BAUwAwEB/zAxBglghkgBhvhCAQ0EJBYiUnVieS9PcGVu
|
851
|
+
U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUJNE0GGaRKmN2qhnO
|
852
|
+
FyBWVl4Qj6owDgYDVR0PAQH/BAQDAgEGMHUGA1UdIwRuMGyAFCTRNBhmkSpjdqoZ
|
853
|
+
zhcgVlZeEI+qoVGkTzBNMQswCQYDVQQGEwJKUDERMA8GA1UECgwIY3Rvci5vcmcx
|
854
|
+
FDASBgNVBAsMC0RldmVsb3BtZW50MRUwEwYDVQQDDAxodHRwLWFjY2VzczKCAQAw
|
855
|
+
DQYJKoZIhvcNAQEFBQADgYEAcBAe+z1vn9CnrN8zZkb+mN1Uw3S0vUeebHtlXNUa
|
856
|
+
orzaGDx5uOiisUyAgTAlnDYQJ0i5M2tQVCAazicAo1dbEM+F2pvhJfRrDJic+rR+
|
857
|
+
tmZsdMCfuxCsy8gnYUT9PXbO/RMz7nvXZqxAcW5Ks1Stv0cTVjxL5IjCkfvAr/fj
|
858
|
+
XQ0=
|
859
|
+
-----END CERTIFICATE-----
|
860
|
+
__DIST_CERT__
|
861
|
+
p7 = OpenSSL::PKCS7.read_smime(File.open(file) { |f| f.read })
|
862
|
+
selfcert = OpenSSL::X509::Certificate.new(dist_cert)
|
863
|
+
store = OpenSSL::X509::Store.new
|
864
|
+
store.add_cert(selfcert)
|
865
|
+
if (p7.verify(nil, store, p7.data, 0))
|
866
|
+
set_trust_ca(file)
|
867
|
+
else
|
868
|
+
STDERR.puts("cacerts: #{file} loading failed")
|
869
|
+
end
|
870
|
+
end
|
871
|
+
end
|
872
|
+
end
|
873
|
+
|
874
|
+
|
875
|
+
# HTTPAccess2::BasicAuth -- BasicAuth repository.
|
876
|
+
#
|
877
|
+
class BasicAuth # :nodoc:
|
878
|
+
attr_reader :scheme
|
879
|
+
|
880
|
+
def initialize
|
881
|
+
@cred = nil
|
882
|
+
@auth = {}
|
883
|
+
@challengeable = {}
|
884
|
+
@scheme = "Basic"
|
885
|
+
end
|
886
|
+
|
887
|
+
def reset_challenge
|
888
|
+
@challengeable.clear
|
889
|
+
end
|
890
|
+
|
891
|
+
# uri == nil for generic purpose
|
892
|
+
def set(uri, user, passwd)
|
893
|
+
if uri.nil?
|
894
|
+
@cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
895
|
+
else
|
896
|
+
uri = Util.uri_dirname(uri)
|
897
|
+
@auth[uri] = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
898
|
+
end
|
899
|
+
end
|
900
|
+
|
901
|
+
# send cred only when a given uri is;
|
902
|
+
# - child page of challengeable(got WWW-Authenticate before) uri and,
|
903
|
+
# - child page of defined credential
|
904
|
+
def get(req)
|
905
|
+
target_uri = req.header.request_uri
|
906
|
+
return nil unless @challengeable.find { |uri, ok|
|
907
|
+
Util.uri_part_of(target_uri, uri) and ok
|
908
|
+
}
|
909
|
+
return @cred if @cred
|
910
|
+
Util.hash_find_value(@auth) { |uri, cred|
|
911
|
+
Util.uri_part_of(target_uri, uri)
|
912
|
+
}
|
913
|
+
end
|
914
|
+
|
915
|
+
def challenge(uri, param_str)
|
916
|
+
@challengeable[uri] = true
|
917
|
+
true
|
918
|
+
end
|
919
|
+
end
|
920
|
+
|
921
|
+
|
922
|
+
# HTTPAccess2::DigestAuth
|
923
|
+
#
|
924
|
+
class DigestAuth # :nodoc:
|
925
|
+
attr_reader :scheme
|
926
|
+
|
927
|
+
def initialize
|
928
|
+
@auth = {}
|
929
|
+
@challenge = {}
|
930
|
+
@nonce_count = 0
|
931
|
+
@scheme = "Digest"
|
932
|
+
end
|
933
|
+
|
934
|
+
def reset_challenge
|
935
|
+
@challenge.clear
|
936
|
+
end
|
937
|
+
|
938
|
+
def set(uri, user, passwd)
|
939
|
+
uri = Util.uri_dirname(uri)
|
940
|
+
@auth[uri] = [user, passwd]
|
941
|
+
end
|
942
|
+
|
943
|
+
# send cred only when a given uri is;
|
944
|
+
# - child page of challengeable(got WWW-Authenticate before) uri and,
|
945
|
+
# - child page of defined credential
|
946
|
+
def get(req)
|
947
|
+
target_uri = req.header.request_uri
|
948
|
+
param = Util.hash_find_value(@challenge) { |uri, param|
|
949
|
+
Util.uri_part_of(target_uri, uri)
|
950
|
+
}
|
951
|
+
return nil unless param
|
952
|
+
user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
|
953
|
+
Util.uri_part_of(target_uri, uri)
|
954
|
+
}
|
955
|
+
return nil unless user
|
956
|
+
uri = req.header.request_uri
|
957
|
+
calc_cred(req.header.request_method, uri, user, passwd, param)
|
958
|
+
end
|
959
|
+
|
960
|
+
def challenge(uri, param_str)
|
961
|
+
@challenge[uri] = Util.parse_challenge_param(param_str)
|
962
|
+
true
|
963
|
+
end
|
964
|
+
|
965
|
+
private
|
966
|
+
|
967
|
+
# this method is implemented by sromano and posted to
|
968
|
+
# http://tools.assembla.com/breakout/wiki/DigestForSoap
|
969
|
+
# Thanks!
|
970
|
+
# supported algorithm: MD5 only for now
|
971
|
+
def calc_cred(method, uri, user, passwd, param)
|
972
|
+
a_1 = "#{user}:#{param['realm']}:#{passwd}"
|
973
|
+
a_2 = "#{method}:#{uri.path}"
|
974
|
+
@nonce_count += 1
|
975
|
+
message_digest = []
|
976
|
+
message_digest << Digest::MD5.hexdigest(a_1)
|
977
|
+
message_digest << param['nonce']
|
978
|
+
message_digest << ('%08x' % @nonce_count)
|
979
|
+
message_digest << param['nonce']
|
980
|
+
message_digest << param['qop']
|
981
|
+
message_digest << Digest::MD5.hexdigest(a_2)
|
982
|
+
header = []
|
983
|
+
header << "username=\"#{user}\""
|
984
|
+
header << "realm=\"#{param['realm']}\""
|
985
|
+
header << "nonce=\"#{param['nonce']}\""
|
986
|
+
header << "uri=\"#{uri.path}\""
|
987
|
+
header << "cnonce=\"#{param['nonce']}\""
|
988
|
+
header << "nc=#{'%08x' % @nonce_count}"
|
989
|
+
header << "qop=\"#{param['qop']}\""
|
990
|
+
header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
|
991
|
+
header << "algorithm=\"MD5\""
|
992
|
+
header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
|
993
|
+
header.join(", ")
|
994
|
+
end
|
995
|
+
end
|
996
|
+
|
997
|
+
|
998
|
+
# HTTPAccess2::NegotiateAuth
|
999
|
+
#
|
1000
|
+
class NegotiateAuth # :nodoc:
|
1001
|
+
attr_reader :scheme
|
1002
|
+
|
1003
|
+
def initialize
|
1004
|
+
@challenge = {}
|
1005
|
+
@scheme = "Negotiate"
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
def reset_challenge
|
1009
|
+
@challenge.clear
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
def get(req)
|
1013
|
+
return nil unless SSPIEnabled
|
1014
|
+
target_uri = req.header.request_uri
|
1015
|
+
param = Util.hash_find_value(@challenge) { |uri, param|
|
1016
|
+
Util.uri_part_of(target_uri, uri)
|
1017
|
+
}
|
1018
|
+
return nil unless param
|
1019
|
+
state = param[:state]
|
1020
|
+
authenticator = param[:authenticator]
|
1021
|
+
authphrase = param[:authphrase]
|
1022
|
+
case state
|
1023
|
+
when :init
|
1024
|
+
authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
|
1025
|
+
return authenticator.get_initial_token
|
1026
|
+
when :response
|
1027
|
+
return authenticator.complete_authentication(authphrase)
|
1028
|
+
end
|
1029
|
+
nil
|
1030
|
+
end
|
1031
|
+
|
1032
|
+
def challenge(uri, param_str)
|
1033
|
+
return false unless SSPIEnabled
|
1034
|
+
if param_str.nil? or @challenge[uri].nil?
|
1035
|
+
c = @challenge[uri] = {}
|
1036
|
+
c[:state] = :init
|
1037
|
+
c[:authenticator] = nil
|
1038
|
+
c[:authphrase] = ""
|
1039
|
+
else
|
1040
|
+
c = @challenge[uri]
|
1041
|
+
c[:state] = :response
|
1042
|
+
c[:authphrase] = param_str
|
1043
|
+
end
|
1044
|
+
true
|
1045
|
+
end
|
1046
|
+
end
|
1047
|
+
|
1048
|
+
|
1049
|
+
class AuthFilterBase # :nodoc:
|
1050
|
+
private
|
1051
|
+
|
1052
|
+
def parse_authentication_header(res, tag)
|
1053
|
+
challenge = res.header[tag]
|
1054
|
+
unless challenge
|
1055
|
+
raise RuntimeError.new("no #{tag} header exists: #{res}")
|
1056
|
+
end
|
1057
|
+
challenge.collect { |c| parse_challenge_header(c) }
|
1058
|
+
end
|
1059
|
+
|
1060
|
+
def parse_challenge_header(challenge)
|
1061
|
+
scheme, param_str = challenge.scan(/\A(\S+)(?:\s+(.*))?\z/)[0]
|
1062
|
+
if scheme.nil?
|
1063
|
+
raise RuntimeError.new("unsupported challenge: #{challenge}")
|
1064
|
+
end
|
1065
|
+
return scheme, param_str
|
1066
|
+
end
|
1067
|
+
end
|
1068
|
+
|
1069
|
+
|
1070
|
+
class WWWAuth < AuthFilterBase # :nodoc:
|
1071
|
+
attr_reader :basic_auth
|
1072
|
+
attr_reader :digest_auth
|
1073
|
+
attr_reader :negotiate_auth
|
1074
|
+
|
1075
|
+
def initialize
|
1076
|
+
@basic_auth = BasicAuth.new
|
1077
|
+
@digest_auth = DigestAuth.new
|
1078
|
+
@negotiate_auth = NegotiateAuth.new
|
1079
|
+
# sort authenticators by priority
|
1080
|
+
@authenticator = [@negotiate_auth, @digest_auth, @basic_auth]
|
1081
|
+
end
|
1082
|
+
|
1083
|
+
def reset_challenge
|
1084
|
+
@authenticator.each do |auth|
|
1085
|
+
auth.reset_challenge
|
1086
|
+
end
|
1087
|
+
end
|
1088
|
+
|
1089
|
+
def set_auth(uri, user, passwd)
|
1090
|
+
@basic_auth.set(uri, user, passwd)
|
1091
|
+
@digest_auth.set(uri, user, passwd)
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
def filter_request(req)
|
1095
|
+
@authenticator.each do |auth|
|
1096
|
+
if cred = auth.get(req)
|
1097
|
+
req.header.set('Authorization', auth.scheme + " " + cred)
|
1098
|
+
return
|
1099
|
+
end
|
1100
|
+
end
|
1101
|
+
end
|
1102
|
+
|
1103
|
+
def filter_response(req, res)
|
1104
|
+
command = nil
|
1105
|
+
uri = req.header.request_uri
|
1106
|
+
if res.status == HTTP::Status::UNAUTHORIZED
|
1107
|
+
if challenge = parse_authentication_header(res, 'www-authenticate')
|
1108
|
+
challenge.each do |scheme, param_str|
|
1109
|
+
@authenticator.each do |auth|
|
1110
|
+
if scheme.downcase == auth.scheme.downcase
|
1111
|
+
challengeable = auth.challenge(uri, param_str)
|
1112
|
+
command = :retry if challengeable
|
1113
|
+
end
|
1114
|
+
end
|
1115
|
+
end
|
1116
|
+
# ignore unknown authentication scheme
|
1117
|
+
end
|
1118
|
+
end
|
1119
|
+
command
|
1120
|
+
end
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
|
1124
|
+
class ProxyAuth < AuthFilterBase # :nodoc:
|
1125
|
+
attr_reader :basic_auth
|
1126
|
+
attr_reader :negotiate_auth
|
1127
|
+
|
1128
|
+
def initialize
|
1129
|
+
@basic_auth = BasicAuth.new
|
1130
|
+
@negotiate_auth = NegotiateAuth.new
|
1131
|
+
# sort authenticators by priority
|
1132
|
+
@authenticator = [@negotiate_auth, @basic_auth]
|
1133
|
+
end
|
1134
|
+
|
1135
|
+
def reset_challenge
|
1136
|
+
@authenticator.each do |auth|
|
1137
|
+
auth.reset_challenge
|
1138
|
+
end
|
1139
|
+
end
|
1140
|
+
|
1141
|
+
def set_auth(user, passwd)
|
1142
|
+
@basic_auth.set(nil, user, passwd)
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
def filter_request(req)
|
1146
|
+
@authenticator.each do |auth|
|
1147
|
+
if cred = auth.get(req)
|
1148
|
+
req.header.set('Proxy-Authorization', auth.scheme + " " + cred)
|
1149
|
+
return
|
1150
|
+
end
|
1151
|
+
end
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
def filter_response(req, res)
|
1155
|
+
command = nil
|
1156
|
+
uri = req.header.request_uri
|
1157
|
+
if res.status == HTTP::Status::PROXY_AUTHENTICATE_REQUIRED
|
1158
|
+
if challenge = parse_authentication_header(res, 'proxy-authenticate')
|
1159
|
+
challenge.each do |scheme, param_str|
|
1160
|
+
@authenticator.each do |auth|
|
1161
|
+
if scheme.downcase == auth.scheme.downcase
|
1162
|
+
challengeable = auth.challenge(uri, param_str)
|
1163
|
+
command = :retry if challengeable
|
1164
|
+
end
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
# ignore unknown authentication scheme
|
1168
|
+
end
|
1169
|
+
end
|
1170
|
+
command
|
1171
|
+
end
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
|
1175
|
+
# HTTPAccess2::Site -- manage a site(host and port)
|
1176
|
+
#
|
1177
|
+
class Site # :nodoc:
|
1178
|
+
attr_accessor :scheme
|
1179
|
+
attr_accessor :host
|
1180
|
+
attr_reader :port
|
1181
|
+
|
1182
|
+
def initialize(uri = nil)
|
1183
|
+
if uri
|
1184
|
+
@uri = uri
|
1185
|
+
@scheme = uri.scheme
|
1186
|
+
@host = uri.host
|
1187
|
+
@port = uri.port.to_i
|
1188
|
+
else
|
1189
|
+
@uri = nil
|
1190
|
+
@scheme = 'tcp'
|
1191
|
+
@host = '0.0.0.0'
|
1192
|
+
@port = 0
|
1193
|
+
end
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
def addr
|
1197
|
+
"#{@scheme}://#{@host}:#{@port.to_s}"
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
def port=(port)
|
1201
|
+
@port = port.to_i
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
def ==(rhs)
|
1205
|
+
if rhs.is_a?(Site)
|
1206
|
+
((@scheme == rhs.scheme) and (@host == rhs.host) and (@port == rhs.port))
|
1207
|
+
else
|
1208
|
+
false
|
1209
|
+
end
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
def to_s
|
1213
|
+
addr
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
def inspect
|
1217
|
+
sprintf("#<%s:0x%x %s>", self.class.name, __id__, @uri || addr)
|
1218
|
+
end
|
1219
|
+
end
|
1220
|
+
|
1221
|
+
|
1222
|
+
# HTTPAccess2::Connection -- magage a connection(one request and response to it).
|
1223
|
+
#
|
1224
|
+
class Connection # :nodoc:
|
1225
|
+
attr_accessor :async_thread
|
1226
|
+
|
1227
|
+
def initialize(header_queue = [], body_queue = [])
|
1228
|
+
@headers = header_queue
|
1229
|
+
@body = body_queue
|
1230
|
+
@async_thread = nil
|
1231
|
+
@queue = Queue.new
|
1232
|
+
end
|
1233
|
+
|
1234
|
+
def finished?
|
1235
|
+
if !@async_thread
|
1236
|
+
# Not in async mode.
|
1237
|
+
true
|
1238
|
+
elsif @async_thread.alive?
|
1239
|
+
# Working...
|
1240
|
+
false
|
1241
|
+
else
|
1242
|
+
# Async thread have been finished.
|
1243
|
+
@async_thread.join
|
1244
|
+
true
|
1245
|
+
end
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
def pop
|
1249
|
+
@queue.pop
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
def push(result)
|
1253
|
+
@queue.push(result)
|
1254
|
+
end
|
1255
|
+
|
1256
|
+
def join
|
1257
|
+
unless @async_thread
|
1258
|
+
false
|
1259
|
+
else
|
1260
|
+
@async_thread.join
|
1261
|
+
end
|
1262
|
+
end
|
1263
|
+
end
|
1264
|
+
|
1265
|
+
|
1266
|
+
# HTTPAccess2::SessionManager -- manage several sessions.
|
1267
|
+
#
|
1268
|
+
class SessionManager # :nodoc:
|
1269
|
+
attr_accessor :agent_name # Name of this client.
|
1270
|
+
attr_accessor :from # Owner of this client.
|
1271
|
+
|
1272
|
+
attr_accessor :protocol_version # Requested protocol version
|
1273
|
+
attr_accessor :chunk_size # Chunk size for chunked request
|
1274
|
+
attr_accessor :debug_dev # Device for dumping log for debugging
|
1275
|
+
attr_accessor :socket_sync # Boolean value for Socket#sync
|
1276
|
+
|
1277
|
+
# These parameters are not used now...
|
1278
|
+
attr_accessor :connect_timeout
|
1279
|
+
attr_accessor :connect_retry # Maximum retry count. 0 for infinite.
|
1280
|
+
attr_accessor :send_timeout
|
1281
|
+
attr_accessor :receive_timeout
|
1282
|
+
attr_accessor :read_block_size
|
1283
|
+
|
1284
|
+
attr_accessor :ssl_config
|
1285
|
+
|
1286
|
+
def initialize
|
1287
|
+
@proxy = nil
|
1288
|
+
|
1289
|
+
@agent_name = nil
|
1290
|
+
@from = nil
|
1291
|
+
|
1292
|
+
@protocol_version = nil
|
1293
|
+
@debug_dev = nil
|
1294
|
+
@socket_sync = true
|
1295
|
+
@chunk_size = 4096
|
1296
|
+
|
1297
|
+
@connect_timeout = 60
|
1298
|
+
@connect_retry = 1
|
1299
|
+
@send_timeout = 120
|
1300
|
+
@receive_timeout = 60 # For each read_block_size bytes
|
1301
|
+
@read_block_size = 8192
|
1302
|
+
|
1303
|
+
@ssl_config = nil
|
1304
|
+
|
1305
|
+
@sess_pool = []
|
1306
|
+
@sess_pool_mutex = Mutex.new
|
1307
|
+
end
|
1308
|
+
|
1309
|
+
def proxy=(proxy)
|
1310
|
+
if proxy.nil?
|
1311
|
+
@proxy = nil
|
1312
|
+
else
|
1313
|
+
@proxy = Site.new(proxy)
|
1314
|
+
end
|
1315
|
+
end
|
1316
|
+
|
1317
|
+
def query(req, proxy)
|
1318
|
+
req.body.chunk_size = @chunk_size
|
1319
|
+
dest_site = Site.new(req.header.request_uri)
|
1320
|
+
proxy_site = if proxy
|
1321
|
+
Site.new(proxy)
|
1322
|
+
else
|
1323
|
+
@proxy
|
1324
|
+
end
|
1325
|
+
sess = open(dest_site, proxy_site)
|
1326
|
+
begin
|
1327
|
+
sess.query(req)
|
1328
|
+
rescue
|
1329
|
+
sess.close
|
1330
|
+
raise
|
1331
|
+
end
|
1332
|
+
sess
|
1333
|
+
end
|
1334
|
+
|
1335
|
+
def reset(uri)
|
1336
|
+
site = Site.new(uri)
|
1337
|
+
close(site)
|
1338
|
+
end
|
1339
|
+
|
1340
|
+
def reset_all
|
1341
|
+
close_all
|
1342
|
+
end
|
1343
|
+
|
1344
|
+
def keep(sess)
|
1345
|
+
add_cached_session(sess)
|
1346
|
+
end
|
1347
|
+
|
1348
|
+
private
|
1349
|
+
|
1350
|
+
def open(dest, proxy = nil)
|
1351
|
+
sess = nil
|
1352
|
+
if cached = get_cached_session(dest)
|
1353
|
+
sess = cached
|
1354
|
+
else
|
1355
|
+
sess = Session.new(dest, @agent_name, @from)
|
1356
|
+
sess.proxy = proxy
|
1357
|
+
sess.socket_sync = @socket_sync
|
1358
|
+
sess.requested_version = @protocol_version if @protocol_version
|
1359
|
+
sess.connect_timeout = @connect_timeout
|
1360
|
+
sess.connect_retry = @connect_retry
|
1361
|
+
sess.send_timeout = @send_timeout
|
1362
|
+
sess.receive_timeout = @receive_timeout
|
1363
|
+
sess.read_block_size = @read_block_size
|
1364
|
+
sess.ssl_config = @ssl_config
|
1365
|
+
sess.debug_dev = @debug_dev
|
1366
|
+
end
|
1367
|
+
sess
|
1368
|
+
end
|
1369
|
+
|
1370
|
+
def close_all
|
1371
|
+
each_sess do |sess|
|
1372
|
+
sess.close
|
1373
|
+
end
|
1374
|
+
@sess_pool.clear
|
1375
|
+
end
|
1376
|
+
|
1377
|
+
def close(dest)
|
1378
|
+
if cached = get_cached_session(dest)
|
1379
|
+
cached.close
|
1380
|
+
true
|
1381
|
+
else
|
1382
|
+
false
|
1383
|
+
end
|
1384
|
+
end
|
1385
|
+
|
1386
|
+
def get_cached_session(dest)
|
1387
|
+
cached = nil
|
1388
|
+
@sess_pool_mutex.synchronize do
|
1389
|
+
new_pool = []
|
1390
|
+
@sess_pool.each do |s|
|
1391
|
+
if s.dest == dest
|
1392
|
+
cached = s
|
1393
|
+
else
|
1394
|
+
new_pool << s
|
1395
|
+
end
|
1396
|
+
end
|
1397
|
+
@sess_pool = new_pool
|
1398
|
+
end
|
1399
|
+
cached
|
1400
|
+
end
|
1401
|
+
|
1402
|
+
def add_cached_session(sess)
|
1403
|
+
@sess_pool_mutex.synchronize do
|
1404
|
+
@sess_pool << sess
|
1405
|
+
end
|
1406
|
+
end
|
1407
|
+
|
1408
|
+
def each_sess
|
1409
|
+
@sess_pool_mutex.synchronize do
|
1410
|
+
@sess_pool.each do |sess|
|
1411
|
+
yield(sess)
|
1412
|
+
end
|
1413
|
+
end
|
1414
|
+
end
|
1415
|
+
end
|
1416
|
+
|
1417
|
+
|
1418
|
+
# HTTPAccess2::SSLSocketWrap
|
1419
|
+
#
|
1420
|
+
class SSLSocketWrap
|
1421
|
+
def initialize(socket, context, debug_dev = nil)
|
1422
|
+
unless SSLEnabled
|
1423
|
+
raise RuntimeError.new(
|
1424
|
+
"Ruby/OpenSSL module is required for https access.")
|
1425
|
+
end
|
1426
|
+
@context = context
|
1427
|
+
@socket = socket
|
1428
|
+
@ssl_socket = create_ssl_socket(@socket)
|
1429
|
+
@debug_dev = debug_dev
|
1430
|
+
end
|
1431
|
+
|
1432
|
+
def ssl_connect
|
1433
|
+
@ssl_socket.connect
|
1434
|
+
end
|
1435
|
+
|
1436
|
+
def post_connection_check(host)
|
1437
|
+
verify_mode = @context.verify_mode || OpenSSL::SSL::VERIFY_NONE
|
1438
|
+
if verify_mode == OpenSSL::SSL::VERIFY_NONE
|
1439
|
+
return
|
1440
|
+
elsif @ssl_socket.peer_cert.nil? and
|
1441
|
+
check_mask(verify_mode, OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT)
|
1442
|
+
raise OpenSSL::SSL::SSLError, "no peer cert"
|
1443
|
+
end
|
1444
|
+
hostname = host.host
|
1445
|
+
if @ssl_socket.respond_to?(:post_connection_check)
|
1446
|
+
@ssl_socket.post_connection_check(hostname)
|
1447
|
+
else
|
1448
|
+
@context.post_connection_check(@ssl_socket.peer_cert, hostname)
|
1449
|
+
end
|
1450
|
+
end
|
1451
|
+
|
1452
|
+
def peer_cert
|
1453
|
+
@ssl_socket.peer_cert
|
1454
|
+
end
|
1455
|
+
|
1456
|
+
def addr
|
1457
|
+
@socket.addr
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
def close
|
1461
|
+
@ssl_socket.close
|
1462
|
+
@socket.close
|
1463
|
+
end
|
1464
|
+
|
1465
|
+
def closed?
|
1466
|
+
@socket.closed?
|
1467
|
+
end
|
1468
|
+
|
1469
|
+
def eof?
|
1470
|
+
@ssl_socket.eof?
|
1471
|
+
end
|
1472
|
+
|
1473
|
+
def gets(*args)
|
1474
|
+
str = @ssl_socket.gets(*args)
|
1475
|
+
@debug_dev << str if @debug_dev
|
1476
|
+
str
|
1477
|
+
end
|
1478
|
+
|
1479
|
+
def read(*args)
|
1480
|
+
str = @ssl_socket.read(*args)
|
1481
|
+
@debug_dev << str if @debug_dev
|
1482
|
+
str
|
1483
|
+
end
|
1484
|
+
|
1485
|
+
def <<(str)
|
1486
|
+
rv = @ssl_socket.write(str)
|
1487
|
+
@debug_dev << str if @debug_dev
|
1488
|
+
rv
|
1489
|
+
end
|
1490
|
+
|
1491
|
+
def flush
|
1492
|
+
@ssl_socket.flush
|
1493
|
+
end
|
1494
|
+
|
1495
|
+
def sync
|
1496
|
+
@ssl_socket.sync
|
1497
|
+
end
|
1498
|
+
|
1499
|
+
def sync=(sync)
|
1500
|
+
@ssl_socket.sync = sync
|
1501
|
+
end
|
1502
|
+
|
1503
|
+
private
|
1504
|
+
|
1505
|
+
def check_mask(value, mask)
|
1506
|
+
value & mask == mask
|
1507
|
+
end
|
1508
|
+
|
1509
|
+
def create_ssl_socket(socket)
|
1510
|
+
ssl_socket = nil
|
1511
|
+
if OpenSSL::SSL.const_defined?("SSLContext")
|
1512
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
1513
|
+
@context.set_context(ctx)
|
1514
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ctx)
|
1515
|
+
else
|
1516
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket)
|
1517
|
+
@context.set_context(ssl_socket)
|
1518
|
+
end
|
1519
|
+
ssl_socket
|
1520
|
+
end
|
1521
|
+
end
|
1522
|
+
|
1523
|
+
|
1524
|
+
# HTTPAccess2::DebugSocket -- debugging support
|
1525
|
+
#
|
1526
|
+
class DebugSocket < TCPSocket
|
1527
|
+
attr_accessor :debug_dev # Device for logging.
|
1528
|
+
|
1529
|
+
class << self
|
1530
|
+
def create_socket(host, port, debug_dev)
|
1531
|
+
debug_dev << "! CONNECT TO #{host}:#{port}\n"
|
1532
|
+
socket = new(host, port)
|
1533
|
+
socket.debug_dev = debug_dev
|
1534
|
+
socket.log_connect
|
1535
|
+
socket
|
1536
|
+
end
|
1537
|
+
|
1538
|
+
private :new
|
1539
|
+
end
|
1540
|
+
|
1541
|
+
def initialize(*args)
|
1542
|
+
super
|
1543
|
+
@debug_dev = nil
|
1544
|
+
end
|
1545
|
+
|
1546
|
+
def log_connect
|
1547
|
+
@debug_dev << '! CONNECTION ESTABLISHED' << "\n"
|
1548
|
+
end
|
1549
|
+
|
1550
|
+
def close
|
1551
|
+
super
|
1552
|
+
@debug_dev << '! CONNECTION CLOSED' << "\n"
|
1553
|
+
end
|
1554
|
+
|
1555
|
+
def gets(*args)
|
1556
|
+
str = super
|
1557
|
+
@debug_dev << str if str
|
1558
|
+
str
|
1559
|
+
end
|
1560
|
+
|
1561
|
+
def read(*args)
|
1562
|
+
str = super
|
1563
|
+
@debug_dev << str if str
|
1564
|
+
str
|
1565
|
+
end
|
1566
|
+
|
1567
|
+
def <<(str)
|
1568
|
+
super
|
1569
|
+
@debug_dev << str
|
1570
|
+
end
|
1571
|
+
end
|
1572
|
+
|
1573
|
+
|
1574
|
+
# HTTPAccess2::Session -- manage http session with one site.
|
1575
|
+
# One or more TCP sessions with the site may be created.
|
1576
|
+
# Only 1 TCP session is live at the same time.
|
1577
|
+
#
|
1578
|
+
class Session # :nodoc:
|
1579
|
+
|
1580
|
+
class Error < StandardError # :nodoc:
|
1581
|
+
end
|
1582
|
+
|
1583
|
+
class InvalidState < Error # :nodoc:
|
1584
|
+
end
|
1585
|
+
|
1586
|
+
class BadResponse < Error # :nodoc:
|
1587
|
+
end
|
1588
|
+
|
1589
|
+
class KeepAliveDisconnected < Error # :nodoc:
|
1590
|
+
end
|
1591
|
+
|
1592
|
+
attr_reader :dest # Destination site
|
1593
|
+
attr_reader :src # Source site
|
1594
|
+
attr_accessor :proxy # Proxy site
|
1595
|
+
attr_accessor :socket_sync # Boolean value for Socket#sync
|
1596
|
+
|
1597
|
+
attr_accessor :requested_version # Requested protocol version
|
1598
|
+
|
1599
|
+
attr_accessor :debug_dev # Device for dumping log for debugging
|
1600
|
+
|
1601
|
+
# These session parameters are not used now...
|
1602
|
+
attr_accessor :connect_timeout
|
1603
|
+
attr_accessor :connect_retry
|
1604
|
+
attr_accessor :send_timeout
|
1605
|
+
attr_accessor :receive_timeout
|
1606
|
+
attr_accessor :read_block_size
|
1607
|
+
|
1608
|
+
attr_accessor :ssl_config
|
1609
|
+
attr_reader :ssl_peer_cert
|
1610
|
+
|
1611
|
+
def initialize(dest, user_agent, from)
|
1612
|
+
@dest = dest
|
1613
|
+
@src = Site.new
|
1614
|
+
@proxy = nil
|
1615
|
+
@socket_sync = true
|
1616
|
+
@requested_version = nil
|
1617
|
+
|
1618
|
+
@debug_dev = nil
|
1619
|
+
|
1620
|
+
@connect_timeout = nil
|
1621
|
+
@connect_retry = 1
|
1622
|
+
@send_timeout = nil
|
1623
|
+
@receive_timeout = nil
|
1624
|
+
@read_block_size = nil
|
1625
|
+
|
1626
|
+
@ssl_config = nil
|
1627
|
+
@ssl_peer_cert = nil
|
1628
|
+
|
1629
|
+
@user_agent = user_agent
|
1630
|
+
@from = from
|
1631
|
+
@state = :INIT
|
1632
|
+
|
1633
|
+
@requests = []
|
1634
|
+
|
1635
|
+
@status = nil
|
1636
|
+
@reason = nil
|
1637
|
+
@headers = []
|
1638
|
+
|
1639
|
+
@socket = nil
|
1640
|
+
end
|
1641
|
+
|
1642
|
+
# Send a request to the server
|
1643
|
+
def query(req)
|
1644
|
+
connect() if @state == :INIT
|
1645
|
+
begin
|
1646
|
+
timeout(@send_timeout) do
|
1647
|
+
set_header(req)
|
1648
|
+
req.dump(@socket)
|
1649
|
+
# flush the IO stream as IO::sync mode is false
|
1650
|
+
@socket.flush unless @socket_sync
|
1651
|
+
end
|
1652
|
+
rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE
|
1653
|
+
close
|
1654
|
+
raise KeepAliveDisconnected.new
|
1655
|
+
rescue
|
1656
|
+
if SSLEnabled and $!.is_a?(OpenSSL::SSL::SSLError)
|
1657
|
+
raise KeepAliveDisconnected.new
|
1658
|
+
elsif $!.is_a?(TimeoutError)
|
1659
|
+
close
|
1660
|
+
raise
|
1661
|
+
else
|
1662
|
+
raise
|
1663
|
+
end
|
1664
|
+
end
|
1665
|
+
|
1666
|
+
@state = :META if @state == :WAIT
|
1667
|
+
@next_connection = nil
|
1668
|
+
@requests.push(req)
|
1669
|
+
end
|
1670
|
+
|
1671
|
+
def close
|
1672
|
+
if !@socket.nil? and !@socket.closed?
|
1673
|
+
@socket.flush rescue nil # try to rescue OpenSSL::SSL::SSLError: cf. #120
|
1674
|
+
@socket.close
|
1675
|
+
end
|
1676
|
+
@state = :INIT
|
1677
|
+
end
|
1678
|
+
|
1679
|
+
def closed?
|
1680
|
+
@state == :INIT
|
1681
|
+
end
|
1682
|
+
|
1683
|
+
def get_status
|
1684
|
+
version = status = reason = nil
|
1685
|
+
begin
|
1686
|
+
if @state != :META
|
1687
|
+
raise RuntimeError.new("get_status must be called at the beginning of a session.")
|
1688
|
+
end
|
1689
|
+
version, status, reason = read_header()
|
1690
|
+
rescue
|
1691
|
+
close
|
1692
|
+
raise
|
1693
|
+
end
|
1694
|
+
return version, status, reason
|
1695
|
+
end
|
1696
|
+
|
1697
|
+
def get_header(&block)
|
1698
|
+
begin
|
1699
|
+
read_header() if @state == :META
|
1700
|
+
rescue
|
1701
|
+
close
|
1702
|
+
raise
|
1703
|
+
end
|
1704
|
+
if block
|
1705
|
+
@headers.each do |line|
|
1706
|
+
block.call(line)
|
1707
|
+
end
|
1708
|
+
else
|
1709
|
+
@headers
|
1710
|
+
end
|
1711
|
+
end
|
1712
|
+
|
1713
|
+
def eof?
|
1714
|
+
if !@content_length.nil?
|
1715
|
+
@content_length == 0
|
1716
|
+
elsif @readbuf.length > 0
|
1717
|
+
false
|
1718
|
+
else
|
1719
|
+
@socket.closed? or @socket.eof?
|
1720
|
+
end
|
1721
|
+
end
|
1722
|
+
|
1723
|
+
def get_data(&block)
|
1724
|
+
begin
|
1725
|
+
read_header() if @state == :META
|
1726
|
+
return nil if @state != :DATA
|
1727
|
+
unless @state == :DATA
|
1728
|
+
raise InvalidState.new('state != DATA')
|
1729
|
+
end
|
1730
|
+
data = nil
|
1731
|
+
if block
|
1732
|
+
while true
|
1733
|
+
begin
|
1734
|
+
timeout(@receive_timeout) do
|
1735
|
+
data = read_body()
|
1736
|
+
end
|
1737
|
+
rescue TimeoutError
|
1738
|
+
raise
|
1739
|
+
end
|
1740
|
+
block.call(data) if data
|
1741
|
+
break if eof?
|
1742
|
+
end
|
1743
|
+
data = nil # Calling with block returns nil.
|
1744
|
+
else
|
1745
|
+
begin
|
1746
|
+
timeout(@receive_timeout) do
|
1747
|
+
data = read_body()
|
1748
|
+
end
|
1749
|
+
rescue TimeoutError
|
1750
|
+
raise
|
1751
|
+
end
|
1752
|
+
end
|
1753
|
+
rescue
|
1754
|
+
close
|
1755
|
+
raise
|
1756
|
+
end
|
1757
|
+
if eof?
|
1758
|
+
if @next_connection
|
1759
|
+
@state = :WAIT
|
1760
|
+
else
|
1761
|
+
close
|
1762
|
+
end
|
1763
|
+
end
|
1764
|
+
data
|
1765
|
+
end
|
1766
|
+
|
1767
|
+
private
|
1768
|
+
|
1769
|
+
LibNames = "(#{RCS_FILE}/#{RCS_REVISION}, #{RUBY_VERSION_STRING})"
|
1770
|
+
|
1771
|
+
def set_header(req)
|
1772
|
+
req.version = @requested_version if @requested_version
|
1773
|
+
if @user_agent
|
1774
|
+
req.header.set('User-Agent', "#{@user_agent} #{LibNames}")
|
1775
|
+
end
|
1776
|
+
if @from
|
1777
|
+
req.header.set('From', @from)
|
1778
|
+
end
|
1779
|
+
req.header.set('Date', HTTP.http_date(Time.now))
|
1780
|
+
end
|
1781
|
+
|
1782
|
+
# Connect to the server
|
1783
|
+
def connect
|
1784
|
+
site = @proxy || @dest
|
1785
|
+
begin
|
1786
|
+
retry_number = 0
|
1787
|
+
timeout(@connect_timeout) do
|
1788
|
+
@socket = create_socket(site)
|
1789
|
+
begin
|
1790
|
+
@src.host = @socket.addr[3]
|
1791
|
+
@src.port = @socket.addr[1]
|
1792
|
+
rescue SocketError
|
1793
|
+
# to avoid IPSocket#addr problem on Mac OS X 10.3 + ruby-1.8.1.
|
1794
|
+
# cf. [ruby-talk:84909], [ruby-talk:95827]
|
1795
|
+
end
|
1796
|
+
if @dest.scheme == 'https'
|
1797
|
+
@socket = create_ssl_socket(@socket)
|
1798
|
+
connect_ssl_proxy(@socket) if @proxy
|
1799
|
+
@socket.ssl_connect
|
1800
|
+
@socket.post_connection_check(@dest)
|
1801
|
+
@ssl_peer_cert = @socket.peer_cert
|
1802
|
+
end
|
1803
|
+
# Use Ruby internal buffering instead of passing data immediatly
|
1804
|
+
# to the underlying layer
|
1805
|
+
# => we need to to call explicitely flush on the socket
|
1806
|
+
@socket.sync = @socket_sync
|
1807
|
+
end
|
1808
|
+
rescue TimeoutError
|
1809
|
+
if @connect_retry == 0
|
1810
|
+
retry
|
1811
|
+
else
|
1812
|
+
retry_number += 1
|
1813
|
+
retry if retry_number < @connect_retry
|
1814
|
+
end
|
1815
|
+
close
|
1816
|
+
raise
|
1817
|
+
end
|
1818
|
+
|
1819
|
+
@state = :WAIT
|
1820
|
+
@readbuf = ''
|
1821
|
+
end
|
1822
|
+
|
1823
|
+
def create_socket(site)
|
1824
|
+
begin
|
1825
|
+
if @debug_dev
|
1826
|
+
DebugSocket.create_socket(site.host, site.port, @debug_dev)
|
1827
|
+
else
|
1828
|
+
TCPSocket.new(site.host, site.port)
|
1829
|
+
end
|
1830
|
+
rescue SystemCallError => e
|
1831
|
+
e.message << " (#{site})"
|
1832
|
+
raise
|
1833
|
+
rescue SocketError => e
|
1834
|
+
e.message << " (#{site})"
|
1835
|
+
raise
|
1836
|
+
end
|
1837
|
+
end
|
1838
|
+
|
1839
|
+
# wrap socket with OpenSSL.
|
1840
|
+
def create_ssl_socket(raw_socket)
|
1841
|
+
SSLSocketWrap.new(raw_socket, @ssl_config, (DEBUG_SSL ? @debug_dev : nil))
|
1842
|
+
end
|
1843
|
+
|
1844
|
+
def connect_ssl_proxy(socket)
|
1845
|
+
socket << sprintf("CONNECT %s:%s HTTP/1.1\r\n\r\n", @dest.host, @dest.port)
|
1846
|
+
parse_header(socket)
|
1847
|
+
unless @status == 200
|
1848
|
+
raise BadResponse.new(
|
1849
|
+
"connect to ssl proxy failed with status #{@status} #{@reason}")
|
1850
|
+
end
|
1851
|
+
end
|
1852
|
+
|
1853
|
+
# Read status block.
|
1854
|
+
def read_header
|
1855
|
+
if @state == :DATA
|
1856
|
+
get_data {}
|
1857
|
+
check_state()
|
1858
|
+
end
|
1859
|
+
unless @state == :META
|
1860
|
+
raise InvalidState, 'state != :META'
|
1861
|
+
end
|
1862
|
+
parse_header(@socket)
|
1863
|
+
@content_length = nil
|
1864
|
+
@chunked = false
|
1865
|
+
@headers.each do |line|
|
1866
|
+
case line
|
1867
|
+
when /^Content-Length:\s+(\d+)/i
|
1868
|
+
@content_length = $1.to_i
|
1869
|
+
when /^Transfer-Encoding:\s+chunked/i
|
1870
|
+
@chunked = true
|
1871
|
+
@content_length = true # how?
|
1872
|
+
@chunk_length = 0
|
1873
|
+
when /^Connection:\s+([\-\w]+)/i, /^Proxy-Connection:\s+([\-\w]+)/i
|
1874
|
+
case $1
|
1875
|
+
when /^Keep-Alive$/i
|
1876
|
+
@next_connection = true
|
1877
|
+
when /^close$/i
|
1878
|
+
@next_connection = false
|
1879
|
+
end
|
1880
|
+
else
|
1881
|
+
# Nothing to parse.
|
1882
|
+
end
|
1883
|
+
end
|
1884
|
+
|
1885
|
+
# Head of the request has been parsed.
|
1886
|
+
@state = :DATA
|
1887
|
+
req = @requests.shift
|
1888
|
+
|
1889
|
+
if req.header.request_method == 'HEAD'
|
1890
|
+
@content_length = 0
|
1891
|
+
if @next_connection
|
1892
|
+
@state = :WAIT
|
1893
|
+
else
|
1894
|
+
close
|
1895
|
+
end
|
1896
|
+
end
|
1897
|
+
@next_connection = false unless @content_length
|
1898
|
+
return [@version, @status, @reason]
|
1899
|
+
end
|
1900
|
+
|
1901
|
+
StatusParseRegexp = %r(\AHTTP/(\d+\.\d+)\s+(\d+)(?:\s+([^\r\n]+))?\r?\n\z)
|
1902
|
+
def parse_header(socket)
|
1903
|
+
begin
|
1904
|
+
timeout(@receive_timeout) do
|
1905
|
+
begin
|
1906
|
+
initial_line = socket.gets("\n")
|
1907
|
+
if initial_line.nil?
|
1908
|
+
raise KeepAliveDisconnected.new
|
1909
|
+
end
|
1910
|
+
if StatusParseRegexp =~ initial_line
|
1911
|
+
@version, @status, @reason = $1, $2.to_i, $3
|
1912
|
+
@next_connection = HTTP.keep_alive_enabled?(@version)
|
1913
|
+
else
|
1914
|
+
@version = '0.9'
|
1915
|
+
@status = nil
|
1916
|
+
@reason = nil
|
1917
|
+
@next_connection = false
|
1918
|
+
@readbuf = initial_line
|
1919
|
+
break
|
1920
|
+
end
|
1921
|
+
@headers = []
|
1922
|
+
while true
|
1923
|
+
line = socket.gets("\n")
|
1924
|
+
unless line
|
1925
|
+
raise BadResponse.new('Unexpected EOF.')
|
1926
|
+
end
|
1927
|
+
line.sub!(/\r?\n\z/, '')
|
1928
|
+
break if line.empty?
|
1929
|
+
if line.sub!(/^\t/, '')
|
1930
|
+
@headers[-1] << line
|
1931
|
+
else
|
1932
|
+
@headers.push(line)
|
1933
|
+
end
|
1934
|
+
end
|
1935
|
+
end while (@version == '1.1' && @status == 100)
|
1936
|
+
end
|
1937
|
+
rescue TimeoutError
|
1938
|
+
raise
|
1939
|
+
end
|
1940
|
+
end
|
1941
|
+
|
1942
|
+
def read_body
|
1943
|
+
if @chunked
|
1944
|
+
return read_body_chunked()
|
1945
|
+
elsif @content_length == 0
|
1946
|
+
return nil
|
1947
|
+
elsif @content_length
|
1948
|
+
return read_body_length()
|
1949
|
+
else
|
1950
|
+
if @readbuf.length > 0
|
1951
|
+
data = @readbuf
|
1952
|
+
@readbuf = ''
|
1953
|
+
return data
|
1954
|
+
else
|
1955
|
+
data = @socket.read(@read_block_size)
|
1956
|
+
data = nil if data and data.empty? # Absorbing interface mismatch.
|
1957
|
+
return data
|
1958
|
+
end
|
1959
|
+
end
|
1960
|
+
end
|
1961
|
+
|
1962
|
+
def read_body_length
|
1963
|
+
maxbytes = @read_block_size
|
1964
|
+
if @readbuf.length > 0
|
1965
|
+
data = @readbuf[0, @content_length]
|
1966
|
+
@readbuf[0, @content_length] = ''
|
1967
|
+
@content_length -= data.length
|
1968
|
+
return data
|
1969
|
+
end
|
1970
|
+
maxbytes = @content_length if maxbytes > @content_length
|
1971
|
+
data = @socket.read(maxbytes)
|
1972
|
+
if data
|
1973
|
+
@content_length -= data.length
|
1974
|
+
else
|
1975
|
+
@content_length = 0
|
1976
|
+
end
|
1977
|
+
return data
|
1978
|
+
end
|
1979
|
+
|
1980
|
+
RS = "\r\n"
|
1981
|
+
def read_body_chunked
|
1982
|
+
if @chunk_length == 0
|
1983
|
+
until (i = @readbuf.index(RS))
|
1984
|
+
@readbuf << @socket.gets(RS)
|
1985
|
+
end
|
1986
|
+
i += 2
|
1987
|
+
@chunk_length = @readbuf[0, i].hex
|
1988
|
+
@readbuf[0, i] = ''
|
1989
|
+
if @chunk_length == 0
|
1990
|
+
@content_length = 0
|
1991
|
+
@socket.gets(RS)
|
1992
|
+
return nil
|
1993
|
+
end
|
1994
|
+
end
|
1995
|
+
while @readbuf.length < @chunk_length + 2
|
1996
|
+
@readbuf << @socket.read(@chunk_length + 2 - @readbuf.length)
|
1997
|
+
end
|
1998
|
+
data = @readbuf[0, @chunk_length]
|
1999
|
+
@readbuf[0, @chunk_length + 2] = ''
|
2000
|
+
@chunk_length = 0
|
2001
|
+
return data
|
2002
|
+
end
|
2003
|
+
|
2004
|
+
def check_state
|
2005
|
+
if @state == :DATA
|
2006
|
+
if eof?
|
2007
|
+
if @next_connection
|
2008
|
+
if @requests.empty?
|
2009
|
+
@state = :WAIT
|
2010
|
+
else
|
2011
|
+
@state = :META
|
2012
|
+
end
|
2013
|
+
end
|
2014
|
+
end
|
2015
|
+
end
|
2016
|
+
end
|
2017
|
+
end
|
2018
|
+
|
2019
|
+
|
2020
|
+
end
|
2021
|
+
|
2022
|
+
|
2023
|
+
HTTPClient = HTTPAccess2::Client
|