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