8232853: AuthenticationFilter.Cache::remove may throw ConcurrentModificationException
Summary: Change implementation to use iterator instead of plain LinkedList
Reviewed-by: dfuchs, vtewari
/*
* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigInteger;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.net.ssl.SSLContext;
import jdk.test.lib.net.SimpleSSLContext;
import sun.net.NetProperties;
import sun.net.www.HeaderParser;
import static java.lang.System.out;
import static java.lang.String.format;
/**
* @test
* @summary this test verifies that a client may provides authorization
* headers directly when connecting with a server.
* @bug 8087112
* @library /test/lib http2/server
* @build jdk.test.lib.net.SimpleSSLContext HttpServerAdapters DigestEchoServer
* ReferenceTracker DigestEchoClient
* @modules java.net.http/jdk.internal.net.http.common
* java.net.http/jdk.internal.net.http.frame
* java.net.http/jdk.internal.net.http.hpack
* java.logging
* java.base/sun.net.www.http
* java.base/sun.net.www
* java.base/sun.net
* @run main/othervm DigestEchoClient
* @run main/othervm -Djdk.http.auth.proxying.disabledSchemes=
* -Djdk.http.auth.tunneling.disabledSchemes=
* DigestEchoClient
*/
public class DigestEchoClient {
static final String data[] = {
"Lorem ipsum",
"dolor sit amet",
"consectetur adipiscing elit, sed do eiusmod tempor",
"quis nostrud exercitation ullamco",
"laboris nisi",
"ut",
"aliquip ex ea commodo consequat." +
"Duis aute irure dolor in reprehenderit in voluptate velit esse" +
"cillum dolore eu fugiat nulla pariatur.",
"Excepteur sint occaecat cupidatat non proident."
};
static final AtomicLong serverCount = new AtomicLong();
static final class EchoServers {
final DigestEchoServer.HttpAuthType authType;
final DigestEchoServer.HttpAuthSchemeType authScheme;
final String protocolScheme;
final String key;
final DigestEchoServer server;
final Version serverVersion;
private EchoServers(DigestEchoServer server,
Version version,
String protocolScheme,
DigestEchoServer.HttpAuthType authType,
DigestEchoServer.HttpAuthSchemeType authScheme) {
this.authType = authType;
this.authScheme = authScheme;
this.protocolScheme = protocolScheme;
this.key = key(version, protocolScheme, authType, authScheme);
this.server = server;
this.serverVersion = version;
}
static String key(Version version,
String protocolScheme,
DigestEchoServer.HttpAuthType authType,
DigestEchoServer.HttpAuthSchemeType authScheme) {
return String.format("%s:%s:%s:%s", version, protocolScheme, authType, authScheme);
}
private static EchoServers create(Version version,
String protocolScheme,
DigestEchoServer.HttpAuthType authType,
DigestEchoServer.HttpAuthSchemeType authScheme) {
try {
serverCount.incrementAndGet();
DigestEchoServer server =
DigestEchoServer.create(version, protocolScheme, authType, authScheme);
return new EchoServers(server, version, protocolScheme, authType, authScheme);
} catch (IOException x) {
throw new UncheckedIOException(x);
}
}
public static DigestEchoServer of(Version version,
String protocolScheme,
DigestEchoServer.HttpAuthType authType,
DigestEchoServer.HttpAuthSchemeType authScheme) {
String key = key(version, protocolScheme, authType, authScheme);
return servers.computeIfAbsent(key, (k) ->
create(version, protocolScheme, authType, authScheme)).server;
}
public static void stop() {
for (EchoServers s : servers.values()) {
s.server.stop();
}
}
private static final ConcurrentMap<String, EchoServers> servers = new ConcurrentHashMap<>();
}
final static String PROXY_DISABLED = NetProperties.get("jdk.http.auth.proxying.disabledSchemes");
final static String TUNNEL_DISABLED = NetProperties.get("jdk.http.auth.tunneling.disabledSchemes");
static {
System.out.println("jdk.http.auth.proxying.disabledSchemes=" + PROXY_DISABLED);
System.out.println("jdk.http.auth.tunneling.disabledSchemes=" + TUNNEL_DISABLED);
}
static final AtomicInteger NC = new AtomicInteger();
static final Random random = new Random();
static final SSLContext context;
static {
try {
context = new SimpleSSLContext().get();
SSLContext.setDefault(context);
} catch (Exception x) {
throw new ExceptionInInitializerError(x);
}
}
static final List<Boolean> BOOLEANS = List.of(true, false);
final boolean useSSL;
final DigestEchoServer.HttpAuthSchemeType authScheme;
final DigestEchoServer.HttpAuthType authType;
DigestEchoClient(boolean useSSL,
DigestEchoServer.HttpAuthSchemeType authScheme,
DigestEchoServer.HttpAuthType authType)
throws IOException {
this.useSSL = useSSL;
this.authScheme = authScheme;
this.authType = authType;
}
static final AtomicLong clientCount = new AtomicLong();
static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;
public HttpClient newHttpClient(DigestEchoServer server) {
clientCount.incrementAndGet();
HttpClient.Builder builder = HttpClient.newBuilder();
builder = builder.proxy(ProxySelector.of(null));
if (useSSL) {
builder.sslContext(context);
}
switch (authScheme) {
case BASIC:
builder = builder.authenticator(DigestEchoServer.AUTHENTICATOR);
break;
case BASICSERVER:
// don't set the authenticator: we will handle the header ourselves.
// builder = builder.authenticator(DigestEchoServer.AUTHENTICATOR);
break;
default:
break;
}
switch (authType) {
case PROXY:
builder = builder.proxy(ProxySelector.of(server.getProxyAddress()));
break;
case PROXY305:
builder = builder.proxy(ProxySelector.of(server.getProxyAddress()));
builder = builder.followRedirects(HttpClient.Redirect.NORMAL);
break;
case SERVER307:
builder = builder.followRedirects(HttpClient.Redirect.NORMAL);
break;
default:
break;
}
return TRACKER.track(builder.build());
}
public static List<Version> serverVersions(Version clientVersion) {
if (clientVersion == Version.HTTP_1_1) {
return List.of(clientVersion);
} else {
return List.of(Version.values());
}
}
public static List<Version> clientVersions() {
return List.of(Version.values());
}
public static List<Boolean> expectContinue(Version serverVersion) {
if (serverVersion == Version.HTTP_1_1) {
return BOOLEANS;
} else {
// our test HTTP/2 server does not support Expect: 100-Continue
return List.of(Boolean.FALSE);
}
}
public static void main(String[] args) throws Exception {
HttpServerAdapters.enableServerLogging();
boolean useSSL = false;
EnumSet<DigestEchoServer.HttpAuthType> types =
EnumSet.complementOf(EnumSet.of(DigestEchoServer.HttpAuthType.PROXY305));
Throwable failed = null;
if (args != null && args.length >= 1) {
useSSL = "SSL".equals(args[0]);
if (args.length > 1) {
List<DigestEchoServer.HttpAuthType> httpAuthTypes =
Stream.of(Arrays.copyOfRange(args, 1, args.length))
.map(DigestEchoServer.HttpAuthType::valueOf)
.collect(Collectors.toList());
types = EnumSet.copyOf(httpAuthTypes);
}
}
try {
for (DigestEchoServer.HttpAuthType authType : types) {
// The test server does not support PROXY305 properly
if (authType == DigestEchoServer.HttpAuthType.PROXY305) continue;
EnumSet<DigestEchoServer.HttpAuthSchemeType> basics =
EnumSet.of(DigestEchoServer.HttpAuthSchemeType.BASICSERVER,
DigestEchoServer.HttpAuthSchemeType.BASIC);
for (DigestEchoServer.HttpAuthSchemeType authScheme : basics) {
DigestEchoClient dec = new DigestEchoClient(useSSL,
authScheme,
authType);
for (Version clientVersion : clientVersions()) {
for (Version serverVersion : serverVersions(clientVersion)) {
for (boolean expectContinue : expectContinue(serverVersion)) {
for (boolean async : BOOLEANS) {
for (boolean preemptive : BOOLEANS) {
dec.testBasic(clientVersion,
serverVersion, async,
expectContinue, preemptive);
}
}
}
}
}
}
EnumSet<DigestEchoServer.HttpAuthSchemeType> digests =
EnumSet.of(DigestEchoServer.HttpAuthSchemeType.DIGEST);
for (DigestEchoServer.HttpAuthSchemeType authScheme : digests) {
DigestEchoClient dec = new DigestEchoClient(useSSL,
authScheme,
authType);
for (Version clientVersion : clientVersions()) {
for (Version serverVersion : serverVersions(clientVersion)) {
for (boolean expectContinue : expectContinue(serverVersion)) {
for (boolean async : BOOLEANS) {
dec.testDigest(clientVersion, serverVersion,
async, expectContinue);
}
}
}
}
}
}
} catch(Throwable t) {
out.println(DigestEchoServer.now()
+ ": Unexpected exception: " + t);
t.printStackTrace();
failed = t;
throw t;
} finally {
Thread.sleep(100);
AssertionError trackFailed = TRACKER.check(500);
EchoServers.stop();
System.out.println(" ---------------------------------------------------------- ");
System.out.println(String.format("DigestEchoClient %s %s", useSSL ? "SSL" : "CLEAR", types));
System.out.println(String.format("Created %d clients and %d servers",
clientCount.get(), serverCount.get()));
System.out.println(String.format("basics: %d requests sent, %d ns / req",
basicCount.get(), basics.get()));
System.out.println(String.format("digests: %d requests sent, %d ns / req",
digestCount.get(), digests.get()));
System.out.println(" ---------------------------------------------------------- ");
if (trackFailed != null) {
if (failed != null) {
failed.addSuppressed(trackFailed);
if (failed instanceof Error) throw (Error) failed;
if (failed instanceof Exception) throw (Exception) failed;
}
throw trackFailed;
}
}
}
boolean isSchemeDisabled() {
String disabledSchemes;
if (isProxy(authType)) {
disabledSchemes = useSSL
? TUNNEL_DISABLED
: PROXY_DISABLED;
} else return false;
if (disabledSchemes == null
|| disabledSchemes.isEmpty()) {
return false;
}
String scheme;
switch (authScheme) {
case DIGEST:
scheme = "Digest";
break;
case BASIC:
scheme = "Basic";
break;
case BASICSERVER:
scheme = "Basic";
break;
case NONE:
return false;
default:
throw new InternalError("Unknown auth scheme: " + authScheme);
}
return Stream.of(disabledSchemes.split(","))
.map(String::trim)
.filter(scheme::equalsIgnoreCase)
.findAny()
.isPresent();
}
final static AtomicLong basics = new AtomicLong();
final static AtomicLong basicCount = new AtomicLong();
// @Test
void testBasic(Version clientVersion, Version serverVersion, boolean async,
boolean expectContinue, boolean preemptive)
throws Exception
{
final boolean addHeaders = authScheme == DigestEchoServer.HttpAuthSchemeType.BASICSERVER;
// !preemptive has no meaning if we don't handle the authorization
// headers ourselves
if (!preemptive && !addHeaders) return;
out.println(format("*** testBasic: client: %s, server: %s, async: %s, useSSL: %s, " +
"authScheme: %s, authType: %s, expectContinue: %s preemptive: %s***",
clientVersion, serverVersion, async, useSSL, authScheme, authType,
expectContinue, preemptive));
DigestEchoServer server = EchoServers.of(serverVersion,
useSSL ? "https" : "http", authType, authScheme);
URI uri = DigestEchoServer.uri(useSSL ? "https" : "http",
server.getServerAddress(), "/foo/");
HttpClient client = newHttpClient(server);
HttpResponse<String> r;
CompletableFuture<HttpResponse<String>> cf1;
String auth = null;
try {
for (int i=0; i<data.length; i++) {
out.println(DigestEchoServer.now() + " ----- iteration " + i + " -----");
List<String> lines = List.of(Arrays.copyOfRange(data, 0, i+1));
assert lines.size() == i + 1;
String body = lines.stream().collect(Collectors.joining("\r\n"));
BodyPublisher reqBody = BodyPublishers.ofString(body);
HttpRequest.Builder builder = HttpRequest.newBuilder(uri).version(clientVersion)
.POST(reqBody).expectContinue(expectContinue);
boolean isTunnel = isProxy(authType) && useSSL;
if (addHeaders) {
// handle authentication ourselves
assert !client.authenticator().isPresent();
if (auth == null) auth = "Basic " + getBasicAuth("arthur");
try {
if ((i > 0 || preemptive)
&& (!isTunnel || i == 0 || isSchemeDisabled())) {
// In case of a SSL tunnel through proxy then only the
// first request should require proxy authorization
// Though this might be invalidated if the server decides
// to close the connection...
out.println(String.format("%s adding %s: %s",
DigestEchoServer.now(),
authorizationKey(authType),
auth));
builder = builder.header(authorizationKey(authType), auth);
}
} catch (IllegalArgumentException x) {
throw x;
}
} else {
// let the stack do the authentication
assert client.authenticator().isPresent();
}
long start = System.nanoTime();
HttpRequest request = builder.build();
HttpResponse<Stream<String>> resp;
try {
if (async) {
resp = client.sendAsync(request, BodyHandlers.ofLines()).join();
} else {
resp = client.send(request, BodyHandlers.ofLines());
}
} catch (Throwable t) {
long stop = System.nanoTime();
synchronized (basicCount) {
long n = basicCount.getAndIncrement();
basics.set((basics.get() * n + (stop - start)) / (n + 1));
}
// unwrap CompletionException
if (t instanceof CompletionException) {
assert t.getCause() != null;
t = t.getCause();
}
out.println(DigestEchoServer.now()
+ ": Unexpected exception: " + t);
throw new RuntimeException("Unexpected exception: " + t, t);
}
if (addHeaders && !preemptive && (i==0 || isSchemeDisabled())) {
assert resp.statusCode() == 401 || resp.statusCode() == 407;
Stream<String> respBody = resp.body();
if (respBody != null) {
System.out.printf("Response body (%s):\n", resp.statusCode());
respBody.forEach(System.out::println);
}
System.out.println(String.format("%s received: adding header %s: %s",
resp.statusCode(), authorizationKey(authType), auth));
request = HttpRequest.newBuilder(uri).version(clientVersion)
.POST(reqBody).header(authorizationKey(authType), auth).build();
if (async) {
resp = client.sendAsync(request, BodyHandlers.ofLines()).join();
} else {
resp = client.send(request, BodyHandlers.ofLines());
}
}
final List<String> respLines;
try {
if (isSchemeDisabled()) {
if (resp.statusCode() != 407) {
throw new RuntimeException("expected 407 not received");
}
System.out.println("Scheme disabled for [" + authType
+ ", " + authScheme
+ ", " + (useSSL ? "HTTP" : "HTTPS")
+ "]: Received expected " + resp.statusCode());
continue;
} else {
System.out.println("Scheme enabled for [" + authType
+ ", " + authScheme
+ ", " + (useSSL ? "HTTPS" : "HTTP")
+ "]: Expecting 200, response is: " + resp);
assert resp.statusCode() == 200 : "200 expected, received " + resp;
respLines = resp.body().collect(Collectors.toList());
}
} finally {
long stop = System.nanoTime();
synchronized (basicCount) {
long n = basicCount.getAndIncrement();
basics.set((basics.get() * n + (stop - start)) / (n + 1));
}
}
if (!lines.equals(respLines)) {
throw new RuntimeException("Unexpected response: " + respLines);
}
}
} finally {
}
System.out.println("OK");
}
String getBasicAuth(String username) {
StringBuilder builder = new StringBuilder(username);
builder.append(':');
for (char c : DigestEchoServer.AUTHENTICATOR.getPassword(username)) {
builder.append(c);
}
return Base64.getEncoder().encodeToString(builder.toString().getBytes(StandardCharsets.UTF_8));
}
final static AtomicLong digests = new AtomicLong();
final static AtomicLong digestCount = new AtomicLong();
// @Test
void testDigest(Version clientVersion, Version serverVersion,
boolean async, boolean expectContinue)
throws Exception
{
String test = format("testDigest: client: %s, server: %s, async: %s, useSSL: %s, " +
"authScheme: %s, authType: %s, expectContinue: %s",
clientVersion, serverVersion, async, useSSL,
authScheme, authType, expectContinue);
out.println("*** " + test + " ***");
DigestEchoServer server = EchoServers.of(serverVersion,
useSSL ? "https" : "http", authType, authScheme);
URI uri = DigestEchoServer.uri(useSSL ? "https" : "http",
server.getServerAddress(), "/foo/");
HttpClient client = newHttpClient(server);
HttpResponse<String> r;
CompletableFuture<HttpResponse<String>> cf1;
byte[] cnonce = new byte[16];
String cnonceStr = null;
DigestEchoServer.DigestResponse challenge = null;
try {
for (int i=0; i<data.length; i++) {
out.println(DigestEchoServer.now() + "----- iteration " + i + " -----");
List<String> lines = List.of(Arrays.copyOfRange(data, 0, i+1));
assert lines.size() == i + 1;
String body = lines.stream().collect(Collectors.joining("\r\n"));
HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublishers.ofString(body);
HttpRequest.Builder reqBuilder = HttpRequest
.newBuilder(uri).version(clientVersion).POST(reqBody)
.expectContinue(expectContinue);
boolean isTunnel = isProxy(authType) && useSSL;
String digestMethod = isTunnel ? "CONNECT" : "POST";
// In case of a tunnel connection only the first request
// which establishes the tunnel needs to authenticate with
// the proxy.
if (challenge != null && (!isTunnel || isSchemeDisabled())) {
assert cnonceStr != null;
String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);
try {
reqBuilder = reqBuilder.header(authorizationKey(authType), auth);
} catch (IllegalArgumentException x) {
throw x;
}
}
long start = System.nanoTime();
HttpRequest request = reqBuilder.build();
HttpResponse<Stream<String>> resp;
if (async) {
resp = client.sendAsync(request, BodyHandlers.ofLines()).join();
} else {
resp = client.send(request, BodyHandlers.ofLines());
}
System.out.println(resp);
assert challenge != null || resp.statusCode() == 401 || resp.statusCode() == 407
: "challenge=" + challenge + ", resp=" + resp + ", test=[" + test + "]";
if (resp.statusCode() == 401 || resp.statusCode() == 407) {
// This assert may need to be relaxed if our server happened to
// decide to close the tunnel connection, in which case we would
// receive 407 again...
assert challenge == null || !isTunnel || isSchemeDisabled()
: "No proxy auth should be required after establishing an SSL tunnel";
System.out.println("Received " + resp.statusCode() + " answering challenge...");
random.nextBytes(cnonce);
cnonceStr = new BigInteger(1, cnonce).toString(16);
System.out.println("Response headers: " + resp.headers());
Optional<String> authenticateOpt = resp.headers().firstValue(authenticateKey(authType));
String authenticate = authenticateOpt.orElseThrow(
() -> new RuntimeException(authenticateKey(authType) + ": not found"));
assert authenticate.startsWith("Digest ");
HeaderParser hp = new HeaderParser(authenticate.substring("Digest ".length()));
String qop = hp.findValue("qop");
String nonce = hp.findValue("nonce");
if (qop == null && nonce == null) {
throw new RuntimeException("QOP and NONCE not found");
}
challenge = DigestEchoServer.DigestResponse
.create(authenticate.substring("Digest ".length()));
String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);
try {
request = HttpRequest.newBuilder(uri).version(clientVersion)
.POST(reqBody).header(authorizationKey(authType), auth).build();
} catch (IllegalArgumentException x) {
throw x;
}
if (async) {
resp = client.sendAsync(request, BodyHandlers.ofLines()).join();
} else {
resp = client.send(request, BodyHandlers.ofLines());
}
System.out.println(resp);
}
final List<String> respLines;
try {
if (isSchemeDisabled()) {
if (resp.statusCode() != 407) {
throw new RuntimeException("expected 407 not received");
}
System.out.println("Scheme disabled for [" + authType
+ ", " + authScheme +
", " + (useSSL ? "HTTP" : "HTTPS")
+ "]: Received expected " + resp.statusCode());
continue;
} else {
assert resp.statusCode() == 200;
respLines = resp.body().collect(Collectors.toList());
}
} finally {
long stop = System.nanoTime();
synchronized (digestCount) {
long n = digestCount.getAndIncrement();
digests.set((digests.get() * n + (stop - start)) / (n + 1));
}
}
if (!lines.equals(respLines)) {
throw new RuntimeException("Unexpected response: " + respLines);
}
}
} finally {
}
System.out.println("OK");
}
// WARNING: This is not a full fledged implementation of DIGEST.
// It does contain bugs and inaccuracy.
static String digestResponse(URI uri, String method, DigestEchoServer.DigestResponse challenge, String cnonce)
throws NoSuchAlgorithmException {
int nc = NC.incrementAndGet();
DigestEchoServer.DigestResponse response1 = new DigestEchoServer.DigestResponse("earth",
"arthur", challenge.nonce, cnonce, String.valueOf(nc), uri.toASCIIString(),
challenge.algorithm, challenge.qop, challenge.opaque, null);
String response = DigestEchoServer.DigestResponse.computeDigest(true, method,
DigestEchoServer.AUTHENTICATOR.getPassword("arthur"), response1);
String auth = "Digest username=\"arthur\", realm=\"earth\""
+ ", response=\"" + response + "\", uri=\""+uri.toASCIIString()+"\""
+ ", qop=\"" + response1.qop + "\", cnonce=\"" + response1.cnonce
+ "\", nc=\"" + nc + "\", nonce=\"" + response1.nonce + "\"";
if (response1.opaque != null) {
auth = auth + ", opaque=\"" + response1.opaque + "\"";
}
return auth;
}
static String authenticateKey(DigestEchoServer.HttpAuthType authType) {
switch (authType) {
case SERVER: return "www-authenticate";
case SERVER307: return "www-authenticate";
case PROXY: return "proxy-authenticate";
case PROXY305: return "proxy-authenticate";
default: throw new InternalError("authType: " + authType);
}
}
static String authorizationKey(DigestEchoServer.HttpAuthType authType) {
switch (authType) {
case SERVER: return "authorization";
case SERVER307: return "Authorization";
case PROXY: return "Proxy-Authorization";
case PROXY305: return "proxy-Authorization";
default: throw new InternalError("authType: " + authType);
}
}
static boolean isProxy(DigestEchoServer.HttpAuthType authType) {
switch (authType) {
case SERVER: return false;
case SERVER307: return false;
case PROXY: return true;
case PROXY305: return true;
default: throw new InternalError("authType: " + authType);
}
}
}