httpclient-fixcerts 2.8.5
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 +98 -0
- data/bin/httpclient +77 -0
- data/bin/jsonclient +85 -0
- data/lib/hexdump.rb +50 -0
- data/lib/http-access2/cookie.rb +1 -0
- data/lib/http-access2/http.rb +1 -0
- data/lib/http-access2.rb +55 -0
- data/lib/httpclient/auth.rb +924 -0
- data/lib/httpclient/cacert.pem +3952 -0
- data/lib/httpclient/cacert1024.pem +3866 -0
- data/lib/httpclient/connection.rb +88 -0
- data/lib/httpclient/cookie.rb +220 -0
- data/lib/httpclient/http.rb +1082 -0
- data/lib/httpclient/include_client.rb +85 -0
- data/lib/httpclient/jruby_ssl_socket.rb +594 -0
- data/lib/httpclient/session.rb +960 -0
- data/lib/httpclient/ssl_config.rb +433 -0
- data/lib/httpclient/ssl_socket.rb +150 -0
- data/lib/httpclient/timeout.rb +140 -0
- data/lib/httpclient/util.rb +222 -0
- data/lib/httpclient/version.rb +3 -0
- data/lib/httpclient/webagent-cookie.rb +459 -0
- data/lib/httpclient.rb +1332 -0
- data/lib/jsonclient.rb +66 -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 +98 -0
- data/test/test_ssl.rb +562 -0
- data/test/test_webagent-cookie.rb +465 -0
- metadata +124 -0
@@ -0,0 +1,924 @@
|
|
1
|
+
# HTTPClient - HTTP client library.
|
2
|
+
# Copyright (C) 2000-2015 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
|
+
require 'mutex_m'
|
12
|
+
|
13
|
+
|
14
|
+
class HTTPClient
|
15
|
+
|
16
|
+
NTLMEnabled = false
|
17
|
+
SSPIEnabled = false
|
18
|
+
GSSAPIEnabled = false
|
19
|
+
|
20
|
+
# Common abstract class for authentication filter.
|
21
|
+
#
|
22
|
+
# There are 2 authentication filters.
|
23
|
+
# WWWAuth:: Authentication filter for handling authentication negotiation
|
24
|
+
# between Web server. Parses 'WWW-Authentication' header in
|
25
|
+
# response and generates 'Authorization' header in request.
|
26
|
+
# ProxyAuth:: Authentication filter for handling authentication negotiation
|
27
|
+
# between Proxy server. Parses 'Proxy-Authentication' header in
|
28
|
+
# response and generates 'Proxy-Authorization' header in request.
|
29
|
+
class AuthFilterBase
|
30
|
+
private
|
31
|
+
|
32
|
+
def parse_authentication_header(res, tag)
|
33
|
+
challenge = res.header[tag]
|
34
|
+
return nil unless challenge
|
35
|
+
challenge.collect { |c| parse_challenge_header(c) }.compact
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_challenge_header(challenge)
|
39
|
+
scheme, param_str = challenge.scan(/\A(\S+)(?:\s+(.*))?\z/)[0]
|
40
|
+
return nil if scheme.nil?
|
41
|
+
return scheme, param_str
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# Authentication filter for handling authentication negotiation between
|
47
|
+
# Web server. Parses 'WWW-Authentication' header in response and
|
48
|
+
# generates 'Authorization' header in request.
|
49
|
+
#
|
50
|
+
# Authentication filter is implemented using request filter of HTTPClient.
|
51
|
+
# It traps HTTP response header and maintains authentication state, and
|
52
|
+
# traps HTTP request header for inserting necessary authentication header.
|
53
|
+
#
|
54
|
+
# WWWAuth has sub filters (BasicAuth, DigestAuth, NegotiateAuth and
|
55
|
+
# SSPINegotiateAuth) and delegates some operations to it.
|
56
|
+
# NegotiateAuth requires 'ruby/ntlm' module (rubyntlm gem).
|
57
|
+
# SSPINegotiateAuth requires 'win32/sspi' module (rubysspi gem).
|
58
|
+
class WWWAuth < AuthFilterBase
|
59
|
+
attr_reader :basic_auth
|
60
|
+
attr_reader :digest_auth
|
61
|
+
attr_reader :negotiate_auth
|
62
|
+
attr_reader :sspi_negotiate_auth
|
63
|
+
attr_reader :oauth
|
64
|
+
|
65
|
+
# Creates new WWWAuth.
|
66
|
+
def initialize
|
67
|
+
@basic_auth = BasicAuth.new
|
68
|
+
@digest_auth = DigestAuth.new
|
69
|
+
@negotiate_auth = NegotiateAuth.new
|
70
|
+
@ntlm_auth = NegotiateAuth.new('NTLM')
|
71
|
+
@sspi_negotiate_auth = SSPINegotiateAuth.new
|
72
|
+
@oauth = OAuth.new
|
73
|
+
# sort authenticators by priority
|
74
|
+
@authenticator = [@oauth, @negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Resets challenge state. See sub filters for more details.
|
78
|
+
def reset_challenge
|
79
|
+
@authenticator.each do |auth|
|
80
|
+
auth.reset_challenge
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Set authentication credential. See sub filters for more details.
|
85
|
+
def set_auth(uri, user, passwd)
|
86
|
+
@authenticator.each do |auth|
|
87
|
+
auth.set(uri, user, passwd)
|
88
|
+
end
|
89
|
+
reset_challenge
|
90
|
+
end
|
91
|
+
|
92
|
+
# Filter API implementation. Traps HTTP request and insert
|
93
|
+
# 'Authorization' header if needed.
|
94
|
+
def filter_request(req)
|
95
|
+
@authenticator.each do |auth|
|
96
|
+
next unless auth.set? # hasn't be set, don't use it
|
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
|
105
|
+
req.header.set('Authorization', auth.scheme + " " + cred)
|
106
|
+
return
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Filter API implementation. Traps HTTP response and parses
|
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.
|
117
|
+
def filter_response(req, res)
|
118
|
+
command = nil
|
119
|
+
if res.status == HTTP::Status::UNAUTHORIZED
|
120
|
+
if challenge = parse_authentication_header(res, 'www-authenticate')
|
121
|
+
uri = req.header.request_uri
|
122
|
+
challenge.each do |scheme, param_str|
|
123
|
+
@authenticator.each do |auth|
|
124
|
+
next unless auth.set? # hasn't be set, don't use it
|
125
|
+
if scheme.downcase == auth.scheme.downcase
|
126
|
+
challengeable = auth.challenge(uri, param_str)
|
127
|
+
command = :retry if challengeable
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
# ignore unknown authentication scheme
|
132
|
+
end
|
133
|
+
end
|
134
|
+
command
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
# Authentication filter for handling authentication negotiation between
|
140
|
+
# Proxy server. Parses 'Proxy-Authentication' header in response and
|
141
|
+
# generates 'Proxy-Authorization' header in request.
|
142
|
+
#
|
143
|
+
# Authentication filter is implemented using request filter of HTTPClient.
|
144
|
+
# It traps HTTP response header and maintains authentication state, and
|
145
|
+
# traps HTTP request header for inserting necessary authentication header.
|
146
|
+
#
|
147
|
+
# ProxyAuth has sub filters (BasicAuth, NegotiateAuth, and SSPINegotiateAuth)
|
148
|
+
# and delegates some operations to it.
|
149
|
+
# NegotiateAuth requires 'ruby/ntlm' module.
|
150
|
+
# SSPINegotiateAuth requires 'win32/sspi' module.
|
151
|
+
class ProxyAuth < AuthFilterBase
|
152
|
+
attr_reader :basic_auth
|
153
|
+
attr_reader :digest_auth
|
154
|
+
attr_reader :negotiate_auth
|
155
|
+
attr_reader :sspi_negotiate_auth
|
156
|
+
|
157
|
+
# Creates new ProxyAuth.
|
158
|
+
def initialize
|
159
|
+
@basic_auth = ProxyBasicAuth.new
|
160
|
+
@negotiate_auth = NegotiateAuth.new
|
161
|
+
@ntlm_auth = NegotiateAuth.new('NTLM')
|
162
|
+
@sspi_negotiate_auth = SSPINegotiateAuth.new
|
163
|
+
@digest_auth = ProxyDigestAuth.new
|
164
|
+
# sort authenticators by priority
|
165
|
+
@authenticator = [@negotiate_auth, @ntlm_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
|
166
|
+
end
|
167
|
+
|
168
|
+
# Resets challenge state. See sub filters for more details.
|
169
|
+
def reset_challenge
|
170
|
+
@authenticator.each do |auth|
|
171
|
+
auth.reset_challenge
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Set authentication credential. See sub filters for more details.
|
176
|
+
def set_auth(user, passwd)
|
177
|
+
@authenticator.each do |auth|
|
178
|
+
auth.set(nil, user, passwd)
|
179
|
+
end
|
180
|
+
reset_challenge
|
181
|
+
end
|
182
|
+
|
183
|
+
# Filter API implementation. Traps HTTP request and insert
|
184
|
+
# 'Proxy-Authorization' header if needed.
|
185
|
+
def filter_request(req)
|
186
|
+
@authenticator.each do |auth|
|
187
|
+
next unless auth.set? # hasn't be set, don't use it
|
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
|
196
|
+
req.header.set('Proxy-Authorization', auth.scheme + " " + cred)
|
197
|
+
return
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Filter API implementation. Traps HTTP response and parses
|
203
|
+
# 'Proxy-Authenticate' header.
|
204
|
+
def filter_response(req, res)
|
205
|
+
command = nil
|
206
|
+
if res.status == HTTP::Status::PROXY_AUTHENTICATE_REQUIRED
|
207
|
+
if challenge = parse_authentication_header(res, 'proxy-authenticate')
|
208
|
+
uri = req.header.request_uri
|
209
|
+
challenge.each do |scheme, param_str|
|
210
|
+
@authenticator.each do |auth|
|
211
|
+
next unless auth.set? # hasn't be set, don't use it
|
212
|
+
if scheme.downcase == auth.scheme.downcase
|
213
|
+
challengeable = auth.challenge(uri, param_str)
|
214
|
+
command = :retry if challengeable
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
# ignore unknown authentication scheme
|
219
|
+
end
|
220
|
+
end
|
221
|
+
command
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Authentication filter base class.
|
226
|
+
class AuthBase
|
227
|
+
include HTTPClient::Util
|
228
|
+
|
229
|
+
# Authentication scheme.
|
230
|
+
attr_reader :scheme
|
231
|
+
|
232
|
+
def initialize(scheme)
|
233
|
+
@scheme = scheme
|
234
|
+
@challenge = {}
|
235
|
+
end
|
236
|
+
|
237
|
+
# Resets challenge state. Do not send '*Authorization' header until the
|
238
|
+
# server sends '*Authentication' again.
|
239
|
+
def reset_challenge
|
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
|
260
|
+
end
|
261
|
+
|
262
|
+
# Set authentication credential.
|
263
|
+
# uri == nil for generic purpose (allow to use user/password for any URL).
|
264
|
+
def set(uri, user, passwd)
|
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
|
272
|
+
end
|
273
|
+
end
|
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
|
+
|
280
|
+
# Response handler: returns credential.
|
281
|
+
# It sends cred only when a given uri is;
|
282
|
+
# * child page of challengeable(got *Authenticate before) uri and,
|
283
|
+
# * child page of defined credential
|
284
|
+
def get(req)
|
285
|
+
target_uri = req.header.request_uri
|
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
|
+
}
|
294
|
+
}
|
295
|
+
end
|
296
|
+
|
297
|
+
# Challenge handler: remember URL for response.
|
298
|
+
def challenge(uri, param_str = nil)
|
299
|
+
synchronize {
|
300
|
+
@challenge[urify(uri)] = true
|
301
|
+
true
|
302
|
+
}
|
303
|
+
end
|
304
|
+
end
|
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
|
328
|
+
|
329
|
+
# Authentication filter for handling DigestAuth negotiation.
|
330
|
+
# Used in WWWAuth.
|
331
|
+
class DigestAuth < AuthBase
|
332
|
+
include Mutex_m
|
333
|
+
|
334
|
+
# Creates new DigestAuth filter.
|
335
|
+
def initialize
|
336
|
+
super('Digest')
|
337
|
+
@auth = {}
|
338
|
+
@nonce_count = 0
|
339
|
+
end
|
340
|
+
|
341
|
+
# Set authentication credential.
|
342
|
+
# uri == nil is ignored.
|
343
|
+
def set(uri, user, passwd)
|
344
|
+
synchronize do
|
345
|
+
if uri
|
346
|
+
uri = Util.uri_dirname(uri)
|
347
|
+
@auth[uri] = [user, passwd]
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
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
|
+
|
357
|
+
# Response handler: returns credential.
|
358
|
+
# It sends cred only when a given uri is;
|
359
|
+
# * child page of challengeable(got *Authenticate before) uri and,
|
360
|
+
# * child page of defined credential
|
361
|
+
def get(req)
|
362
|
+
target_uri = req.header.request_uri
|
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)
|
373
|
+
}
|
374
|
+
end
|
375
|
+
|
376
|
+
# Challenge handler: remember URL and challenge token for response.
|
377
|
+
def challenge(uri, param_str)
|
378
|
+
synchronize {
|
379
|
+
@challenge[uri] = parse_challenge_param(param_str)
|
380
|
+
true
|
381
|
+
}
|
382
|
+
end
|
383
|
+
|
384
|
+
private
|
385
|
+
|
386
|
+
# this method is implemented by sromano and posted to
|
387
|
+
# http://tools.assembla.com/breakout/wiki/DigestForSoap
|
388
|
+
# Thanks!
|
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
|
393
|
+
a_1 = "#{user}:#{param['realm']}:#{passwd}"
|
394
|
+
a_2 = "#{method}:#{path}"
|
395
|
+
qop = param['qop']
|
396
|
+
nonce = param['nonce']
|
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
|
408
|
+
message_digest = []
|
409
|
+
message_digest << a_1_md5sum
|
410
|
+
message_digest << nonce
|
411
|
+
if qop
|
412
|
+
@nonce_count += 1
|
413
|
+
message_digest << ('%08x' % @nonce_count)
|
414
|
+
message_digest << cnonce
|
415
|
+
message_digest << param['qop']
|
416
|
+
end
|
417
|
+
message_digest << Digest::MD5.hexdigest(a_2)
|
418
|
+
header = []
|
419
|
+
header << "username=\"#{user}\""
|
420
|
+
header << "realm=\"#{param['realm']}\""
|
421
|
+
header << "nonce=\"#{nonce}\""
|
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
|
430
|
+
header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
|
431
|
+
header << "algorithm=#{algorithm}"
|
432
|
+
header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
|
433
|
+
header.join(", ")
|
434
|
+
end
|
435
|
+
|
436
|
+
# cf. WEBrick::HTTPAuth::DigestAuth#generate_next_nonce(aTime)
|
437
|
+
def generate_cnonce
|
438
|
+
now = "%012d" % Time.now.to_i
|
439
|
+
pk = Digest::MD5.hexdigest([now, self.__id__, Process.pid, rand(65535)].join)[0, 32]
|
440
|
+
[now + ':' + pk].pack('m*').chop
|
441
|
+
end
|
442
|
+
|
443
|
+
def parse_challenge_param(param_str)
|
444
|
+
param = {}
|
445
|
+
param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
|
446
|
+
key, value = str[0].scan(/\A([^=]+)=(.*)\z/)[0]
|
447
|
+
if /\A"(.*)"\z/ =~ value
|
448
|
+
value = $1.gsub(/\\(.)/, '\1')
|
449
|
+
end
|
450
|
+
param[key] = value
|
451
|
+
end
|
452
|
+
param
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
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
|
+
|
495
|
+
# Authentication filter for handling Negotiate/NTLM negotiation.
|
496
|
+
# Used in WWWAuth and ProxyAuth.
|
497
|
+
#
|
498
|
+
# NegotiateAuth depends on 'ruby/ntlm' module.
|
499
|
+
class NegotiateAuth < AuthBase
|
500
|
+
include Mutex_m
|
501
|
+
|
502
|
+
# NTLM opt for ruby/ntlm. {:ntlmv2 => true} by default.
|
503
|
+
attr_reader :ntlm_opt
|
504
|
+
|
505
|
+
# Creates new NegotiateAuth filter.
|
506
|
+
def initialize(scheme = "Negotiate")
|
507
|
+
super(scheme)
|
508
|
+
@auth = {}
|
509
|
+
@auth_default = nil
|
510
|
+
@ntlm_opt = {
|
511
|
+
:ntlmv2 => true
|
512
|
+
}
|
513
|
+
end
|
514
|
+
|
515
|
+
# Set authentication credential.
|
516
|
+
# uri == nil for generic purpose (allow to use user/password for any URL).
|
517
|
+
def set(uri, user, passwd)
|
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
|
525
|
+
end
|
526
|
+
end
|
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
|
+
|
533
|
+
# Response handler: returns credential.
|
534
|
+
# See ruby/ntlm for negotiation state transition.
|
535
|
+
def get(req)
|
536
|
+
target_uri = req.header.request_uri
|
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
|
571
|
+
}
|
572
|
+
end
|
573
|
+
|
574
|
+
# Challenge handler: remember URL and challenge token for response.
|
575
|
+
def challenge(uri, param_str)
|
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
|
+
}
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
|
592
|
+
# Authentication filter for handling Negotiate/NTLM negotiation.
|
593
|
+
# Used in ProxyAuth.
|
594
|
+
#
|
595
|
+
# SSPINegotiateAuth depends on 'win32/sspi' module.
|
596
|
+
class SSPINegotiateAuth < AuthBase
|
597
|
+
include Mutex_m
|
598
|
+
|
599
|
+
# Creates new SSPINegotiateAuth filter.
|
600
|
+
def initialize
|
601
|
+
super('Negotiate')
|
602
|
+
end
|
603
|
+
|
604
|
+
# Set authentication credential.
|
605
|
+
# NOT SUPPORTED: username and necessary data is retrieved by win32/sspi.
|
606
|
+
# See win32/sspi for more details.
|
607
|
+
def set(*args)
|
608
|
+
# not supported
|
609
|
+
end
|
610
|
+
|
611
|
+
# Check always (not effective but it works)
|
612
|
+
def set?
|
613
|
+
!@challenge.empty?
|
614
|
+
end
|
615
|
+
|
616
|
+
# Response handler: returns credential.
|
617
|
+
# See win32/sspi for negotiation state transition.
|
618
|
+
def get(req)
|
619
|
+
target_uri = req.header.request_uri
|
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
|
651
|
+
}
|
652
|
+
end
|
653
|
+
|
654
|
+
# Challenge handler: remember URL and challenge token for response.
|
655
|
+
def challenge(uri, param_str)
|
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
|
832
|
+
else
|
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")
|
916
|
+
end
|
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
|
920
|
+
end
|
921
|
+
end
|
922
|
+
|
923
|
+
|
924
|
+
end
|