vmc 0.0.4 → 0.0.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.
@@ -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