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.
@@ -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