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.
- data/lib/hexdump.rb +36 -0
- data/lib/httpclient.rb +83 -43
- data/lib/httpclient/auth.rb +260 -15
- data/lib/httpclient/cacert.p7s +545 -266
- data/lib/httpclient/cacert_sha1.p7s +552 -273
- data/lib/httpclient/connection.rb +5 -1
- data/lib/httpclient/cookie.rb +1 -2
- data/lib/httpclient/http.rb +106 -51
- data/lib/httpclient/session.rb +101 -21
- data/lib/httpclient/ssl_config.rb +3 -3
- data/lib/httpclient/timeout.rb +11 -9
- data/lib/httpclient/util.rb +17 -0
- data/lib/oauthclient.rb +110 -0
- metadata +27 -17
- data/lib/tags +0 -908
data/lib/hexdump.rb
ADDED
@@ -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
|
data/lib/httpclient.rb
CHANGED
@@ -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.
|
213
|
+
VERSION = '2.1.6'
|
203
214
|
RUBY_VERSION_STRING = "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
|
204
|
-
/: (\S+) (\S+)/ =~ %q$Id
|
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
|
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
|
-
#
|
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
|
-
#
|
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.
|
531
|
-
#
|
532
|
-
#
|
533
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
data/lib/httpclient/auth.rb
CHANGED
@@ -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
|
-
|
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(
|
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}:#{
|
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=\"#{
|
344
|
+
header << "uri=\"#{path}\""
|
338
345
|
header << "cnonce=\"#{cnonce}\""
|
339
346
|
header << "nc=#{'%08x' % @nonce_count}"
|
340
|
-
header << "qop
|
347
|
+
header << "qop=#{param['qop']}"
|
341
348
|
header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
|
342
|
-
header << "algorithm
|
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 =
|
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
|