httpclient 2.1.5 → 2.8.3

Sign up to get free protection for your applications and to get access to all the features.
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