Skip to content

Commit

Permalink
Implemented NTLM authentication (untested)
Browse files Browse the repository at this point in the history
Added Mechanize::UnauthorizedError
  • Loading branch information
drbrain committed Oct 31, 2011
1 parent aa0a4ea commit 5c8e802
Show file tree
Hide file tree
Showing 15 changed files with 181 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
be used for the old 'Mac Safari' alias.
* When given multiple HTTP authentication options mechanize can better pick
the strongest method.
* Added support for NTLM authentication, but this has not been tested.

* Bug fixes
* Mechanize#cookie_jar= works again. Issue #126
Expand Down
5 changes: 5 additions & 0 deletions Manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ lib/mechanize/history.rb
lib/mechanize/http.rb
lib/mechanize/http/agent.rb
lib/mechanize/http/auth_challenge.rb
lib/mechanize/http/auth_realm.rb
lib/mechanize/http/www_authenticate_parser.rb
lib/mechanize/inspect.rb
lib/mechanize/monkey_patch.rb
lib/mechanize/page.rb
Expand All @@ -55,6 +57,7 @@ lib/mechanize/redirect_not_get_or_head_error.rb
lib/mechanize/response_code_error.rb
lib/mechanize/response_read_error.rb
lib/mechanize/robots_disallowed_error.rb
lib/mechanize/unauthorized_error.rb
lib/mechanize/unsupported_scheme_error.rb
lib/mechanize/util.rb
test/data/htpasswd
Expand Down Expand Up @@ -152,6 +155,8 @@ test/test_mechanize_form_image_button.rb
test/test_mechanize_form_textarea.rb
test/test_mechanize_http_agent.rb
test/test_mechanize_http_auth_challenge.rb
test/test_mechanize_http_auth_realm.rb
test/test_mechanize_http_www_authenticate_parser.rb
test/test_mechanize_link.rb
test/test_mechanize_page_encoding.rb
test/test_mechanize_page_link.rb
Expand Down
1 change: 1 addition & 0 deletions lib/mechanize.rb
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ def add_to_history(page)
require 'mechanize/redirect_limit_reached_error'
require 'mechanize/redirect_not_get_or_head_error'
require 'mechanize/response_code_error'
require 'mechanize/unauthorized_error'
require 'mechanize/response_read_error'
require 'mechanize/robots_disallowed_error'
require 'mechanize/unsupported_scheme_error'
Expand Down
30 changes: 26 additions & 4 deletions lib/mechanize/http/agent.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require 'tempfile'
require 'net/ntlm'
require 'kconv'

##
# An HTTP (and local disk access) user agent
Expand Down Expand Up @@ -742,9 +744,10 @@ def response_redirect response, method, page, redirects, referer = current_page

def response_authenticate(response, page, uri, request, headers, params,
referer)
raise Mechanize::ResponseCodeError, page unless @user || @password
raise Mechanize::UnauthorizedError, page unless @user || @password

challenges = @authenticate_parser.parse response['www-authenticate']

if challenge = challenges.find { |c| c.scheme =~ /^Digest$/i } then
realm = challenge.realm uri

Expand All @@ -756,22 +759,41 @@ def response_authenticate(response, page, uri, request, headers, params,

existing_realms = @authenticate_methods[realm.uri][auth_scheme]

raise Mechanize::ResponseCodeError, page if
raise Mechanize::UnauthorizedError, page if
existing_realms.include? realm

existing_realms << realm
@digest_challenges[realm] = challenge
elsif challenge = challenges.find { |c| c.scheme == 'NTLM' } then
existing_realms = @authenticate_methods[uri + '/'][:ntlm]

raise Mechanize::UnauthorizedError, page if
existing_realms.include?(realm) and not challenge.params

existing_realms << realm

if challenge.params then
type_2 = Net::NTLM::Message.decode64 challenge.params

type_3 = type_2.response({ :user => @user, :password => @password, },
{ :ntlmv2 => true }).encode64

headers['Authorization'] = "NTLM #{type_3}"
else
type_1 = Net::NTLM::Message::Type1.new.encode64
headers['Authorization'] = "NTLM #{type_1}"
end
elsif challenge = challenges.find { |c| c.scheme == 'Basic' } then
realm = challenge.realm uri

existing_realms = @authenticate_methods[realm.uri][:basic]

raise Mechanize::ResponseCodeError, page if
raise Mechanize::UnauthorizedError, page if
existing_realms.include? realm

existing_realms << realm
else
raise Mechanize::ResponseCodeError, page
raise Mechanize::UnauthorizedError, page
end

fetch uri, request.method.downcase.to_sym, headers, params, referer
Expand Down
2 changes: 1 addition & 1 deletion lib/mechanize/http/auth_realm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Mechanize::HTTP::AuthRealm
def initialize scheme, uri, realm
@scheme = scheme
@uri = uri
@realm = realm.downcase
@realm = realm.downcase if realm
end

def == other
Expand Down
13 changes: 12 additions & 1 deletion lib/mechanize/http/www_authenticate_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,18 @@ def parse www_authenticate
next unless scheme
challenge.scheme = scheme

next unless spaces
space = spaces

if scheme == 'NTLM' then
if space then
challenge.params = @scanner.scan(/.*/)
end

challenges << challenge
next
end

next unless space

params = {}

Expand Down
2 changes: 1 addition & 1 deletion lib/mechanize/response_code_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ def to_s
"#{@response_code} => #{Net::HTTPResponse::CODE_TO_OBJ[@response_code]}"
end

def inspect; to_s; end
alias inspect to_s
end
end
3 changes: 3 additions & 0 deletions lib/mechanize/unauthorized_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Mechanize::UnauthorizedError < Mechanize::ResponseCodeError
end

38 changes: 20 additions & 18 deletions test/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def do_start
'/one_cookie_no_space' => OneCookieNoSpacesTest,
'/many_cookies' => ManyCookiesTest,
'/many_cookies_as_string' => ManyCookiesAsStringTest,
'/ntlm' => NTLMServlet,
'/send_cookies' => SendCookiesTest,
'/quoted_value_cookie' => QuotedValueCookieTest,
'/if_modified_since' => ModifiedSinceServlet,
Expand All @@ -61,30 +62,31 @@ def do_start

alias :old_request :request

def request(request, *data, &block)
url = URI.parse(request.path)
def request(req, *data, &block)
url = URI.parse(req.path)
path = WEBrick::HTTPUtils.unescape(url.path)

path = '/index.html' if path == '/'

res = ::Response.new
res.query_params = url.query

request.query = if 'POST' != request.method && url.query then
WEBrick::HTTPUtils.parse_query url.query
elsif request['content-type'] =~ /www-form-urlencoded/ then
WEBrick::HTTPUtils.parse_query request.body
elsif request['content-type'] =~ /boundary=(.+)/ then
boundary = WEBrick::HTTPUtils.dequote $1
WEBrick::HTTPUtils.parse_form_data request.body, boundary
else
{}
end

request.cookies = WEBrick::Cookie.parse(request['Cookie'])

if SERVLETS[path]
SERVLETS[path].new({}).send("do_#{request.method}", request, res)
req.query = if 'POST' != req.method && url.query then
WEBrick::HTTPUtils.parse_query url.query
elsif req['content-type'] =~ /www-form-urlencoded/ then
WEBrick::HTTPUtils.parse_query req.body
elsif req['content-type'] =~ /boundary=(.+)/ then
boundary = WEBrick::HTTPUtils.dequote $1
WEBrick::HTTPUtils.parse_form_data req.body, boundary
else
{}
end

req.cookies = WEBrick::Cookie.parse(req['Cookie'])

if servlet_klass = SERVLETS[path]
servlet = servlet_klass.new({})
servlet.send "do_#{req.method}", req, res
else
filename = "htdocs#{path.gsub(/[^\/\\.\w\s]/, '_')}"
unless PAGE_CACHE[filename]
Expand Down Expand Up @@ -126,7 +128,7 @@ def io.read clen, dest, _
dest << string[0, clen]
end

body_exist = request.response_body_permitted? &&
body_exist = req.response_body_permitted? &&
response_klass.body_permitted?

response.instance_variable_set :@body_exist, body_exist
Expand Down
30 changes: 30 additions & 0 deletions test/servlets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,36 @@ def do_GET(req, res)
end
end

class NTLMServlet < WEBrick::HTTPServlet::AbstractServlet

def do_GET(req, res)
if req['Authorization'] =~ /^NTLM (.*)/ then
authorization = $1.unpack('m*').first

if authorization =~ /^NTLMSSP\000\001/ then
type_2 = 'TlRMTVNTUAACAAAADAAMADAAAAABAoEAASNFZ4mr' \
'ze8AAAAAAAAAAGIAYgA8AAAARABPAE0AQQBJAE4A' \
'AgAMAEQATwBNAEEASQBOAAEADABTAEUAUgBWAEUA' \
'UgAEABQAZABvAG0AYQBpAG4ALgBjAG8AbQADACIA' \
'cwBlAHIAdgBlAHIALgBkAG8AbQBhAGkAbgAuAGMA' \
'bwBtAAAAAAA='

res['WWW-Authenticate'] = "NTLM #{type_2}"
res.status = 401
elsif authorization =~ /^NTLMSSP\000\003/ then
res.body = 'ok'
else
res['WWW-Authenticate'] = 'NTLM'
res.status = 401
end
else
res['WWW-Authenticate'] = 'NTLM'
res.status = 401
end
end

end

class SendCookiesTest < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res)
res['Content-Type'] = "text/html"
Expand Down
4 changes: 2 additions & 2 deletions test/test_mechanize.rb
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,15 @@ def test_get_bad_url
def test_get_basic_auth_bad
@mech.basic_auth('aaron', 'aaron')

e = assert_raises Mechanize::ResponseCodeError do
e = assert_raises Mechanize::UnauthorizedError do
@mech.get("http://localhost/basic_auth")
end

assert_equal("401", e.response_code)
end

def test_get_basic_auth_none
e = assert_raises Mechanize::ResponseCodeError do
e = assert_raises Mechanize::UnauthorizedError do
@mech.get("http://localhost/basic_auth")
end

Expand Down
26 changes: 16 additions & 10 deletions test/test_mechanize_http_agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,9 @@ def test_response_authenticate_digest
base_uri = @uri + '/'
realm = Mechanize::HTTP::AuthRealm.new 'Digest', base_uri, 'r'
assert_equal [realm], @agent.authenticate_methods[base_uri][:digest]

challenge = Mechanize::HTTP::AuthChallenge.new 'Digest', 'realm' => 'r'
assert_equal challenge, @agent.digest_challenges[realm]
end

def test_response_authenticate_digest_iis
Expand All @@ -458,15 +461,6 @@ def test_response_authenticate_digest_iis
assert_equal [realm], @agent.authenticate_methods[base_uri][:iis_digest]
end

def test_response_authenticate_no_account
page = Mechanize::File.new nil, nil, nil, 401
@res.instance_variable_set :@header, 'www-authenticate' => 'Basic realm=r'

assert_raises Mechanize::ResponseCodeError do
@agent.response_authenticate @res, page, @uri, @req, nil, nil, nil
end
end

def test_response_authenticate_multiple
@res.instance_variable_set(:@header,
'www-authenticate' =>
Expand All @@ -483,14 +477,26 @@ def test_response_authenticate_multiple
assert_empty @agent.authenticate_methods[base_uri][:basic]
end

def test_response_authenticate_ntlm
@uri += '/ntlm'
@res.instance_variable_set(:@header,
'www-authenticate' => ['NTLM'])
@agent.user = 'user'
@agent.password = 'password'

page = @agent.response_authenticate @res, nil, @uri, @req, {}, nil, nil

assert_equal 'ok', page.body # lame test
end

def test_response_authenticate_unknown
@agent.user = 'user'
@agent.password = 'password'
page = Mechanize::File.new nil, nil, nil, 401
@res.instance_variable_set(:@header,
'www-authenticate' => ['Unknown realm=r'])

assert_raises Mechanize::ResponseCodeError do
assert_raises Mechanize::UnauthorizedError do
@agent.response_authenticate @res, page, @uri, @req, nil, nil, nil
end
end
Expand Down
37 changes: 37 additions & 0 deletions test/test_mechanize_http_auth_challenge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require 'helper'

class TestMechanizeHttpAuthChallenge < MiniTest::Unit::TestCase

def setup
@uri = URI 'http://example/'
@AR = Mechanize::HTTP::AuthRealm
@AC = Mechanize::HTTP::AuthChallenge
@challenge = @AC.new 'Digest', 'realm' => 'r'
end

def test_realm_basic
@challenge.scheme = 'Basic'

expected = @AR.new 'Basic', @uri, 'r'

assert_equal expected, @challenge.realm(@uri + '/foo')
end

def test_realm_digest
expected = @AR.new 'Digest', @uri, 'r'

assert_equal expected, @challenge.realm(@uri + '/foo')
end

def test_realm_unknown
@challenge.scheme = 'Unknown'

e = assert_raises Mechanize::Error do
@challenge.realm(@uri + '/foo')
end

assert_equal 'unknown HTTP authentication scheme Unknown', e.message
end

end

10 changes: 10 additions & 0 deletions test/test_mechanize_http_auth_realm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ def setup
@realm = @AR.new 'Digest', @uri, 'r'
end

def test_initialize
assert_equal 'r', @realm.realm

realm = @AR.new 'Digest', @uri, 'R'
assert_equal 'r', realm.realm

realm = @AR.new 'Digest', @uri, nil
assert_nil realm.realm
end

def test_equals2
other = @realm.dup
assert_equal @realm, other
Expand Down
16 changes: 16 additions & 0 deletions test/test_mechanize_http_www_authenticate_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ def test_parse_multiple_blank
assert_equal expected, @parser.parse('Basic realm=foo,, Digest realm=foo')
end

def test_parse_ntlm_init
expected = [
challenge('NTLM', nil),
]

assert_equal expected, @parser.parse('NTLM')
end

def test_parse_ntlm_type_2_3
expected = [
challenge('NTLM', 'foo='),
]

assert_equal expected, @parser.parse('NTLM foo=')
end

def test_quoted_string
@parser.scanner = StringScanner.new '"text"'

Expand Down

0 comments on commit 5c8e802

Please sign in to comment.