Merge pull request #22 from Kanezoh/add_basic_auth

Add basic auth
master
Kanezoh 2022-01-09 09:43:44 +09:00 committed by GitHub
commit 64bccad495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 975 additions and 9 deletions

View File

@ -16,3 +16,8 @@ development_dependencies:
webmock:
github: manastech/webmock.cr
branch: master
kemal:
github: kemalcr/kemal
version: ~> 1.0.0
kemal-basic-auth:
github: kemalcr/kemal-basic-auth

View File

@ -0,0 +1,61 @@
require "./spec_helper"
describe "Mechanize AuthChallenge test" do
it "test_realm_basic" do
uri = URI.parse "http://example.com/"
challenge = Mechanize::HTTP::AuthChallenge.new "Digest", {"realm" => "r"}, "Digest realm=r"
challenge.scheme = "Basic"
expected = Mechanize::HTTP::AuthRealm.new "Basic", uri, "r"
uri_path = URI.parse("http://example.com/foo")
challenge.realm(uri_path).should eq expected
end
it "test_realm_digest" do
uri = URI.parse "http://example.com/"
challenge = Mechanize::HTTP::AuthChallenge.new "Digest", {"realm" => "r"}, "Digest realm=r"
expected = Mechanize::HTTP::AuthRealm.new "Digest", uri, "r"
uri_path = URI.parse("http://example.com/foo")
challenge.realm(uri_path).should eq expected
end
it "test_realm_digest_case" do
uri = URI.parse "http://example.com/"
challenge = Mechanize::HTTP::AuthChallenge.new "Digest", {"realm" => "R"}, "Digest realm=R"
expected = Mechanize::HTTP::AuthRealm.new "Digest", uri, "R"
uri_path = URI.parse("http://example.com/foo")
challenge.realm(uri_path).should eq expected
end
it "test_realm_unknown" do
challenge = Mechanize::HTTP::AuthChallenge.new "Digest", {"realm" => "R"}, "Digest realm=R"
challenge.scheme = "Unknown"
uri_path = URI.parse("http://example.com/foo")
expect_raises(Exception, "unknown HTTP authentication scheme #{challenge.scheme}") do
challenge.realm(uri_path)
end
end
it "test_realm_name" do
uri = URI.parse "http://example.com/"
challenge = Mechanize::HTTP::AuthChallenge.new "Digest", {"realm" => "r"}, "Digest realm=r"
challenge.realm_name.should eq "r"
end
it "test_realm_name_case" do
uri = URI.parse "http://example.com/"
challenge = Mechanize::HTTP::AuthChallenge.new "Digest", {"realm" => "R"}, "Digest realm=R"
challenge.realm_name.should eq "R"
end
it "test_realm_name_ntlm" do
challenge = Mechanize::HTTP::AuthChallenge.new "Negotiate, NTLM"
challenge.realm_name.should eq nil
end
end

View File

@ -0,0 +1,48 @@
require "./spec_helper"
describe "Mechanize AuthRealm test" do
it "test_initialize" do
uri = URI.parse("http://example.com")
realm = Mechanize::HTTP::AuthRealm.new "Digest", uri, "r"
realm.realm.should eq "r"
realm = Mechanize::HTTP::AuthRealm.new "Digest", uri, "R"
realm.realm.should_not eq "r"
realm.realm.should eq "R"
realm = Mechanize::HTTP::AuthRealm.new "Digest", uri, nil
realm.realm.should eq nil
end
it "test_equals2" do
uri = URI.parse("http://example.com")
realm = Mechanize::HTTP::AuthRealm.new "Digest", uri, "r"
other = realm.dup
realm.should eq other
other = Mechanize::HTTP::AuthRealm.new "Basic", uri, "r"
realm.should_not eq other
other = Mechanize::HTTP::AuthRealm.new "Digest", URI.parse("http://other.example/"), "r"
realm.should_not eq other
other = Mechanize::HTTP::AuthRealm.new "Digest", uri, "R"
realm.should_not eq other
other = Mechanize::HTTP::AuthRealm.new "Digest", uri, "s"
realm.should_not eq other
end
it "test_hash" do
uri = URI.parse("http://example.com")
realm = Mechanize::HTTP::AuthRealm.new "Digest", uri, "r"
h = Hash(Mechanize::HTTP::AuthRealm, Int32).new(0)
h[realm] = 1
other = realm.dup
h[other].should eq 1
other = Mechanize::HTTP::AuthRealm.new "Basic", uri, "r"
h[other].should eq 0
end
end

19
spec/http_auth_spec.cr Normal file
View File

@ -0,0 +1,19 @@
require "./spec_helper"
require "./server.cr"
describe "Mechanize HTTP Authentication test" do
WebMock.allow_net_connect = true
it "should be unsuccessful without credentials " do
agent = Mechanize.new
page = agent.get("#{TEST_SERVER_URL}/secret")
page.code.should eq 401
# WebMock.allow_net_connect = false
end
it "should be successful with credentials " do
agent = Mechanize.new
agent.add_auth("#{TEST_SERVER_URL}", "username", "password")
page = agent.get("#{TEST_SERVER_URL}/secret")
page.code.should eq 200
end
end

View File

@ -0,0 +1,139 @@
require "./spec_helper"
describe "Mechanize AuthStore test" do
it "add_auth" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
user = "kanezoh"
password = "password"
realm = ""
auth_store.add_auth(url, user, password)
auth_store.auth_accounts[url].size.should eq 1
auth_store.auth_accounts[url][realm].should eq(["kanezoh", "password", ""])
end
it "test_add_auth_domain" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
user = "kanezoh"
password = "password"
domain = "domain"
realm = ""
auth_store.add_auth(url, user, password, nil, domain)
auth_store.auth_accounts[url][realm].should eq(["kanezoh", "password", "domain"])
end
it "test_add_auth_realm" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
auth_store.add_auth url, "user1", "pass"
auth_store.add_auth url, "user2", "pass", "realm"
expected = {
url => {
"" => ["user1", "pass", ""],
"realm" => ["user2", "pass", ""],
},
}
auth_store.auth_accounts.should eq expected
end
it "test_add_auth_realm_case" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
auth_store.add_auth url, "user1", "pass", "realm"
auth_store.add_auth url, "user2", "pass", "Realm"
expected = {
url => {
"realm" => ["user1", "pass", ""],
"Realm" => ["user2", "pass", ""],
},
}
auth_store.auth_accounts.should eq expected
end
it "test_add_auth_string" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
auth_store.add_auth "#{url}/path", "user", "pass"
expected = {
url => {
"" => ["user", "pass", ""],
},
}
auth_store.auth_accounts.should eq expected
end
it "test_credentials_eh" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
challenges = [
Mechanize::HTTP::AuthChallenge.new("Basic", {"realm" => "r"}),
Mechanize::HTTP::AuthChallenge.new("Digest", {"realm" => "r"}),
]
auth_store.credentials?(url, challenges).should_not eq true
auth_store.add_auth url, "user", "pass"
auth_store.credentials?(url, challenges).should eq true
auth_store.credentials?("#{url}/path", challenges).should eq true
end
it "test_credentials_for" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
auth_store.credentials_for(url, "realm").should eq nil
auth_store.add_auth url, "user", "pass", "realm"
auth_store.credentials_for(url, "realm").should eq ["user", "pass", ""]
auth_store.credentials_for(url.to_s, "realm").should eq ["user", "pass", ""]
auth_store.credentials_for(url, "other").should eq nil
end
it "test_credentials_for_no_realm" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
auth_store.add_auth url, "user", "pass" # no realm set
auth_store.credentials_for(url, "realm").should eq ["user", "pass", ""]
end
it "test_credentials_for_realm" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
auth_store.add_auth url, "user1", "pass"
auth_store.add_auth url, "user2", "pass", "realm"
auth_store.credentials_for(url, "realm").should eq ["user2", "pass", ""]
auth_store.credentials_for(url, "other").should eq ["user1", "pass", ""]
end
it "test_credentials_for_realm_case" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
auth_store.add_auth url, "user1", "pass", "realm"
auth_store.add_auth url, "user2", "pass", "Realm"
auth_store.credentials_for(url, "realm").should eq ["user1", "pass", ""]
auth_store.credentials_for(url, "Realm").should eq ["user2", "pass", ""]
end
it "test_credentials_for_path" do
auth_store = Mechanize::HTTP::AuthStore.new
url = URI.parse("http://example.com/")
auth_store.add_auth url, "user", "pass", "realm"
url = url.to_s + "/path"
auth_store.credentials_for(url, "realm").should eq ["user", "pass", ""]
end
end

37
spec/server.cr Normal file
View File

@ -0,0 +1,37 @@
require "kemal"
require "kemal-basic-auth"
TEST_SERVER_HOST = "0.0.0.0"
TEST_SERVER_PORT = 4567
TEST_SERVER_URL = "http://#{TEST_SERVER_HOST}:#{TEST_SERVER_PORT}"
class BasicAuthHandler < Kemal::BasicAuth::Handler
only ["/secret"]
def call(env)
return call_next(env) unless only_match?(env)
super
end
end
add_handler BasicAuthHandler.new("username", "password")
get "/secret" do
"Authorized"
end
kemal_config = Kemal.config
kemal_config.env = "development"
kemal_config.logging = false
spawn do
Kemal.run(port: TEST_SERVER_PORT)
end
spawn do
sleep 300000
end
until Kemal.config.running
sleep 1.millisecond
end

View File

@ -0,0 +1,250 @@
require "./spec_helper"
describe "Mechanize HTTP Authentication test" do
it "auth_param" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new("realm=here")
parser.auth_param.should eq ["realm", "here"]
end
it "auth_param bad no value" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new("realm=")
parser.auth_param.should eq nil
end
it "auth_param bad token" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new("realm")
parser.auth_param.should eq nil
end
it "auth_param bad value" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new("realm=\"this ")
parser.auth_param.should eq nil
end
it "auth_param with quote" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new("realm=\"this site\"")
parser.auth_param.should eq ["realm", "this site"]
end
it "test parse" do
expect = [challenge("Basic", {"realm" => "foo", "qop" => "auth,auth-int"}, "Basic realm=foo, qop=\"auth,auth-int\"")]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("Basic realm=foo, qop=\"auth,auth-int\"")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
end
it "test_parse_without_comma_delimiter" do
expect = [challenge("Basic", {"realm" => "foo", "qop" => "auth,auth-int"}, "Basic realm=foo qop=\"auth,auth-int\"")]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("Basic realm=foo qop=\"auth,auth-int\"")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
end
it "test_parse_multiple" do
expect = [
challenge("Basic", {"realm" => "foo"}, "Basic realm=foo"),
challenge("Digest", {"realm" => "bar"}, "Digest realm=bar"),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("Basic realm=foo, Digest realm=bar")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
result[1].scheme.should eq expect[1].scheme
result[1].params.should eq expect[1].params
result[1].raw.should eq expect[1].raw
end
it "test_parse_multiple_without_comma_delimiter" do
expect = [
challenge("Basic", {"realm" => "foo"}, "Basic realm=foo"),
challenge("Digest", {"realm" => "bar"}, "Digest realm=bar"),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("Basic realm=foo, Digest realm=bar")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
result[1].scheme.should eq expect[1].scheme
result[1].params.should eq expect[1].params
result[1].raw.should eq expect[1].raw
end
it "test_parse_multiple_blank" do
expect = [
challenge("Basic", {"realm" => "foo"}, "Basic realm=foo"),
challenge("Digest", {"realm" => "bar"}, "Digest realm=bar"),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("Basic realm=foo, Digest realm=bar")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
result[1].scheme.should eq expect[1].scheme
result[1].params.should eq expect[1].params
result[1].raw.should eq expect[1].raw
end
it "test_parse_ntlm_init" do
expect = [
challenge("NTLM", nil, "NTLM"),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("NTLM")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
end
it "test_parse_ntlm_type_2_3" do
expect = [
challenge("NTLM", "foo=", "NTLM foo="),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("NTLM foo=")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
end
it "test_parse_realm_uppercase" do
expect = [
challenge("Basic", {"realm" => "foo"}, "Basic ReAlM=foo"),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("Basic ReAlM=foo")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
end
it "test_parse_realm_value_case" do
expect = [
challenge("Basic", {"realm" => "Foo"}, "Basic realm=Foo"),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("Basic realm=Foo")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
end
it "test_parse_scheme_uppercase" do
expect = [
challenge("Basic", {"realm" => "foo"}, "BaSiC realm=foo"),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("BaSiC realm=foo")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
end
it "test_parse_bad_whitespace_around_auth_param" do
expect = [
challenge("Basic", {"realm" => "foo"}, "Basic realm = \"foo\""),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("Basic realm = \"foo\"")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
end
it "test_parse_bad_single_quote" do
expect = [
challenge("Basic", {"realm" => "'foo"}, "Basic realm='foo"),
]
parser = Mechanize::HTTP::WWWAuthenticateParser.new
result = parser.parse("Basic realm='foo bar', qop='baz'")
result[0].scheme.should eq expect[0].scheme
result[0].params.should eq expect[0].params
result[0].raw.should eq expect[0].raw
end
it "test_quoted_string" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new "\"text\""
string = parser.quoted_string
string.should eq "text"
end
it "test_quoted_string_bad" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new "\"text"
string = parser.quoted_string
string.should eq nil
end
it "test_quoted_string_quote" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new "\"escaped \\\" here\""
string = parser.quoted_string
string.should eq "escaped \\\" here"
end
it "test_quoted_string_quote_end" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new "\"end \\\"\""
string = parser.quoted_string
string.should eq "end \\\""
end
it "test_token" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new "text"
string = parser.token
string.should eq "text"
end
it "test_token_space" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new "t ext"
string = parser.token
string.should eq "t"
end
it "test_token_special" do
parser = Mechanize::HTTP::WWWAuthenticateParser.new
parser.scanner = StringScanner.new "t\text"
string = parser.token
string.should eq "t"
end
end
private def challenge(scheme, params, raw)
Mechanize::HTTP::AuthChallenge.new(scheme, params, raw)
end

View File

@ -296,6 +296,15 @@ class Mechanize
end
end
# set basic auth credentials.
# ```
# # make download.html whose content is http://example.com's html.
# agent.add_auth("http://example.com", "username", "password")
# ```
def add_auth(uri : String, user : String, pass : String)
@agent.add_auth(uri, user, pass)
end
# Runs given block, then resets the page history as it was before.
private def transact
# save the previous history status.

View File

@ -1,5 +1,7 @@
require "../cookie"
require "../history"
require "./auth_store"
require "./www_authenticate_parser"
class Mechanize
module HTTP
@ -10,6 +12,9 @@ class Mechanize
property history : History
property user_agent : String
property request_cookies : ::HTTP::Cookies
getter auth_store : AuthStore
getter authenticate_methods : Hash(URI, Hash(String, Array(AuthRealm)))
getter authenticate_parser : WWWAuthenticateParser
def initialize(@context : Mechanize? = nil)
@history = History.new
@ -17,6 +22,9 @@ class Mechanize
@context = context
@request_cookies = ::HTTP::Cookies.new
@user_agent = ""
@auth_store = AuthStore.new
@authenticate_methods = Hash(URI, Hash(String, Array(AuthRealm))).new
@authenticate_parser = WWWAuthenticateParser.new
end
# send http request and return page.
@ -33,10 +41,13 @@ class Mechanize
set_user_agent
set_request_referer(referer)
uri, params = resolve_parameters(uri, method, params)
response = http_request(uri, method, params, body)
client = ::HTTP::Client.new(uri)
request_auth client, uri
response = http_request(client, uri, method, params, body)
body = response.not_nil!.body
page = response_parse(response, body, uri)
response_log(response)
# save cookies
save_response_cookies(response, uri, page)
@ -44,6 +55,10 @@ class Mechanize
return follow_redirect(response, headers, page)
end
if response && response.status.unauthorized?
return response_authenticate(response, page, uri, params, referer)
end
page
end
@ -62,26 +77,57 @@ class Mechanize
end
# send http request
private def http_request(uri, method, params, body) : ::HTTP::Client::Response?
private def http_request(client : ::HTTP::Client,
uri : URI,
method : Symbol,
params : Hash(String, String)?,
body : String?) : ::HTTP::Client::Response?
request_log(uri, method)
path = uri.path
case uri.scheme.not_nil!.downcase
when "http", "https"
case method
when :get
::HTTP::Client.get(uri, headers: request_headers)
client.get(path, headers: request_headers)
when :post
::HTTP::Client.post(uri, headers: request_headers, form: params.not_nil!.fetch("value", ""))
client.post(path, headers: request_headers, form: params.not_nil!.fetch("value", ""))
when :put
::HTTP::Client.put(uri, headers: request_headers, body: body)
client.put(path, headers: request_headers, body: body)
when :delete
::HTTP::Client.delete(uri, headers: request_headers, body: body)
client.delete(path, headers: request_headers, body: body)
when :head
::HTTP::Client.head(uri, headers: request_headers)
client.head(path, headers: request_headers)
end
end
end
private def request_auth(client : ::HTTP::Client, uri : URI)
base_uri = uri.dup
base_uri.path = "/"
base_uri.user &&= nil
base_uri.password &&= nil
schemes = @authenticate_methods.fetch(base_uri, nil)
return if schemes.nil?
if realm = schemes["basic"].find { |r| r.uri == base_uri }
res = @auth_store.credentials_for uri, realm.realm
if res
user, password = res
client.basic_auth user, password
end
end
# if realm = schemes[:digest].find { |r| r.uri == base_uri } then
# request_auth_digest request, uri, realm, base_uri, false
# elsif realm = schemes[:iis_digest].find { |r| r.uri == base_uri } then
# request_auth_digest request, uri, realm, base_uri, true
# elsif realm = schemes[:basic].find { |r| r.uri == base_uri } then
# user, password, = @auth_store.credentials_for uri, realm.realm
# request.basic_auth user, password
# end
end
# returns the page now mechanize visiting.
# ```
# agent.current_page
@ -114,6 +160,15 @@ class Mechanize
@history.max_size = length
end
# set basic auth credentials.
# ```
# # make download.html whose content is http://example.com's html.
# agent.add_auth("http://example.com", "username", "password")
# ```
def add_auth(uri : String, user : String, pass : String)
@auth_store.add_auth(uri, user, pass)
end
private def set_request_headers(uri, headers)
reset_request_header_cookies
headers.each do |k, v|
@ -129,7 +184,7 @@ class Mechanize
end
# Sets a Referer header.
def set_request_referer(referer : Page?)
private def set_request_referer(referer : Page?)
return unless referer
request_headers["Referer"] = referer.uri.to_s
@ -138,7 +193,7 @@ class Mechanize
private def resolve_parameters(uri, method, params)
case method
when :get
return uri, nil if params.empty?
return uri, nil if params.nil? || params.empty?
query = URI::Params.encode(params)
uri.query = query
return uri, nil
@ -213,6 +268,39 @@ class Mechanize
target_url
end
private def response_authenticate(response, page, uri, params, referer) : Page
www_authenticate = response.headers["www-authenticate"]
unless www_authenticate = response.headers["www-authenticate"]
# TODO: raise error
end
challenges = @authenticate_parser.parse(www_authenticate)
unless @auth_store.credentials?(uri, challenges)
# TODO: raise error
return page
end
if challenge = challenges.find { |c| c.scheme == "Basic" }
realm = challenge.realm uri
if realm
@authenticate_methods[realm.uri] = Hash(String, Array(AuthRealm)).new([] of AuthRealm) unless @authenticate_methods.has_key?(realm.uri)
existing_realms = @authenticate_methods[realm.uri]["basic"]
if existing_realms && existing_realms.includes? realm
# TODO: raise error
end
existing_realms << realm
end
fetch(uri, headers: request_headers, params: params, referer: referer)
else
# TODO: raise error
raise Exception.new("error")
end
end
# reset request cookie before setting headers.
private def reset_request_header_cookies
request_headers.delete("Cookie")

View File

@ -0,0 +1,65 @@
require "./auth_realm"
class Mechanize
module HTTP
##
# A parsed WWW-Authenticate header
class AuthChallenge
property scheme : String?
property params : String? | Hash(String, String)?
property raw : String
def initialize(scheme = nil, params = nil, raw = "")
@scheme = scheme
@params = params
@raw = raw
end
def [](param) : String?
params_value = params
if params_value.is_a?(Hash)
params_value[param] # NTLM has a string for params
else
nil
end
end
##
# Constructs an AuthRealm for this challenge
def realm(uri) : AuthRealm
target_uri = uri.dup
target_uri.path = "/"
case scheme
when "Basic"
# raise ArgumentError, "provide uri for Basic authentication" unless uri
AuthRealm.new scheme, target_uri, self["realm"]
when "Digest"
AuthRealm.new scheme, target_uri, self["realm"]
else
# raise Mechanize::Error, "unknown HTTP authentication scheme #{scheme}"
raise Exception.new("unknown HTTP authentication scheme #{scheme}")
end
end
##
# The name of the realm for this challenge
def realm_name : String?
params_value = params
if params_value.is_a?(Hash)
params_value["realm"] # NTLM has a string for params
else
nil
end
end
##
# The raw authentication challenge
# alias to_s raw
end
end
end

View File

@ -0,0 +1,19 @@
# This class represents realm attribute of www-authenticate header.
class Mechanize::HTTP::AuthRealm
getter scheme : String?
getter uri : URI
getter realm : String?
def initialize(scheme, uri, realm)
@scheme = scheme
@uri = uri
@realm = realm if realm
end
def ==(other) : Bool
self.class === other &&
@scheme == other.scheme &&
@uri == other.uri &&
@realm == other.realm
end
end

View File

@ -0,0 +1,60 @@
class Mechanize
module HTTP
# This class store info for HTTP Authentication.
class AuthStore
getter auth_accounts : Hash(URI, Hash(String, Array(String)))
def initialize
@auth_accounts = Hash(URI, Hash(String, Array(String))).new
end
def add_auth(uri : String | URI, user : String, pass : String, realm : String? = nil, domain : String? = nil)
target_uri = uri.dup
unless uri.is_a?(URI)
target_uri = URI.parse(uri)
end
target_uri = target_uri.as(URI)
target_uri.path = "/"
target_uri.user = nil
target_uri.password = nil
realm = "" if realm.nil?
domain = "" if domain.nil?
realm_hash = {realm => [user, pass, domain]}
if auth_accounts.has_key?(target_uri)
auth_accounts[target_uri].merge!(realm_hash)
else
auth_accounts[target_uri] = realm_hash
end
end
##
# Returns true if credentials exist for the +challenges+ from the server at
# +uri+.
def credentials?(uri, challenges) : Bool
challenges.any? do |challenge|
credentials_for uri, challenge.realm_name
end
end
# Retrieves credentials for +realm+ on the server at +uri+.
def credentials_for(uri : String | URI, realm : String?) : Array(String)?
target_uri = uri.dup
unless uri.is_a?(URI)
target_uri = URI.parse(uri)
end
target_uri = target_uri.as(URI)
target_uri.path = "/"
target_uri.user = nil
target_uri.password = nil
realm = "" if realm.nil?
realms = auth_accounts.fetch(target_uri, nil)
return nil if realms.nil?
realms.fetch(realm, nil) || realms.fetch("", nil)
end
end
end
end

View File

@ -0,0 +1,166 @@
require "string_scanner"
require "./auth_challenge"
##
# Parses the WWW-Authenticate HTTP header into separate challenges.
class Mechanize
module HTTP
class WWWAuthenticateParser
property scanner : StringScanner
# Creates a new header parser for WWW-Authenticate headers
def initialize
@scanner = StringScanner.new("")
end
# Parsers the header. Returns an Array of challenges as strings
def parse(www_authenticate : String) : Array(AuthChallenge)
challenges = [] of AuthChallenge
@scanner = StringScanner.new(www_authenticate)
loop do
break if scanner.eos?
start = scanner.offset
challenge = AuthChallenge.new
scheme = auth_scheme
if scheme == "Negotiate"
scan_comma_spaces
end
break unless scheme
challenge.scheme = scheme
space = spaces
if scheme == "NTLM"
if space
challenge.params = scanner.scan(/.*/)
end
challenge.raw = www_authenticate[start, scanner.offset]
challenges << challenge
next
else
challenge.scheme = scheme.capitalize
end
next unless space
params = Hash(String, String).new
loop do
offset = scanner.offset
param = auth_param
if param
name, value = param
name = name.downcase if name =~ /^realm$/i
params[name] = value
else
challenge.params = params
challenges << challenge
if scanner.eos?
challenge.raw = www_authenticate[start, scanner.offset]
break
end
scanner.offset = offset # rewind
challenge.raw = www_authenticate[start, scanner.offset].sub(/(,+)? *$/, "")
challenge = nil # a token should be next, new challenge
break
end
spaces
scanner.scan(/(, *)+/)
end
end
challenges
end
# scans a comma followed by spaces
# needed for Negotiation, NTLM
private def scan_comma_spaces
scanner.scan(/, +/)
end
# token = 1*<any CHAR except CTLs or separators>
#
# Parses a token
def token
scanner.scan(/[^\000-\037\177()<>@,;:\\"\/\[\]?={} ]+/)
end
private def auth_scheme
token
end
##
# 1*SP
#
# Parses spaces
private def spaces
scanner.scan(/ +/)
end
# auth-param = token "=" ( token | quoted-string )
#
# Parses an auth parameter
def auth_param : Array(String)?
return nil unless name = token
return nil unless scanner.scan(/ *= */)
value = if scanner.peek(1) == "\""
quoted_string
else
token
end
return nil unless value
return [name, value]
end
##
# quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
# qdtext = <any TEXT except <">>
# quoted-pair = "\" CHAR
#
# For TEXT, the rules of RFC 2047 are ignored.
def quoted_string : String?
return nil unless @scanner.scan(/"/)
text = String.new
loop do
chunk = scanner.scan(/[\r\n \t\x21\x23-\x7e\x{0080}-\x{00ff}]+/) # not " which is \x22
if chunk
text += chunk
text += (scanner.scan(/./) || "") if chunk.ends_with?("\\") && "\"" == scanner.peek(1)
else
if "\"" == scanner.peek(1)
scanner.scan(/./)
break
else
return nil
end
end
end
text
end
end
end
end