httpclient 2.1.2 → 2.1.3

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,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