httpclient 2.1.5.2 → 2.1.6

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