maedana-httpclient 2.1.5.2.1

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