diff --git a/spec/www_authenticate_parser_spec.cr b/spec/www_authenticate_parser_spec.cr new file mode 100644 index 0000000..582c389 --- /dev/null +++ b/spec/www_authenticate_parser_spec.cr @@ -0,0 +1,33 @@ +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 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 +end diff --git a/src/mechanize/http/agent.cr b/src/mechanize/http/agent.cr index 18e5b5f..9a4f687 100644 --- a/src/mechanize/http/agent.cr +++ b/src/mechanize/http/agent.cr @@ -1,6 +1,7 @@ require "../cookie" require "../history" require "./auth_store" +require "./www_authenticate_parser" class Mechanize module HTTP @@ -169,7 +170,7 @@ class Mechanize private def save_response_cookies(response, uri, page) if page.body =~ /Set-Cookie/ page.css("head meta[http-equiv=\"Set-Cookie\"]").each do |meta| - cookie = meta["content"].split(";")[0] + cookie = meta["content"].split(";") # [0] key, value = cookie.split("=") cookie = ::HTTP::Cookie.new(name: key, value: value) save_cookies(uri, [cookie]) diff --git a/src/mechanize/http/auth_challenge.cr b/src/mechanize/http/auth_challenge.cr new file mode 100644 index 0000000..ae8f4ce --- /dev/null +++ b/src/mechanize/http/auth_challenge.cr @@ -0,0 +1,46 @@ +class Mechanize + module HTTP + ## + # A parsed WWW-Authenticate header + + class AuthChallenge + property scheme : String? + property params : String? + + def initialize(scheme = nil, params = nil) + end + + # def [] param + # params[param] + # end + + ## + # Constructs an AuthRealm for this challenge + + def realm(uri) + case scheme + when "Basic" + # raise ArgumentError, "provide uri for Basic authentication" unless uri + Mechanize::HTTP::AuthRealm.new scheme, uri + '/', self["realm"] + when "Digest" + Mechanize::HTTP::AuthRealm.new scheme, uri + '/', self["realm"] + else + # raise Mechanize::Error, "unknown HTTP authentication scheme #{scheme}" + end + end + + ## + # The name of the realm for this challenge + + def realm_name + params["realm"] if Hash === params # NTLM has a string for params + end + + ## + # The raw authentication challenge + + # alias to_s raw + + end + end +end diff --git a/src/mechanize/http/www_authenticate_parser.cr b/src/mechanize/http/www_authenticate_parser.cr new file mode 100644 index 0000000..13ef8e3 --- /dev/null +++ b/src/mechanize/http/www_authenticate_parser.cr @@ -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) + challenges = [Mechanize::HTTP::AuthChallenge] + scanner = StringScanner.new(www_authenticate) + + loop do + break if scanner.eos? + start = scanner.pos + challenge = Mechanize::HTTP::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.pos] + challenges << challenge + next + else + scheme.capitalize! + end + + # next unless space + # params = {} + + # while true do + # pos = @scanner.pos + # name, value = auth_param + + # name.downcase! if name =~ /^realm$/i + + # unless name then + # challenge.params = params + # challenges << challenge + + # if @scanner.eos? then + # challenge.raw = www_authenticate[start, @scanner.pos] + # break + # end + + # @scanner.pos = pos # rewind + # challenge.raw = www_authenticate[start, @scanner.pos].sub(/(,+)? *$/, '') + # challenge = nil # a token should be next, new challenge + # break + # else + # params[name] = value + # end + + # spaces + + # @scanner.scan(/(, *)+/) + # end + end + + challenges + end + + # scans a comma followed by spaces + # needed for Negotiation, NTLM + + def scan_comma_spaces + scanner.scan(/, +/) + end + + # token = 1* + # + # Parses a token + + def token + scanner.scan(/[^\000-\037\177()<>@,;:\\"\/\[\]?={} ]+/) + end + + def auth_scheme + token + end + + ## + # 1*SP + # + # Parses spaces + + 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 = > + # quoted-pair = "\" CHAR + # + # For TEXT, the rules of RFC 2047 are ignored. + + def quoted_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