httpclient-fixcerts 2.8.5

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