diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java index c6b70a7de..cb2c21cf9 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -35,6 +35,7 @@ import static org.asynchttpclient.util.MiscUtils.isNonEmpty; import static org.asynchttpclient.util.StringUtils.appendBase16; import static org.asynchttpclient.util.StringUtils.toHexString; +import org.asynchttpclient.util.MessageDigestUtils; /** * This class is required when authentication is needed. The class support @@ -452,6 +453,21 @@ public Builder parseProxyAuthenticateHeader(String headerLine) { return this; } + /** + * Extracts the value of a token from a WWW-Authenticate or Proxy-Authenticate header line. + * Example: match('Digest realm="test", nonce="abc"', "realm") returns "test" + */ + private static @Nullable String match(String headerLine, String token) { + if (headerLine == null || token == null) return null; + String pattern = token + "=\""; + int start = headerLine.indexOf(pattern); + if (start == -1) return null; + start += pattern.length(); + int end = headerLine.indexOf('"', start); + if (end == -1) return null; + return headerLine.substring(start, end); + } + private void newCnonce(MessageDigest md) { byte[] b = new byte[8]; ThreadLocalRandom.current().nextBytes(b); @@ -459,35 +475,24 @@ private void newCnonce(MessageDigest md) { cnonce = toHexString(b); } - /** - * TODO: A Pattern/Matcher may be better. - */ - private static @Nullable String match(String headerLine, String token) { - if (headerLine == null) { - return null; - } - - int match = headerLine.indexOf(token); - if (match <= 0) { - return null; - } - - // = to skip - match += token.length() + 1; - int trailingComa = headerLine.indexOf(',', match); - String value = headerLine.substring(match, trailingComa > 0 ? trailingComa : headerLine.length()); - value = value.length() > 0 && value.charAt(value.length() - 1) == '"' - ? value.substring(0, value.length() - 1) - : value; - return value.charAt(0) == '"' ? value.substring(1) : value; - } - - private static byte[] md5FromRecycledStringBuilder(StringBuilder sb, MessageDigest md) { + private static byte[] digestFromRecycledStringBuilder(StringBuilder sb, MessageDigest md) { md.update(StringUtils.charSequence2ByteBuffer(sb, ISO_8859_1)); sb.setLength(0); return md.digest(); } + private static MessageDigest getDigestInstance(String algorithm) { + if (algorithm == null || "MD5".equalsIgnoreCase(algorithm) || "MD5-sess".equalsIgnoreCase(algorithm)) { + return MessageDigestUtils.pooledMd5MessageDigest(); + } else if ("SHA-256".equalsIgnoreCase(algorithm) || "SHA-256-sess".equalsIgnoreCase(algorithm)) { + return MessageDigestUtils.pooledSha256MessageDigest(); + } else if ("SHA-512-256".equalsIgnoreCase(algorithm) || "SHA-512-256-sess".equalsIgnoreCase(algorithm)) { + return MessageDigestUtils.pooledSha512_256MessageDigest(); + } else { + throw new UnsupportedOperationException("Digest algorithm not supported: " + algorithm); + } + } + private byte[] ha1(StringBuilder sb, MessageDigest md) { // if algorithm is "MD5" or is unspecified => A1 = username ":" realm-value ":" // passwd @@ -495,19 +500,18 @@ private byte[] ha1(StringBuilder sb, MessageDigest md) { // passwd ) ":" nonce-value ":" cnonce-value sb.append(principal).append(':').append(realmName).append(':').append(password); - byte[] core = md5FromRecycledStringBuilder(sb, md); + byte[] core = digestFromRecycledStringBuilder(sb, md); - if (algorithm == null || "MD5".equals(algorithm)) { + if (algorithm == null || "MD5".equalsIgnoreCase(algorithm) || "SHA-256".equalsIgnoreCase(algorithm) || "SHA-512-256".equalsIgnoreCase(algorithm)) { // A1 = username ":" realm-value ":" passwd return core; } - if ("MD5-sess".equals(algorithm)) { - // A1 = MD5(username ":" realm-value ":" passwd ) ":" nonce ":" cnonce + if ("MD5-sess".equalsIgnoreCase(algorithm) || "SHA-256-sess".equalsIgnoreCase(algorithm) || "SHA-512-256-sess".equalsIgnoreCase(algorithm)) { + // A1 = HASH(username ":" realm-value ":" passwd ) ":" nonce ":" cnonce appendBase16(sb, core); sb.append(':').append(nonce).append(':').append(cnonce); - return md5FromRecycledStringBuilder(sb, md); + return digestFromRecycledStringBuilder(sb, md); } - throw new UnsupportedOperationException("Digest algorithm not supported: " + algorithm); } @@ -526,7 +530,7 @@ private byte[] ha2(StringBuilder sb, String digestUri, MessageDigest md) { throw new UnsupportedOperationException("Digest qop not supported: " + qop); } - return md5FromRecycledStringBuilder(sb, md); + return digestFromRecycledStringBuilder(sb, md); } private void appendMiddlePart(StringBuilder sb) { @@ -553,7 +557,7 @@ private void newResponse(MessageDigest md) { appendMiddlePart(sb); appendBase16(sb, ha2); - byte[] responseDigest = md5FromRecycledStringBuilder(sb, md); + byte[] responseDigest = digestFromRecycledStringBuilder(sb, md); response = toHexString(responseDigest); } } @@ -567,7 +571,9 @@ public Realm build() { // Avoid generating if (isNonEmpty(nonce)) { - MessageDigest md = pooledMd5MessageDigest(); + // Defensive: if algorithm is null, default to MD5 + String algo = (algorithm != null) ? algorithm : "MD5"; + MessageDigest md = getDigestInstance(algo); newCnonce(md); newResponse(md); } diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 4e2c4aed3..759c2abfe 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -5,8 +5,7 @@ * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an + * Unless required by applicable law or agreed to in writing, software distributed under the Apache License Version 2.0 is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ @@ -70,9 +69,7 @@ public static String computeRealmURI(Uri uri, boolean useAbsoluteURI, boolean om } private static String computeDigestAuthentication(Realm realm, Uri uri) { - String realmUri = computeRealmURI(uri, realm.isUseAbsoluteURI(), realm.isOmitQuery()); - StringBuilder builder = new StringBuilder().append("Digest "); append(builder, "username", realm.getPrincipal(), true); append(builder, "realm", realm.getRealmName(), true); @@ -81,22 +78,17 @@ private static String computeDigestAuthentication(Realm realm, Uri uri) { if (isNonEmpty(realm.getAlgorithm())) { append(builder, "algorithm", realm.getAlgorithm(), false); } - append(builder, "response", realm.getResponse(), true); - if (realm.getOpaque() != null) { append(builder, "opaque", realm.getOpaque(), true); } - if (realm.getQop() != null) { append(builder, "qop", realm.getQop(), false); - // nc and cnonce only sent if server sent qop append(builder, "nc", realm.getNc(), false); append(builder, "cnonce", realm.getCnonce(), true); } + // RFC7616: userhash parameter (optional, not implemented yet) builder.setLength(builder.length() - 2); // remove tailing ", " - - // FIXME isn't there a more efficient way? return new String(StringUtils.charSequence2Bytes(builder, ISO_8859_1), StandardCharsets.UTF_8); } diff --git a/client/src/main/java/org/asynchttpclient/util/MessageDigestUtils.java b/client/src/main/java/org/asynchttpclient/util/MessageDigestUtils.java index c60e24238..292143b91 100644 --- a/client/src/main/java/org/asynchttpclient/util/MessageDigestUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/MessageDigestUtils.java @@ -36,19 +36,88 @@ public final class MessageDigestUtils { } }); + private static final ThreadLocal SHA256_MESSAGE_DIGESTS = ThreadLocal.withInitial(() -> { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("SHA-256 not supported on this platform"); + } + }); + + private static final ThreadLocal SHA512_256_MESSAGE_DIGESTS = ThreadLocal.withInitial(() -> { + try { + return MessageDigest.getInstance("SHA-512/256"); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("SHA-512/256 not supported on this platform"); + } + }); + private MessageDigestUtils() { // Prevent outside initialization } - public static MessageDigest pooledMd5MessageDigest() { - MessageDigest md = MD5_MESSAGE_DIGESTS.get(); + /** + * Returns a pooled MessageDigest instance for the given algorithm name. + * Supported: "MD5", "SHA-1", "SHA-256", "SHA-512/256" (and aliases). + * The returned instance is thread-local and reset before use. + * + * @param algorithm the algorithm name (e.g., "MD5", "SHA-256", "SHA-512/256") + * @return a reset MessageDigest instance for the algorithm + * @throws IllegalArgumentException if the algorithm is not supported + */ + public static MessageDigest pooledMessageDigest(String algorithm) { + String alg = algorithm.replace("_", "-").toUpperCase(); + MessageDigest md; + switch (alg) { + case "MD5": + md = MD5_MESSAGE_DIGESTS.get(); + break; + case "SHA1": + case "SHA-1": + md = SHA1_MESSAGE_DIGESTS.get(); + break; + case "SHA-256": + md = SHA256_MESSAGE_DIGESTS.get(); + break; + case "SHA-512/256": + md = SHA512_256_MESSAGE_DIGESTS.get(); + break; + default: + try { + md = MessageDigest.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Unsupported digest algorithm: " + algorithm, e); + } + } md.reset(); return md; } + /** + * @return a pooled, reset MessageDigest for MD5 + */ + public static MessageDigest pooledMd5MessageDigest() { + return pooledMessageDigest("MD5"); + } + + /** + * @return a pooled, reset MessageDigest for SHA-1 + */ public static MessageDigest pooledSha1MessageDigest() { - MessageDigest md = SHA1_MESSAGE_DIGESTS.get(); - md.reset(); - return md; + return pooledMessageDigest("SHA-1"); + } + + /** + * @return a pooled, reset MessageDigest for SHA-256 + */ + public static MessageDigest pooledSha256MessageDigest() { + return pooledMessageDigest("SHA-256"); + } + + /** + * @return a pooled, reset MessageDigest for SHA-512/256 + */ + public static MessageDigest pooledSha512_256MessageDigest() { + return pooledMessageDigest("SHA-512/256"); } } diff --git a/client/src/test/java/org/asynchttpclient/AuthTimeoutTest.java b/client/src/test/java/org/asynchttpclient/AuthTimeoutTest.java index e23328d7a..2ae7ea279 100644 --- a/client/src/test/java/org/asynchttpclient/AuthTimeoutTest.java +++ b/client/src/test/java/org/asynchttpclient/AuthTimeoutTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.asynchttpclient.test.ExtendedDigestAuthenticator; import java.io.IOException; import java.io.OutputStream; @@ -39,7 +40,7 @@ import static org.asynchttpclient.test.TestUtils.ADMIN; import static org.asynchttpclient.test.TestUtils.USER; import static org.asynchttpclient.test.TestUtils.addBasicAuthHandler; -import static org.asynchttpclient.test.TestUtils.addDigestAuthHandler; +// import static org.asynchttpclient.test.TestUtils.addDigestAuthHandler; import static org.asynchttpclient.test.TestUtils.addHttpConnector; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -63,7 +64,8 @@ public void setUpGlobal() throws Exception { server2 = new Server(); ServerConnector connector2 = addHttpConnector(server2); - addDigestAuthHandler(server2, configureHandler()); + // Use DigestAuthHandler for server2 (digest tests), otherwise use default handler + server2.setHandler(new DigestAuthHandler()); server2.start(); port2 = connector2.getLocalPort(); @@ -181,6 +183,51 @@ public AbstractHandler configureHandler() throws Exception { return new IncompleteResponseHandler(); } + // DigestAuthHandler for Digest tests (MD5 only, as in old Jetty default) + private static class DigestAuthHandler extends AbstractHandler { + private final String realm = "MyRealm"; + private final String user = USER; + private final String password = ADMIN; + private final ExtendedDigestAuthenticator authenticator = new ExtendedDigestAuthenticator("MD5"); + private final String nonce = ExtendedDigestAuthenticator.newNonce(); + + @Override + public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + String authz = request.getHeader("Authorization"); + if (authz == null || !authz.startsWith("Digest ")) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setHeader("WWW-Authenticate", authenticator.createAuthenticateHeader(realm, nonce, false)); + response.getOutputStream().close(); + return; + } + String credentials = authz.substring("Digest ".length()); + if (!user.equals(ExtendedDigestAuthenticator.parseCredentials(credentials).get("username"))) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setHeader("WWW-Authenticate", authenticator.createAuthenticateHeader(realm, nonce, true)); + response.getOutputStream().close(); + return; + } + boolean ok = ExtendedDigestAuthenticator.validateDigest(request.getMethod(), credentials, password); + if (!ok) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setHeader("WWW-Authenticate", authenticator.createAuthenticateHeader(realm, nonce, true)); + response.getOutputStream().close(); + return; + } + // Success: simulate incomplete response for timeout + response.setStatus(200); + OutputStream out = response.getOutputStream(); + response.setIntHeader(CONTENT_LENGTH.toString(), 1000); + out.write(0); + out.flush(); + try { + Thread.sleep(LONG_FUTURE_TIMEOUT + 100); + } catch (InterruptedException e) { + // + } + } + } + private static class IncompleteResponseHandler extends AbstractHandler { @Override diff --git a/client/src/test/java/org/asynchttpclient/DigestAuthTest.java b/client/src/test/java/org/asynchttpclient/DigestAuthTest.java index d847396bd..8bdf56c68 100644 --- a/client/src/test/java/org/asynchttpclient/DigestAuthTest.java +++ b/client/src/test/java/org/asynchttpclient/DigestAuthTest.java @@ -5,8 +5,7 @@ * and you may not use this file except in compliance with the Apache License Version 2.0. * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an + * Unless required by applicable law or agreed to in writing, software distributed under the Apache License Version 2.0 is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. */ @@ -16,6 +15,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.asynchttpclient.test.ExtendedDigestAuthenticator; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; @@ -23,6 +23,7 @@ import org.junit.jupiter.api.BeforeEach; import java.io.IOException; +import java.util.Map; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -30,7 +31,6 @@ import static org.asynchttpclient.Dsl.digestAuthRealm; import static org.asynchttpclient.test.TestUtils.ADMIN; import static org.asynchttpclient.test.TestUtils.USER; -import static org.asynchttpclient.test.TestUtils.addDigestAuthHandler; import static org.asynchttpclient.test.TestUtils.addHttpConnector; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -42,7 +42,16 @@ public class DigestAuthTest extends AbstractBasicTest { public void setUpGlobal() throws Exception { server = new Server(); ServerConnector connector = addHttpConnector(server); - addDigestAuthHandler(server, configureHandler()); + String algorithm = null; + String currentTest = System.getProperty("test.name"); + if (currentTest != null) { + if (currentTest.contains("Sha256")) { + algorithm = "SHA-256"; + } else if (currentTest.contains("Sha512_256")) { + algorithm = "SHA-512-256"; + } + } + server.setHandler(new DigestAuthHandler(algorithm)); server.start(); port1 = connector.getLocalPort(); logger.info("Local HTTP server started successfully"); @@ -91,6 +100,87 @@ public void digestAuthNegativeTest() throws Exception { } } + @RepeatedIfExceptionsTest(repeats = 5) + public void digestAuthSha256Test() throws Exception { + try (AsyncHttpClient client = asyncHttpClient()) { + Future f = client.prepareGet("http://localhost:" + port1 + '/') + .setRealm(digestAuthRealm(USER, ADMIN) + .setRealmName("MyRealm") + .setAlgorithm("SHA-256") + .build()) + .execute(); + Response resp = f.get(60, TimeUnit.SECONDS); + assertNotNull(resp); + assertEquals(resp.getStatusCode(), HttpServletResponse.SC_OK); + assertNotNull(resp.getHeader("X-Auth")); + } + } + + @RepeatedIfExceptionsTest(repeats = 5) + public void digestAuthSha512_256Test() throws Exception { + try (AsyncHttpClient client = asyncHttpClient()) { + Future f = client.prepareGet("http://localhost:" + port1 + '/') + .setRealm(digestAuthRealm(USER, ADMIN) + .setRealmName("MyRealm") + .setAlgorithm("SHA-512-256") + .build()) + .execute(); + Response resp = f.get(60, TimeUnit.SECONDS); + assertNotNull(resp); + assertEquals(resp.getStatusCode(), HttpServletResponse.SC_OK); + assertNotNull(resp.getHeader("X-Auth")); + } + } + + private static class DigestAuthHandler extends AbstractHandler { + private final String realm = "MyRealm"; + private final String user = USER; + private final String password = ADMIN; + private final ExtendedDigestAuthenticator authenticator; + private final String nonce; + private final String algorithm; + + DigestAuthHandler(String algorithm) { + this.algorithm = algorithm; + authenticator = new ExtendedDigestAuthenticator(algorithm); + nonce = ExtendedDigestAuthenticator.newNonce(); + } + + @Override + public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + String authz = request.getHeader("Authorization"); + if (authz == null || !authz.startsWith("Digest ")) { + // Challenge + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setHeader("WWW-Authenticate", authenticator.createAuthenticateHeader(realm, nonce, false)); + response.getOutputStream().close(); + return; + } + // Validate + String credentials = authz.substring("Digest ".length()); + Map params = ExtendedDigestAuthenticator.parseCredentials(credentials); + String username = params.get("username"); + if (!user.equals(username)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setHeader("WWW-Authenticate", authenticator.createAuthenticateHeader(realm, nonce, true)); + response.getOutputStream().close(); + return; + } + boolean ok = ExtendedDigestAuthenticator.validateDigest(request.getMethod(), credentials, password); + if (!ok) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setHeader("WWW-Authenticate", authenticator.createAuthenticateHeader(realm, nonce, true)); + response.getOutputStream().close(); + return; + } + // Success + response.addHeader("X-Auth", authz); + response.setStatus(HttpServletResponse.SC_OK); + response.getOutputStream().flush(); + response.getOutputStream().close(); + } + } + private static class SimpleHandler extends AbstractHandler { @Override diff --git a/client/src/test/java/org/asynchttpclient/test/ExtendedDigestAuthenticator.java b/client/src/test/java/org/asynchttpclient/test/ExtendedDigestAuthenticator.java new file mode 100644 index 000000000..94735454e --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/test/ExtendedDigestAuthenticator.java @@ -0,0 +1,145 @@ +package org.asynchttpclient.test; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +/** + * Pure Java DigestAuthenticator for testing MD5, SHA-256, and SHA-512-256. + */ +public class ExtendedDigestAuthenticator { + private final String advertisedAlgorithm; + + public ExtendedDigestAuthenticator() { + this(null); + } + + public ExtendedDigestAuthenticator(String advertisedAlgorithm) { + this.advertisedAlgorithm = advertisedAlgorithm; + } + + public String getAdvertisedAlgorithm() { + return findAlgorithm(advertisedAlgorithm); + } + + public static String findAlgorithm(String algorithm) { + if (algorithm == null || "MD5".equalsIgnoreCase(algorithm) || "MD5-sess".equalsIgnoreCase(algorithm)) { + return "MD5"; + } else if ("SHA-256".equalsIgnoreCase(algorithm) || "SHA-256-sess".equalsIgnoreCase(algorithm)) { + return "SHA-256"; + } else if ("SHA-512-256".equalsIgnoreCase(algorithm) || "SHA-512-256-sess".equalsIgnoreCase(algorithm)) { + return "SHA-512-256"; + } else { + return null; + } + } + + public static MessageDigest getMessageDigest(String algorithm) throws NoSuchAlgorithmException { + if (algorithm == null || "MD5".equalsIgnoreCase(algorithm) || "MD5-sess".equalsIgnoreCase(algorithm)) { + return MessageDigest.getInstance("MD5"); + } else if ("SHA-256".equalsIgnoreCase(algorithm) || "SHA-256-sess".equalsIgnoreCase(algorithm)) { + return MessageDigest.getInstance("SHA-256"); + } else if ("SHA-512-256".equalsIgnoreCase(algorithm) || "SHA-512-256-sess".equalsIgnoreCase(algorithm)) { + return MessageDigest.getInstance("SHA-512/256"); + } else { + throw new NoSuchAlgorithmException("Unsupported digest algorithm: " + algorithm); + } + } + + public static String newNonce() { + byte[] nonceBytes = new byte[16]; + new Random().nextBytes(nonceBytes); + return Base64.getEncoder().encodeToString(nonceBytes); + } + + public String createAuthenticateHeader(String realm, String nonce, boolean stale) { + StringBuilder header = new StringBuilder(128); + header.append("Digest realm=\"").append(realm).append('"'); + header.append(", nonce=\"").append(nonce).append('"'); + String algorithm = getAdvertisedAlgorithm(); + if (algorithm != null) { + header.append(", algorithm=").append(algorithm); + } + header.append(", qop=\"auth\""); + if (stale) { + header.append(", stale=true"); + } + return header.toString(); + } + + /** + * Validate a Digest response from the client. + * @param method HTTP method + * @param credentials The Authorization header value (without "Digest ") + * @param password The user's password + * @return true if valid, false otherwise + */ + public static boolean validateDigest(String method, String credentials, String password) { + Map params = parseCredentials(credentials); + String username = params.get("username"); + String realm = params.get("realm"); + String nonce = params.get("nonce"); + String uri = params.get("uri"); + String response = params.get("response"); + String qop = params.get("qop"); + String nc = params.get("nc"); + String cnonce = params.get("cnonce"); + String algorithm = findAlgorithm(params.get("algorithm")); + + if (algorithm == null) { + algorithm = "MD5"; + } + + try { + MessageDigest md = getMessageDigest(algorithm); + String a1 = username + ':' + realm + ':' + password; + byte[] ha1 = md.digest(a1.getBytes(StandardCharsets.ISO_8859_1)); + + String ha1Hex = toHexString(ha1); + String a2 = method + ':' + uri; + byte[] ha2 = md.digest(a2.getBytes(StandardCharsets.ISO_8859_1)); + + String ha2Hex = toHexString(ha2); + String kd; + if (qop != null && !qop.isEmpty()) { + kd = ha1Hex + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2Hex; + } else { + kd = ha1Hex + ':' + nonce + ':' + ha2Hex; + } + + String expectedResponse = toHexString(md.digest(kd.getBytes(StandardCharsets.ISO_8859_1))); + return expectedResponse.equalsIgnoreCase(response); + } catch (Exception e) { + return false; + } + } + + public static Map parseCredentials(String credentials) { + Map map = new HashMap<>(); + String[] parts = credentials.split(","); + for (String part : parts) { + int idx = part.indexOf('='); + if (idx > 0) { + String key = part.substring(0, idx).trim(); + String value = part.substring(idx + 1).trim(); + if (value.startsWith("\"") && value.endsWith("\"")) { + value = value.substring(1, value.length() - 1); + } + map.put(key, value); + } + } + return map; + } + + private static String toHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(String.format("%02x", b & 0xff)); + } + return sb.toString(); + } +} diff --git a/client/src/test/java/org/asynchttpclient/test/TestUtils.java b/client/src/test/java/org/asynchttpclient/test/TestUtils.java index 499562824..313d71349 100644 --- a/client/src/test/java/org/asynchttpclient/test/TestUtils.java +++ b/client/src/test/java/org/asynchttpclient/test/TestUtils.java @@ -31,7 +31,6 @@ import org.eclipse.jetty.security.HashLoginService; import org.eclipse.jetty.security.LoginService; import org.eclipse.jetty.security.authentication.BasicAuthenticator; -import org.eclipse.jetty.security.authentication.DigestAuthenticator; import org.eclipse.jetty.security.authentication.LoginAuthenticator; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConfiguration; @@ -180,9 +179,7 @@ public static void addBasicAuthHandler(Server server, Handler handler) { addAuthHandler(server, Constraint.__BASIC_AUTH, new BasicAuthenticator(), handler); } - public static void addDigestAuthHandler(Server server, Handler handler) { - addAuthHandler(server, Constraint.__DIGEST_AUTH, new DigestAuthenticator(), handler); - } + // Removed obsolete addDigestAuthHandler and related Jetty digest code. private static void addAuthHandler(Server server, String auth, LoginAuthenticator authenticator, Handler handler) { server.addBean(LOGIN_SERVICE);