vmc 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,522 @@
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
+
30
+ # Common abstract class for authentication filter.
31
+ #
32
+ # There are 2 authentication filters.
33
+ # WWWAuth:: Authentication filter for handling authentication negotiation
34
+ # between Web server. Parses 'WWW-Authentication' header in
35
+ # response and generates 'Authorization' header in request.
36
+ # ProxyAuth:: Authentication filter for handling authentication negotiation
37
+ # between Proxy server. Parses 'Proxy-Authentication' header in
38
+ # response and generates 'Proxy-Authorization' header in request.
39
+ class AuthFilterBase
40
+ private
41
+
42
+ def parse_authentication_header(res, tag)
43
+ challenge = res.header[tag]
44
+ return nil unless challenge
45
+ challenge.collect { |c| parse_challenge_header(c) }.compact
46
+ end
47
+
48
+ def parse_challenge_header(challenge)
49
+ scheme, param_str = challenge.scan(/\A(\S+)(?:\s+(.*))?\z/)[0]
50
+ return nil if scheme.nil?
51
+ return scheme, param_str
52
+ end
53
+ end
54
+
55
+
56
+ # Authentication filter for handling authentication negotiation between
57
+ # Web server. Parses 'WWW-Authentication' header in response and
58
+ # generates 'Authorization' header in request.
59
+ #
60
+ # Authentication filter is implemented using request filter of HTTPClient.
61
+ # It traps HTTP response header and maintains authentication state, and
62
+ # traps HTTP request header for inserting necessary authentication header.
63
+ #
64
+ # WWWAuth has sub filters (BasicAuth, DigestAuth, NegotiateAuth and
65
+ # SSPINegotiateAuth) and delegates some operations to it.
66
+ # NegotiateAuth requires 'ruby/ntlm' module.
67
+ # SSPINegotiateAuth requires 'win32/sspi' module.
68
+ class WWWAuth < AuthFilterBase
69
+ attr_reader :basic_auth
70
+ attr_reader :digest_auth
71
+ attr_reader :negotiate_auth
72
+ attr_reader :sspi_negotiate_auth
73
+
74
+ # Creates new WWWAuth.
75
+ def initialize
76
+ @basic_auth = BasicAuth.new
77
+ @digest_auth = DigestAuth.new
78
+ @negotiate_auth = NegotiateAuth.new
79
+ @sspi_negotiate_auth = SSPINegotiateAuth.new
80
+ # sort authenticators by priority
81
+ @authenticator = [@negotiate_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
82
+ end
83
+
84
+ # Resets challenge state. See sub filters for more details.
85
+ def reset_challenge
86
+ @authenticator.each do |auth|
87
+ auth.reset_challenge
88
+ end
89
+ end
90
+
91
+ # Set authentication credential. See sub filters for more details.
92
+ def set_auth(uri, user, passwd)
93
+ @authenticator.each do |auth|
94
+ auth.set(uri, user, passwd)
95
+ end
96
+ reset_challenge
97
+ end
98
+
99
+ # Filter API implementation. Traps HTTP request and insert
100
+ # 'Authorization' header if needed.
101
+ def filter_request(req)
102
+ @authenticator.each do |auth|
103
+ if cred = auth.get(req)
104
+ req.header.set('Authorization', auth.scheme + " " + cred)
105
+ return
106
+ end
107
+ end
108
+ end
109
+
110
+ # Filter API implementation. Traps HTTP response and parses
111
+ # 'WWW-Authenticate' header.
112
+ def filter_response(req, res)
113
+ command = nil
114
+ if res.status == HTTP::Status::UNAUTHORIZED
115
+ if challenge = parse_authentication_header(res, 'www-authenticate')
116
+ uri = req.header.request_uri
117
+ challenge.each do |scheme, param_str|
118
+ @authenticator.each do |auth|
119
+ if scheme.downcase == auth.scheme.downcase
120
+ challengeable = auth.challenge(uri, param_str)
121
+ command = :retry if challengeable
122
+ end
123
+ end
124
+ end
125
+ # ignore unknown authentication scheme
126
+ end
127
+ end
128
+ command
129
+ end
130
+ end
131
+
132
+
133
+ # Authentication filter for handling authentication negotiation between
134
+ # Proxy server. Parses 'Proxy-Authentication' header in response and
135
+ # generates 'Proxy-Authorization' header in request.
136
+ #
137
+ # Authentication filter is implemented using request filter of HTTPClient.
138
+ # It traps HTTP response header and maintains authentication state, and
139
+ # traps HTTP request header for inserting necessary authentication header.
140
+ #
141
+ # ProxyAuth has sub filters (BasicAuth, NegotiateAuth, and SSPINegotiateAuth)
142
+ # and delegates some operations to it.
143
+ # NegotiateAuth requires 'ruby/ntlm' module.
144
+ # SSPINegotiateAuth requires 'win32/sspi' module.
145
+ class ProxyAuth < AuthFilterBase
146
+ attr_reader :basic_auth
147
+ attr_reader :negotiate_auth
148
+ attr_reader :sspi_negotiate_auth
149
+
150
+ # Creates new ProxyAuth.
151
+ def initialize
152
+ @basic_auth = BasicAuth.new
153
+ @negotiate_auth = NegotiateAuth.new
154
+ @sspi_negotiate_auth = SSPINegotiateAuth.new
155
+ # sort authenticators by priority
156
+ @authenticator = [@negotiate_auth, @sspi_negotiate_auth, @basic_auth]
157
+ end
158
+
159
+ # Resets challenge state. See sub filters for more details.
160
+ def reset_challenge
161
+ @authenticator.each do |auth|
162
+ auth.reset_challenge
163
+ end
164
+ end
165
+
166
+ # Set authentication credential. See sub filters for more details.
167
+ def set_auth(user, passwd)
168
+ @authenticator.each do |auth|
169
+ auth.set(nil, user, passwd)
170
+ end
171
+ reset_challenge
172
+ end
173
+
174
+ # Filter API implementation. Traps HTTP request and insert
175
+ # 'Proxy-Authorization' header if needed.
176
+ def filter_request(req)
177
+ @authenticator.each do |auth|
178
+ if cred = auth.get(req)
179
+ req.header.set('Proxy-Authorization', auth.scheme + " " + cred)
180
+ return
181
+ end
182
+ end
183
+ end
184
+
185
+ # Filter API implementation. Traps HTTP response and parses
186
+ # 'Proxy-Authenticate' header.
187
+ def filter_response(req, res)
188
+ command = nil
189
+ if res.status == HTTP::Status::PROXY_AUTHENTICATE_REQUIRED
190
+ if challenge = parse_authentication_header(res, 'proxy-authenticate')
191
+ uri = req.header.request_uri
192
+ challenge.each do |scheme, param_str|
193
+ @authenticator.each do |auth|
194
+ if scheme.downcase == auth.scheme.downcase
195
+ challengeable = auth.challenge(uri, param_str)
196
+ command = :retry if challengeable
197
+ end
198
+ end
199
+ end
200
+ # ignore unknown authentication scheme
201
+ end
202
+ end
203
+ command
204
+ end
205
+ end
206
+
207
+ # Authentication filter for handling BasicAuth negotiation.
208
+ # Used in WWWAuth and ProxyAuth.
209
+ class BasicAuth
210
+ # Authentication scheme.
211
+ attr_reader :scheme
212
+
213
+ # Creates new BasicAuth filter.
214
+ def initialize
215
+ @cred = nil
216
+ @auth = {}
217
+ @challengeable = {}
218
+ @scheme = "Basic"
219
+ end
220
+
221
+ # Resets challenge state. Do not send '*Authorization' header until the
222
+ # server sends '*Authentication' again.
223
+ def reset_challenge
224
+ @challengeable.clear
225
+ end
226
+
227
+ # Set authentication credential.
228
+ # uri == nil for generic purpose (allow to use user/password for any URL).
229
+ def set(uri, user, passwd)
230
+ if uri.nil?
231
+ @cred = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
232
+ else
233
+ uri = Util.uri_dirname(uri)
234
+ @auth[uri] = ["#{user}:#{passwd}"].pack('m').tr("\n", '')
235
+ end
236
+ end
237
+
238
+ # Response handler: returns credential.
239
+ # It sends cred only when a given uri is;
240
+ # * child page of challengeable(got *Authenticate before) uri and,
241
+ # * child page of defined credential
242
+ def get(req)
243
+ target_uri = req.header.request_uri
244
+ return nil unless @challengeable.find { |uri, ok|
245
+ Util.uri_part_of(target_uri, uri) and ok
246
+ }
247
+ return @cred if @cred
248
+ Util.hash_find_value(@auth) { |uri, cred|
249
+ Util.uri_part_of(target_uri, uri)
250
+ }
251
+ end
252
+
253
+ # Challenge handler: remember URL for response.
254
+ def challenge(uri, param_str)
255
+ @challengeable[uri] = true
256
+ true
257
+ end
258
+ end
259
+
260
+
261
+ # Authentication filter for handling DigestAuth negotiation.
262
+ # Used in WWWAuth.
263
+ class DigestAuth
264
+ # Authentication scheme.
265
+ attr_reader :scheme
266
+
267
+ # Creates new DigestAuth filter.
268
+ def initialize
269
+ @auth = {}
270
+ @challenge = {}
271
+ @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
+ end
280
+
281
+ # Set authentication credential.
282
+ # uri == nil is ignored.
283
+ def set(uri, user, passwd)
284
+ if uri
285
+ uri = Util.uri_dirname(uri)
286
+ @auth[uri] = [user, passwd]
287
+ end
288
+ end
289
+
290
+ # Response handler: returns credential.
291
+ # It sends cred only when a given uri is;
292
+ # * child page of challengeable(got *Authenticate before) uri and,
293
+ # * child page of defined credential
294
+ def get(req)
295
+ target_uri = req.header.request_uri
296
+ param = Util.hash_find_value(@challenge) { |uri, v|
297
+ Util.uri_part_of(target_uri, uri)
298
+ }
299
+ return nil unless param
300
+ user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
301
+ Util.uri_part_of(target_uri, uri)
302
+ }
303
+ return nil unless user
304
+ uri = req.header.request_uri
305
+ calc_cred(req.header.request_method, uri, user, passwd, param)
306
+ end
307
+
308
+ # Challenge handler: remember URL and challenge token for response.
309
+ def challenge(uri, param_str)
310
+ @challenge[uri] = parse_challenge_param(param_str)
311
+ true
312
+ end
313
+
314
+ private
315
+
316
+ # this method is implemented by sromano and posted to
317
+ # http://tools.assembla.com/breakout/wiki/DigestForSoap
318
+ # Thanks!
319
+ # supported algorithm: MD5 only for now
320
+ def calc_cred(method, uri, user, passwd, param)
321
+ a_1 = "#{user}:#{param['realm']}:#{passwd}"
322
+ a_2 = "#{method}:#{uri.path}"
323
+ nonce = param['nonce']
324
+ cnonce = generate_cnonce()
325
+ @nonce_count += 1
326
+ message_digest = []
327
+ message_digest << Digest::MD5.hexdigest(a_1)
328
+ message_digest << nonce
329
+ message_digest << ('%08x' % @nonce_count)
330
+ message_digest << cnonce
331
+ message_digest << param['qop']
332
+ message_digest << Digest::MD5.hexdigest(a_2)
333
+ header = []
334
+ header << "username=\"#{user}\""
335
+ header << "realm=\"#{param['realm']}\""
336
+ header << "nonce=\"#{nonce}\""
337
+ header << "uri=\"#{uri.path}\""
338
+ header << "cnonce=\"#{cnonce}\""
339
+ header << "nc=#{'%08x' % @nonce_count}"
340
+ header << "qop=\"#{param['qop']}\""
341
+ header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
342
+ header << "algorithm=\"MD5\""
343
+ header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
344
+ header.join(", ")
345
+ end
346
+
347
+ # cf. WEBrick::HTTPAuth::DigestAuth#generate_next_nonce(aTime)
348
+ def generate_cnonce
349
+ now = "%012d" % Time.now.to_i
350
+ pk = Digest::MD5.hexdigest([now, self.__id__, Process.pid, rand(65535)].join)[0, 32]
351
+ [now + ':' + pk].pack('m*').chop
352
+ end
353
+
354
+ def parse_challenge_param(param_str)
355
+ param = {}
356
+ param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
357
+ key, value = str[0].scan(/\A([^=]+)=(.*)\z/)[0]
358
+ if /\A"(.*)"\z/ =~ value
359
+ value = $1.gsub(/\\(.)/, '\1')
360
+ end
361
+ param[key] = value
362
+ end
363
+ param
364
+ end
365
+ end
366
+
367
+
368
+ # Authentication filter for handling Negotiate/NTLM negotiation.
369
+ # Used in WWWAuth and ProxyAuth.
370
+ #
371
+ # NegotiateAuth depends on 'ruby/ntlm' module.
372
+ class NegotiateAuth
373
+ # Authentication scheme.
374
+ attr_reader :scheme
375
+ # NTLM opt for ruby/ntlm. {:ntlmv2 => true} by default.
376
+ attr_reader :ntlm_opt
377
+
378
+ # Creates new NegotiateAuth filter.
379
+ def initialize
380
+ @auth = {}
381
+ @auth_default = nil
382
+ @challenge = {}
383
+ @scheme = "Negotiate"
384
+ @ntlm_opt = {
385
+ :ntlmv2 => true
386
+ }
387
+ end
388
+
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
+ # Set authentication credential.
396
+ # uri == nil for generic purpose (allow to use user/password for any URL).
397
+ def set(uri, user, passwd)
398
+ if uri
399
+ uri = Util.uri_dirname(uri)
400
+ @auth[uri] = [user, passwd]
401
+ else
402
+ @auth_default = [user, passwd]
403
+ end
404
+ end
405
+
406
+ # Response handler: returns credential.
407
+ # See ruby/ntlm for negotiation state transition.
408
+ def get(req)
409
+ return nil unless NTLMEnabled
410
+ target_uri = req.header.request_uri
411
+ domain_uri, param = @challenge.find { |uri, v|
412
+ Util.uri_part_of(target_uri, uri)
413
+ }
414
+ return nil unless param
415
+ user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
416
+ Util.uri_part_of(target_uri, uri)
417
+ }
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
+ end
436
+
437
+ # Challenge handler: remember URL and challenge token for response.
438
+ def challenge(uri, param_str)
439
+ return false unless NTLMEnabled
440
+ if param_str.nil? or @challenge[uri].nil?
441
+ c = @challenge[uri] = {}
442
+ c[:state] = :init
443
+ c[:authphrase] = ""
444
+ else
445
+ c = @challenge[uri]
446
+ c[:state] = :response
447
+ c[:authphrase] = param_str
448
+ end
449
+ true
450
+ end
451
+ end
452
+
453
+
454
+ # Authentication filter for handling Negotiate/NTLM negotiation.
455
+ # Used in ProxyAuth.
456
+ #
457
+ # SSPINegotiateAuth depends on 'win32/sspi' module.
458
+ class SSPINegotiateAuth
459
+ # Authentication scheme.
460
+ attr_reader :scheme
461
+
462
+ # Creates new SSPINegotiateAuth filter.
463
+ def initialize
464
+ @challenge = {}
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
472
+ end
473
+
474
+ # Set authentication credential.
475
+ # NOT SUPPORTED: username and necessary data is retrieved by win32/sspi.
476
+ # See win32/sspi for more details.
477
+ def set(uri, user, passwd)
478
+ # not supported
479
+ end
480
+
481
+ # Response handler: returns credential.
482
+ # See win32/sspi for negotiation state transition.
483
+ def get(req)
484
+ return nil unless SSPIEnabled
485
+ target_uri = req.header.request_uri
486
+ domain_uri, param = @challenge.find { |uri, v|
487
+ Util.uri_part_of(target_uri, uri)
488
+ }
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
+ end
503
+
504
+ # Challenge handler: remember URL and challenge token for response.
505
+ def challenge(uri, param_str)
506
+ return false unless SSPIEnabled
507
+ if param_str.nil? or @challenge[uri].nil?
508
+ c = @challenge[uri] = {}
509
+ c[:state] = :init
510
+ c[:authenticator] = nil
511
+ c[:authphrase] = ""
512
+ else
513
+ c = @challenge[uri]
514
+ c[:state] = :response
515
+ c[:authphrase] = param_str
516
+ end
517
+ true
518
+ end
519
+ end
520
+
521
+
522
+ end