httpclient 2.1.5.2 → 2.1.6

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,36 @@
1
+ # This was written by Arai-san and published at
2
+ # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/31987
3
+
4
+
5
+ module HexDump
6
+ def encode(str)
7
+ offset = 0
8
+ result = []
9
+ while raw = str.slice(offset, 16) and raw.length > 0
10
+ # data field
11
+ data = ''
12
+ for v in raw.unpack('N* a*')
13
+ if v.kind_of? Integer
14
+ data << sprintf("%08x ", v)
15
+ else
16
+ v.each_byte {|c| data << sprintf("%02x", c) }
17
+ end
18
+ end
19
+ # text field
20
+ text = raw.tr("\000-\037\177-\377", ".")
21
+ result << sprintf("%08x %-36s %s", offset, data, text)
22
+ offset += 16
23
+ # omit duplicate line
24
+ if /^(#{ Regexp.quote(raw) })+/n =~ str[offset .. -1]
25
+ result << sprintf("%08x ...", offset)
26
+ offset += $&.length
27
+ # should print at the end
28
+ if offset == str.length
29
+ result << sprintf("%08x %-36s %s", offset-16, data, text)
30
+ end
31
+ end
32
+ end
33
+ result
34
+ end
35
+ module_function :encode
36
+ end
@@ -19,7 +19,7 @@ require 'httpclient/http'
19
19
  require 'httpclient/auth'
20
20
  require 'httpclient/cookie'
21
21
 
22
-
22
+ # :main:HTTPClient
23
23
  # The HTTPClient class provides several methods for accessing Web resources
24
24
  # via HTTP.
25
25
  #
@@ -98,6 +98,17 @@ require 'httpclient/cookie'
98
98
  # res = clnt.post(uri, body)
99
99
  # end
100
100
  #
101
+ # 3. Do multipart wth custom body.
102
+ #
103
+ # File.open('/tmp/post_data') do |file|
104
+ # body = [{ 'Content-Type' => 'application/atom+xml; charset=UTF-8',
105
+ # :content => '<entry>...</entry>' },
106
+ # { 'Content-Type' => 'video/mp4',
107
+ # 'Content-Transfer-Encoding' => 'binary',
108
+ # :content => file }]
109
+ # res = clnt.post(uri, body)
110
+ # end
111
+ #
101
112
  # === Accessing via SSL
102
113
  #
103
114
  # Ruby needs to be compiled with OpenSSL.
@@ -199,9 +210,9 @@ require 'httpclient/cookie'
199
210
  # ruby -rhttpclient -e 'p HTTPClient.head(ARGV.shift).header["last-modified"]' http://dev.ctor.org/
200
211
  #
201
212
  class HTTPClient
202
- VERSION = '2.1.5'
213
+ VERSION = '2.1.6'
203
214
  RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
204
- /: (\S+) (\S+)/ =~ %q$Id: httpclient.rb 280 2009-06-02 15:44:28Z nahi $
215
+ /: (\S+) (\S+)/ =~ %q$Id$
205
216
  LIB_NAME = "(#{$1}/#{$2}, #{RUBY_VERSION_STRING})"
206
217
 
207
218
  include Util
@@ -317,6 +328,8 @@ class HTTPClient
317
328
  # An array of response HTTP String (not a HTTP message body) which is used
318
329
  # for loopback test. See test/* to see how to use it.
319
330
  attr_proxy(:test_loopback_http_response)
331
+ # Decompress a compressed (with gzip or deflate) content body transparently. false by default.
332
+ attr_proxy(:transparent_gzip_decompression, true)
320
333
 
321
334
  # Default extheader for PROPFIND request.
322
335
  PROPFIND_DEFAULT_EXTHEADER = { 'Depth' => '0' }
@@ -336,7 +349,7 @@ class HTTPClient
336
349
  # You can use a keyword argument style Hash. Keys are :proxy, :agent_name
337
350
  # and :from.
338
351
  #
339
- # HTTPClient.new(:agent_name = 'MyAgent/0.1')
352
+ # HTTPClient.new(:agent_name => 'MyAgent/0.1')
340
353
  def initialize(*args)
341
354
  proxy, agent_name, from = keyword_argument(args, :proxy, :agent_name, :from)
342
355
  @proxy = nil # assigned later.
@@ -522,17 +535,25 @@ class HTTPClient
522
535
  # Posts a content.
523
536
  #
524
537
  # uri:: a String or an URI object which represents an URL of web resource.
525
- # body:: a Hash or an Array of body part.
526
- # e.g. { "a" => "b" } => 'a=b'.
538
+ # body:: a Hash or an Array of body part. e.g.
539
+ # { "a" => "b" } => 'a=b'
527
540
  # Give an array to pass multiple value like
528
- # [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
541
+ # [["a", "b"], ["a", "c"]] => 'a=b&a=c'
529
542
  # When you pass a File as a value, it will be posted as a
530
- # multipart/form-data. e.g. { 'upload' => file }
531
- # extheader:: a Hash or an Array of extra headers. e.g.
532
- # { 'Accept' => '*/*' } or
533
- # [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
543
+ # multipart/form-data. e.g.
544
+ # { 'upload' => file }
545
+ # You can also send custom multipart by passing an array of hashes.
546
+ # Each part must have a :content attribute which can be a file, all
547
+ # other keys will become headers.
548
+ # [{ 'Content-Type' => 'text/plain', :content => "some text" },
549
+ # { 'Content-Type' => 'video/mp4', :content => File.new('video.mp4') }]
550
+ # => <Two parts with custom Content-Type header>
551
+ # extheader:: a Hash or an Array of extra headers. e.g.
552
+ # { 'Accept' => '*/*' }
553
+ # or
554
+ # [['Accept', 'image/jpeg'], ['Accept', 'image/png']].
534
555
  # &block:: Give a block to get chunked message-body of response like
535
- # post_content(uri) { |chunked_body| ... }.
556
+ # post_content(uri) { |chunked_body| ... }.
536
557
  # Size of each chunk may not be the same.
537
558
  #
538
559
  # post_content follows HTTP redirect status (see HTTP::Status.redirect?)
@@ -569,14 +590,14 @@ class HTTPClient
569
590
  # in HTTP header.
570
591
  def default_redirect_uri_callback(uri, res)
571
592
  newuri = URI.parse(res.header['location'][0])
572
- if https?(uri) && !https?(newuri)
573
- raise BadResponseError.new("redirecting to non-https resource")
574
- end
575
593
  unless newuri.is_a?(URI::HTTP)
576
594
  newuri = uri + newuri
577
595
  STDERR.puts("could be a relative URI in location header which is not recommended")
578
596
  STDERR.puts("'The field value consists of a single absolute URI' in HTTP spec")
579
597
  end
598
+ if https?(uri) && !https?(newuri)
599
+ raise BadResponseError.new("redirecting to non-https resource")
600
+ end
580
601
  puts "redirect to: #{newuri}" if $DEBUG
581
602
  newuri
582
603
  end
@@ -634,13 +655,21 @@ class HTTPClient
634
655
  # e.g. { "a" => "b" } => 'http://host/part?a=b'
635
656
  # Give an array to pass multiple value like
636
657
  # [["a", "b"], ["a", "c"]] => 'http://host/part?a=b&a=c'
637
- # body:: a Hash or an Array of body part.
638
- # e.g. { "a" => "b" } => 'a=b'.
658
+ # body:: a Hash or an Array of body part. e.g.
659
+ # { "a" => "b" }
660
+ # => 'a=b'
639
661
  # Give an array to pass multiple value like
640
- # [["a", "b"], ["a", "c"]] => 'a=b&a=c'.
662
+ # [["a", "b"], ["a", "c"]]
663
+ # => 'a=b&a=c'.
641
664
  # When the given method is 'POST' and the given body contains a file
642
- # as a value, it will be posted as a multipart/form-data.
643
- # e.g. { 'upload' => file }
665
+ # as a value, it will be posted as a multipart/form-data. e.g.
666
+ # { 'upload' => file }
667
+ # You can also send custom multipart by passing an array of hashes.
668
+ # Each part must have a :content attribute which can be a file, all
669
+ # other keys will become headers.
670
+ # [{ 'Content-Type' => 'text/plain', :content => "some text" },
671
+ # { 'Content-Type' => 'video/mp4', :content => File.new('video.mp4') }]
672
+ # => <Two parts with custom Content-Type header>
644
673
  # See HTTP::Message.file? for actual condition of 'a file'.
645
674
  # extheader:: a Hash or an Array of extra headers. e.g.
646
675
  # { 'Accept' => '*/*' } or
@@ -747,6 +776,10 @@ private
747
776
  end
748
777
 
749
778
  class KeepAliveDisconnected < StandardError # :nodoc:
779
+ attr_reader :sess
780
+ def initialize(sess = nil)
781
+ @sess = sess
782
+ end
750
783
  end
751
784
 
752
785
  def do_request(method, uri, query, body, extheader, &block)
@@ -777,22 +810,26 @@ private
777
810
  def do_request_async(method, uri, query, body, extheader)
778
811
  conn = Connection.new
779
812
  t = Thread.new(conn) { |tconn|
780
- if HTTP::Message.file?(body)
781
- pos = body.pos rescue nil
782
- end
783
- retry_count = @session_manager.protocol_retry_count
784
- proxy = no_proxy?(uri) ? nil : @proxy
785
- while retry_count > 0
786
- body.pos = pos if pos
787
- req = create_request(method, uri, query, body, extheader)
788
- begin
789
- protect_keep_alive_disconnected do
790
- do_get_stream(req, proxy, tconn)
813
+ begin
814
+ if HTTP::Message.file?(body)
815
+ pos = body.pos rescue nil
816
+ end
817
+ retry_count = @session_manager.protocol_retry_count
818
+ proxy = no_proxy?(uri) ? nil : @proxy
819
+ while retry_count > 0
820
+ body.pos = pos if pos
821
+ req = create_request(method, uri, query, body, extheader)
822
+ begin
823
+ protect_keep_alive_disconnected do
824
+ do_get_stream(req, proxy, tconn)
825
+ end
826
+ break
827
+ rescue RetryableResponse
828
+ retry_count -= 1
791
829
  end
792
- break
793
- rescue RetryableResponse
794
- retry_count -= 1
795
830
  end
831
+ rescue Exception
832
+ conn.push $!
796
833
  end
797
834
  }
798
835
  conn.async_thread = t
@@ -846,7 +883,10 @@ private
846
883
  def protect_keep_alive_disconnected
847
884
  begin
848
885
  yield
849
- rescue KeepAliveDisconnected
886
+ rescue KeepAliveDisconnected => e
887
+ if e.sess
888
+ @session_manager.invalidate(e.sess.dest)
889
+ end
850
890
  yield
851
891
  end
852
892
  end
@@ -860,7 +900,7 @@ private
860
900
  end
861
901
  boundary = nil
862
902
  if body
863
- dummy, content_type = extheader.find { |key, value|
903
+ _, content_type = extheader.find { |key, value|
864
904
  key.downcase == 'content-type'
865
905
  }
866
906
  if content_type
@@ -932,10 +972,6 @@ private
932
972
  false
933
973
  end
934
974
 
935
- def https?(uri)
936
- uri.scheme.downcase == 'https'
937
- end
938
-
939
975
  # !! CAUTION !!
940
976
  # Method 'do_get*' runs under MT conditon. Be careful to change.
941
977
  def do_get_block(req, proxy, conn, &block)
@@ -956,12 +992,15 @@ private
956
992
  do_get_header(req, res, sess)
957
993
  conn.push(res)
958
994
  sess.get_body do |part|
995
+ force_binary(part)
959
996
  if block
960
997
  block.call(res, part)
961
998
  else
962
999
  content << part
963
1000
  end
964
1001
  end
1002
+ # there could be a race condition but it's OK to cache unreusable
1003
+ # connection because we do retry for that case.
965
1004
  @session_manager.keep(sess) unless sess.closed?
966
1005
  commands = @request_filter.collect { |filter|
967
1006
  filter.filter_response(req, res)
@@ -989,18 +1028,19 @@ private
989
1028
  do_get_header(req, res, sess)
990
1029
  conn.push(res)
991
1030
  sess.get_body do |part|
992
- pipew.syswrite(part)
1031
+ force_binary(part)
1032
+ pipew.write(part)
993
1033
  end
994
1034
  pipew.close
995
1035
  @session_manager.keep(sess) unless sess.closed?
996
- commands = @request_filter.collect { |filter|
1036
+ _ = @request_filter.collect { |filter|
997
1037
  filter.filter_response(req, res)
998
1038
  }
999
1039
  # ignore commands (not retryable in async mode)
1000
1040
  end
1001
1041
 
1002
1042
  def do_get_header(req, res, sess)
1003
- res.version, res.status, res.reason, headers = sess.get_header
1043
+ res.http_version, res.status, res.reason, headers = sess.get_header
1004
1044
  headers.each do |key, value|
1005
1045
  res.header.add(key, value)
1006
1046
  end
@@ -63,22 +63,25 @@ class HTTPClient
63
63
  #
64
64
  # WWWAuth has sub filters (BasicAuth, DigestAuth, NegotiateAuth and
65
65
  # SSPINegotiateAuth) and delegates some operations to it.
66
- # NegotiateAuth requires 'ruby/ntlm' module.
67
- # SSPINegotiateAuth requires 'win32/sspi' module.
66
+ # NegotiateAuth requires 'ruby/ntlm' module (rubyntlm gem).
67
+ # SSPINegotiateAuth requires 'win32/sspi' module (rubysspi gem).
68
68
  class WWWAuth < AuthFilterBase
69
69
  attr_reader :basic_auth
70
70
  attr_reader :digest_auth
71
71
  attr_reader :negotiate_auth
72
72
  attr_reader :sspi_negotiate_auth
73
+ attr_reader :oauth
73
74
 
74
75
  # Creates new WWWAuth.
75
76
  def initialize
76
77
  @basic_auth = BasicAuth.new
77
78
  @digest_auth = DigestAuth.new
78
79
  @negotiate_auth = NegotiateAuth.new
80
+ @ntlm_auth = NegotiateAuth.new('NTLM')
79
81
  @sspi_negotiate_auth = SSPINegotiateAuth.new
82
+ @oauth = OAuth.new
80
83
  # sort authenticators by priority
81
- @authenticator = [@negotiate_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
84
+ @authenticator = [@oauth, @negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
82
85
  end
83
86
 
84
87
  # Resets challenge state. See sub filters for more details.
@@ -151,9 +154,10 @@ class HTTPClient
151
154
  def initialize
152
155
  @basic_auth = BasicAuth.new
153
156
  @negotiate_auth = NegotiateAuth.new
157
+ @ntlm_auth = NegotiateAuth.new('NTLM')
154
158
  @sspi_negotiate_auth = SSPINegotiateAuth.new
155
159
  # sort authenticators by priority
156
- @authenticator = [@negotiate_auth, @sspi_negotiate_auth, @basic_auth]
160
+ @authenticator = [@negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @basic_auth]
157
161
  end
158
162
 
159
163
  # Resets challenge state. See sub filters for more details.
@@ -207,6 +211,8 @@ class HTTPClient
207
211
  # Authentication filter for handling BasicAuth negotiation.
208
212
  # Used in WWWAuth and ProxyAuth.
209
213
  class BasicAuth
214
+ include HTTPClient::Util
215
+
210
216
  # Authentication scheme.
211
217
  attr_reader :scheme
212
218
 
@@ -251,8 +257,8 @@ class HTTPClient
251
257
  end
252
258
 
253
259
  # Challenge handler: remember URL for response.
254
- def challenge(uri, param_str)
255
- @challengeable[uri] = true
260
+ def challenge(uri, param_str = nil)
261
+ @challengeable[urify(uri)] = true
256
262
  true
257
263
  end
258
264
  end
@@ -301,8 +307,7 @@ class HTTPClient
301
307
  Util.uri_part_of(target_uri, uri)
302
308
  }
303
309
  return nil unless user
304
- uri = req.header.request_uri
305
- calc_cred(req.header.request_method, uri, user, passwd, param)
310
+ calc_cred(req, user, passwd, param)
306
311
  end
307
312
 
308
313
  # Challenge handler: remember URL and challenge token for response.
@@ -317,9 +322,11 @@ class HTTPClient
317
322
  # http://tools.assembla.com/breakout/wiki/DigestForSoap
318
323
  # Thanks!
319
324
  # supported algorithm: MD5 only for now
320
- def calc_cred(method, uri, user, passwd, param)
325
+ def calc_cred(req, user, passwd, param)
326
+ method = req.header.request_method
327
+ path = req.header.create_query_uri
321
328
  a_1 = "#{user}:#{param['realm']}:#{passwd}"
322
- a_2 = "#{method}:#{uri.path}"
329
+ a_2 = "#{method}:#{path}"
323
330
  nonce = param['nonce']
324
331
  cnonce = generate_cnonce()
325
332
  @nonce_count += 1
@@ -334,12 +341,12 @@ class HTTPClient
334
341
  header << "username=\"#{user}\""
335
342
  header << "realm=\"#{param['realm']}\""
336
343
  header << "nonce=\"#{nonce}\""
337
- header << "uri=\"#{uri.path}\""
344
+ header << "uri=\"#{path}\""
338
345
  header << "cnonce=\"#{cnonce}\""
339
346
  header << "nc=#{'%08x' % @nonce_count}"
340
- header << "qop=\"#{param['qop']}\""
347
+ header << "qop=#{param['qop']}"
341
348
  header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
342
- header << "algorithm=\"MD5\""
349
+ header << "algorithm=MD5"
343
350
  header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
344
351
  header.join(", ")
345
352
  end
@@ -376,11 +383,11 @@ class HTTPClient
376
383
  attr_reader :ntlm_opt
377
384
 
378
385
  # Creates new NegotiateAuth filter.
379
- def initialize
386
+ def initialize(scheme = "Negotiate")
380
387
  @auth = {}
381
388
  @auth_default = nil
382
389
  @challenge = {}
383
- @scheme = "Negotiate"
390
+ @scheme = scheme
384
391
  @ntlm_opt = {
385
392
  :ntlmv2 => true
386
393
  }
@@ -518,5 +525,243 @@ class HTTPClient
518
525
  end
519
526
  end
520
527
 
528
+ # Authentication filter for handling OAuth negotiation.
529
+ # Used in WWWAuth.
530
+ #
531
+ # CAUTION: This impl only support '#7 Accessing Protected Resources' in OAuth
532
+ # Core 1.0 spec for now. You need to obtain Access token and Access secret by
533
+ # yourself.
534
+ #
535
+ # CAUTION: This impl does NOT support OAuth Request Body Hash spec for now.
536
+ # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
537
+ #
538
+ class OAuth
539
+ include HTTPClient::Util
540
+
541
+ # Authentication scheme.
542
+ attr_reader :scheme
543
+
544
+ class Config
545
+ include HTTPClient::Util
546
+
547
+ attr_accessor :http_method
548
+ attr_accessor :realm
549
+ attr_accessor :consumer_key
550
+ attr_accessor :consumer_secret
551
+ attr_accessor :token
552
+ attr_accessor :secret
553
+ attr_accessor :signature_method
554
+ attr_accessor :version
555
+ attr_accessor :callback
556
+ attr_accessor :verifier
557
+
558
+ # for OAuth Session 1.0 (draft)
559
+ attr_accessor :session_handle
560
+
561
+ attr_reader :signature_handler
562
+
563
+ attr_accessor :debug_timestamp
564
+ attr_accessor :debug_nonce
565
+
566
+ def initialize(*args)
567
+ @http_method,
568
+ @realm,
569
+ @consumer_key,
570
+ @consumer_secret,
571
+ @token,
572
+ @secret,
573
+ @signature_method,
574
+ @version,
575
+ @callback,
576
+ @verifier =
577
+ keyword_argument(args,
578
+ :http_method,
579
+ :realm,
580
+ :consumer_key,
581
+ :consumer_secret,
582
+ :token,
583
+ :secret,
584
+ :signature_method,
585
+ :version,
586
+ :callback,
587
+ :verifier
588
+ )
589
+ @http_method ||= :post
590
+ @session_handle = nil
591
+ @signature_handler = {}
592
+ end
593
+ end
594
+
595
+ def self.escape(str) # :nodoc:
596
+ if str.respond_to?(:force_encoding)
597
+ str.dup.force_encoding('BINARY').gsub(/([^a-zA-Z0-9_.~-]+)/) {
598
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
599
+ }
600
+ else
601
+ str.gsub(/([^a-zA-Z0-9_.~-]+)/n) {
602
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
603
+ }
604
+ end
605
+ end
606
+
607
+ def escape(str)
608
+ self.class.escape(str)
609
+ end
610
+
611
+ # Creates new DigestAuth filter.
612
+ def initialize
613
+ @config = nil # common config
614
+ @auth = {} # configs for each site
615
+ @challengeable = {}
616
+ @nonce_count = 0
617
+ @signature_handler = {
618
+ 'HMAC-SHA1' => method(:sign_hmac_sha1)
619
+ }
620
+ @scheme = "OAuth"
621
+ end
622
+
623
+ # Resets challenge state. Do not send '*Authorization' header until the
624
+ # server sends '*Authentication' again.
625
+ def reset_challenge
626
+ @challengeable.clear
627
+ end
628
+
629
+ # Set authentication credential.
630
+ # You cannot set OAuth config via WWWAuth#set_auth. Use OAuth#config=
631
+ def set(uri, user, passwd)
632
+ # not supported
633
+ end
634
+
635
+ # Set authentication credential.
636
+ def set_config(uri, config)
637
+ if uri.nil?
638
+ @config = config
639
+ else
640
+ uri = Util.uri_dirname(urify(uri))
641
+ @auth[uri] = config
642
+ end
643
+ end
644
+
645
+ # Get authentication credential.
646
+ def get_config(uri = nil)
647
+ if uri.nil?
648
+ @config
649
+ else
650
+ uri = urify(uri)
651
+ Util.hash_find_value(@auth) { |cand_uri, cred|
652
+ Util.uri_part_of(uri, cand_uri)
653
+ }
654
+ end
655
+ end
656
+
657
+ # Response handler: returns credential.
658
+ # It sends cred only when a given uri is;
659
+ # * child page of challengeable(got *Authenticate before) uri and,
660
+ # * child page of defined credential
661
+ def get(req)
662
+ target_uri = req.header.request_uri
663
+ return nil unless @challengeable[nil] or @challengeable.find { |uri, ok|
664
+ Util.uri_part_of(target_uri, uri) and ok
665
+ }
666
+ config = get_config(target_uri) || @config
667
+ return nil unless config
668
+ calc_cred(req, config)
669
+ end
670
+
671
+ # Challenge handler: remember URL for response.
672
+ def challenge(uri, param_str = nil)
673
+ if uri.nil?
674
+ @challengeable[nil] = true
675
+ else
676
+ @challengeable[urify(uri)] = true
677
+ end
678
+ true
679
+ end
680
+
681
+ private
682
+
683
+ def calc_cred(req, config)
684
+ header = {}
685
+ header['oauth_consumer_key'] = config.consumer_key
686
+ header['oauth_token'] = config.token
687
+ header['oauth_signature_method'] = config.signature_method
688
+ header['oauth_timestamp'] = config.debug_timestamp || Time.now.to_i.to_s
689
+ header['oauth_nonce'] = config.debug_nonce || generate_nonce()
690
+ header['oauth_version'] = config.version if config.version
691
+ header['oauth_callback'] = config.callback if config.callback
692
+ header['oauth_verifier'] = config.verifier if config.verifier
693
+ header['oauth_session_handle'] = config.session_handle if config.session_handle
694
+ signature = sign(config, header, req)
695
+ header['oauth_signature'] = signature
696
+ # no need to do but we should sort for easier to test.
697
+ str = header.sort_by { |k, v| k }.map { |k, v| encode_header(k, v) }.join(', ')
698
+ if config.realm
699
+ str = %Q(realm="#{config.realm}", ) + str
700
+ end
701
+ str
702
+ end
703
+
704
+ def generate_nonce
705
+ @nonce_count += 1
706
+ now = "%012d" % Time.now.to_i
707
+ pk = Digest::MD5.hexdigest([@nonce_count.to_s, now, self.__id__, Process.pid, rand(65535)].join)[0, 32]
708
+ [now + ':' + pk].pack('m*').chop
709
+ end
710
+
711
+ def encode_header(k, v)
712
+ %Q(#{escape(k.to_s)}="#{escape(v.to_s)}")
713
+ end
714
+
715
+ def encode_param(params)
716
+ params.map { |k, v|
717
+ [v].flatten.map { |vv|
718
+ %Q(#{escape(k.to_s)}=#{escape(vv.to_s)})
719
+ }
720
+ }.flatten
721
+ end
722
+
723
+ def sign(config, header, req)
724
+ base_string = create_base_string(config, header, req)
725
+ if handler = config.signature_handler[config.signature_method] || @signature_handler[config.signature_method.to_s]
726
+ handler.call(config, base_string)
727
+ else
728
+ raise ConfigurationError.new("Unknown OAuth signature method: #{config.signature_method}")
729
+ end
730
+ end
731
+
732
+ def create_base_string(config, header, req)
733
+ params = encode_param(header)
734
+ query = req.header.request_query
735
+ if query and HTTP::Message.multiparam_query?(query)
736
+ params += encode_param(query)
737
+ end
738
+ # captures HTTP Message body only for 'application/x-www-form-urlencoded'
739
+ if req.header.contenttype == 'application/x-www-form-urlencoded' and req.body.size
740
+ params += encode_param(HTTP::Message.parse(req.body.content))
741
+ end
742
+ uri = req.header.request_uri
743
+ if uri.query
744
+ params += encode_param(HTTP::Message.parse(uri.query))
745
+ end
746
+ if uri.port == uri.default_port
747
+ request_url = "#{uri.scheme.downcase}://#{uri.host}#{uri.path}"
748
+ else
749
+ request_url = "#{uri.scheme.downcase}://#{uri.host}:#{uri.port}#{uri.path}"
750
+ end
751
+ [req.header.request_method.upcase, request_url, params.sort.join('&')].map { |e|
752
+ escape(e)
753
+ }.join('&')
754
+ end
755
+
756
+ def sign_hmac_sha1(config, base_string)
757
+ unless SSLEnabled
758
+ raise ConfigurationError.new("openssl required for OAuth implementation")
759
+ end
760
+ key = [escape(config.consumer_secret.to_s), escape(config.secret.to_s)].join('&')
761
+ digester = OpenSSL::Digest::SHA1.new
762
+ [OpenSSL::HMAC.digest(digester, key, base_string)].pack('m*').chomp
763
+ end
764
+ end
765
+
521
766
 
522
767
  end