glebtv-httpclient 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.rdoc +108 -0
  3. data/bin/httpclient +65 -0
  4. data/lib/glebtv-httpclient.rb +1 -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 +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 +1050 -0
  14. data/lib/httpclient/include_client.rb +83 -0
  15. data/lib/httpclient/session.rb +1031 -0
  16. data/lib/httpclient/ssl_config.rb +403 -0
  17. data/lib/httpclient/timeout.rb +140 -0
  18. data/lib/httpclient/util.rb +186 -0
  19. data/lib/httpclient/version.rb +3 -0
  20. data/lib/httpclient.rb +1157 -0
  21. data/lib/oauthclient.rb +110 -0
  22. data/sample/async.rb +8 -0
  23. data/sample/auth.rb +11 -0
  24. data/sample/cookie.rb +18 -0
  25. data/sample/dav.rb +103 -0
  26. data/sample/howto.rb +49 -0
  27. data/sample/oauth_buzz.rb +57 -0
  28. data/sample/oauth_friendfeed.rb +59 -0
  29. data/sample/oauth_twitter.rb +61 -0
  30. data/sample/ssl/0cert.pem +22 -0
  31. data/sample/ssl/0key.pem +30 -0
  32. data/sample/ssl/1000cert.pem +19 -0
  33. data/sample/ssl/1000key.pem +18 -0
  34. data/sample/ssl/htdocs/index.html +10 -0
  35. data/sample/ssl/ssl_client.rb +22 -0
  36. data/sample/ssl/webrick_httpsd.rb +29 -0
  37. data/sample/stream.rb +21 -0
  38. data/sample/thread.rb +27 -0
  39. data/sample/wcat.rb +21 -0
  40. data/test/ca-chain.cert +44 -0
  41. data/test/ca.cert +23 -0
  42. data/test/client.cert +19 -0
  43. data/test/client.key +15 -0
  44. data/test/helper.rb +129 -0
  45. data/test/htdigest +1 -0
  46. data/test/htpasswd +2 -0
  47. data/test/runner.rb +2 -0
  48. data/test/server.cert +19 -0
  49. data/test/server.key +15 -0
  50. data/test/sslsvr.rb +65 -0
  51. data/test/subca.cert +21 -0
  52. data/test/test_auth.rb +321 -0
  53. data/test/test_cookie.rb +412 -0
  54. data/test/test_hexdump.rb +14 -0
  55. data/test/test_http-access2.rb +507 -0
  56. data/test/test_httpclient.rb +1801 -0
  57. data/test/test_include_client.rb +52 -0
  58. data/test/test_ssl.rb +235 -0
  59. metadata +102 -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