httpclient-jgraichen 2.3.4.2
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.
- checksums.yaml +7 -0
- data/README.txt +759 -0
- data/bin/httpclient +65 -0
- data/lib/hexdump.rb +50 -0
- data/lib/http-access2.rb +55 -0
- data/lib/http-access2/cookie.rb +1 -0
- data/lib/http-access2/http.rb +1 -0
- data/lib/httpclient.rb +1156 -0
- data/lib/httpclient/auth.rb +899 -0
- data/lib/httpclient/cacert.p7s +1912 -0
- data/lib/httpclient/connection.rb +88 -0
- data/lib/httpclient/cookie.rb +438 -0
- data/lib/httpclient/http.rb +1046 -0
- data/lib/httpclient/include_client.rb +83 -0
- data/lib/httpclient/session.rb +1028 -0
- data/lib/httpclient/ssl_config.rb +405 -0
- data/lib/httpclient/timeout.rb +140 -0
- data/lib/httpclient/util.rb +178 -0
- data/lib/httpclient/version.rb +3 -0
- data/lib/oauthclient.rb +110 -0
- data/sample/async.rb +8 -0
- data/sample/auth.rb +11 -0
- data/sample/cookie.rb +18 -0
- data/sample/dav.rb +103 -0
- data/sample/howto.rb +49 -0
- data/sample/oauth_buzz.rb +57 -0
- data/sample/oauth_friendfeed.rb +59 -0
- data/sample/oauth_twitter.rb +61 -0
- data/sample/ssl/0cert.pem +22 -0
- data/sample/ssl/0key.pem +30 -0
- data/sample/ssl/1000cert.pem +19 -0
- data/sample/ssl/1000key.pem +18 -0
- data/sample/ssl/htdocs/index.html +10 -0
- data/sample/ssl/ssl_client.rb +22 -0
- data/sample/ssl/webrick_httpsd.rb +29 -0
- data/sample/stream.rb +21 -0
- data/sample/thread.rb +27 -0
- data/sample/wcat.rb +21 -0
- data/test/ca-chain.cert +44 -0
- data/test/ca.cert +23 -0
- data/test/client.cert +19 -0
- data/test/client.key +15 -0
- data/test/helper.rb +129 -0
- data/test/htdigest +1 -0
- data/test/htpasswd +2 -0
- data/test/runner.rb +2 -0
- data/test/server.cert +19 -0
- data/test/server.key +15 -0
- data/test/sslsvr.rb +65 -0
- data/test/subca.cert +21 -0
- data/test/test_auth.rb +348 -0
- data/test/test_cookie.rb +412 -0
- data/test/test_hexdump.rb +14 -0
- data/test/test_http-access2.rb +507 -0
- data/test/test_httpclient.rb +1783 -0
- data/test/test_include_client.rb +52 -0
- data/test/test_ssl.rb +235 -0
- metadata +100 -0
@@ -0,0 +1,899 @@
|
|
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
|
+
begin
|
30
|
+
require 'gssapi'
|
31
|
+
GSSAPIEnabled = true
|
32
|
+
rescue LoadError
|
33
|
+
GSSAPIEnabled = false
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# Common abstract class for authentication filter.
|
38
|
+
#
|
39
|
+
# There are 2 authentication filters.
|
40
|
+
# WWWAuth:: Authentication filter for handling authentication negotiation
|
41
|
+
# between Web server. Parses 'WWW-Authentication' header in
|
42
|
+
# response and generates 'Authorization' header in request.
|
43
|
+
# ProxyAuth:: Authentication filter for handling authentication negotiation
|
44
|
+
# between Proxy server. Parses 'Proxy-Authentication' header in
|
45
|
+
# response and generates 'Proxy-Authorization' header in request.
|
46
|
+
class AuthFilterBase
|
47
|
+
private
|
48
|
+
|
49
|
+
def parse_authentication_header(res, tag)
|
50
|
+
challenge = res.header[tag]
|
51
|
+
return nil unless challenge
|
52
|
+
challenge.collect { |c| parse_challenge_header(c) }.compact
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_challenge_header(challenge)
|
56
|
+
scheme, param_str = challenge.scan(/\A(\S+)(?:\s+(.*))?\z/)[0]
|
57
|
+
return nil if scheme.nil?
|
58
|
+
return scheme, param_str
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
# Authentication filter for handling authentication negotiation between
|
64
|
+
# Web server. Parses 'WWW-Authentication' header in response and
|
65
|
+
# generates 'Authorization' header in request.
|
66
|
+
#
|
67
|
+
# Authentication filter is implemented using request filter of HTTPClient.
|
68
|
+
# It traps HTTP response header and maintains authentication state, and
|
69
|
+
# traps HTTP request header for inserting necessary authentication header.
|
70
|
+
#
|
71
|
+
# WWWAuth has sub filters (BasicAuth, DigestAuth, NegotiateAuth and
|
72
|
+
# SSPINegotiateAuth) and delegates some operations to it.
|
73
|
+
# NegotiateAuth requires 'ruby/ntlm' module (rubyntlm gem).
|
74
|
+
# SSPINegotiateAuth requires 'win32/sspi' module (rubysspi gem).
|
75
|
+
class WWWAuth < AuthFilterBase
|
76
|
+
attr_reader :basic_auth
|
77
|
+
attr_reader :digest_auth
|
78
|
+
attr_reader :negotiate_auth
|
79
|
+
attr_reader :sspi_negotiate_auth
|
80
|
+
attr_reader :oauth
|
81
|
+
|
82
|
+
# Creates new WWWAuth.
|
83
|
+
def initialize
|
84
|
+
@basic_auth = BasicAuth.new
|
85
|
+
@digest_auth = DigestAuth.new
|
86
|
+
@negotiate_auth = NegotiateAuth.new
|
87
|
+
@ntlm_auth = NegotiateAuth.new('NTLM')
|
88
|
+
@sspi_negotiate_auth = SSPINegotiateAuth.new
|
89
|
+
@oauth = OAuth.new
|
90
|
+
# sort authenticators by priority
|
91
|
+
@authenticator = [@oauth, @negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
|
92
|
+
end
|
93
|
+
|
94
|
+
# Resets challenge state. See sub filters for more details.
|
95
|
+
def reset_challenge
|
96
|
+
@authenticator.each do |auth|
|
97
|
+
auth.reset_challenge
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Set authentication credential. See sub filters for more details.
|
102
|
+
def set_auth(uri, user, passwd)
|
103
|
+
@authenticator.each do |auth|
|
104
|
+
auth.set(uri, user, passwd)
|
105
|
+
end
|
106
|
+
reset_challenge
|
107
|
+
end
|
108
|
+
|
109
|
+
# Filter API implementation. Traps HTTP request and insert
|
110
|
+
# 'Authorization' header if needed.
|
111
|
+
def filter_request(req)
|
112
|
+
@authenticator.each do |auth|
|
113
|
+
next unless auth.set? # hasn't be set, don't use it
|
114
|
+
if cred = auth.get(req)
|
115
|
+
req.header.set('Authorization', auth.scheme + " " + cred)
|
116
|
+
return
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Filter API implementation. Traps HTTP response and parses
|
122
|
+
# 'WWW-Authenticate' header.
|
123
|
+
#
|
124
|
+
# This remembers the challenges for all authentication methods
|
125
|
+
# available to the client. On the subsequent retry of the request,
|
126
|
+
# filter_request will select the strongest method.
|
127
|
+
def filter_response(req, res)
|
128
|
+
command = nil
|
129
|
+
if res.status == HTTP::Status::UNAUTHORIZED
|
130
|
+
if challenge = parse_authentication_header(res, 'www-authenticate')
|
131
|
+
uri = req.header.request_uri
|
132
|
+
challenge.each do |scheme, param_str|
|
133
|
+
@authenticator.each do |auth|
|
134
|
+
next unless auth.set? # hasn't be set, don't use it
|
135
|
+
if scheme.downcase == auth.scheme.downcase
|
136
|
+
challengeable = auth.challenge(uri, param_str)
|
137
|
+
command = :retry if challengeable
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
# ignore unknown authentication scheme
|
142
|
+
end
|
143
|
+
end
|
144
|
+
command
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# Authentication filter for handling authentication negotiation between
|
150
|
+
# Proxy server. Parses 'Proxy-Authentication' header in response and
|
151
|
+
# generates 'Proxy-Authorization' header in request.
|
152
|
+
#
|
153
|
+
# Authentication filter is implemented using request filter of HTTPClient.
|
154
|
+
# It traps HTTP response header and maintains authentication state, and
|
155
|
+
# traps HTTP request header for inserting necessary authentication header.
|
156
|
+
#
|
157
|
+
# ProxyAuth has sub filters (BasicAuth, NegotiateAuth, and SSPINegotiateAuth)
|
158
|
+
# and delegates some operations to it.
|
159
|
+
# NegotiateAuth requires 'ruby/ntlm' module.
|
160
|
+
# SSPINegotiateAuth requires 'win32/sspi' module.
|
161
|
+
class ProxyAuth < AuthFilterBase
|
162
|
+
attr_reader :basic_auth
|
163
|
+
attr_reader :digest_auth
|
164
|
+
attr_reader :negotiate_auth
|
165
|
+
attr_reader :sspi_negotiate_auth
|
166
|
+
|
167
|
+
# Creates new ProxyAuth.
|
168
|
+
def initialize
|
169
|
+
@basic_auth = ProxyBasicAuth.new
|
170
|
+
@negotiate_auth = NegotiateAuth.new
|
171
|
+
@ntlm_auth = NegotiateAuth.new('NTLM')
|
172
|
+
@sspi_negotiate_auth = SSPINegotiateAuth.new
|
173
|
+
@digest_auth = ProxyDigestAuth.new
|
174
|
+
# sort authenticators by priority
|
175
|
+
@authenticator = [@negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
|
176
|
+
end
|
177
|
+
|
178
|
+
# Resets challenge state. See sub filters for more details.
|
179
|
+
def reset_challenge
|
180
|
+
@authenticator.each do |auth|
|
181
|
+
auth.reset_challenge
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Set authentication credential. See sub filters for more details.
|
186
|
+
def set_auth(user, passwd)
|
187
|
+
@authenticator.each do |auth|
|
188
|
+
auth.set(nil, user, passwd)
|
189
|
+
end
|
190
|
+
reset_challenge
|
191
|
+
end
|
192
|
+
|
193
|
+
# Filter API implementation. Traps HTTP request and insert
|
194
|
+
# 'Proxy-Authorization' header if needed.
|
195
|
+
def filter_request(req)
|
196
|
+
@authenticator.each do |auth|
|
197
|
+
next unless auth.set? # hasn't be set, don't use it
|
198
|
+
if cred = auth.get(req)
|
199
|
+
req.header.set('Proxy-Authorization', auth.scheme + " " + cred)
|
200
|
+
return
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# Filter API implementation. Traps HTTP response and parses
|
206
|
+
# 'Proxy-Authenticate' header.
|
207
|
+
def filter_response(req, res)
|
208
|
+
command = nil
|
209
|
+
if res.status == HTTP::Status::PROXY_AUTHENTICATE_REQUIRED
|
210
|
+
if challenge = parse_authentication_header(res, 'proxy-authenticate')
|
211
|
+
uri = req.header.request_uri
|
212
|
+
challenge.each do |scheme, param_str|
|
213
|
+
@authenticator.each do |auth|
|
214
|
+
next unless auth.set? # hasn't be set, don't use it
|
215
|
+
if scheme.downcase == auth.scheme.downcase
|
216
|
+
challengeable = auth.challenge(uri, param_str)
|
217
|
+
command = :retry if challengeable
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
# ignore unknown authentication scheme
|
222
|
+
end
|
223
|
+
end
|
224
|
+
command
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Authentication filter for handling BasicAuth negotiation.
|
229
|
+
# Used in WWWAuth and ProxyAuth.
|
230
|
+
class BasicAuth
|
231
|
+
include HTTPClient::Util
|
232
|
+
|
233
|
+
# Authentication scheme.
|
234
|
+
attr_reader :scheme
|
235
|
+
|
236
|
+
# Creates new BasicAuth filter.
|
237
|
+
def initialize
|
238
|
+
@cred = nil
|
239
|
+
@set = false
|
240
|
+
@auth = {}
|
241
|
+
@challengeable = {}
|
242
|
+
@scheme = "Basic"
|
243
|
+
end
|
244
|
+
|
245
|
+
# Resets challenge state. Do not send '*Authorization' header until the
|
246
|
+
# server sends '*Authentication' again.
|
247
|
+
def reset_challenge
|
248
|
+
@challengeable.clear
|
249
|
+
end
|
250
|
+
|
251
|
+
# Set authentication credential.
|
252
|
+
# uri == nil for generic purpose (allow to use user/password for any URL).
|
253
|
+
def set(uri, user, passwd)
|
254
|
+
@set = true
|
255
|
+
if uri.nil?
|
256
|
+
@cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
257
|
+
else
|
258
|
+
uri = Util.uri_dirname(uri)
|
259
|
+
@auth[uri] = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# have we marked this as set - ie that it's valid to use in this context?
|
264
|
+
def set?
|
265
|
+
@set == true
|
266
|
+
end
|
267
|
+
|
268
|
+
# Response handler: returns credential.
|
269
|
+
# It sends cred only when a given uri is;
|
270
|
+
# * child page of challengeable(got *Authenticate before) uri and,
|
271
|
+
# * child page of defined credential
|
272
|
+
def get(req)
|
273
|
+
target_uri = req.header.request_uri
|
274
|
+
return nil unless @challengeable.find { |uri, ok|
|
275
|
+
Util.uri_part_of(target_uri, uri) and ok
|
276
|
+
}
|
277
|
+
return @cred if @cred
|
278
|
+
Util.hash_find_value(@auth) { |uri, cred|
|
279
|
+
Util.uri_part_of(target_uri, uri)
|
280
|
+
}
|
281
|
+
end
|
282
|
+
|
283
|
+
# Challenge handler: remember URL for response.
|
284
|
+
def challenge(uri, param_str = nil)
|
285
|
+
@challengeable[urify(uri)] = true
|
286
|
+
true
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
class ProxyBasicAuth < BasicAuth
|
291
|
+
|
292
|
+
def set(uri, user, passwd)
|
293
|
+
@set = true
|
294
|
+
@cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
|
295
|
+
end
|
296
|
+
|
297
|
+
def get(req)
|
298
|
+
target_uri = req.header.request_uri
|
299
|
+
return nil unless @challengeable['challenged']
|
300
|
+
@cred
|
301
|
+
end
|
302
|
+
|
303
|
+
# Challenge handler: remember URL for response.
|
304
|
+
def challenge(uri, param_str = nil)
|
305
|
+
@challengeable['challenged'] = true
|
306
|
+
true
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Authentication filter for handling DigestAuth negotiation.
|
311
|
+
# Used in WWWAuth.
|
312
|
+
class DigestAuth
|
313
|
+
# Authentication scheme.
|
314
|
+
attr_reader :scheme
|
315
|
+
|
316
|
+
# Creates new DigestAuth filter.
|
317
|
+
def initialize
|
318
|
+
@auth = {}
|
319
|
+
@challenge = {}
|
320
|
+
@set = false
|
321
|
+
@nonce_count = 0
|
322
|
+
@scheme = "Digest"
|
323
|
+
end
|
324
|
+
|
325
|
+
# Resets challenge state. Do not send '*Authorization' header until the
|
326
|
+
# server sends '*Authentication' again.
|
327
|
+
def reset_challenge
|
328
|
+
@challenge.clear
|
329
|
+
end
|
330
|
+
|
331
|
+
# Set authentication credential.
|
332
|
+
# uri == nil is ignored.
|
333
|
+
def set(uri, user, passwd)
|
334
|
+
@set = true
|
335
|
+
if uri
|
336
|
+
uri = Util.uri_dirname(uri)
|
337
|
+
@auth[uri] = [user, passwd]
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# have we marked this as set - ie that it's valid to use in this context?
|
342
|
+
def set?
|
343
|
+
@set == true
|
344
|
+
end
|
345
|
+
|
346
|
+
# Response handler: returns credential.
|
347
|
+
# It sends cred only when a given uri is;
|
348
|
+
# * child page of challengeable(got *Authenticate before) uri and,
|
349
|
+
# * child page of defined credential
|
350
|
+
def get(req)
|
351
|
+
target_uri = req.header.request_uri
|
352
|
+
param = Util.hash_find_value(@challenge) { |uri, v|
|
353
|
+
Util.uri_part_of(target_uri, uri)
|
354
|
+
}
|
355
|
+
return nil unless param
|
356
|
+
user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
|
357
|
+
Util.uri_part_of(target_uri, uri)
|
358
|
+
}
|
359
|
+
return nil unless user
|
360
|
+
calc_cred(req, user, passwd, param)
|
361
|
+
end
|
362
|
+
|
363
|
+
# Challenge handler: remember URL and challenge token for response.
|
364
|
+
def challenge(uri, param_str)
|
365
|
+
@challenge[uri] = parse_challenge_param(param_str)
|
366
|
+
true
|
367
|
+
end
|
368
|
+
|
369
|
+
private
|
370
|
+
|
371
|
+
# this method is implemented by sromano and posted to
|
372
|
+
# http://tools.assembla.com/breakout/wiki/DigestForSoap
|
373
|
+
# Thanks!
|
374
|
+
# supported algorithms: MD5, MD5-sess
|
375
|
+
def calc_cred(req, user, passwd, param)
|
376
|
+
method = req.header.request_method
|
377
|
+
path = req.header.create_query_uri
|
378
|
+
a_1 = "#{user}:#{param['realm']}:#{passwd}"
|
379
|
+
a_2 = "#{method}:#{path}"
|
380
|
+
qop = param['qop']
|
381
|
+
nonce = param['nonce']
|
382
|
+
cnonce = nil
|
383
|
+
if qop || param['algorithm'] =~ /MD5-sess/
|
384
|
+
cnonce = generate_cnonce()
|
385
|
+
end
|
386
|
+
a_1_md5sum = Digest::MD5.hexdigest(a_1)
|
387
|
+
if param['algorithm'] =~ /MD5-sess/
|
388
|
+
a_1_md5sum = Digest::MD5.hexdigest("#{a_1_md5sum}:#{nonce}:#{cnonce}")
|
389
|
+
algorithm = "MD5-sess"
|
390
|
+
else
|
391
|
+
algorithm = "MD5"
|
392
|
+
end
|
393
|
+
message_digest = []
|
394
|
+
message_digest << a_1_md5sum
|
395
|
+
message_digest << nonce
|
396
|
+
if qop
|
397
|
+
@nonce_count += 1
|
398
|
+
message_digest << ('%08x' % @nonce_count)
|
399
|
+
message_digest << cnonce
|
400
|
+
message_digest << param['qop']
|
401
|
+
end
|
402
|
+
message_digest << Digest::MD5.hexdigest(a_2)
|
403
|
+
header = []
|
404
|
+
header << "username=\"#{user}\""
|
405
|
+
header << "realm=\"#{param['realm']}\""
|
406
|
+
header << "nonce=\"#{nonce}\""
|
407
|
+
header << "uri=\"#{path}\""
|
408
|
+
if cnonce
|
409
|
+
header << "cnonce=\"#{cnonce}\""
|
410
|
+
end
|
411
|
+
if qop
|
412
|
+
header << "nc=#{'%08x' % @nonce_count}"
|
413
|
+
header << "qop=#{param['qop']}"
|
414
|
+
end
|
415
|
+
header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
|
416
|
+
header << "algorithm=#{algorithm}"
|
417
|
+
header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
|
418
|
+
header.join(", ")
|
419
|
+
end
|
420
|
+
|
421
|
+
# cf. WEBrick::HTTPAuth::DigestAuth#generate_next_nonce(aTime)
|
422
|
+
def generate_cnonce
|
423
|
+
now = "%012d" % Time.now.to_i
|
424
|
+
pk = Digest::MD5.hexdigest([now, self.__id__, Process.pid, rand(65535)].join)[0, 32]
|
425
|
+
[now + ':' + pk].pack('m*').chop
|
426
|
+
end
|
427
|
+
|
428
|
+
def parse_challenge_param(param_str)
|
429
|
+
param = {}
|
430
|
+
param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
|
431
|
+
key, value = str[0].scan(/\A([^=]+)=(.*)\z/)[0]
|
432
|
+
if /\A"(.*)"\z/ =~ value
|
433
|
+
value = $1.gsub(/\\(.)/, '\1')
|
434
|
+
end
|
435
|
+
param[key] = value
|
436
|
+
end
|
437
|
+
param
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
|
442
|
+
# Authentication filter for handling DigestAuth negotiation.
|
443
|
+
# Ignores uri argument. Used in ProxyAuth.
|
444
|
+
class ProxyDigestAuth < DigestAuth
|
445
|
+
|
446
|
+
# overrides DigestAuth#set. sets default user name and password. uri is not used.
|
447
|
+
def set(uri, user, passwd)
|
448
|
+
@set = true
|
449
|
+
@auth = [user, passwd]
|
450
|
+
end
|
451
|
+
|
452
|
+
# overrides DigestAuth#get. Uses default user name and password
|
453
|
+
# regardless of target uri if the proxy has required authentication
|
454
|
+
# before
|
455
|
+
def get(req)
|
456
|
+
target_uri = req.header.request_uri
|
457
|
+
param = @challenge
|
458
|
+
return nil unless param
|
459
|
+
user, passwd = @auth
|
460
|
+
return nil unless user
|
461
|
+
calc_cred(req, user, passwd, param)
|
462
|
+
end
|
463
|
+
|
464
|
+
def reset_challenge
|
465
|
+
@challenge = nil
|
466
|
+
end
|
467
|
+
|
468
|
+
def challenge(uri, param_str)
|
469
|
+
@challenge = parse_challenge_param(param_str)
|
470
|
+
true
|
471
|
+
end
|
472
|
+
|
473
|
+
end
|
474
|
+
|
475
|
+
# Authentication filter for handling Negotiate/NTLM negotiation.
|
476
|
+
# Used in WWWAuth and ProxyAuth.
|
477
|
+
#
|
478
|
+
# NegotiateAuth depends on 'ruby/ntlm' module.
|
479
|
+
class NegotiateAuth
|
480
|
+
# Authentication scheme.
|
481
|
+
attr_reader :scheme
|
482
|
+
# NTLM opt for ruby/ntlm. {:ntlmv2 => true} by default.
|
483
|
+
attr_reader :ntlm_opt
|
484
|
+
|
485
|
+
# Creates new NegotiateAuth filter.
|
486
|
+
def initialize(scheme = "Negotiate")
|
487
|
+
@auth = {}
|
488
|
+
@auth_default = nil
|
489
|
+
@challenge = {}
|
490
|
+
@scheme = scheme
|
491
|
+
@set = false
|
492
|
+
@ntlm_opt = {
|
493
|
+
:ntlmv2 => true
|
494
|
+
}
|
495
|
+
end
|
496
|
+
|
497
|
+
# Resets challenge state. Do not send '*Authorization' header until the
|
498
|
+
# server sends '*Authentication' again.
|
499
|
+
def reset_challenge
|
500
|
+
@challenge.clear
|
501
|
+
end
|
502
|
+
|
503
|
+
# Set authentication credential.
|
504
|
+
# uri == nil for generic purpose (allow to use user/password for any URL).
|
505
|
+
def set(uri, user, passwd)
|
506
|
+
@set = true
|
507
|
+
if uri
|
508
|
+
uri = Util.uri_dirname(uri)
|
509
|
+
@auth[uri] = [user, passwd]
|
510
|
+
else
|
511
|
+
@auth_default = [user, passwd]
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
# have we marked this as set - ie that it's valid to use in this context?
|
516
|
+
def set?
|
517
|
+
@set == true
|
518
|
+
end
|
519
|
+
|
520
|
+
# Response handler: returns credential.
|
521
|
+
# See ruby/ntlm for negotiation state transition.
|
522
|
+
def get(req)
|
523
|
+
return nil unless NTLMEnabled
|
524
|
+
target_uri = req.header.request_uri
|
525
|
+
domain_uri, param = @challenge.find { |uri, v|
|
526
|
+
Util.uri_part_of(target_uri, uri)
|
527
|
+
}
|
528
|
+
return nil unless param
|
529
|
+
user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
|
530
|
+
Util.uri_part_of(target_uri, uri)
|
531
|
+
}
|
532
|
+
unless user
|
533
|
+
user, passwd = @auth_default
|
534
|
+
end
|
535
|
+
return nil unless user
|
536
|
+
domain = nil
|
537
|
+
domain, user = user.split("\\") if user.index("\\")
|
538
|
+
state = param[:state]
|
539
|
+
authphrase = param[:authphrase]
|
540
|
+
case state
|
541
|
+
when :init
|
542
|
+
t1 = Net::NTLM::Message::Type1.new
|
543
|
+
t1.domain = domain if domain
|
544
|
+
return t1.encode64
|
545
|
+
when :response
|
546
|
+
t2 = Net::NTLM::Message.decode64(authphrase)
|
547
|
+
param = {:user => user, :password => passwd}
|
548
|
+
param[:domain] = domain if domain
|
549
|
+
t3 = t2.response(param, @ntlm_opt.dup)
|
550
|
+
@challenge.delete(domain_uri)
|
551
|
+
return t3.encode64
|
552
|
+
end
|
553
|
+
nil
|
554
|
+
end
|
555
|
+
|
556
|
+
# Challenge handler: remember URL and challenge token for response.
|
557
|
+
def challenge(uri, param_str)
|
558
|
+
return false unless NTLMEnabled
|
559
|
+
if param_str.nil? or @challenge[uri].nil?
|
560
|
+
c = @challenge[uri] = {}
|
561
|
+
c[:state] = :init
|
562
|
+
c[:authphrase] = ""
|
563
|
+
else
|
564
|
+
c = @challenge[uri]
|
565
|
+
c[:state] = :response
|
566
|
+
c[:authphrase] = param_str
|
567
|
+
end
|
568
|
+
true
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
|
573
|
+
# Authentication filter for handling Negotiate/NTLM negotiation.
|
574
|
+
# Used in ProxyAuth.
|
575
|
+
#
|
576
|
+
# SSPINegotiateAuth depends on 'win32/sspi' module.
|
577
|
+
class SSPINegotiateAuth
|
578
|
+
# Authentication scheme.
|
579
|
+
attr_reader :scheme
|
580
|
+
|
581
|
+
# Creates new SSPINegotiateAuth filter.
|
582
|
+
def initialize
|
583
|
+
@challenge = {}
|
584
|
+
@scheme = "Negotiate"
|
585
|
+
end
|
586
|
+
|
587
|
+
# Resets challenge state. Do not send '*Authorization' header until the
|
588
|
+
# server sends '*Authentication' again.
|
589
|
+
def reset_challenge
|
590
|
+
@challenge.clear
|
591
|
+
end
|
592
|
+
|
593
|
+
# Set authentication credential.
|
594
|
+
# NOT SUPPORTED: username and necessary data is retrieved by win32/sspi.
|
595
|
+
# See win32/sspi for more details.
|
596
|
+
def set(*args)
|
597
|
+
# not supported
|
598
|
+
end
|
599
|
+
|
600
|
+
# have we marked this as set - ie that it's valid to use in this context?
|
601
|
+
def set?
|
602
|
+
SSPIEnabled || GSSAPIEnabled
|
603
|
+
end
|
604
|
+
|
605
|
+
# Response handler: returns credential.
|
606
|
+
# See win32/sspi for negotiation state transition.
|
607
|
+
def get(req)
|
608
|
+
return nil unless SSPIEnabled || GSSAPIEnabled
|
609
|
+
target_uri = req.header.request_uri
|
610
|
+
domain_uri, param = @challenge.find { |uri, v|
|
611
|
+
Util.uri_part_of(target_uri, uri)
|
612
|
+
}
|
613
|
+
return nil unless param
|
614
|
+
state = param[:state]
|
615
|
+
authenticator = param[:authenticator]
|
616
|
+
authphrase = param[:authphrase]
|
617
|
+
case state
|
618
|
+
when :init
|
619
|
+
if SSPIEnabled
|
620
|
+
authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
|
621
|
+
return authenticator.get_initial_token(@scheme)
|
622
|
+
else # use GSSAPI
|
623
|
+
authenticator = param[:authenticator] = GSSAPI::Simple.new(domain_uri.host, 'HTTP')
|
624
|
+
# Base64 encode the context token
|
625
|
+
return [authenticator.init_context].pack('m').gsub(/\n/,'')
|
626
|
+
end
|
627
|
+
when :response
|
628
|
+
@challenge.delete(domain_uri)
|
629
|
+
if SSPIEnabled
|
630
|
+
return authenticator.complete_authentication(authphrase)
|
631
|
+
else # use GSSAPI
|
632
|
+
return authenticator.init_context(authphrase.unpack('m').pop)
|
633
|
+
end
|
634
|
+
end
|
635
|
+
nil
|
636
|
+
end
|
637
|
+
|
638
|
+
# Challenge handler: remember URL and challenge token for response.
|
639
|
+
def challenge(uri, param_str)
|
640
|
+
return false unless SSPIEnabled || GSSAPIEnabled
|
641
|
+
if param_str.nil? or @challenge[uri].nil?
|
642
|
+
c = @challenge[uri] = {}
|
643
|
+
c[:state] = :init
|
644
|
+
c[:authenticator] = nil
|
645
|
+
c[:authphrase] = ""
|
646
|
+
else
|
647
|
+
c = @challenge[uri]
|
648
|
+
c[:state] = :response
|
649
|
+
c[:authphrase] = param_str
|
650
|
+
end
|
651
|
+
true
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
# Authentication filter for handling OAuth negotiation.
|
656
|
+
# Used in WWWAuth.
|
657
|
+
#
|
658
|
+
# CAUTION: This impl only support '#7 Accessing Protected Resources' in OAuth
|
659
|
+
# Core 1.0 spec for now. You need to obtain Access token and Access secret by
|
660
|
+
# yourself.
|
661
|
+
#
|
662
|
+
# CAUTION: This impl does NOT support OAuth Request Body Hash spec for now.
|
663
|
+
# http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
|
664
|
+
#
|
665
|
+
class OAuth
|
666
|
+
include HTTPClient::Util
|
667
|
+
|
668
|
+
# Authentication scheme.
|
669
|
+
attr_reader :scheme
|
670
|
+
|
671
|
+
class Config
|
672
|
+
include HTTPClient::Util
|
673
|
+
|
674
|
+
attr_accessor :http_method
|
675
|
+
attr_accessor :realm
|
676
|
+
attr_accessor :consumer_key
|
677
|
+
attr_accessor :consumer_secret
|
678
|
+
attr_accessor :token
|
679
|
+
attr_accessor :secret
|
680
|
+
attr_accessor :signature_method
|
681
|
+
attr_accessor :version
|
682
|
+
attr_accessor :callback
|
683
|
+
attr_accessor :verifier
|
684
|
+
|
685
|
+
# for OAuth Session 1.0 (draft)
|
686
|
+
attr_accessor :session_handle
|
687
|
+
|
688
|
+
attr_reader :signature_handler
|
689
|
+
|
690
|
+
attr_accessor :debug_timestamp
|
691
|
+
attr_accessor :debug_nonce
|
692
|
+
|
693
|
+
def initialize(*args)
|
694
|
+
@http_method,
|
695
|
+
@realm,
|
696
|
+
@consumer_key,
|
697
|
+
@consumer_secret,
|
698
|
+
@token,
|
699
|
+
@secret,
|
700
|
+
@signature_method,
|
701
|
+
@version,
|
702
|
+
@callback,
|
703
|
+
@verifier =
|
704
|
+
keyword_argument(args,
|
705
|
+
:http_method,
|
706
|
+
:realm,
|
707
|
+
:consumer_key,
|
708
|
+
:consumer_secret,
|
709
|
+
:token,
|
710
|
+
:secret,
|
711
|
+
:signature_method,
|
712
|
+
:version,
|
713
|
+
:callback,
|
714
|
+
:verifier
|
715
|
+
)
|
716
|
+
@http_method ||= :post
|
717
|
+
@session_handle = nil
|
718
|
+
@signature_handler = {}
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
def self.escape(str) # :nodoc:
|
723
|
+
if str.respond_to?(:force_encoding)
|
724
|
+
str.dup.force_encoding('BINARY').gsub(/([^a-zA-Z0-9_.~-]+)/) {
|
725
|
+
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
726
|
+
}
|
727
|
+
else
|
728
|
+
str.gsub(/([^a-zA-Z0-9_.~-]+)/n) {
|
729
|
+
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
730
|
+
}
|
731
|
+
end
|
732
|
+
end
|
733
|
+
|
734
|
+
def escape(str)
|
735
|
+
self.class.escape(str)
|
736
|
+
end
|
737
|
+
|
738
|
+
# Creates new DigestAuth filter.
|
739
|
+
def initialize
|
740
|
+
@config = nil # common config
|
741
|
+
@auth = {} # configs for each site
|
742
|
+
@challengeable = {}
|
743
|
+
@nonce_count = 0
|
744
|
+
@signature_handler = {
|
745
|
+
'HMAC-SHA1' => method(:sign_hmac_sha1)
|
746
|
+
}
|
747
|
+
@scheme = "OAuth"
|
748
|
+
end
|
749
|
+
|
750
|
+
# Resets challenge state. Do not send '*Authorization' header until the
|
751
|
+
# server sends '*Authentication' again.
|
752
|
+
def reset_challenge
|
753
|
+
@challengeable.clear
|
754
|
+
end
|
755
|
+
|
756
|
+
# Set authentication credential.
|
757
|
+
# You cannot set OAuth config via WWWAuth#set_auth. Use OAuth#config=
|
758
|
+
def set(*args)
|
759
|
+
# not supported
|
760
|
+
end
|
761
|
+
|
762
|
+
# have we marked this as set - ie that it's valid to use in this context?
|
763
|
+
def set?
|
764
|
+
true
|
765
|
+
end
|
766
|
+
|
767
|
+
# Set authentication credential.
|
768
|
+
def set_config(uri, config)
|
769
|
+
if uri.nil?
|
770
|
+
@config = config
|
771
|
+
else
|
772
|
+
uri = Util.uri_dirname(urify(uri))
|
773
|
+
@auth[uri] = config
|
774
|
+
end
|
775
|
+
end
|
776
|
+
|
777
|
+
# Get authentication credential.
|
778
|
+
def get_config(uri = nil)
|
779
|
+
if uri.nil?
|
780
|
+
@config
|
781
|
+
else
|
782
|
+
uri = urify(uri)
|
783
|
+
Util.hash_find_value(@auth) { |cand_uri, cred|
|
784
|
+
Util.uri_part_of(uri, cand_uri)
|
785
|
+
}
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
# Response handler: returns credential.
|
790
|
+
# It sends cred only when a given uri is;
|
791
|
+
# * child page of challengeable(got *Authenticate before) uri and,
|
792
|
+
# * child page of defined credential
|
793
|
+
def get(req)
|
794
|
+
target_uri = req.header.request_uri
|
795
|
+
return nil unless @challengeable[nil] or @challengeable.find { |uri, ok|
|
796
|
+
Util.uri_part_of(target_uri, uri) and ok
|
797
|
+
}
|
798
|
+
config = get_config(target_uri) || @config
|
799
|
+
return nil unless config
|
800
|
+
calc_cred(req, config)
|
801
|
+
end
|
802
|
+
|
803
|
+
# Challenge handler: remember URL for response.
|
804
|
+
def challenge(uri, param_str = nil)
|
805
|
+
if uri.nil?
|
806
|
+
@challengeable[nil] = true
|
807
|
+
else
|
808
|
+
@challengeable[urify(uri)] = true
|
809
|
+
end
|
810
|
+
true
|
811
|
+
end
|
812
|
+
|
813
|
+
private
|
814
|
+
|
815
|
+
def calc_cred(req, config)
|
816
|
+
header = {}
|
817
|
+
header['oauth_consumer_key'] = config.consumer_key
|
818
|
+
header['oauth_token'] = config.token
|
819
|
+
header['oauth_signature_method'] = config.signature_method
|
820
|
+
header['oauth_timestamp'] = config.debug_timestamp || Time.now.to_i.to_s
|
821
|
+
header['oauth_nonce'] = config.debug_nonce || generate_nonce()
|
822
|
+
header['oauth_version'] = config.version if config.version
|
823
|
+
header['oauth_callback'] = config.callback if config.callback
|
824
|
+
header['oauth_verifier'] = config.verifier if config.verifier
|
825
|
+
header['oauth_session_handle'] = config.session_handle if config.session_handle
|
826
|
+
signature = sign(config, header, req)
|
827
|
+
header['oauth_signature'] = signature
|
828
|
+
# no need to do but we should sort for easier to test.
|
829
|
+
str = header.sort_by { |k, v| k }.map { |k, v| encode_header(k, v) }.join(', ')
|
830
|
+
if config.realm
|
831
|
+
str = %Q(realm="#{config.realm}", ) + str
|
832
|
+
end
|
833
|
+
str
|
834
|
+
end
|
835
|
+
|
836
|
+
def generate_nonce
|
837
|
+
@nonce_count += 1
|
838
|
+
now = "%012d" % Time.now.to_i
|
839
|
+
pk = Digest::MD5.hexdigest([@nonce_count.to_s, now, self.__id__, Process.pid, rand(65535)].join)[0, 32]
|
840
|
+
[now + ':' + pk].pack('m*').chop
|
841
|
+
end
|
842
|
+
|
843
|
+
def encode_header(k, v)
|
844
|
+
%Q(#{escape(k.to_s)}="#{escape(v.to_s)}")
|
845
|
+
end
|
846
|
+
|
847
|
+
def encode_param(params)
|
848
|
+
params.map { |k, v|
|
849
|
+
[v].flatten.map { |vv|
|
850
|
+
%Q(#{escape(k.to_s)}=#{escape(vv.to_s)})
|
851
|
+
}
|
852
|
+
}.flatten
|
853
|
+
end
|
854
|
+
|
855
|
+
def sign(config, header, req)
|
856
|
+
base_string = create_base_string(config, header, req)
|
857
|
+
if handler = config.signature_handler[config.signature_method] || @signature_handler[config.signature_method.to_s]
|
858
|
+
handler.call(config, base_string)
|
859
|
+
else
|
860
|
+
raise ConfigurationError.new("Unknown OAuth signature method: #{config.signature_method}")
|
861
|
+
end
|
862
|
+
end
|
863
|
+
|
864
|
+
def create_base_string(config, header, req)
|
865
|
+
params = encode_param(header)
|
866
|
+
query = req.header.request_query
|
867
|
+
if query and HTTP::Message.multiparam_query?(query)
|
868
|
+
params += encode_param(query)
|
869
|
+
end
|
870
|
+
# captures HTTP Message body only for 'application/x-www-form-urlencoded'
|
871
|
+
if req.header.contenttype == 'application/x-www-form-urlencoded' and req.http_body.size
|
872
|
+
params += encode_param(HTTP::Message.parse(req.http_body.content))
|
873
|
+
end
|
874
|
+
uri = req.header.request_uri
|
875
|
+
if uri.query
|
876
|
+
params += encode_param(HTTP::Message.parse(uri.query))
|
877
|
+
end
|
878
|
+
if uri.port == uri.default_port
|
879
|
+
request_url = "#{uri.scheme.downcase}://#{uri.host}#{uri.path}"
|
880
|
+
else
|
881
|
+
request_url = "#{uri.scheme.downcase}://#{uri.host}:#{uri.port}#{uri.path}"
|
882
|
+
end
|
883
|
+
[req.header.request_method.upcase, request_url, params.sort.join('&')].map { |e|
|
884
|
+
escape(e)
|
885
|
+
}.join('&')
|
886
|
+
end
|
887
|
+
|
888
|
+
def sign_hmac_sha1(config, base_string)
|
889
|
+
unless SSLEnabled
|
890
|
+
raise ConfigurationError.new("openssl required for OAuth implementation")
|
891
|
+
end
|
892
|
+
key = [escape(config.consumer_secret.to_s), escape(config.secret.to_s)].join('&')
|
893
|
+
digester = OpenSSL::Digest::SHA1.new
|
894
|
+
[OpenSSL::HMAC.digest(digester, key, base_string)].pack('m*').chomp
|
895
|
+
end
|
896
|
+
end
|
897
|
+
|
898
|
+
|
899
|
+
end
|