httpclient 2.1.5 → 2.1.5.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,513 +0,0 @@
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