httpclient 2.1.5 → 2.8.3

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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +85 -0
  3. data/bin/httpclient +77 -0
  4. data/bin/jsonclient +85 -0
  5. data/lib/hexdump.rb +50 -0
  6. data/lib/http-access2.rb +6 -4
  7. data/lib/httpclient/auth.rb +575 -173
  8. data/lib/httpclient/cacert.pem +3952 -0
  9. data/lib/httpclient/cacert1024.pem +3866 -0
  10. data/lib/httpclient/connection.rb +6 -2
  11. data/lib/httpclient/cookie.rb +162 -504
  12. data/lib/httpclient/http.rb +334 -119
  13. data/lib/httpclient/include_client.rb +85 -0
  14. data/lib/httpclient/jruby_ssl_socket.rb +588 -0
  15. data/lib/httpclient/session.rb +385 -288
  16. data/lib/httpclient/ssl_config.rb +195 -155
  17. data/lib/httpclient/ssl_socket.rb +150 -0
  18. data/lib/httpclient/timeout.rb +14 -10
  19. data/lib/httpclient/util.rb +142 -6
  20. data/lib/httpclient/version.rb +3 -0
  21. data/lib/httpclient/webagent-cookie.rb +459 -0
  22. data/lib/httpclient.rb +509 -202
  23. data/lib/jsonclient.rb +63 -0
  24. data/lib/oauthclient.rb +111 -0
  25. data/sample/async.rb +8 -0
  26. data/sample/auth.rb +11 -0
  27. data/sample/cookie.rb +18 -0
  28. data/sample/dav.rb +103 -0
  29. data/sample/howto.rb +49 -0
  30. data/sample/jsonclient.rb +67 -0
  31. data/sample/oauth_buzz.rb +57 -0
  32. data/sample/oauth_friendfeed.rb +59 -0
  33. data/sample/oauth_twitter.rb +61 -0
  34. data/sample/ssl/0cert.pem +22 -0
  35. data/sample/ssl/0key.pem +30 -0
  36. data/sample/ssl/1000cert.pem +19 -0
  37. data/sample/ssl/1000key.pem +18 -0
  38. data/sample/ssl/htdocs/index.html +10 -0
  39. data/sample/ssl/ssl_client.rb +22 -0
  40. data/sample/ssl/webrick_httpsd.rb +29 -0
  41. data/sample/stream.rb +21 -0
  42. data/sample/thread.rb +27 -0
  43. data/sample/wcat.rb +21 -0
  44. data/test/ca-chain.pem +44 -0
  45. data/test/ca.cert +23 -0
  46. data/test/client-pass.key +18 -0
  47. data/test/client.cert +19 -0
  48. data/test/client.key +15 -0
  49. data/test/helper.rb +131 -0
  50. data/test/htdigest +1 -0
  51. data/test/htpasswd +2 -0
  52. data/test/jruby_ssl_socket/test_pemutils.rb +32 -0
  53. data/test/runner.rb +2 -0
  54. data/test/server.cert +19 -0
  55. data/test/server.key +15 -0
  56. data/test/sslsvr.rb +65 -0
  57. data/test/subca.cert +21 -0
  58. data/test/test_auth.rb +492 -0
  59. data/test/test_cookie.rb +309 -0
  60. data/test/test_hexdump.rb +14 -0
  61. data/test/test_http-access2.rb +508 -0
  62. data/test/test_httpclient.rb +2145 -0
  63. data/test/test_include_client.rb +52 -0
  64. data/test/test_jsonclient.rb +80 -0
  65. data/test/test_ssl.rb +559 -0
  66. data/test/test_webagent-cookie.rb +465 -0
  67. metadata +85 -44
  68. data/lib/httpclient/auth.rb.orig +0 -513
  69. data/lib/httpclient/cacert.p7s +0 -1579
  70. data/lib/httpclient.rb.orig +0 -1020
  71. data/lib/tags +0 -908
@@ -1,5 +1,5 @@
1
1
  # HTTPClient - HTTP client library.
2
- # Copyright (C) 2000-2009 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
2
+ # Copyright (C) 2000-2015 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
3
3
  #
4
4
  # This program is copyrighted free software by NAKAMURA, Hiroshi. You can
5
5
  # redistribute it and/or modify it under the same terms of Ruby's license;
@@ -8,24 +8,14 @@
8
8
 
9
9
  require 'digest/md5'
10
10
  require 'httpclient/session'
11
+ require 'mutex_m'
11
12
 
12
13
 
13
14
  class HTTPClient
14
15
 
15
- begin
16
- require 'net/ntlm'
17
- NTLMEnabled = true
18
- rescue LoadError
19
- NTLMEnabled = false
20
- end
21
-
22
- begin
23
- require 'win32/sspi'
24
- SSPIEnabled = true
25
- rescue LoadError
26
- SSPIEnabled = false
27
- end
28
-
16
+ NTLMEnabled = false
17
+ SSPIEnabled = false
18
+ GSSAPIEnabled = false
29
19
 
30
20
  # Common abstract class for authentication filter.
31
21
  #
@@ -63,22 +53,25 @@ class HTTPClient
63
53
  #
64
54
  # WWWAuth has sub filters (BasicAuth, DigestAuth, NegotiateAuth and
65
55
  # SSPINegotiateAuth) and delegates some operations to it.
66
- # NegotiateAuth requires 'ruby/ntlm' module.
67
- # SSPINegotiateAuth requires 'win32/sspi' module.
56
+ # NegotiateAuth requires 'ruby/ntlm' module (rubyntlm gem).
57
+ # SSPINegotiateAuth requires 'win32/sspi' module (rubysspi gem).
68
58
  class WWWAuth < AuthFilterBase
69
59
  attr_reader :basic_auth
70
60
  attr_reader :digest_auth
71
61
  attr_reader :negotiate_auth
72
62
  attr_reader :sspi_negotiate_auth
63
+ attr_reader :oauth
73
64
 
74
65
  # Creates new WWWAuth.
75
66
  def initialize
76
67
  @basic_auth = BasicAuth.new
77
68
  @digest_auth = DigestAuth.new
78
69
  @negotiate_auth = NegotiateAuth.new
70
+ @ntlm_auth = NegotiateAuth.new('NTLM')
79
71
  @sspi_negotiate_auth = SSPINegotiateAuth.new
72
+ @oauth = OAuth.new
80
73
  # sort authenticators by priority
81
- @authenticator = [@negotiate_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
74
+ @authenticator = [@oauth, @negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
82
75
  end
83
76
 
84
77
  # Resets challenge state. See sub filters for more details.
@@ -100,7 +93,15 @@ class HTTPClient
100
93
  # 'Authorization' header if needed.
101
94
  def filter_request(req)
102
95
  @authenticator.each do |auth|
96
+ next unless auth.set? # hasn't be set, don't use it
103
97
  if cred = auth.get(req)
98
+ if cred == :skip
99
+ # some authenticator (NTLM and Negotiate) does not
100
+ # need to send extra header after authorization. In such case
101
+ # it should block other authenticators to respond and :skip is
102
+ # the marker for such case.
103
+ return
104
+ end
104
105
  req.header.set('Authorization', auth.scheme + " " + cred)
105
106
  return
106
107
  end
@@ -109,6 +110,10 @@ class HTTPClient
109
110
 
110
111
  # Filter API implementation. Traps HTTP response and parses
111
112
  # 'WWW-Authenticate' header.
113
+ #
114
+ # This remembers the challenges for all authentication methods
115
+ # available to the client. On the subsequent retry of the request,
116
+ # filter_request will select the strongest method.
112
117
  def filter_response(req, res)
113
118
  command = nil
114
119
  if res.status == HTTP::Status::UNAUTHORIZED
@@ -116,6 +121,7 @@ class HTTPClient
116
121
  uri = req.header.request_uri
117
122
  challenge.each do |scheme, param_str|
118
123
  @authenticator.each do |auth|
124
+ next unless auth.set? # hasn't be set, don't use it
119
125
  if scheme.downcase == auth.scheme.downcase
120
126
  challengeable = auth.challenge(uri, param_str)
121
127
  command = :retry if challengeable
@@ -144,16 +150,19 @@ class HTTPClient
144
150
  # SSPINegotiateAuth requires 'win32/sspi' module.
145
151
  class ProxyAuth < AuthFilterBase
146
152
  attr_reader :basic_auth
153
+ attr_reader :digest_auth
147
154
  attr_reader :negotiate_auth
148
155
  attr_reader :sspi_negotiate_auth
149
156
 
150
157
  # Creates new ProxyAuth.
151
158
  def initialize
152
- @basic_auth = BasicAuth.new
159
+ @basic_auth = ProxyBasicAuth.new
153
160
  @negotiate_auth = NegotiateAuth.new
161
+ @ntlm_auth = NegotiateAuth.new('NTLM')
154
162
  @sspi_negotiate_auth = SSPINegotiateAuth.new
163
+ @digest_auth = ProxyDigestAuth.new
155
164
  # sort authenticators by priority
156
- @authenticator = [@negotiate_auth, @sspi_negotiate_auth, @basic_auth]
165
+ @authenticator = [@negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
157
166
  end
158
167
 
159
168
  # Resets challenge state. See sub filters for more details.
@@ -175,7 +184,15 @@ class HTTPClient
175
184
  # 'Proxy-Authorization' header if needed.
176
185
  def filter_request(req)
177
186
  @authenticator.each do |auth|
187
+ next unless auth.set? # hasn't be set, don't use it
178
188
  if cred = auth.get(req)
189
+ if cred == :skip
190
+ # some authenticator (NTLM and Negotiate) does not
191
+ # need to send extra header after authorization. In such case
192
+ # it should block other authenticators to respond and :skip is
193
+ # the marker for such case.
194
+ return
195
+ end
179
196
  req.header.set('Proxy-Authorization', auth.scheme + " " + cred)
180
197
  return
181
198
  end
@@ -191,6 +208,7 @@ class HTTPClient
191
208
  uri = req.header.request_uri
192
209
  challenge.each do |scheme, param_str|
193
210
  @authenticator.each do |auth|
211
+ next unless auth.set? # hasn't be set, don't use it
194
212
  if scheme.downcase == auth.scheme.downcase
195
213
  challengeable = auth.challenge(uri, param_str)
196
214
  command = :retry if challengeable
@@ -204,111 +222,163 @@ class HTTPClient
204
222
  end
205
223
  end
206
224
 
207
- # Authentication filter for handling BasicAuth negotiation.
208
- # Used in WWWAuth and ProxyAuth.
209
- class BasicAuth
225
+ # Authentication filter base class.
226
+ class AuthBase
227
+ include HTTPClient::Util
228
+
210
229
  # Authentication scheme.
211
230
  attr_reader :scheme
212
231
 
213
- # Creates new BasicAuth filter.
214
- def initialize
215
- @cred = nil
216
- @auth = {}
217
- @challengeable = {}
218
- @scheme = "Basic"
232
+ def initialize(scheme)
233
+ @scheme = scheme
234
+ @challenge = {}
219
235
  end
220
236
 
221
237
  # Resets challenge state. Do not send '*Authorization' header until the
222
238
  # server sends '*Authentication' again.
223
239
  def reset_challenge
224
- @challengeable.clear
240
+ synchronize do
241
+ @challenge.clear
242
+ end
243
+ end
244
+ end
245
+
246
+ # Authentication filter for handling BasicAuth negotiation.
247
+ # Used in WWWAuth and ProxyAuth.
248
+ class BasicAuth < AuthBase
249
+ include Mutex_m
250
+
251
+ # Send Authorization Header without receiving 401
252
+ attr_accessor :force_auth
253
+
254
+ # Creates new BasicAuth filter.
255
+ def initialize
256
+ super('Basic')
257
+ @cred = nil
258
+ @auth = {}
259
+ @force_auth = false
225
260
  end
226
261
 
227
262
  # Set authentication credential.
228
263
  # uri == nil for generic purpose (allow to use user/password for any URL).
229
264
  def set(uri, user, passwd)
230
- if uri.nil?
231
- @cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
232
- else
233
- uri = Util.uri_dirname(uri)
234
- @auth[uri] = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
265
+ synchronize do
266
+ if uri.nil?
267
+ @cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
268
+ else
269
+ uri = Util.uri_dirname(uri)
270
+ @auth[uri] = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
271
+ end
235
272
  end
236
273
  end
237
274
 
275
+ # have we marked this as set - ie that it's valid to use in this context?
276
+ def set?
277
+ @cred || @auth.any?
278
+ end
279
+
238
280
  # Response handler: returns credential.
239
281
  # It sends cred only when a given uri is;
240
282
  # * child page of challengeable(got *Authenticate before) uri and,
241
283
  # * child page of defined credential
242
284
  def get(req)
243
285
  target_uri = req.header.request_uri
244
- return nil unless @challengeable.find { |uri, ok|
245
- Util.uri_part_of(target_uri, uri) and ok
246
- }
247
- return @cred if @cred
248
- Util.hash_find_value(@auth) { |uri, cred|
249
- Util.uri_part_of(target_uri, uri)
286
+ synchronize {
287
+ return nil if !@force_auth and !@challenge.any? { |uri, ok|
288
+ Util.uri_part_of(target_uri, uri) and ok
289
+ }
290
+ return @cred if @cred
291
+ Util.hash_find_value(@auth) { |uri, cred|
292
+ Util.uri_part_of(target_uri, uri)
293
+ }
250
294
  }
251
295
  end
252
296
 
253
297
  # Challenge handler: remember URL for response.
254
- def challenge(uri, param_str)
255
- @challengeable[uri] = true
256
- true
298
+ def challenge(uri, param_str = nil)
299
+ synchronize {
300
+ @challenge[urify(uri)] = true
301
+ true
302
+ }
257
303
  end
258
304
  end
259
305
 
306
+ class ProxyBasicAuth < BasicAuth
307
+ def set(uri, user, passwd)
308
+ synchronize do
309
+ @cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
310
+ end
311
+ end
312
+
313
+ def get(req)
314
+ synchronize {
315
+ return nil if !@force_auth and !@challenge['challenged']
316
+ @cred
317
+ }
318
+ end
319
+
320
+ # Challenge handler: remember URL for response.
321
+ def challenge(uri, param_str = nil)
322
+ synchronize {
323
+ @challenge['challenged'] = true
324
+ true
325
+ }
326
+ end
327
+ end
260
328
 
261
329
  # Authentication filter for handling DigestAuth negotiation.
262
330
  # Used in WWWAuth.
263
- class DigestAuth
264
- # Authentication scheme.
265
- attr_reader :scheme
331
+ class DigestAuth < AuthBase
332
+ include Mutex_m
266
333
 
267
334
  # Creates new DigestAuth filter.
268
335
  def initialize
336
+ super('Digest')
269
337
  @auth = {}
270
- @challenge = {}
271
338
  @nonce_count = 0
272
- @scheme = "Digest"
273
- end
274
-
275
- # Resets challenge state. Do not send '*Authorization' header until the
276
- # server sends '*Authentication' again.
277
- def reset_challenge
278
- @challenge.clear
279
339
  end
280
340
 
281
341
  # Set authentication credential.
282
342
  # uri == nil is ignored.
283
343
  def set(uri, user, passwd)
284
- if uri
285
- uri = Util.uri_dirname(uri)
286
- @auth[uri] = [user, passwd]
344
+ synchronize do
345
+ if uri
346
+ uri = Util.uri_dirname(uri)
347
+ @auth[uri] = [user, passwd]
348
+ end
287
349
  end
288
350
  end
289
351
 
352
+ # have we marked this as set - ie that it's valid to use in this context?
353
+ def set?
354
+ @auth.any?
355
+ end
356
+
290
357
  # Response handler: returns credential.
291
358
  # It sends cred only when a given uri is;
292
359
  # * child page of challengeable(got *Authenticate before) uri and,
293
360
  # * child page of defined credential
294
361
  def get(req)
295
362
  target_uri = req.header.request_uri
296
- param = Util.hash_find_value(@challenge) { |uri, v|
297
- Util.uri_part_of(target_uri, uri)
298
- }
299
- return nil unless param
300
- user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
301
- Util.uri_part_of(target_uri, uri)
363
+ synchronize {
364
+ param = Util.hash_find_value(@challenge) { |uri, v|
365
+ Util.uri_part_of(target_uri, uri)
366
+ }
367
+ return nil unless param
368
+ user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
369
+ Util.uri_part_of(target_uri, uri)
370
+ }
371
+ return nil unless user
372
+ calc_cred(req, user, passwd, param)
302
373
  }
303
- return nil unless user
304
- uri = req.header.request_uri
305
- calc_cred(req.header.request_method, uri, user, passwd, param)
306
374
  end
307
375
 
308
376
  # Challenge handler: remember URL and challenge token for response.
309
377
  def challenge(uri, param_str)
310
- @challenge[uri] = parse_challenge_param(param_str)
311
- true
378
+ synchronize {
379
+ @challenge[uri] = parse_challenge_param(param_str)
380
+ true
381
+ }
312
382
  end
313
383
 
314
384
  private
@@ -316,30 +386,49 @@ class HTTPClient
316
386
  # this method is implemented by sromano and posted to
317
387
  # http://tools.assembla.com/breakout/wiki/DigestForSoap
318
388
  # Thanks!
319
- # supported algorithm: MD5 only for now
320
- def calc_cred(method, uri, user, passwd, param)
389
+ # supported algorithms: MD5, MD5-sess
390
+ def calc_cred(req, user, passwd, param)
391
+ method = req.header.request_method
392
+ path = req.header.create_query_uri
321
393
  a_1 = "#{user}:#{param['realm']}:#{passwd}"
322
- a_2 = "#{method}:#{uri.path}"
394
+ a_2 = "#{method}:#{path}"
395
+ qop = param['qop']
323
396
  nonce = param['nonce']
324
- cnonce = generate_cnonce()
325
- @nonce_count += 1
397
+ cnonce = nil
398
+ if qop || param['algorithm'] =~ /MD5-sess/
399
+ cnonce = generate_cnonce()
400
+ end
401
+ a_1_md5sum = Digest::MD5.hexdigest(a_1)
402
+ if param['algorithm'] =~ /MD5-sess/
403
+ a_1_md5sum = Digest::MD5.hexdigest("#{a_1_md5sum}:#{nonce}:#{cnonce}")
404
+ algorithm = "MD5-sess"
405
+ else
406
+ algorithm = "MD5"
407
+ end
326
408
  message_digest = []
327
- message_digest << Digest::MD5.hexdigest(a_1)
409
+ message_digest << a_1_md5sum
328
410
  message_digest << nonce
329
- message_digest << ('%08x' % @nonce_count)
330
- message_digest << cnonce
331
- message_digest << param['qop']
411
+ if qop
412
+ @nonce_count += 1
413
+ message_digest << ('%08x' % @nonce_count)
414
+ message_digest << cnonce
415
+ message_digest << param['qop']
416
+ end
332
417
  message_digest << Digest::MD5.hexdigest(a_2)
333
418
  header = []
334
419
  header << "username=\"#{user}\""
335
420
  header << "realm=\"#{param['realm']}\""
336
421
  header << "nonce=\"#{nonce}\""
337
- header << "uri=\"#{uri.path}\""
338
- header << "cnonce=\"#{cnonce}\""
339
- header << "nc=#{'%08x' % @nonce_count}"
340
- header << "qop=\"#{param['qop']}\""
422
+ header << "uri=\"#{path}\""
423
+ if cnonce
424
+ header << "cnonce=\"#{cnonce}\""
425
+ end
426
+ if qop
427
+ header << "nc=#{'%08x' % @nonce_count}"
428
+ header << "qop=#{param['qop']}"
429
+ end
341
430
  header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
342
- header << "algorithm=\"MD5\""
431
+ header << "algorithm=#{algorithm}"
343
432
  header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
344
433
  header.join(", ")
345
434
  end
@@ -365,88 +454,137 @@ class HTTPClient
365
454
  end
366
455
 
367
456
 
457
+ # Authentication filter for handling DigestAuth negotiation.
458
+ # Ignores uri argument. Used in ProxyAuth.
459
+ class ProxyDigestAuth < DigestAuth
460
+
461
+ # overrides DigestAuth#set. sets default user name and password. uri is not used.
462
+ def set(uri, user, passwd)
463
+ synchronize do
464
+ @auth = [user, passwd]
465
+ end
466
+ end
467
+
468
+ # overrides DigestAuth#get. Uses default user name and password
469
+ # regardless of target uri if the proxy has required authentication
470
+ # before
471
+ def get(req)
472
+ synchronize {
473
+ param = @challenge
474
+ return nil unless param
475
+ user, passwd = @auth
476
+ return nil unless user
477
+ calc_cred(req, user, passwd, param)
478
+ }
479
+ end
480
+
481
+ def reset_challenge
482
+ synchronize do
483
+ @challenge = nil
484
+ end
485
+ end
486
+
487
+ def challenge(uri, param_str)
488
+ synchronize {
489
+ @challenge = parse_challenge_param(param_str)
490
+ true
491
+ }
492
+ end
493
+ end
494
+
368
495
  # Authentication filter for handling Negotiate/NTLM negotiation.
369
496
  # Used in WWWAuth and ProxyAuth.
370
497
  #
371
498
  # NegotiateAuth depends on 'ruby/ntlm' module.
372
- class NegotiateAuth
373
- # Authentication scheme.
374
- attr_reader :scheme
499
+ class NegotiateAuth < AuthBase
500
+ include Mutex_m
501
+
375
502
  # NTLM opt for ruby/ntlm. {:ntlmv2 => true} by default.
376
503
  attr_reader :ntlm_opt
377
504
 
378
505
  # Creates new NegotiateAuth filter.
379
- def initialize
506
+ def initialize(scheme = "Negotiate")
507
+ super(scheme)
380
508
  @auth = {}
381
509
  @auth_default = nil
382
- @challenge = {}
383
- @scheme = "Negotiate"
384
510
  @ntlm_opt = {
385
511
  :ntlmv2 => true
386
512
  }
387
513
  end
388
514
 
389
- # Resets challenge state. Do not send '*Authorization' header until the
390
- # server sends '*Authentication' again.
391
- def reset_challenge
392
- @challenge.clear
393
- end
394
-
395
515
  # Set authentication credential.
396
516
  # uri == nil for generic purpose (allow to use user/password for any URL).
397
517
  def set(uri, user, passwd)
398
- if uri
399
- uri = Util.uri_dirname(uri)
400
- @auth[uri] = [user, passwd]
401
- else
402
- @auth_default = [user, passwd]
518
+ synchronize do
519
+ if uri
520
+ uri = Util.uri_dirname(uri)
521
+ @auth[uri] = [user, passwd]
522
+ else
523
+ @auth_default = [user, passwd]
524
+ end
403
525
  end
404
526
  end
405
527
 
528
+ # have we marked this as set - ie that it's valid to use in this context?
529
+ def set?
530
+ @auth_default || @auth.any?
531
+ end
532
+
406
533
  # Response handler: returns credential.
407
534
  # See ruby/ntlm for negotiation state transition.
408
535
  def get(req)
409
- return nil unless NTLMEnabled
410
536
  target_uri = req.header.request_uri
411
- domain_uri, param = @challenge.find { |uri, v|
412
- Util.uri_part_of(target_uri, uri)
413
- }
414
- return nil unless param
415
- user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
416
- Util.uri_part_of(target_uri, uri)
537
+ synchronize {
538
+ _domain_uri, param = @challenge.find { |uri, v|
539
+ Util.uri_part_of(target_uri, uri)
540
+ }
541
+ return nil unless param
542
+ user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
543
+ Util.uri_part_of(target_uri, uri)
544
+ }
545
+ unless user
546
+ user, passwd = @auth_default
547
+ end
548
+ return nil unless user
549
+ Util.try_require('net/ntlm') || return
550
+ domain = nil
551
+ domain, user = user.split("\\") if user.index("\\")
552
+ state = param[:state]
553
+ authphrase = param[:authphrase]
554
+ case state
555
+ when :init
556
+ t1 = Net::NTLM::Message::Type1.new
557
+ t1.domain = domain if domain
558
+ t1.encode64
559
+ when :response
560
+ t2 = Net::NTLM::Message.decode64(authphrase)
561
+ param = {:user => user, :password => passwd}
562
+ param[:domain] = domain if domain
563
+ t3 = t2.response(param, @ntlm_opt.dup)
564
+ @challenge[target_uri][:state] = :done
565
+ t3.encode64
566
+ when :done
567
+ :skip
568
+ else
569
+ nil
570
+ end
417
571
  }
418
- unless user
419
- user, passwd = @auth_default
420
- end
421
- return nil unless user
422
- state = param[:state]
423
- authphrase = param[:authphrase]
424
- case state
425
- when :init
426
- t1 = Net::NTLM::Message::Type1.new
427
- return t1.encode64
428
- when :response
429
- t2 = Net::NTLM::Message.decode64(authphrase)
430
- t3 = t2.response({:user => user, :password => passwd}, @ntlm_opt.dup)
431
- @challenge.delete(domain_uri)
432
- return t3.encode64
433
- end
434
- nil
435
572
  end
436
573
 
437
574
  # Challenge handler: remember URL and challenge token for response.
438
575
  def challenge(uri, param_str)
439
- return false unless NTLMEnabled
440
- if param_str.nil? or @challenge[uri].nil?
441
- c = @challenge[uri] = {}
442
- c[:state] = :init
443
- c[:authphrase] = ""
444
- else
445
- c = @challenge[uri]
446
- c[:state] = :response
447
- c[:authphrase] = param_str
448
- end
449
- true
576
+ synchronize {
577
+ if param_str.nil? or @challenge[uri].nil?
578
+ c = @challenge[uri] = {}
579
+ c[:state] = :init
580
+ c[:authphrase] = ""
581
+ else
582
+ c = @challenge[uri]
583
+ c[:state] = :response
584
+ c[:authphrase] = param_str
585
+ end
586
+ true
587
+ }
450
588
  end
451
589
  end
452
590
 
@@ -455,66 +593,330 @@ class HTTPClient
455
593
  # Used in ProxyAuth.
456
594
  #
457
595
  # SSPINegotiateAuth depends on 'win32/sspi' module.
458
- class SSPINegotiateAuth
459
- # Authentication scheme.
460
- attr_reader :scheme
596
+ class SSPINegotiateAuth < AuthBase
597
+ include Mutex_m
461
598
 
462
599
  # Creates new SSPINegotiateAuth filter.
463
600
  def initialize
464
- @challenge = {}
465
- @scheme = "Negotiate"
466
- end
467
-
468
- # Resets challenge state. Do not send '*Authorization' header until the
469
- # server sends '*Authentication' again.
470
- def reset_challenge
471
- @challenge.clear
601
+ super('Negotiate')
472
602
  end
473
603
 
474
604
  # Set authentication credential.
475
605
  # NOT SUPPORTED: username and necessary data is retrieved by win32/sspi.
476
606
  # See win32/sspi for more details.
477
- def set(uri, user, passwd)
607
+ def set(*args)
478
608
  # not supported
479
609
  end
480
610
 
611
+ # Check always (not effective but it works)
612
+ def set?
613
+ !@challenge.empty?
614
+ end
615
+
481
616
  # Response handler: returns credential.
482
617
  # See win32/sspi for negotiation state transition.
483
618
  def get(req)
484
- return nil unless SSPIEnabled
485
619
  target_uri = req.header.request_uri
486
- domain_uri, param = @challenge.find { |uri, v|
487
- Util.uri_part_of(target_uri, uri)
620
+ synchronize {
621
+ domain_uri, param = @challenge.find { |uri, v|
622
+ Util.uri_part_of(target_uri, uri)
623
+ }
624
+ return nil unless param
625
+ Util.try_require('win32/sspi') || Util.try_require('gssapi') || return
626
+ state = param[:state]
627
+ authenticator = param[:authenticator]
628
+ authphrase = param[:authphrase]
629
+ case state
630
+ when :init
631
+ if defined?(Win32::SSPI)
632
+ authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
633
+ authenticator.get_initial_token(@scheme)
634
+ else # use GSSAPI
635
+ authenticator = param[:authenticator] = GSSAPI::Simple.new(domain_uri.host, 'HTTP')
636
+ # Base64 encode the context token
637
+ [authenticator.init_context].pack('m').gsub(/\n/,'')
638
+ end
639
+ when :response
640
+ @challenge[target_uri][:state] = :done
641
+ if defined?(Win32::SSPI)
642
+ authenticator.complete_authentication(authphrase)
643
+ else # use GSSAPI
644
+ authenticator.init_context(authphrase.unpack('m').pop)
645
+ end
646
+ when :done
647
+ :skip
648
+ else
649
+ nil
650
+ end
488
651
  }
489
- return nil unless param
490
- state = param[:state]
491
- authenticator = param[:authenticator]
492
- authphrase = param[:authphrase]
493
- case state
494
- when :init
495
- authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
496
- return authenticator.get_initial_token(@scheme)
497
- when :response
498
- @challenge.delete(domain_uri)
499
- return authenticator.complete_authentication(authphrase)
500
- end
501
- nil
502
652
  end
503
653
 
504
654
  # Challenge handler: remember URL and challenge token for response.
505
655
  def challenge(uri, param_str)
506
- return false unless SSPIEnabled
507
- if param_str.nil? or @challenge[uri].nil?
508
- c = @challenge[uri] = {}
509
- c[:state] = :init
510
- c[:authenticator] = nil
511
- c[:authphrase] = ""
656
+ synchronize {
657
+ if param_str.nil? or @challenge[uri].nil?
658
+ c = @challenge[uri] = {}
659
+ c[:state] = :init
660
+ c[:authenticator] = nil
661
+ c[:authphrase] = ""
662
+ else
663
+ c = @challenge[uri]
664
+ c[:state] = :response
665
+ c[:authphrase] = param_str
666
+ end
667
+ true
668
+ }
669
+ end
670
+ end
671
+
672
+ # Authentication filter for handling OAuth negotiation.
673
+ # Used in WWWAuth.
674
+ #
675
+ # CAUTION: This impl only support '#7 Accessing Protected Resources' in OAuth
676
+ # Core 1.0 spec for now. You need to obtain Access token and Access secret by
677
+ # yourself.
678
+ #
679
+ # CAUTION: This impl does NOT support OAuth Request Body Hash spec for now.
680
+ # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
681
+ #
682
+ class OAuth < AuthBase
683
+ include Mutex_m
684
+
685
+ class Config
686
+ include HTTPClient::Util
687
+
688
+ attr_accessor :http_method
689
+ attr_accessor :realm
690
+ attr_accessor :consumer_key
691
+ attr_accessor :consumer_secret
692
+ attr_accessor :token
693
+ attr_accessor :secret
694
+ attr_accessor :signature_method
695
+ attr_accessor :version
696
+ attr_accessor :callback
697
+ attr_accessor :verifier
698
+
699
+ # for OAuth Session 1.0 (draft)
700
+ attr_accessor :session_handle
701
+
702
+ attr_reader :signature_handler
703
+
704
+ attr_accessor :debug_timestamp
705
+ attr_accessor :debug_nonce
706
+
707
+ def initialize(*args)
708
+ @http_method,
709
+ @realm,
710
+ @consumer_key,
711
+ @consumer_secret,
712
+ @token,
713
+ @secret,
714
+ @signature_method,
715
+ @version,
716
+ @callback,
717
+ @verifier =
718
+ keyword_argument(args,
719
+ :http_method,
720
+ :realm,
721
+ :consumer_key,
722
+ :consumer_secret,
723
+ :token,
724
+ :secret,
725
+ :signature_method,
726
+ :version,
727
+ :callback,
728
+ :verifier
729
+ )
730
+ @http_method ||= :post
731
+ @session_handle = nil
732
+ @signature_handler = {}
733
+ end
734
+ end
735
+
736
+ def self.escape(str) # :nodoc:
737
+ if str.respond_to?(:force_encoding)
738
+ str.dup.force_encoding('BINARY').gsub(/([^a-zA-Z0-9_.~-]+)/) {
739
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
740
+ }
741
+ else
742
+ str.gsub(/([^a-zA-Z0-9_.~-]+)/n) {
743
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
744
+ }
745
+ end
746
+ end
747
+
748
+ def escape(str)
749
+ self.class.escape(str)
750
+ end
751
+
752
+ # Creates new DigestAuth filter.
753
+ def initialize
754
+ super('OAuth')
755
+ @config = nil # common config
756
+ @auth = {} # configs for each site
757
+ @nonce_count = 0
758
+ @signature_handler = {
759
+ 'HMAC-SHA1' => method(:sign_hmac_sha1)
760
+ }
761
+ end
762
+
763
+ # Set authentication credential.
764
+ # You cannot set OAuth config via WWWAuth#set_auth. Use OAuth#config=
765
+ def set(*args)
766
+ # not supported
767
+ end
768
+
769
+ # Check always (not effective but it works)
770
+ def set?
771
+ !@challenge.empty?
772
+ end
773
+
774
+ # Set authentication credential.
775
+ def set_config(uri, config)
776
+ synchronize do
777
+ if uri.nil?
778
+ @config = config
779
+ else
780
+ uri = Util.uri_dirname(urify(uri))
781
+ @auth[uri] = config
782
+ end
783
+ end
784
+ end
785
+
786
+ # Get authentication credential.
787
+ def get_config(uri = nil)
788
+ synchronize {
789
+ do_get_config(uri)
790
+ }
791
+ end
792
+
793
+ # Response handler: returns credential.
794
+ # It sends cred only when a given uri is;
795
+ # * child page of challengeable(got *Authenticate before) uri and,
796
+ # * child page of defined credential
797
+ def get(req)
798
+ target_uri = req.header.request_uri
799
+ synchronize {
800
+ return nil unless @challenge[nil] or @challenge.find { |uri, ok|
801
+ Util.uri_part_of(target_uri, uri) and ok
802
+ }
803
+ config = do_get_config(target_uri) || @config
804
+ return nil unless config
805
+ calc_cred(req, config)
806
+ }
807
+ end
808
+
809
+ # Challenge handler: remember URL for response.
810
+ #
811
+ # challenge() in OAuth handler always returns false to avoid connection
812
+ # retry which should not work in OAuth authentication context. This
813
+ # method just remember URL (nil means 'any') for the next connection.
814
+ # Normally OAuthClient handles this correctly but see how it uses when
815
+ # you need to use this class directly.
816
+ def challenge(uri, param_str = nil)
817
+ synchronize {
818
+ if uri.nil?
819
+ @challenge[nil] = true
820
+ else
821
+ @challenge[urify(uri)] = true
822
+ end
823
+ false
824
+ }
825
+ end
826
+
827
+ private
828
+
829
+ def do_get_config(uri = nil)
830
+ if uri.nil?
831
+ @config
512
832
  else
513
- c = @challenge[uri]
514
- c[:state] = :response
515
- c[:authphrase] = param_str
833
+ uri = urify(uri)
834
+ Util.hash_find_value(@auth) { |cand_uri, cred|
835
+ Util.uri_part_of(uri, cand_uri)
836
+ }
837
+ end
838
+ end
839
+
840
+ def calc_cred(req, config)
841
+ header = {}
842
+ header['oauth_consumer_key'] = config.consumer_key
843
+ header['oauth_signature_method'] = config.signature_method
844
+ header['oauth_timestamp'] = config.debug_timestamp || Time.now.to_i.to_s
845
+ header['oauth_nonce'] = config.debug_nonce || generate_nonce()
846
+ header['oauth_token'] = config.token if config.token
847
+ header['oauth_version'] = config.version if config.version
848
+ header['oauth_callback'] = config.callback if config.callback
849
+ header['oauth_verifier'] = config.verifier if config.verifier
850
+ header['oauth_session_handle'] = config.session_handle if config.session_handle
851
+ signature = sign(config, header, req)
852
+ header['oauth_signature'] = signature
853
+ # no need to do but we should sort for easier to test.
854
+ str = header.sort_by { |k, v| k }.map { |k, v| encode_header(k, v) }.join(', ')
855
+ if config.realm
856
+ str = %Q(realm="#{config.realm}", ) + str
857
+ end
858
+ str
859
+ end
860
+
861
+ def generate_nonce
862
+ @nonce_count += 1
863
+ now = "%012d" % Time.now.to_i
864
+ pk = Digest::MD5.hexdigest([@nonce_count.to_s, now, self.__id__, Process.pid, rand(65535)].join)[0, 32]
865
+ [now + ':' + pk].pack('m*').chop
866
+ end
867
+
868
+ def encode_header(k, v)
869
+ %Q(#{escape(k.to_s)}="#{escape(v.to_s)}")
870
+ end
871
+
872
+ def encode_param(params)
873
+ params.map { |k, v|
874
+ [v].flatten.map { |vv|
875
+ %Q(#{escape(k.to_s)}=#{escape(vv.to_s)})
876
+ }
877
+ }.flatten
878
+ end
879
+
880
+ def sign(config, header, req)
881
+ base_string = create_base_string(config, header, req)
882
+ if handler = config.signature_handler[config.signature_method] || @signature_handler[config.signature_method.to_s]
883
+ handler.call(config, base_string)
884
+ else
885
+ raise ConfigurationError.new("Unknown OAuth signature method: #{config.signature_method}")
886
+ end
887
+ end
888
+
889
+ def create_base_string(config, header, req)
890
+ params = encode_param(header)
891
+ query = req.header.request_query
892
+ if query and HTTP::Message.multiparam_query?(query)
893
+ params += encode_param(query)
894
+ end
895
+ # captures HTTP Message body only for 'application/x-www-form-urlencoded'
896
+ if req.header.contenttype == 'application/x-www-form-urlencoded' and req.http_body.size
897
+ params += encode_param(HTTP::Message.parse(req.http_body.content))
898
+ end
899
+ uri = req.header.request_uri
900
+ if uri.query
901
+ params += encode_param(HTTP::Message.parse(uri.query))
902
+ end
903
+ if uri.port == uri.default_port
904
+ request_url = "#{uri.scheme.downcase}://#{uri.host}#{uri.path}"
905
+ else
906
+ request_url = "#{uri.scheme.downcase}://#{uri.host}:#{uri.port}#{uri.path}"
907
+ end
908
+ [req.header.request_method.upcase, request_url, params.sort.join('&')].map { |e|
909
+ escape(e)
910
+ }.join('&')
911
+ end
912
+
913
+ def sign_hmac_sha1(config, base_string)
914
+ unless SSLEnabled
915
+ raise ConfigurationError.new("openssl required for OAuth implementation")
516
916
  end
517
- true
917
+ key = [escape(config.consumer_secret.to_s), escape(config.secret.to_s)].join('&')
918
+ digester = OpenSSL::Digest::SHA1.new
919
+ [OpenSSL::HMAC.digest(digester, key, base_string)].pack('m*').chomp
518
920
  end
519
921
  end
520
922