httpclient-xaop 2.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,762 @@
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
+ url = uri.path
328
+ url = "#{url}?#{uri.query}" if uri.query
329
+ a_1 = "#{user}:#{param['realm']}:#{passwd}"
330
+ a_2 = "#{method}:#{url}"
331
+ nonce = param['nonce']
332
+ cnonce = generate_cnonce()
333
+ @nonce_count += 1
334
+ message_digest = []
335
+ message_digest << Digest::MD5.hexdigest(a_1)
336
+ message_digest << nonce
337
+ message_digest << ('%08x' % @nonce_count)
338
+ message_digest << cnonce
339
+ message_digest << param['qop']
340
+ message_digest << Digest::MD5.hexdigest(a_2)
341
+ header = []
342
+ header << "username=\"#{user}\""
343
+ header << "realm=\"#{param['realm']}\""
344
+ header << "nonce=\"#{nonce}\""
345
+ header << "uri=\"#{uri.path}\""
346
+ header << "cnonce=\"#{cnonce}\""
347
+ header << "nc=#{'%08x' % @nonce_count}"
348
+ header << "qop=\"#{param['qop']}\""
349
+ header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
350
+ header << "algorithm=\"MD5\""
351
+ header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
352
+ header.join(", ")
353
+ end
354
+
355
+ # cf. WEBrick::HTTPAuth::DigestAuth#generate_next_nonce(aTime)
356
+ def generate_cnonce
357
+ now = "%012d" % Time.now.to_i
358
+ pk = Digest::MD5.hexdigest([now, self.__id__, Process.pid, rand(65535)].join)[0, 32]
359
+ [now + ':' + pk].pack('m*').chop
360
+ end
361
+
362
+ def parse_challenge_param(param_str)
363
+ param = {}
364
+ param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
365
+ key, value = str[0].scan(/\A([^=]+)=(.*)\z/)[0]
366
+ if /\A"(.*)"\z/ =~ value
367
+ value = $1.gsub(/\\(.)/, '\1')
368
+ end
369
+ param[key] = value
370
+ end
371
+ param
372
+ end
373
+ end
374
+
375
+
376
+ # Authentication filter for handling Negotiate/NTLM negotiation.
377
+ # Used in WWWAuth and ProxyAuth.
378
+ #
379
+ # NegotiateAuth depends on 'ruby/ntlm' module.
380
+ class NegotiateAuth
381
+ # Authentication scheme.
382
+ attr_reader :scheme
383
+ # NTLM opt for ruby/ntlm. {:ntlmv2 => true} by default.
384
+ attr_reader :ntlm_opt
385
+
386
+ # Creates new NegotiateAuth filter.
387
+ def initialize(scheme = "Negotiate")
388
+ @auth = {}
389
+ @auth_default = nil
390
+ @challenge = {}
391
+ @scheme = scheme
392
+ @ntlm_opt = {
393
+ :ntlmv2 => true
394
+ }
395
+ end
396
+
397
+ # Resets challenge state. Do not send '*Authorization' header until the
398
+ # server sends '*Authentication' again.
399
+ def reset_challenge
400
+ @challenge.clear
401
+ end
402
+
403
+ # Set authentication credential.
404
+ # uri == nil for generic purpose (allow to use user/password for any URL).
405
+ def set(uri, user, passwd)
406
+ if uri
407
+ uri = Util.uri_dirname(uri)
408
+ @auth[uri] = [user, passwd]
409
+ else
410
+ @auth_default = [user, passwd]
411
+ end
412
+ end
413
+
414
+ # Response handler: returns credential.
415
+ # See ruby/ntlm for negotiation state transition.
416
+ def get(req)
417
+ return nil unless NTLMEnabled
418
+ target_uri = req.header.request_uri
419
+ domain_uri, param = @challenge.find { |uri, v|
420
+ Util.uri_part_of(target_uri, uri)
421
+ }
422
+ return nil unless param
423
+ user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
424
+ Util.uri_part_of(target_uri, uri)
425
+ }
426
+ unless user
427
+ user, passwd = @auth_default
428
+ end
429
+ return nil unless user
430
+ state = param[:state]
431
+ authphrase = param[:authphrase]
432
+ case state
433
+ when :init
434
+ t1 = Net::NTLM::Message::Type1.new
435
+ return t1.encode64
436
+ when :response
437
+ t2 = Net::NTLM::Message.decode64(authphrase)
438
+ t3 = t2.response({:user => user, :password => passwd}, @ntlm_opt.dup)
439
+ @challenge.delete(domain_uri)
440
+ return t3.encode64
441
+ end
442
+ nil
443
+ end
444
+
445
+ # Challenge handler: remember URL and challenge token for response.
446
+ def challenge(uri, param_str)
447
+ return false unless NTLMEnabled
448
+ if param_str.nil? or @challenge[uri].nil?
449
+ c = @challenge[uri] = {}
450
+ c[:state] = :init
451
+ c[:authphrase] = ""
452
+ else
453
+ c = @challenge[uri]
454
+ c[:state] = :response
455
+ c[:authphrase] = param_str
456
+ end
457
+ true
458
+ end
459
+ end
460
+
461
+
462
+ # Authentication filter for handling Negotiate/NTLM negotiation.
463
+ # Used in ProxyAuth.
464
+ #
465
+ # SSPINegotiateAuth depends on 'win32/sspi' module.
466
+ class SSPINegotiateAuth
467
+ # Authentication scheme.
468
+ attr_reader :scheme
469
+
470
+ # Creates new SSPINegotiateAuth filter.
471
+ def initialize
472
+ @challenge = {}
473
+ @scheme = "Negotiate"
474
+ end
475
+
476
+ # Resets challenge state. Do not send '*Authorization' header until the
477
+ # server sends '*Authentication' again.
478
+ def reset_challenge
479
+ @challenge.clear
480
+ end
481
+
482
+ # Set authentication credential.
483
+ # NOT SUPPORTED: username and necessary data is retrieved by win32/sspi.
484
+ # See win32/sspi for more details.
485
+ def set(uri, user, passwd)
486
+ # not supported
487
+ end
488
+
489
+ # Response handler: returns credential.
490
+ # See win32/sspi for negotiation state transition.
491
+ def get(req)
492
+ return nil unless SSPIEnabled
493
+ target_uri = req.header.request_uri
494
+ domain_uri, param = @challenge.find { |uri, v|
495
+ Util.uri_part_of(target_uri, uri)
496
+ }
497
+ return nil unless param
498
+ state = param[:state]
499
+ authenticator = param[:authenticator]
500
+ authphrase = param[:authphrase]
501
+ case state
502
+ when :init
503
+ authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
504
+ return authenticator.get_initial_token(@scheme)
505
+ when :response
506
+ @challenge.delete(domain_uri)
507
+ return authenticator.complete_authentication(authphrase)
508
+ end
509
+ nil
510
+ end
511
+
512
+ # Challenge handler: remember URL and challenge token for response.
513
+ def challenge(uri, param_str)
514
+ return false unless SSPIEnabled
515
+ if param_str.nil? or @challenge[uri].nil?
516
+ c = @challenge[uri] = {}
517
+ c[:state] = :init
518
+ c[:authenticator] = nil
519
+ c[:authphrase] = ""
520
+ else
521
+ c = @challenge[uri]
522
+ c[:state] = :response
523
+ c[:authphrase] = param_str
524
+ end
525
+ true
526
+ end
527
+ end
528
+
529
+ # Authentication filter for handling OAuth negotiation.
530
+ # Used in WWWAuth.
531
+ #
532
+ # CAUTION: This impl only support '#7 Accessing Protected Resources' in OAuth
533
+ # Core 1.0 spec for now. You need to obtain Access token and Access secret by
534
+ # yourself.
535
+ #
536
+ # CAUTION: This impl does NOT support OAuth Request Body Hash spec for now.
537
+ # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
538
+ #
539
+ class OAuth
540
+ include HTTPClient::Util
541
+
542
+ # Authentication scheme.
543
+ attr_reader :scheme
544
+
545
+ class Config
546
+ include HTTPClient::Util
547
+
548
+ attr_accessor :http_method
549
+ attr_accessor :realm
550
+ attr_accessor :consumer_key
551
+ attr_accessor :consumer_secret
552
+ attr_accessor :token
553
+ attr_accessor :secret
554
+ attr_accessor :signature_method
555
+ attr_accessor :version
556
+ attr_accessor :callback
557
+ attr_accessor :verifier
558
+ attr_reader :signature_handler
559
+
560
+ attr_accessor :debug_timestamp
561
+ attr_accessor :debug_nonce
562
+
563
+ def initialize(*args)
564
+ @http_method,
565
+ @realm,
566
+ @consumer_key,
567
+ @consumer_secret,
568
+ @token,
569
+ @secret,
570
+ @signature_method,
571
+ @version,
572
+ @callback,
573
+ @verifier =
574
+ keyword_argument(args,
575
+ :http_method,
576
+ :realm,
577
+ :consumer_key,
578
+ :consumer_secret,
579
+ :token,
580
+ :secret,
581
+ :signature_method,
582
+ :version,
583
+ :callback,
584
+ :verifier
585
+ )
586
+ @http_method ||= :post
587
+ @signature_handler = {}
588
+ end
589
+ end
590
+
591
+ def self.escape(str) # :nodoc:
592
+ if str.respond_to?(:force_encoding)
593
+ s = str.dup.force_encoding('BINARY').gsub(/([^a-zA-Z0-9_.~-]+)/) {
594
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
595
+ }
596
+ else
597
+ str.gsub(/([^a-zA-Z0-9_.~-]+)/n) {
598
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
599
+ }
600
+ end
601
+ end
602
+
603
+ def escape(str)
604
+ self.class.escape(str)
605
+ end
606
+
607
+ # Creates new DigestAuth filter.
608
+ def initialize
609
+ @config = nil # common config
610
+ @auth = {} # configs for each site
611
+ @challengeable = {}
612
+ @nonce_count = 0
613
+ @signature_handler = {
614
+ 'HMAC-SHA1' => method(:sign_hmac_sha1)
615
+ }
616
+ @scheme = "OAuth"
617
+ end
618
+
619
+ # Resets challenge state. Do not send '*Authorization' header until the
620
+ # server sends '*Authentication' again.
621
+ def reset_challenge
622
+ @challengeable.clear
623
+ end
624
+
625
+ # Set authentication credential.
626
+ # You cannot set OAuth config via WWWAuth#set_auth. Use OAuth#config=
627
+ def set(uri, user, passwd)
628
+ # not supported
629
+ end
630
+
631
+ # Set authentication credential.
632
+ def set_config(uri, config)
633
+ if uri.nil?
634
+ @config = config
635
+ else
636
+ uri = Util.uri_dirname(urify(uri))
637
+ @auth[uri] = config
638
+ end
639
+ end
640
+
641
+ # Get authentication credential.
642
+ def get_config(uri = nil)
643
+ if uri.nil?
644
+ @config
645
+ else
646
+ uri = urify(uri)
647
+ Util.hash_find_value(@auth) { |cand_uri, cred|
648
+ Util.uri_part_of(uri, cand_uri)
649
+ }
650
+ end
651
+ end
652
+
653
+ # Response handler: returns credential.
654
+ # It sends cred only when a given uri is;
655
+ # * child page of challengeable(got *Authenticate before) uri and,
656
+ # * child page of defined credential
657
+ def get(req)
658
+ target_uri = req.header.request_uri
659
+ return nil unless @challengeable[nil] or @challengeable.find { |uri, ok|
660
+ Util.uri_part_of(target_uri, uri) and ok
661
+ }
662
+ config = get_config(target_uri) || @config
663
+ return nil unless config
664
+ calc_cred(req, config)
665
+ end
666
+
667
+ # Challenge handler: remember URL for response.
668
+ def challenge(uri, param_str = nil)
669
+ if uri.nil?
670
+ @challengeable[nil] = true
671
+ else
672
+ @challengeable[urify(uri)] = true
673
+ end
674
+ true
675
+ end
676
+
677
+ private
678
+
679
+ def calc_cred(req, config)
680
+ header = {}
681
+ header['oauth_consumer_key'] = config.consumer_key
682
+ header['oauth_token'] = config.token
683
+ header['oauth_signature_method'] = config.signature_method
684
+ header['oauth_timestamp'] = config.debug_timestamp || Time.now.to_i.to_s
685
+ header['oauth_nonce'] = config.debug_nonce || generate_nonce()
686
+ header['oauth_version'] = config.version if config.version
687
+ header['oauth_callback'] = config.callback if config.callback
688
+ header['oauth_verifier'] = config.verifier if config.verifier
689
+ signature = sign(config, header, req)
690
+ header['oauth_signature'] = signature
691
+ # no need to do but we should sort for easier to test.
692
+ str = header.sort_by { |k, v| k }.map { |k, v| encode_header(k, v) }.join(', ')
693
+ if config.realm
694
+ str = %Q(realm="#{config.realm}", ) + str
695
+ end
696
+ str
697
+ end
698
+
699
+ def generate_nonce
700
+ @nonce_count += 1
701
+ now = "%012d" % Time.now.to_i
702
+ pk = Digest::MD5.hexdigest([@nonce_count.to_s, now, self.__id__, Process.pid, rand(65535)].join)[0, 32]
703
+ [now + ':' + pk].pack('m*').chop
704
+ end
705
+
706
+ def encode_header(k, v)
707
+ %Q(#{escape(k.to_s)}="#{escape(v.to_s)}")
708
+ end
709
+
710
+ def encode_param(params)
711
+ params.map { |k, v|
712
+ [v].flatten.map { |vv|
713
+ %Q(#{escape(k.to_s)}=#{escape(vv.to_s)})
714
+ }
715
+ }.flatten
716
+ end
717
+
718
+ def sign(config, header, req)
719
+ base_string = create_base_string(config, header, req)
720
+ if handler = config.signature_handler[config.signature_method] || @signature_handler[config.signature_method.to_s]
721
+ handler.call(config, base_string)
722
+ else
723
+ raise ConfigurationError.new("Unknown OAuth signature method: #{config.signature_method}")
724
+ end
725
+ end
726
+
727
+ def create_base_string(config, header, req)
728
+ params = encode_param(header)
729
+ query = req.header.request_query
730
+ if query and HTTP::Message.multiparam_query?(query)
731
+ params += encode_param(query)
732
+ end
733
+ # captures HTTP Message body only for 'application/x-www-form-urlencoded'
734
+ if req.header.contenttype == 'application/x-www-form-urlencoded' and req.body.size
735
+ params += encode_param(HTTP::Message.parse(req.body.content))
736
+ end
737
+ uri = req.header.request_uri
738
+ if uri.query
739
+ params += encode_param(HTTP::Message.parse(uri.query))
740
+ end
741
+ if uri.port == uri.default_port
742
+ request_url = "#{uri.scheme.downcase}://#{uri.host}#{uri.path}"
743
+ else
744
+ request_url = "#{uri.scheme.downcase}://#{uri.host}:#{uri.port}#{uri.path}"
745
+ end
746
+ [req.header.request_method.upcase, request_url, params.sort.join('&')].map { |e|
747
+ escape(e)
748
+ }.join('&')
749
+ end
750
+
751
+ def sign_hmac_sha1(config, base_string)
752
+ unless SSLEnabled
753
+ raise ConfigurationError.new("openssl required for OAuth implementation")
754
+ end
755
+ key = [escape(config.consumer_secret.to_s), escape(config.secret.to_s)].join('&')
756
+ digester = OpenSSL::Digest::SHA1.new
757
+ [OpenSSL::HMAC.digest(digester, key, base_string)].pack('m*').chomp
758
+ end
759
+ end
760
+
761
+
762
+ end