httpclient-jgraichen 2.3.4.2

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