httpclient 2.1.4 → 2.1.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.
@@ -61,21 +61,24 @@ class HTTPClient
61
61
  # It traps HTTP response header and maintains authentication state, and
62
62
  # traps HTTP request header for inserting necessary authentication header.
63
63
  #
64
- # WWWAuth has sub filters (BasicAuth, DigestAuth, and NegotiateAuth) and
65
- # delegates some operations to it.
64
+ # WWWAuth has sub filters (BasicAuth, DigestAuth, NegotiateAuth and
65
+ # SSPINegotiateAuth) and delegates some operations to it.
66
66
  # NegotiateAuth requires 'ruby/ntlm' module.
67
+ # SSPINegotiateAuth requires 'win32/sspi' module.
67
68
  class WWWAuth < AuthFilterBase
68
69
  attr_reader :basic_auth
69
70
  attr_reader :digest_auth
70
71
  attr_reader :negotiate_auth
72
+ attr_reader :sspi_negotiate_auth
71
73
 
72
74
  # Creates new WWWAuth.
73
75
  def initialize
74
76
  @basic_auth = BasicAuth.new
75
77
  @digest_auth = DigestAuth.new
76
78
  @negotiate_auth = NegotiateAuth.new
79
+ @sspi_negotiate_auth = SSPINegotiateAuth.new
77
80
  # sort authenticators by priority
78
- @authenticator = [@negotiate_auth, @digest_auth, @basic_auth]
81
+ @authenticator = [@negotiate_auth, @sspi_negotiate_auth, @digest_auth, @basic_auth]
79
82
  end
80
83
 
81
84
  # Resets challenge state. See sub filters for more details.
@@ -317,20 +320,22 @@ class HTTPClient
317
320
  def calc_cred(method, uri, user, passwd, param)
318
321
  a_1 = "#{user}:#{param['realm']}:#{passwd}"
319
322
  a_2 = "#{method}:#{uri.path}"
323
+ nonce = param['nonce']
324
+ cnonce = generate_cnonce()
320
325
  @nonce_count += 1
321
326
  message_digest = []
322
327
  message_digest << Digest::MD5.hexdigest(a_1)
323
- message_digest << param['nonce']
328
+ message_digest << nonce
324
329
  message_digest << ('%08x' % @nonce_count)
325
- message_digest << param['nonce']
330
+ message_digest << cnonce
326
331
  message_digest << param['qop']
327
332
  message_digest << Digest::MD5.hexdigest(a_2)
328
333
  header = []
329
334
  header << "username=\"#{user}\""
330
335
  header << "realm=\"#{param['realm']}\""
331
- header << "nonce=\"#{param['nonce']}\""
336
+ header << "nonce=\"#{nonce}\""
332
337
  header << "uri=\"#{uri.path}\""
333
- header << "cnonce=\"#{param['nonce']}\""
338
+ header << "cnonce=\"#{cnonce}\""
334
339
  header << "nc=#{'%08x' % @nonce_count}"
335
340
  header << "qop=\"#{param['qop']}\""
336
341
  header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
@@ -339,6 +344,13 @@ class HTTPClient
339
344
  header.join(", ")
340
345
  end
341
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
+
342
354
  def parse_challenge_param(param_str)
343
355
  param = {}
344
356
  param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
@@ -481,7 +493,7 @@ class HTTPClient
481
493
  case state
482
494
  when :init
483
495
  authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
484
- return authenticator.get_initial_token
496
+ return authenticator.get_initial_token(@scheme)
485
497
  when :response
486
498
  @challenge.delete(domain_uri)
487
499
  return authenticator.complete_authentication(authphrase)
@@ -0,0 +1,513 @@
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_count += 1
324
+ message_digest = []
325
+ message_digest << Digest::MD5.hexdigest(a_1)
326
+ message_digest << param['nonce']
327
+ message_digest << ('%08x' % @nonce_count)
328
+ message_digest << param['nonce']
329
+ message_digest << param['qop']
330
+ message_digest << Digest::MD5.hexdigest(a_2)
331
+ header = []
332
+ header << "username=\"#{user}\""
333
+ header << "realm=\"#{param['realm']}\""
334
+ header << "nonce=\"#{param['nonce']}\""
335
+ header << "uri=\"#{uri.path}\""
336
+ header << "cnonce=\"#{param['nonce']}\""
337
+ header << "nc=#{'%08x' % @nonce_count}"
338
+ header << "qop=\"#{param['qop']}\""
339
+ header << "response=\"#{Digest::MD5.hexdigest(message_digest.join(":"))}\""
340
+ header << "algorithm=\"MD5\""
341
+ header << "opaque=\"#{param['opaque']}\"" if param.key?('opaque')
342
+ header.join(", ")
343
+ end
344
+
345
+ def parse_challenge_param(param_str)
346
+ param = {}
347
+ param_str.scan(/\s*([^\,]+(?:\\.[^\,]*)*)/).each do |str|
348
+ key, value = str[0].scan(/\A([^=]+)=(.*)\z/)[0]
349
+ if /\A"(.*)"\z/ =~ value
350
+ value = $1.gsub(/\\(.)/, '\1')
351
+ end
352
+ param[key] = value
353
+ end
354
+ param
355
+ end
356
+ end
357
+
358
+
359
+ # Authentication filter for handling Negotiate/NTLM negotiation.
360
+ # Used in WWWAuth and ProxyAuth.
361
+ #
362
+ # NegotiateAuth depends on 'ruby/ntlm' module.
363
+ class NegotiateAuth
364
+ # Authentication scheme.
365
+ attr_reader :scheme
366
+ # NTLM opt for ruby/ntlm. {:ntlmv2 => true} by default.
367
+ attr_reader :ntlm_opt
368
+
369
+ # Creates new NegotiateAuth filter.
370
+ def initialize
371
+ @auth = {}
372
+ @auth_default = nil
373
+ @challenge = {}
374
+ @scheme = "Negotiate"
375
+ @ntlm_opt = {
376
+ :ntlmv2 => true
377
+ }
378
+ end
379
+
380
+ # Resets challenge state. Do not send '*Authorization' header until the
381
+ # server sends '*Authentication' again.
382
+ def reset_challenge
383
+ @challenge.clear
384
+ end
385
+
386
+ # Set authentication credential.
387
+ # uri == nil for generic purpose (allow to use user/password for any URL).
388
+ def set(uri, user, passwd)
389
+ if uri
390
+ uri = Util.uri_dirname(uri)
391
+ @auth[uri] = [user, passwd]
392
+ else
393
+ @auth_default = [user, passwd]
394
+ end
395
+ end
396
+
397
+ # Response handler: returns credential.
398
+ # See ruby/ntlm for negotiation state transition.
399
+ def get(req)
400
+ return nil unless NTLMEnabled
401
+ target_uri = req.header.request_uri
402
+ domain_uri, param = @challenge.find { |uri, v|
403
+ Util.uri_part_of(target_uri, uri)
404
+ }
405
+ return nil unless param
406
+ user, passwd = Util.hash_find_value(@auth) { |uri, auth_data|
407
+ Util.uri_part_of(target_uri, uri)
408
+ }
409
+ unless user
410
+ user, passwd = @auth_default
411
+ end
412
+ return nil unless user
413
+ state = param[:state]
414
+ authphrase = param[:authphrase]
415
+ case state
416
+ when :init
417
+ t1 = Net::NTLM::Message::Type1.new
418
+ return t1.encode64
419
+ when :response
420
+ t2 = Net::NTLM::Message.decode64(authphrase)
421
+ t3 = t2.response({:user => user, :password => passwd}, @ntlm_opt.dup)
422
+ @challenge.delete(domain_uri)
423
+ return t3.encode64
424
+ end
425
+ nil
426
+ end
427
+
428
+ # Challenge handler: remember URL and challenge token for response.
429
+ def challenge(uri, param_str)
430
+ return false unless NTLMEnabled
431
+ if param_str.nil? or @challenge[uri].nil?
432
+ c = @challenge[uri] = {}
433
+ c[:state] = :init
434
+ c[:authphrase] = ""
435
+ else
436
+ c = @challenge[uri]
437
+ c[:state] = :response
438
+ c[:authphrase] = param_str
439
+ end
440
+ true
441
+ end
442
+ end
443
+
444
+
445
+ # Authentication filter for handling Negotiate/NTLM negotiation.
446
+ # Used in ProxyAuth.
447
+ #
448
+ # SSPINegotiateAuth depends on 'win32/sspi' module.
449
+ class SSPINegotiateAuth
450
+ # Authentication scheme.
451
+ attr_reader :scheme
452
+
453
+ # Creates new SSPINegotiateAuth filter.
454
+ def initialize
455
+ @challenge = {}
456
+ @scheme = "Negotiate"
457
+ end
458
+
459
+ # Resets challenge state. Do not send '*Authorization' header until the
460
+ # server sends '*Authentication' again.
461
+ def reset_challenge
462
+ @challenge.clear
463
+ end
464
+
465
+ # Set authentication credential.
466
+ # NOT SUPPORTED: username and necessary data is retrieved by win32/sspi.
467
+ # See win32/sspi for more details.
468
+ def set(uri, user, passwd)
469
+ # not supported
470
+ end
471
+
472
+ # Response handler: returns credential.
473
+ # See win32/sspi for negotiation state transition.
474
+ def get(req)
475
+ return nil unless SSPIEnabled
476
+ target_uri = req.header.request_uri
477
+ domain_uri, param = @challenge.find { |uri, v|
478
+ Util.uri_part_of(target_uri, uri)
479
+ }
480
+ return nil unless param
481
+ state = param[:state]
482
+ authenticator = param[:authenticator]
483
+ authphrase = param[:authphrase]
484
+ case state
485
+ when :init
486
+ authenticator = param[:authenticator] = Win32::SSPI::NegotiateAuth.new
487
+ return authenticator.get_initial_token(@scheme)
488
+ when :response
489
+ @challenge.delete(domain_uri)
490
+ return authenticator.complete_authentication(authphrase)
491
+ end
492
+ nil
493
+ end
494
+
495
+ # Challenge handler: remember URL and challenge token for response.
496
+ def challenge(uri, param_str)
497
+ return false unless SSPIEnabled
498
+ if param_str.nil? or @challenge[uri].nil?
499
+ c = @challenge[uri] = {}
500
+ c[:state] = :init
501
+ c[:authenticator] = nil
502
+ c[:authphrase] = ""
503
+ else
504
+ c = @challenge[uri]
505
+ c[:state] = :response
506
+ c[:authphrase] = param_str
507
+ end
508
+ true
509
+ end
510
+ end
511
+
512
+
513
+ end