httpclient 2.1.2 → 2.1.3

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