httpclient-fixcerts 2.8.5

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +98 -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/cookie.rb +1 -0
  7. data/lib/http-access2/http.rb +1 -0
  8. data/lib/http-access2.rb +55 -0
  9. data/lib/httpclient/auth.rb +924 -0
  10. data/lib/httpclient/cacert.pem +3952 -0
  11. data/lib/httpclient/cacert1024.pem +3866 -0
  12. data/lib/httpclient/connection.rb +88 -0
  13. data/lib/httpclient/cookie.rb +220 -0
  14. data/lib/httpclient/http.rb +1082 -0
  15. data/lib/httpclient/include_client.rb +85 -0
  16. data/lib/httpclient/jruby_ssl_socket.rb +594 -0
  17. data/lib/httpclient/session.rb +960 -0
  18. data/lib/httpclient/ssl_config.rb +433 -0
  19. data/lib/httpclient/ssl_socket.rb +150 -0
  20. data/lib/httpclient/timeout.rb +140 -0
  21. data/lib/httpclient/util.rb +222 -0
  22. data/lib/httpclient/version.rb +3 -0
  23. data/lib/httpclient/webagent-cookie.rb +459 -0
  24. data/lib/httpclient.rb +1332 -0
  25. data/lib/jsonclient.rb +66 -0
  26. data/lib/oauthclient.rb +111 -0
  27. data/sample/async.rb +8 -0
  28. data/sample/auth.rb +11 -0
  29. data/sample/cookie.rb +18 -0
  30. data/sample/dav.rb +103 -0
  31. data/sample/howto.rb +49 -0
  32. data/sample/jsonclient.rb +67 -0
  33. data/sample/oauth_buzz.rb +57 -0
  34. data/sample/oauth_friendfeed.rb +59 -0
  35. data/sample/oauth_twitter.rb +61 -0
  36. data/sample/ssl/0cert.pem +22 -0
  37. data/sample/ssl/0key.pem +30 -0
  38. data/sample/ssl/1000cert.pem +19 -0
  39. data/sample/ssl/1000key.pem +18 -0
  40. data/sample/ssl/htdocs/index.html +10 -0
  41. data/sample/ssl/ssl_client.rb +22 -0
  42. data/sample/ssl/webrick_httpsd.rb +29 -0
  43. data/sample/stream.rb +21 -0
  44. data/sample/thread.rb +27 -0
  45. data/sample/wcat.rb +21 -0
  46. data/test/ca-chain.pem +44 -0
  47. data/test/ca.cert +23 -0
  48. data/test/client-pass.key +18 -0
  49. data/test/client.cert +19 -0
  50. data/test/client.key +15 -0
  51. data/test/helper.rb +131 -0
  52. data/test/htdigest +1 -0
  53. data/test/htpasswd +2 -0
  54. data/test/jruby_ssl_socket/test_pemutils.rb +32 -0
  55. data/test/runner.rb +2 -0
  56. data/test/server.cert +19 -0
  57. data/test/server.key +15 -0
  58. data/test/sslsvr.rb +65 -0
  59. data/test/subca.cert +21 -0
  60. data/test/test_auth.rb +492 -0
  61. data/test/test_cookie.rb +309 -0
  62. data/test/test_hexdump.rb +14 -0
  63. data/test/test_http-access2.rb +508 -0
  64. data/test/test_httpclient.rb +2145 -0
  65. data/test/test_include_client.rb +52 -0
  66. data/test/test_jsonclient.rb +98 -0
  67. data/test/test_ssl.rb +562 -0
  68. data/test/test_webagent-cookie.rb +465 -0
  69. metadata +124 -0
@@ -0,0 +1,924 @@
1
+ # HTTPClient - HTTP client library.
2
+ # Copyright (C) 2000-2015 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
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
+
9
+ require 'digest/md5'
10
+ require 'httpclient/session'
11
+ require 'mutex_m'
12
+
13
+
14
+ class HTTPClient
15
+
16
+ NTLMEnabled = false
17
+ SSPIEnabled = false
18
+ GSSAPIEnabled = false
19
+
20
+ # Common abstract class for authentication filter.
21
+ #
22
+ # There are 2 authentication filters.
23
+ # WWWAuth:: Authentication filter for handling authentication negotiation
24
+ # between Web server. Parses 'WWW-Authentication' header in
25
+ # response and generates 'Authorization' header in request.
26
+ # ProxyAuth:: Authentication filter for handling authentication negotiation
27
+ # between Proxy server. Parses 'Proxy-Authentication' header in
28
+ # response and generates 'Proxy-Authorization' header in request.
29
+ class AuthFilterBase
30
+ private
31
+
32
+ def parse_authentication_header(res, tag)
33
+ challenge = res.header[tag]
34
+ return nil unless challenge
35
+ challenge.collect { |c| parse_challenge_header(c) }.compact
36
+ end
37
+
38
+ def parse_challenge_header(challenge)
39
+ scheme, param_str = challenge.scan(/\A(\S+)(?:\s+(.*))?\z/)[0]
40
+ return nil if scheme.nil?
41
+ return scheme, param_str
42
+ end
43
+ end
44
+
45
+
46
+ # Authentication filter for handling authentication negotiation between
47
+ # Web server. Parses 'WWW-Authentication' header in response and
48
+ # generates 'Authorization' header in request.
49
+ #
50
+ # Authentication filter is implemented using request filter of HTTPClient.
51
+ # It traps HTTP response header and maintains authentication state, and
52
+ # traps HTTP request header for inserting necessary authentication header.
53
+ #
54
+ # WWWAuth has sub filters (BasicAuth, DigestAuth, NegotiateAuth and
55
+ # SSPINegotiateAuth) and delegates some operations to it.
56
+ # NegotiateAuth requires 'ruby/ntlm' module (rubyntlm gem).
57
+ # SSPINegotiateAuth requires 'win32/sspi' module (rubysspi gem).
58
+ class WWWAuth < AuthFilterBase
59
+ attr_reader :basic_auth
60
+ attr_reader :digest_auth
61
+ attr_reader :negotiate_auth
62
+ attr_reader :sspi_negotiate_auth
63
+ attr_reader :oauth
64
+
65
+ # Creates new WWWAuth.
66
+ def initialize
67
+ @basic_auth = BasicAuth.new
68
+ @digest_auth = DigestAuth.new
69
+ @negotiate_auth = NegotiateAuth.new
70
+ @ntlm_auth = NegotiateAuth.new('NTLM')
71
+ @sspi_negotiate_auth = SSPINegotiateAuth.new
72
+ @oauth = OAuth.new
73
+ # sort authenticators by priority
74
+ @authenticator = [@oauth, @negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
75
+ end
76
+
77
+ # Resets challenge state. See sub filters for more details.
78
+ def reset_challenge
79
+ @authenticator.each do |auth|
80
+ auth.reset_challenge
81
+ end
82
+ end
83
+
84
+ # Set authentication credential. See sub filters for more details.
85
+ def set_auth(uri, user, passwd)
86
+ @authenticator.each do |auth|
87
+ auth.set(uri, user, passwd)
88
+ end
89
+ reset_challenge
90
+ end
91
+
92
+ # Filter API implementation. Traps HTTP request and insert
93
+ # 'Authorization' header if needed.
94
+ def filter_request(req)
95
+ @authenticator.each do |auth|
96
+ next unless auth.set? # hasn't be set, don't use it
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
105
+ req.header.set('Authorization', auth.scheme + " " + cred)
106
+ return
107
+ end
108
+ end
109
+ end
110
+
111
+ # Filter API implementation. Traps HTTP response and parses
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.
117
+ def filter_response(req, res)
118
+ command = nil
119
+ if res.status == HTTP::Status::UNAUTHORIZED
120
+ if challenge = parse_authentication_header(res, 'www-authenticate')
121
+ uri = req.header.request_uri
122
+ challenge.each do |scheme, param_str|
123
+ @authenticator.each do |auth|
124
+ next unless auth.set? # hasn't be set, don't use it
125
+ if scheme.downcase == auth.scheme.downcase
126
+ challengeable = auth.challenge(uri, param_str)
127
+ command = :retry if challengeable
128
+ end
129
+ end
130
+ end
131
+ # ignore unknown authentication scheme
132
+ end
133
+ end
134
+ command
135
+ end
136
+ end
137
+
138
+
139
+ # Authentication filter for handling authentication negotiation between
140
+ # Proxy server. Parses 'Proxy-Authentication' header in response and
141
+ # generates 'Proxy-Authorization' header in request.
142
+ #
143
+ # Authentication filter is implemented using request filter of HTTPClient.
144
+ # It traps HTTP response header and maintains authentication state, and
145
+ # traps HTTP request header for inserting necessary authentication header.
146
+ #
147
+ # ProxyAuth has sub filters (BasicAuth, NegotiateAuth, and SSPINegotiateAuth)
148
+ # and delegates some operations to it.
149
+ # NegotiateAuth requires 'ruby/ntlm' module.
150
+ # SSPINegotiateAuth requires 'win32/sspi' module.
151
+ class ProxyAuth < AuthFilterBase
152
+ attr_reader :basic_auth
153
+ attr_reader :digest_auth
154
+ attr_reader :negotiate_auth
155
+ attr_reader :sspi_negotiate_auth
156
+
157
+ # Creates new ProxyAuth.
158
+ def initialize
159
+ @basic_auth = ProxyBasicAuth.new
160
+ @negotiate_auth = NegotiateAuth.new
161
+ @ntlm_auth = NegotiateAuth.new('NTLM')
162
+ @sspi_negotiate_auth = SSPINegotiateAuth.new
163
+ @digest_auth = ProxyDigestAuth.new
164
+ # sort authenticators by priority
165
+ @authenticator = [@negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
166
+ end
167
+
168
+ # Resets challenge state. See sub filters for more details.
169
+ def reset_challenge
170
+ @authenticator.each do |auth|
171
+ auth.reset_challenge
172
+ end
173
+ end
174
+
175
+ # Set authentication credential. See sub filters for more details.
176
+ def set_auth(user, passwd)
177
+ @authenticator.each do |auth|
178
+ auth.set(nil, user, passwd)
179
+ end
180
+ reset_challenge
181
+ end
182
+
183
+ # Filter API implementation. Traps HTTP request and insert
184
+ # 'Proxy-Authorization' header if needed.
185
+ def filter_request(req)
186
+ @authenticator.each do |auth|
187
+ next unless auth.set? # hasn't be set, don't use it
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
196
+ req.header.set('Proxy-Authorization', auth.scheme + " " + cred)
197
+ return
198
+ end
199
+ end
200
+ end
201
+
202
+ # Filter API implementation. Traps HTTP response and parses
203
+ # 'Proxy-Authenticate' header.
204
+ def filter_response(req, res)
205
+ command = nil
206
+ if res.status == HTTP::Status::PROXY_AUTHENTICATE_REQUIRED
207
+ if challenge = parse_authentication_header(res, 'proxy-authenticate')
208
+ uri = req.header.request_uri
209
+ challenge.each do |scheme, param_str|
210
+ @authenticator.each do |auth|
211
+ next unless auth.set? # hasn't be set, don't use it
212
+ if scheme.downcase == auth.scheme.downcase
213
+ challengeable = auth.challenge(uri, param_str)
214
+ command = :retry if challengeable
215
+ end
216
+ end
217
+ end
218
+ # ignore unknown authentication scheme
219
+ end
220
+ end
221
+ command
222
+ end
223
+ end
224
+
225
+ # Authentication filter base class.
226
+ class AuthBase
227
+ include HTTPClient::Util
228
+
229
+ # Authentication scheme.
230
+ attr_reader :scheme
231
+
232
+ def initialize(scheme)
233
+ @scheme = scheme
234
+ @challenge = {}
235
+ end
236
+
237
+ # Resets challenge state. Do not send '*Authorization' header until the
238
+ # server sends '*Authentication' again.
239
+ def reset_challenge
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
260
+ end
261
+
262
+ # Set authentication credential.
263
+ # uri == nil for generic purpose (allow to use user/password for any URL).
264
+ def set(uri, user, passwd)
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
272
+ end
273
+ end
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
+
280
+ # Response handler: returns credential.
281
+ # It sends cred only when a given uri is;
282
+ # * child page of challengeable(got *Authenticate before) uri and,
283
+ # * child page of defined credential
284
+ def get(req)
285
+ target_uri = req.header.request_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
+ }
294
+ }
295
+ end
296
+
297
+ # Challenge handler: remember URL for response.
298
+ def challenge(uri, param_str = nil)
299
+ synchronize {
300
+ @challenge[urify(uri)] = true
301
+ true
302
+ }
303
+ end
304
+ end
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
328
+
329
+ # Authentication filter for handling DigestAuth negotiation.
330
+ # Used in WWWAuth.
331
+ class DigestAuth < AuthBase
332
+ include Mutex_m
333
+
334
+ # Creates new DigestAuth filter.
335
+ def initialize
336
+ super('Digest')
337
+ @auth = {}
338
+ @nonce_count = 0
339
+ end
340
+
341
+ # Set authentication credential.
342
+ # uri == nil is ignored.
343
+ def set(uri, user, passwd)
344
+ synchronize do
345
+ if uri
346
+ uri = Util.uri_dirname(uri)
347
+ @auth[uri] = [user, passwd]
348
+ end
349
+ end
350
+ end
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
+
357
+ # Response handler: returns credential.
358
+ # It sends cred only when a given uri is;
359
+ # * child page of challengeable(got *Authenticate before) uri and,
360
+ # * child page of defined credential
361
+ def get(req)
362
+ target_uri = req.header.request_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)
373
+ }
374
+ end
375
+
376
+ # Challenge handler: remember URL and challenge token for response.
377
+ def challenge(uri, param_str)
378
+ synchronize {
379
+ @challenge[uri] = parse_challenge_param(param_str)
380
+ true
381
+ }
382
+ end
383
+
384
+ private
385
+
386
+ # this method is implemented by sromano and posted to
387
+ # http://tools.assembla.com/breakout/wiki/DigestForSoap
388
+ # Thanks!
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
393
+ a_1 = "#{user}:#{param['realm']}:#{passwd}"
394
+ a_2 = "#{method}:#{path}"
395
+ qop = param['qop']
396
+ nonce = param['nonce']
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
408
+ message_digest = []
409
+ message_digest << a_1_md5sum
410
+ message_digest << nonce
411
+ if qop
412
+ @nonce_count += 1
413
+ message_digest << ('%08x' % @nonce_count)
414
+ message_digest << cnonce
415
+ message_digest << param['qop']
416
+ end
417
+ message_digest << Digest::MD5.hexdigest(a_2)
418
+ header = []
419
+ header << "username=\"#{user}\""
420
+ header << "realm=\"#{param['realm']}\""
421
+ header << "nonce=\"#{nonce}\""
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
430
+ header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
431
+ header << "algorithm=#{algorithm}"
432
+ header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
433
+ header.join(", ")
434
+ end
435
+
436
+ # cf. WEBrick::HTTPAuth::DigestAuth#generate_next_nonce(aTime)
437
+ def generate_cnonce
438
+ now = "%012d" % Time.now.to_i
439
+ pk = Digest::MD5.hexdigest([now, self.__id__, Process.pid, rand(65535)].join)[0, 32]
440
+ [now + ':' + pk].pack('m*').chop
441
+ end
442
+
443
+ def parse_challenge_param(param_str)
444
+ param = {}
445
+ param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
446
+ key, value = str[0].scan(/\A([^=]+)=(.*)\z/)[0]
447
+ if /\A"(.*)"\z/ =~ value
448
+ value = $1.gsub(/\\(.)/, '\1')
449
+ end
450
+ param[key] = value
451
+ end
452
+ param
453
+ end
454
+ end
455
+
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
+
495
+ # Authentication filter for handling Negotiate/NTLM negotiation.
496
+ # Used in WWWAuth and ProxyAuth.
497
+ #
498
+ # NegotiateAuth depends on 'ruby/ntlm' module.
499
+ class NegotiateAuth < AuthBase
500
+ include Mutex_m
501
+
502
+ # NTLM opt for ruby/ntlm. {:ntlmv2 => true} by default.
503
+ attr_reader :ntlm_opt
504
+
505
+ # Creates new NegotiateAuth filter.
506
+ def initialize(scheme = "Negotiate")
507
+ super(scheme)
508
+ @auth = {}
509
+ @auth_default = nil
510
+ @ntlm_opt = {
511
+ :ntlmv2 => true
512
+ }
513
+ end
514
+
515
+ # Set authentication credential.
516
+ # uri == nil for generic purpose (allow to use user/password for any URL).
517
+ def set(uri, 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
525
+ end
526
+ end
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
+
533
+ # Response handler: returns credential.
534
+ # See ruby/ntlm for negotiation state transition.
535
+ def get(req)
536
+ target_uri = req.header.request_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
571
+ }
572
+ end
573
+
574
+ # Challenge handler: remember URL and challenge token for response.
575
+ def challenge(uri, param_str)
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
+ }
588
+ end
589
+ end
590
+
591
+
592
+ # Authentication filter for handling Negotiate/NTLM negotiation.
593
+ # Used in ProxyAuth.
594
+ #
595
+ # SSPINegotiateAuth depends on 'win32/sspi' module.
596
+ class SSPINegotiateAuth < AuthBase
597
+ include Mutex_m
598
+
599
+ # Creates new SSPINegotiateAuth filter.
600
+ def initialize
601
+ super('Negotiate')
602
+ end
603
+
604
+ # Set authentication credential.
605
+ # NOT SUPPORTED: username and necessary data is retrieved by win32/sspi.
606
+ # See win32/sspi for more details.
607
+ def set(*args)
608
+ # not supported
609
+ end
610
+
611
+ # Check always (not effective but it works)
612
+ def set?
613
+ !@challenge.empty?
614
+ end
615
+
616
+ # Response handler: returns credential.
617
+ # See win32/sspi for negotiation state transition.
618
+ def get(req)
619
+ target_uri = req.header.request_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
651
+ }
652
+ end
653
+
654
+ # Challenge handler: remember URL and challenge token for response.
655
+ def challenge(uri, param_str)
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
832
+ else
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")
916
+ end
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
920
+ end
921
+ end
922
+
923
+
924
+ end