diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java index 396cb488679..c6a160ea214 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/AuthenticationProtocolHandler.java @@ -20,12 +20,14 @@ package org.eclipse.jetty.client; import java.net.URI; import java.util.ArrayList; -import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.Authentication.HeaderInfo; import org.eclipse.jetty.client.api.Connection; import org.eclipse.jetty.client.api.ContentProvider; import org.eclipse.jetty.client.api.ContentResponse; @@ -35,6 +37,8 @@ import org.eclipse.jetty.client.api.Result; import org.eclipse.jetty.client.util.BufferingResponseListener; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.QuotedCSV; +import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; @@ -42,7 +46,11 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler { public static final int DEFAULT_MAX_CONTENT_LENGTH = 16*1024; public static final Logger LOG = Log.getLogger(AuthenticationProtocolHandler.class); - private static final Pattern AUTHENTICATE_PATTERN = Pattern.compile("([^\\s]+)\\s+(.*,\\s*)?realm=\"([^\"]*)\"\\s*,?\\s*(.*)", Pattern.CASE_INSENSITIVE); + + private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=([^=]+)?"); + private static final Pattern TYPE_PATTERN = Pattern.compile("([^\\s]+)(\\s+(.*))?"); + private static final Pattern MULTIPLE_CHALLENGE_PATTERN = Pattern.compile("(.*?)\\s*,\\s*([^=\\s,]+(\\s+[^=\\s].*)?)"); + private static final Pattern BASE64_PATTERN = Pattern.compile("[\\+\\-\\.\\/\\dA-Z_a-z~]+=*"); private final HttpClient client; private final int maxContentLength; @@ -74,6 +82,80 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler // Return new instances every time to keep track of the response content return new AuthenticationListener(); } + + + + protected List getHeaderInfo(String value) throws IllegalArgumentException + { + String header = value; + List headerInfos = new ArrayList<>(); + + while(true) + { + Matcher m = MULTIPLE_CHALLENGE_PATTERN.matcher(header); + if (m.matches()) + { + headerInfos.add(newHeaderInfo(m.group(1))); + header = m.group(2); + } + else + { + headerInfos.add(newHeaderInfo(header)); + break; + } + } + + return headerInfos; + } + + protected HeaderInfo newHeaderInfo(String value) throws IllegalArgumentException + { + String type; + Map params = new HashMap<>(); + + Matcher m = TYPE_PATTERN.matcher(value); + if (m.matches()) + { + type = m.group(1); + if (m.group(2) != null) + params = parseParameters(m.group(3)); + } + else + { + throw new IllegalArgumentException("Invalid Authentication Format"); + } + + return new HeaderInfo(getAuthorizationHeader(), type, params); + } + + protected Map parseParameters(String wwwAuthenticate) throws IllegalArgumentException + { + Map result = new HashMap<>(); + + Matcher b64 = BASE64_PATTERN.matcher(wwwAuthenticate); + if (b64.matches()) + { + result.put("base64", wwwAuthenticate); + return result; + } + + QuotedCSV parts = new QuotedCSV(false, wwwAuthenticate); + for (String part : parts) + { + Matcher params = PARAM_PATTERN.matcher(part); + if (params.matches()) + { + String name = StringUtil.asciiToLowerCase(params.group(1)); + String value = (params.group(2)==null) ? "" : params.group(2); + result.put(name, value); + } + else + { + throw new IllegalArgumentException("Invalid Authentication Format"); + } + } + return result; + } private class AuthenticationListener extends BufferingResponseListener { @@ -234,25 +316,17 @@ public abstract class AuthenticationProtocolHandler implements ProtocolHandler { // TODO: these should be ordered by strength List result = new ArrayList<>(); - List values = Collections.list(response.getHeaders().getValues(header.asString())); + List values = response.getHeaders().getValuesList(header); for (String value : values) { - Matcher matcher = AUTHENTICATE_PATTERN.matcher(value); - if (matcher.matches()) + try { - String type = matcher.group(1); - String realm = matcher.group(3); - String beforeRealm = matcher.group(2); - String afterRealm = matcher.group(4); - - String params; - if (beforeRealm != null) - params = beforeRealm + afterRealm; - else - params = afterRealm; - - Authentication.HeaderInfo headerInfo = new Authentication.HeaderInfo(type, realm, params, getAuthorizationHeader()); - result.add(headerInfo); + result.addAll(getHeaderInfo(value)); + } + catch(IllegalArgumentException e) + { + if (LOG.isDebugEnabled()) + LOG.debug("Failed to parse authentication header", e); } } return result; diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Authentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Authentication.java index 34bed19f306..6333a377f35 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/api/Authentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/api/Authentication.java @@ -19,9 +19,11 @@ package org.eclipse.jetty.client.api; import java.net.URI; +import java.util.Map; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.Attributes; +import org.eclipse.jetty.util.StringUtil; /** * {@link Authentication} represents a mechanism to authenticate requests for protected resources. @@ -76,19 +78,18 @@ public interface Authentication */ public static class HeaderInfo { - private final String type; - private final String realm; - private final String params; private final HttpHeader header; + private final String type; + private final Map params; - public HeaderInfo(String type, String realm, String params, HttpHeader header) + + public HeaderInfo(HttpHeader header, String type, Map params) throws IllegalArgumentException { - this.type = type; - this.realm = realm; - this.params = params; this.header = header; + this.type = type; + this.params = params; } - + /** * @return the authentication type (for example "Basic" or "Digest") */ @@ -98,20 +99,36 @@ public interface Authentication } /** - * @return the realm name + * @return the realm name or null if there is no realm parameter */ public String getRealm() { - return realm; + return params.get("realm"); } + /** + * @return the base64 content as a string if it exists otherwise null + */ + public String getBase64() + { + return params.get("base64"); + } + /** * @return additional authentication parameters */ - public String getParameters() + public Map getParameters() { return params; } + + /** + * @return specified authentication parameter or null if does not exist + */ + public String getParameter(String paramName) + { + return params.get(StringUtil.asciiToLowerCase(paramName)); + } /** * @return the {@code Authorization} (or {@code Proxy-Authorization}) header diff --git a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java index 7a5e16bedcf..fc2fb8363d6 100644 --- a/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java +++ b/jetty-client/src/main/java/org/eclipse/jetty/client/util/DigestAuthentication.java @@ -22,15 +22,11 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.AuthenticationStore; @@ -40,6 +36,7 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.Attributes; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.log.Log; /** * Implementation of the HTTP "Digest" authentication defined in RFC 2617. @@ -50,8 +47,6 @@ import org.eclipse.jetty.util.TypeUtil; */ public class DigestAuthentication extends AbstractAuthentication { - private static final Pattern PARAM_PATTERN = Pattern.compile("([^=]+)=(.*)"); - private final String user; private final String password; @@ -74,10 +69,21 @@ public class DigestAuthentication extends AbstractAuthentication return "Digest"; } + + @Override + public boolean matches(String type, URI uri, String realm) + { + // digest authenication requires a realm + if (realm == null) + return false; + + return super.matches(type,uri,realm); + } + @Override public Result authenticate(Request request, ContentResponse response, HeaderInfo headerInfo, Attributes context) { - Map params = parseParameters(headerInfo.getParameters()); + Map params = headerInfo.getParameters(); String nonce = params.get("nonce"); if (nonce == null || nonce.length() == 0) return null; @@ -105,58 +111,6 @@ public class DigestAuthentication extends AbstractAuthentication return new DigestResult(headerInfo.getHeader(), response.getContent(), realm, user, password, algorithm, nonce, clientQOP, opaque); } - private Map parseParameters(String wwwAuthenticate) - { - Map result = new HashMap<>(); - List parts = splitParams(wwwAuthenticate); - for (String part : parts) - { - Matcher matcher = PARAM_PATTERN.matcher(part); - if (matcher.matches()) - { - String name = matcher.group(1).trim().toLowerCase(Locale.ENGLISH); - String value = matcher.group(2).trim(); - if (value.startsWith("\"") && value.endsWith("\"")) - value = value.substring(1, value.length() - 1); - result.put(name, value); - } - } - return result; - } - - private List splitParams(String paramString) - { - List result = new ArrayList<>(); - int start = 0; - for (int i = 0; i < paramString.length(); ++i) - { - int quotes = 0; - char ch = paramString.charAt(i); - switch (ch) - { - case '\\': - ++i; - break; - case '"': - ++quotes; - break; - case ',': - if (quotes % 2 == 0) - { - String element = paramString.substring(start, i).trim(); - if (element.length() > 0) - result.add(element); - start = i + 1; - } - break; - default: - break; - } - } - result.add(paramString.substring(start, paramString.length()).trim()); - return result; - } - private MessageDigest getMessageDigest(String algorithm) { try diff --git a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java index 3dd0d4a1c99..ccaacd51dbd 100644 --- a/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java +++ b/jetty-client/src/test/java/org/eclipse/jetty/client/HttpClientAuthenticationTest.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.net.URI; import java.nio.ByteBuffer; import java.util.Iterator; +import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -43,6 +44,7 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response.Listener; import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.api.Authentication.HeaderInfo; import org.eclipse.jetty.client.util.BasicAuthentication; import org.eclipse.jetty.client.util.DeferredContentProvider; import org.eclipse.jetty.client.util.DigestAuthentication; @@ -63,6 +65,7 @@ import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.security.Constraint; import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Test; @@ -613,4 +616,120 @@ public class HttpClientAuthenticationTest extends AbstractHttpClientServerTest }; } } + + @Test + public void testTestHeaderInfoParsing() { + AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client); + + HeaderInfo headerInfo = aph.getHeaderInfo("Digest realm=\"thermostat\", qop=\"auth\", nonce=\"1523430383\"").get(0); + Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest")); + Assert.assertTrue(headerInfo.getParameter("qop").equals("auth")); + Assert.assertTrue(headerInfo.getParameter("realm").equals("thermostat")); + Assert.assertTrue(headerInfo.getParameter("nonce").equals("1523430383")); + + headerInfo = aph.getHeaderInfo("Digest qop=\"auth\", realm=\"thermostat\", nonce=\"1523430383\"").get(0); + Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest")); + Assert.assertTrue(headerInfo.getParameter("qop").equals("auth")); + Assert.assertTrue(headerInfo.getParameter("realm").equals("thermostat")); + Assert.assertTrue(headerInfo.getParameter("nonce").equals("1523430383")); + + headerInfo = aph.getHeaderInfo("Digest qop=\"auth\", nonce=\"1523430383\", realm=\"thermostat\"").get(0); + Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest")); + Assert.assertTrue(headerInfo.getParameter("qop").equals("auth")); + Assert.assertTrue(headerInfo.getParameter("realm").equals("thermostat")); + Assert.assertTrue(headerInfo.getParameter("nonce").equals("1523430383")); + + headerInfo = aph.getHeaderInfo("Digest qop=\"auth\", nonce=\"1523430383\"").get(0); + Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Digest")); + Assert.assertTrue(headerInfo.getParameter("qop").equals("auth")); + Assert.assertTrue(headerInfo.getParameter("realm") == null); + Assert.assertTrue(headerInfo.getParameter("nonce").equals("1523430383")); + + + // test multiple authentications + List headerInfoList = aph.getHeaderInfo("Digest qop=\"auth\", realm=\"thermostat\", nonce=\"1523430383\", " + + "Digest realm=\"thermostat2\", qop=\"auth2\", nonce=\"4522530354\", " + + "Digest qop=\"auth3\", nonce=\"9523570528\", realm=\"thermostat3\", " + + "Digest qop=\"auth4\", nonce=\"3526435321\""); + + Assert.assertTrue(headerInfoList.get(0).getType().equalsIgnoreCase("Digest")); + Assert.assertTrue(headerInfoList.get(0).getParameter("qop").equals("auth")); + Assert.assertTrue(headerInfoList.get(0).getParameter("realm").equals("thermostat")); + Assert.assertTrue(headerInfoList.get(0).getParameter("nonce").equals("1523430383")); + + Assert.assertTrue(headerInfoList.get(1).getType().equalsIgnoreCase("Digest")); + Assert.assertTrue(headerInfoList.get(1).getParameter("qop").equals("auth2")); + Assert.assertTrue(headerInfoList.get(1).getParameter("realm").equals("thermostat2")); + Assert.assertTrue(headerInfoList.get(1).getParameter("nonce").equals("4522530354")); + + Assert.assertTrue(headerInfoList.get(2).getType().equalsIgnoreCase("Digest")); + Assert.assertTrue(headerInfoList.get(2).getParameter("qop").equals("auth3")); + Assert.assertTrue(headerInfoList.get(2).getParameter("realm").equals("thermostat3")); + Assert.assertTrue(headerInfoList.get(2).getParameter("nonce").equals("9523570528")); + + Assert.assertTrue(headerInfoList.get(3).getType().equalsIgnoreCase("Digest")); + Assert.assertTrue(headerInfoList.get(3).getParameter("qop").equals("auth4")); + Assert.assertTrue(headerInfoList.get(3).getParameter("realm") == null); + Assert.assertTrue(headerInfoList.get(3).getParameter("nonce").equals("3526435321")); + + List headerInfos = aph.getHeaderInfo("Newauth realm=\"apps\", type=1, title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\""); + Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Newauth")); + Assert.assertTrue(headerInfos.get(0).getParameter("realm").equals("apps")); + Assert.assertTrue(headerInfos.get(0).getParameter("type").equals("1")); + Assert.assertThat(headerInfos.get(0).getParameter("title"), Matchers.equalTo("Login to \"apps\"")); + Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Basic")); + Assert.assertTrue(headerInfos.get(1).getParameter("realm").equals("simple")); + } + + @Test + public void testTestHeaderInfoParsingUnusualCases() { + AuthenticationProtocolHandler aph = new WWWAuthenticationProtocolHandler(client); + + HeaderInfo headerInfo = aph.getHeaderInfo("Scheme").get(0); + Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Scheme")); + Assert.assertTrue(headerInfo.getParameter("realm") == null); + + List headerInfos = aph.getHeaderInfo("Scheme1 , Scheme2 , Scheme3"); + Assert.assertEquals(3, headerInfos.size()); + Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Scheme1")); + Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Scheme2")); + Assert.assertTrue(headerInfos.get(2).getType().equalsIgnoreCase("Scheme3")); + + headerInfo = aph.getHeaderInfo("Scheme name=\"value\", other=\"value2\"").get(0); + Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Scheme")); + Assert.assertTrue(headerInfo.getParameter("name").equals("value")); + Assert.assertTrue(headerInfo.getParameter("other").equals("value2")); + + headerInfo = aph.getHeaderInfo("Scheme name = value , other = \"value2\" ").get(0); + Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Scheme")); + Assert.assertTrue(headerInfo.getParameter("name").equals("value")); + Assert.assertTrue(headerInfo.getParameter("other").equals("value2")); + + headerInfos = aph.getHeaderInfo("Scheme name=value, Scheme2 name=value2"); + Assert.assertEquals(headerInfos.size(), 2); + Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Scheme")); + Assert.assertTrue(headerInfos.get(0).getParameter("nAmE").equals("value")); + Assert.assertThat(headerInfos.get(1).getType(), Matchers.equalToIgnoringCase("Scheme2")); + Assert.assertTrue(headerInfos.get(1).getParameter("nAmE").equals("value2")); + + headerInfos = aph.getHeaderInfo("Scheme , ,, ,, name=value, Scheme2 name=value2"); + Assert.assertEquals(headerInfos.size(), 2); + Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Scheme")); + Assert.assertTrue(headerInfos.get(0).getParameter("name").equals("value")); + Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Scheme2")); + Assert.assertTrue(headerInfos.get(1).getParameter("name").equals("value2")); + + //Negotiate with base64 Content + headerInfo = aph.getHeaderInfo("Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAFAs4OAAAADw==").get(0); + Assert.assertTrue(headerInfo.getType().equalsIgnoreCase("Negotiate")); + Assert.assertTrue(headerInfo.getBase64().equals("TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAFAs4OAAAADw==")); + + headerInfos = aph.getHeaderInfo("Negotiate TlRMTVNTUAABAAAAAAAAAFAs4OAAAADw==, " + + "Negotiate YIIJvwYGKwYBBQUCoIIJszCCCa+gJDAi="); + Assert.assertTrue(headerInfos.get(0).getType().equalsIgnoreCase("Negotiate")); + Assert.assertTrue(headerInfos.get(0).getBase64().equals("TlRMTVNTUAABAAAAAAAAAFAs4OAAAADw==")); + + Assert.assertTrue(headerInfos.get(1).getType().equalsIgnoreCase("Negotiate")); + Assert.assertTrue(headerInfos.get(1).getBase64().equals("YIIJvwYGKwYBBQUCoIIJszCCCa+gJDAi=")); + } }