Path: blob/master/test/jdk/java/net/httpclient/DigestEchoClient.java
41152 views
/*1* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation.7*8* This code is distributed in the hope that it will be useful, but WITHOUT9* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or10* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License11* version 2 for more details (a copy is included in the LICENSE file that12* accompanied this code).13*14* You should have received a copy of the GNU General Public License version15* 2 along with this work; if not, write to the Free Software Foundation,16* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.17*18* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA19* or visit www.oracle.com if you need additional information or have any20* questions.21*/2223import java.io.IOException;24import java.io.UncheckedIOException;25import java.math.BigInteger;26import java.net.ProxySelector;27import java.net.URI;28import java.net.http.HttpClient;29import java.net.http.HttpClient.Version;30import java.net.http.HttpRequest;31import java.net.http.HttpRequest.BodyPublisher;32import java.net.http.HttpRequest.BodyPublishers;33import java.net.http.HttpResponse;34import java.net.http.HttpResponse.BodyHandler;35import java.net.http.HttpResponse.BodyHandlers;36import java.nio.charset.StandardCharsets;37import java.security.NoSuchAlgorithmException;38import java.util.Arrays;39import java.util.Base64;40import java.util.EnumSet;41import java.util.List;42import java.util.Optional;43import java.util.Random;44import java.util.concurrent.CompletableFuture;45import java.util.concurrent.CompletionException;46import java.util.concurrent.ConcurrentHashMap;47import java.util.concurrent.ConcurrentMap;48import java.util.concurrent.atomic.AtomicInteger;49import java.util.concurrent.atomic.AtomicLong;50import java.util.stream.Collectors;51import java.util.stream.Stream;52import javax.net.ssl.SSLContext;53import jdk.test.lib.net.SimpleSSLContext;54import sun.net.NetProperties;55import sun.net.www.HeaderParser;56import static java.lang.System.out;57import static java.lang.String.format;5859/**60* @test61* @summary this test verifies that a client may provides authorization62* headers directly when connecting with a server.63* @bug 808711264* @library /test/lib http2/server65* @build jdk.test.lib.net.SimpleSSLContext HttpServerAdapters DigestEchoServer66* ReferenceTracker DigestEchoClient67* @modules java.net.http/jdk.internal.net.http.common68* java.net.http/jdk.internal.net.http.frame69* java.net.http/jdk.internal.net.http.hpack70* java.logging71* java.base/sun.net.www.http72* java.base/sun.net.www73* java.base/sun.net74* @run main/othervm DigestEchoClient75* @run main/othervm -Djdk.http.auth.proxying.disabledSchemes=76* -Djdk.http.auth.tunneling.disabledSchemes=77* DigestEchoClient78*/7980public class DigestEchoClient {8182static final String data[] = {83"Lorem ipsum",84"dolor sit amet",85"consectetur adipiscing elit, sed do eiusmod tempor",86"quis nostrud exercitation ullamco",87"laboris nisi",88"ut",89"aliquip ex ea commodo consequat." +90"Duis aute irure dolor in reprehenderit in voluptate velit esse" +91"cillum dolore eu fugiat nulla pariatur.",92"Excepteur sint occaecat cupidatat non proident."93};9495static final AtomicLong serverCount = new AtomicLong();96static final class EchoServers {97final DigestEchoServer.HttpAuthType authType;98final DigestEchoServer.HttpAuthSchemeType authScheme;99final String protocolScheme;100final String key;101final DigestEchoServer server;102final Version serverVersion;103104private EchoServers(DigestEchoServer server,105Version version,106String protocolScheme,107DigestEchoServer.HttpAuthType authType,108DigestEchoServer.HttpAuthSchemeType authScheme) {109this.authType = authType;110this.authScheme = authScheme;111this.protocolScheme = protocolScheme;112this.key = key(version, protocolScheme, authType, authScheme);113this.server = server;114this.serverVersion = version;115}116117static String key(Version version,118String protocolScheme,119DigestEchoServer.HttpAuthType authType,120DigestEchoServer.HttpAuthSchemeType authScheme) {121return String.format("%s:%s:%s:%s", version, protocolScheme, authType, authScheme);122}123124private static EchoServers create(Version version,125String protocolScheme,126DigestEchoServer.HttpAuthType authType,127DigestEchoServer.HttpAuthSchemeType authScheme) {128try {129serverCount.incrementAndGet();130DigestEchoServer server =131DigestEchoServer.create(version, protocolScheme, authType, authScheme);132return new EchoServers(server, version, protocolScheme, authType, authScheme);133} catch (IOException x) {134throw new UncheckedIOException(x);135}136}137138public static DigestEchoServer of(Version version,139String protocolScheme,140DigestEchoServer.HttpAuthType authType,141DigestEchoServer.HttpAuthSchemeType authScheme) {142String key = key(version, protocolScheme, authType, authScheme);143return servers.computeIfAbsent(key, (k) ->144create(version, protocolScheme, authType, authScheme)).server;145}146147public static void stop() {148for (EchoServers s : servers.values()) {149s.server.stop();150}151}152153private static final ConcurrentMap<String, EchoServers> servers = new ConcurrentHashMap<>();154}155156final static String PROXY_DISABLED = NetProperties.get("jdk.http.auth.proxying.disabledSchemes");157final static String TUNNEL_DISABLED = NetProperties.get("jdk.http.auth.tunneling.disabledSchemes");158static {159System.out.println("jdk.http.auth.proxying.disabledSchemes=" + PROXY_DISABLED);160System.out.println("jdk.http.auth.tunneling.disabledSchemes=" + TUNNEL_DISABLED);161}162163164165static final AtomicInteger NC = new AtomicInteger();166static final Random random = new Random();167static final SSLContext context;168static {169try {170context = new SimpleSSLContext().get();171SSLContext.setDefault(context);172} catch (Exception x) {173throw new ExceptionInInitializerError(x);174}175}176static final List<Boolean> BOOLEANS = List.of(true, false);177178final boolean useSSL;179final DigestEchoServer.HttpAuthSchemeType authScheme;180final DigestEchoServer.HttpAuthType authType;181DigestEchoClient(boolean useSSL,182DigestEchoServer.HttpAuthSchemeType authScheme,183DigestEchoServer.HttpAuthType authType)184throws IOException {185this.useSSL = useSSL;186this.authScheme = authScheme;187this.authType = authType;188}189190static final AtomicLong clientCount = new AtomicLong();191static final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE;192public HttpClient newHttpClient(DigestEchoServer server) {193clientCount.incrementAndGet();194HttpClient.Builder builder = HttpClient.newBuilder();195builder = builder.proxy(ProxySelector.of(null));196if (useSSL) {197builder.sslContext(context);198}199switch (authScheme) {200case BASIC:201builder = builder.authenticator(DigestEchoServer.AUTHENTICATOR);202break;203case BASICSERVER:204// don't set the authenticator: we will handle the header ourselves.205// builder = builder.authenticator(DigestEchoServer.AUTHENTICATOR);206break;207default:208break;209}210switch (authType) {211case PROXY:212builder = builder.proxy(ProxySelector.of(server.getProxyAddress()));213break;214case PROXY305:215builder = builder.proxy(ProxySelector.of(server.getProxyAddress()));216builder = builder.followRedirects(HttpClient.Redirect.NORMAL);217break;218case SERVER307:219builder = builder.followRedirects(HttpClient.Redirect.NORMAL);220break;221default:222break;223}224return TRACKER.track(builder.build());225}226227public static List<Version> serverVersions(Version clientVersion) {228if (clientVersion == Version.HTTP_1_1) {229return List.of(clientVersion);230} else {231return List.of(Version.values());232}233}234235public static List<Version> clientVersions() {236return List.of(Version.values());237}238239public static List<Boolean> expectContinue(Version serverVersion) {240if (serverVersion == Version.HTTP_1_1) {241return BOOLEANS;242} else {243// our test HTTP/2 server does not support Expect: 100-Continue244return List.of(Boolean.FALSE);245}246}247248public static void main(String[] args) throws Exception {249HttpServerAdapters.enableServerLogging();250boolean useSSL = false;251EnumSet<DigestEchoServer.HttpAuthType> types =252EnumSet.complementOf(EnumSet.of(DigestEchoServer.HttpAuthType.PROXY305));253Throwable failed = null;254if (args != null && args.length >= 1) {255useSSL = "SSL".equals(args[0]);256if (args.length > 1) {257List<DigestEchoServer.HttpAuthType> httpAuthTypes =258Stream.of(Arrays.copyOfRange(args, 1, args.length))259.map(DigestEchoServer.HttpAuthType::valueOf)260.collect(Collectors.toList());261types = EnumSet.copyOf(httpAuthTypes);262}263}264try {265for (DigestEchoServer.HttpAuthType authType : types) {266// The test server does not support PROXY305 properly267if (authType == DigestEchoServer.HttpAuthType.PROXY305) continue;268EnumSet<DigestEchoServer.HttpAuthSchemeType> basics =269EnumSet.of(DigestEchoServer.HttpAuthSchemeType.BASICSERVER,270DigestEchoServer.HttpAuthSchemeType.BASIC);271for (DigestEchoServer.HttpAuthSchemeType authScheme : basics) {272DigestEchoClient dec = new DigestEchoClient(useSSL,273authScheme,274authType);275for (Version clientVersion : clientVersions()) {276for (Version serverVersion : serverVersions(clientVersion)) {277for (boolean expectContinue : expectContinue(serverVersion)) {278for (boolean async : BOOLEANS) {279for (boolean preemptive : BOOLEANS) {280dec.testBasic(clientVersion,281serverVersion, async,282expectContinue, preemptive);283}284}285}286}287}288}289EnumSet<DigestEchoServer.HttpAuthSchemeType> digests =290EnumSet.of(DigestEchoServer.HttpAuthSchemeType.DIGEST);291for (DigestEchoServer.HttpAuthSchemeType authScheme : digests) {292DigestEchoClient dec = new DigestEchoClient(useSSL,293authScheme,294authType);295for (Version clientVersion : clientVersions()) {296for (Version serverVersion : serverVersions(clientVersion)) {297for (boolean expectContinue : expectContinue(serverVersion)) {298for (boolean async : BOOLEANS) {299dec.testDigest(clientVersion, serverVersion,300async, expectContinue);301}302}303}304}305}306}307} catch(Throwable t) {308out.println(DigestEchoServer.now()309+ ": Unexpected exception: " + t);310t.printStackTrace();311failed = t;312throw t;313} finally {314Thread.sleep(100);315AssertionError trackFailed = TRACKER.check(500);316EchoServers.stop();317System.out.println(" ---------------------------------------------------------- ");318System.out.println(String.format("DigestEchoClient %s %s", useSSL ? "SSL" : "CLEAR", types));319System.out.println(String.format("Created %d clients and %d servers",320clientCount.get(), serverCount.get()));321System.out.println(String.format("basics: %d requests sent, %d ns / req",322basicCount.get(), basics.get()));323System.out.println(String.format("digests: %d requests sent, %d ns / req",324digestCount.get(), digests.get()));325System.out.println(" ---------------------------------------------------------- ");326if (trackFailed != null) {327if (failed != null) {328failed.addSuppressed(trackFailed);329if (failed instanceof Error) throw (Error) failed;330if (failed instanceof Exception) throw (Exception) failed;331}332throw trackFailed;333}334}335}336337boolean isSchemeDisabled() {338String disabledSchemes;339if (isProxy(authType)) {340disabledSchemes = useSSL341? TUNNEL_DISABLED342: PROXY_DISABLED;343} else return false;344if (disabledSchemes == null345|| disabledSchemes.isEmpty()) {346return false;347}348String scheme;349switch (authScheme) {350case DIGEST:351scheme = "Digest";352break;353case BASIC:354scheme = "Basic";355break;356case BASICSERVER:357scheme = "Basic";358break;359case NONE:360return false;361default:362throw new InternalError("Unknown auth scheme: " + authScheme);363}364return Stream.of(disabledSchemes.split(","))365.map(String::trim)366.filter(scheme::equalsIgnoreCase)367.findAny()368.isPresent();369}370371final static AtomicLong basics = new AtomicLong();372final static AtomicLong basicCount = new AtomicLong();373// @Test374void testBasic(Version clientVersion, Version serverVersion, boolean async,375boolean expectContinue, boolean preemptive)376throws Exception377{378final boolean addHeaders = authScheme == DigestEchoServer.HttpAuthSchemeType.BASICSERVER;379// !preemptive has no meaning if we don't handle the authorization380// headers ourselves381if (!preemptive && !addHeaders) return;382383out.println(format("*** testBasic: client: %s, server: %s, async: %s, useSSL: %s, " +384"authScheme: %s, authType: %s, expectContinue: %s preemptive: %s***",385clientVersion, serverVersion, async, useSSL, authScheme, authType,386expectContinue, preemptive));387388DigestEchoServer server = EchoServers.of(serverVersion,389useSSL ? "https" : "http", authType, authScheme);390URI uri = DigestEchoServer.uri(useSSL ? "https" : "http",391server.getServerAddress(), "/foo/");392393HttpClient client = newHttpClient(server);394HttpResponse<String> r;395CompletableFuture<HttpResponse<String>> cf1;396String auth = null;397398try {399for (int i=0; i<data.length; i++) {400out.println(DigestEchoServer.now() + " ----- iteration " + i + " -----");401List<String> lines = List.of(Arrays.copyOfRange(data, 0, i+1));402assert lines.size() == i + 1;403String body = lines.stream().collect(Collectors.joining("\r\n"));404BodyPublisher reqBody = BodyPublishers.ofString(body);405HttpRequest.Builder builder = HttpRequest.newBuilder(uri).version(clientVersion)406.POST(reqBody).expectContinue(expectContinue);407boolean isTunnel = isProxy(authType) && useSSL;408if (addHeaders) {409// handle authentication ourselves410assert !client.authenticator().isPresent();411if (auth == null) auth = "Basic " + getBasicAuth("arthur");412try {413if ((i > 0 || preemptive)414&& (!isTunnel || i == 0 || isSchemeDisabled())) {415// In case of a SSL tunnel through proxy then only the416// first request should require proxy authorization417// Though this might be invalidated if the server decides418// to close the connection...419out.println(String.format("%s adding %s: %s",420DigestEchoServer.now(),421authorizationKey(authType),422auth));423builder = builder.header(authorizationKey(authType), auth);424}425} catch (IllegalArgumentException x) {426throw x;427}428} else {429// let the stack do the authentication430assert client.authenticator().isPresent();431}432long start = System.nanoTime();433HttpRequest request = builder.build();434HttpResponse<Stream<String>> resp;435try {436if (async) {437resp = client.sendAsync(request, BodyHandlers.ofLines()).join();438} else {439resp = client.send(request, BodyHandlers.ofLines());440}441} catch (Throwable t) {442long stop = System.nanoTime();443synchronized (basicCount) {444long n = basicCount.getAndIncrement();445basics.set((basics.get() * n + (stop - start)) / (n + 1));446}447// unwrap CompletionException448if (t instanceof CompletionException) {449assert t.getCause() != null;450t = t.getCause();451}452out.println(DigestEchoServer.now()453+ ": Unexpected exception: " + t);454throw new RuntimeException("Unexpected exception: " + t, t);455}456457if (addHeaders && !preemptive && (i==0 || isSchemeDisabled())) {458assert resp.statusCode() == 401 || resp.statusCode() == 407;459Stream<String> respBody = resp.body();460if (respBody != null) {461System.out.printf("Response body (%s):\n", resp.statusCode());462respBody.forEach(System.out::println);463}464System.out.println(String.format("%s received: adding header %s: %s",465resp.statusCode(), authorizationKey(authType), auth));466request = HttpRequest.newBuilder(uri).version(clientVersion)467.POST(reqBody).header(authorizationKey(authType), auth).build();468if (async) {469resp = client.sendAsync(request, BodyHandlers.ofLines()).join();470} else {471resp = client.send(request, BodyHandlers.ofLines());472}473}474final List<String> respLines;475try {476if (isSchemeDisabled()) {477if (resp.statusCode() != 407) {478throw new RuntimeException("expected 407 not received");479}480System.out.println("Scheme disabled for [" + authType481+ ", " + authScheme482+ ", " + (useSSL ? "HTTP" : "HTTPS")483+ "]: Received expected " + resp.statusCode());484continue;485} else {486System.out.println("Scheme enabled for [" + authType487+ ", " + authScheme488+ ", " + (useSSL ? "HTTPS" : "HTTP")489+ "]: Expecting 200, response is: " + resp);490assert resp.statusCode() == 200 : "200 expected, received " + resp;491respLines = resp.body().collect(Collectors.toList());492}493} finally {494long stop = System.nanoTime();495synchronized (basicCount) {496long n = basicCount.getAndIncrement();497basics.set((basics.get() * n + (stop - start)) / (n + 1));498}499}500if (!lines.equals(respLines)) {501throw new RuntimeException("Unexpected response: " + respLines);502}503}504} finally {505}506System.out.println("OK");507}508509String getBasicAuth(String username) {510StringBuilder builder = new StringBuilder(username);511builder.append(':');512for (char c : DigestEchoServer.AUTHENTICATOR.getPassword(username)) {513builder.append(c);514}515return Base64.getEncoder().encodeToString(builder.toString().getBytes(StandardCharsets.UTF_8));516}517518final static AtomicLong digests = new AtomicLong();519final static AtomicLong digestCount = new AtomicLong();520// @Test521void testDigest(Version clientVersion, Version serverVersion,522boolean async, boolean expectContinue)523throws Exception524{525String test = format("testDigest: client: %s, server: %s, async: %s, useSSL: %s, " +526"authScheme: %s, authType: %s, expectContinue: %s",527clientVersion, serverVersion, async, useSSL,528authScheme, authType, expectContinue);529out.println("*** " + test + " ***");530DigestEchoServer server = EchoServers.of(serverVersion,531useSSL ? "https" : "http", authType, authScheme);532533URI uri = DigestEchoServer.uri(useSSL ? "https" : "http",534server.getServerAddress(), "/foo/");535536HttpClient client = newHttpClient(server);537HttpResponse<String> r;538CompletableFuture<HttpResponse<String>> cf1;539byte[] cnonce = new byte[16];540String cnonceStr = null;541DigestEchoServer.DigestResponse challenge = null;542543try {544for (int i=0; i<data.length; i++) {545out.println(DigestEchoServer.now() + "----- iteration " + i + " -----");546List<String> lines = List.of(Arrays.copyOfRange(data, 0, i+1));547assert lines.size() == i + 1;548String body = lines.stream().collect(Collectors.joining("\r\n"));549HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublishers.ofString(body);550HttpRequest.Builder reqBuilder = HttpRequest551.newBuilder(uri).version(clientVersion).POST(reqBody)552.expectContinue(expectContinue);553554boolean isTunnel = isProxy(authType) && useSSL;555String digestMethod = isTunnel ? "CONNECT" : "POST";556557// In case of a tunnel connection only the first request558// which establishes the tunnel needs to authenticate with559// the proxy.560if (challenge != null && (!isTunnel || isSchemeDisabled())) {561assert cnonceStr != null;562String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);563try {564reqBuilder = reqBuilder.header(authorizationKey(authType), auth);565} catch (IllegalArgumentException x) {566throw x;567}568}569570long start = System.nanoTime();571HttpRequest request = reqBuilder.build();572HttpResponse<Stream<String>> resp;573if (async) {574resp = client.sendAsync(request, BodyHandlers.ofLines()).join();575} else {576resp = client.send(request, BodyHandlers.ofLines());577}578System.out.println(resp);579assert challenge != null || resp.statusCode() == 401 || resp.statusCode() == 407580: "challenge=" + challenge + ", resp=" + resp + ", test=[" + test + "]";581if (resp.statusCode() == 401 || resp.statusCode() == 407) {582// This assert may need to be relaxed if our server happened to583// decide to close the tunnel connection, in which case we would584// receive 407 again...585assert challenge == null || !isTunnel || isSchemeDisabled()586: "No proxy auth should be required after establishing an SSL tunnel";587588System.out.println("Received " + resp.statusCode() + " answering challenge...");589random.nextBytes(cnonce);590cnonceStr = new BigInteger(1, cnonce).toString(16);591System.out.println("Response headers: " + resp.headers());592Optional<String> authenticateOpt = resp.headers().firstValue(authenticateKey(authType));593String authenticate = authenticateOpt.orElseThrow(594() -> new RuntimeException(authenticateKey(authType) + ": not found"));595assert authenticate.startsWith("Digest ");596HeaderParser hp = new HeaderParser(authenticate.substring("Digest ".length()));597String qop = hp.findValue("qop");598String nonce = hp.findValue("nonce");599if (qop == null && nonce == null) {600throw new RuntimeException("QOP and NONCE not found");601}602challenge = DigestEchoServer.DigestResponse603.create(authenticate.substring("Digest ".length()));604String auth = digestResponse(uri, digestMethod, challenge, cnonceStr);605try {606request = HttpRequest.newBuilder(uri).version(clientVersion)607.POST(reqBody).header(authorizationKey(authType), auth).build();608} catch (IllegalArgumentException x) {609throw x;610}611612if (async) {613resp = client.sendAsync(request, BodyHandlers.ofLines()).join();614} else {615resp = client.send(request, BodyHandlers.ofLines());616}617System.out.println(resp);618}619final List<String> respLines;620try {621if (isSchemeDisabled()) {622if (resp.statusCode() != 407) {623throw new RuntimeException("expected 407 not received");624}625System.out.println("Scheme disabled for [" + authType626+ ", " + authScheme +627", " + (useSSL ? "HTTP" : "HTTPS")628+ "]: Received expected " + resp.statusCode());629continue;630} else {631assert resp.statusCode() == 200;632respLines = resp.body().collect(Collectors.toList());633}634} finally {635long stop = System.nanoTime();636synchronized (digestCount) {637long n = digestCount.getAndIncrement();638digests.set((digests.get() * n + (stop - start)) / (n + 1));639}640}641if (!lines.equals(respLines)) {642throw new RuntimeException("Unexpected response: " + respLines);643}644}645} finally {646}647System.out.println("OK");648}649650// WARNING: This is not a full fledged implementation of DIGEST.651// It does contain bugs and inaccuracy.652static String digestResponse(URI uri, String method, DigestEchoServer.DigestResponse challenge, String cnonce)653throws NoSuchAlgorithmException {654int nc = NC.incrementAndGet();655DigestEchoServer.DigestResponse response1 = new DigestEchoServer.DigestResponse("earth",656"arthur", challenge.nonce, cnonce, String.valueOf(nc), uri.toASCIIString(),657challenge.algorithm, challenge.qop, challenge.opaque, null);658String response = DigestEchoServer.DigestResponse.computeDigest(true, method,659DigestEchoServer.AUTHENTICATOR.getPassword("arthur"), response1);660String auth = "Digest username=\"arthur\", realm=\"earth\""661+ ", response=\"" + response + "\", uri=\""+uri.toASCIIString()+"\""662+ ", qop=\"" + response1.qop + "\", cnonce=\"" + response1.cnonce663+ "\", nc=\"" + nc + "\", nonce=\"" + response1.nonce + "\"";664if (response1.opaque != null) {665auth = auth + ", opaque=\"" + response1.opaque + "\"";666}667return auth;668}669670static String authenticateKey(DigestEchoServer.HttpAuthType authType) {671switch (authType) {672case SERVER: return "www-authenticate";673case SERVER307: return "www-authenticate";674case PROXY: return "proxy-authenticate";675case PROXY305: return "proxy-authenticate";676default: throw new InternalError("authType: " + authType);677}678}679680static String authorizationKey(DigestEchoServer.HttpAuthType authType) {681switch (authType) {682case SERVER: return "authorization";683case SERVER307: return "Authorization";684case PROXY: return "Proxy-Authorization";685case PROXY305: return "proxy-Authorization";686default: throw new InternalError("authType: " + authType);687}688}689690static boolean isProxy(DigestEchoServer.HttpAuthType authType) {691switch (authType) {692case SERVER: return false;693case SERVER307: return false;694case PROXY: return true;695case PROXY305: return true;696default: throw new InternalError("authType: " + authType);697}698}699}700701702