maedana-httpclient 2.1.5.2.1

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