test/jdk/java/net/httpclient/DigestEchoClient.java
branchhttp-client-branch
changeset 56033 db102c5ca88a
child 56041 b4b5e09ef3cc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/net/httpclient/DigestEchoClient.java	Fri Jan 19 15:57:21 2018 +0000
@@ -0,0 +1,526 @@
+/*
+ * 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.URISyntaxException;
+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.Set;
+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 javax.net.ServerSocketFactory;
+import javax.net.ssl.SSLServerSocketFactory;
+import jdk.incubator.http.HttpClient;
+import jdk.incubator.http.HttpClient.Version;
+import jdk.incubator.http.HttpRequest;
+import jdk.incubator.http.HttpResponse;
+import jdk.testlibrary.SimpleSSLContext;
+import sun.net.www.HeaderParser;
+import static java.lang.System.out;
+import static java.lang.String.format;
+import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
+
+/**
+ * @test
+ * @summary this test verifies that a client may provides authorization
+ *          headers directly when connecting with a server.
+ * @bug 8087112
+ * @library /lib/testlibrary
+ * @build jdk.testlibrary.SimpleSSLContext DigestEchoServer DigestEchoClient
+ * @modules jdk.incubator.httpclient
+ *          java.base/sun.net.www
+ * @run main/othervm 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;
+
+        private EchoServers(DigestEchoServer server,
+                    String protocolScheme,
+                    DigestEchoServer.HttpAuthType authType,
+                    DigestEchoServer.HttpAuthSchemeType authScheme) {
+            this.authType = authType;
+            this.authScheme = authScheme;
+            this.protocolScheme = protocolScheme;
+            this.key = key(protocolScheme, authType, authScheme);
+            this.server = server;
+        }
+
+        static String key(String protocolScheme,
+                          DigestEchoServer.HttpAuthType authType,
+                          DigestEchoServer.HttpAuthSchemeType authScheme) {
+            return String.format("%s:%s:%s", protocolScheme, authType, authScheme);
+        }
+
+        private static EchoServers create(String protocolScheme,
+                                   DigestEchoServer.HttpAuthType authType,
+                                   DigestEchoServer.HttpAuthSchemeType authScheme) {
+            try {
+                serverCount.incrementAndGet();
+                DigestEchoServer server =
+                    DigestEchoServer.create(protocolScheme, authType, authScheme);
+                return new EchoServers(server, protocolScheme, authType, authScheme);
+            } catch (IOException x) {
+                throw new UncheckedIOException(x);
+            }
+        }
+
+        public static DigestEchoServer of(String protocolScheme,
+                                    DigestEchoServer.HttpAuthType authType,
+                                    DigestEchoServer.HttpAuthSchemeType authScheme) {
+            String key = key(protocolScheme, authType, authScheme);
+            return servers.computeIfAbsent(key, (k) ->
+                    create(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<>();
+    }
+
+
+    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);
+        }
+    }
+
+    final ServerSocketFactory factory;
+    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;
+        factory = useSSL ? SSLServerSocketFactory.getDefault()
+                         : ServerSocketFactory.getDefault();
+    }
+
+    static final AtomicLong clientCount = new AtomicLong();
+    public HttpClient newHttpClient(DigestEchoServer server) {
+        clientCount.incrementAndGet();
+        HttpClient.Builder builder = HttpClient.newBuilder();
+        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.SAME_PROTOCOL);
+                break;
+            case SERVER307:
+                builder = builder.followRedirects(HttpClient.Redirect.SAME_PROTOCOL);
+                break;
+            default:
+                break;
+        }
+        return builder.build();
+    }
+
+    public static void main(String[] args) throws Exception {
+        boolean useSSL = false;
+        EnumSet<DigestEchoServer.HttpAuthType> types =
+                EnumSet.complementOf(EnumSet.of(DigestEchoServer.HttpAuthType.PROXY305));
+        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 version : HttpClient.Version.values()) {
+                        dec.testBasic(version, true);
+                    }
+                }
+                EnumSet<DigestEchoServer.HttpAuthSchemeType> digests =
+                        EnumSet.of(DigestEchoServer.HttpAuthSchemeType.DIGEST);
+                for (DigestEchoServer.HttpAuthSchemeType authScheme : digests) {
+                    DigestEchoClient dec = new DigestEchoClient(useSSL,
+                            authScheme,
+                            authType);
+                    for (Version version : HttpClient.Version.values()) {
+                        dec.testDigest(version, true);
+                    }
+                }
+            }
+        } finally {
+            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(" ---------------------------------------------------------- ");
+        }
+    }
+
+    final static AtomicLong basics = new AtomicLong();
+    final static AtomicLong basicCount = new AtomicLong();
+    // @Test
+    void testBasic(HttpClient.Version version, boolean async)
+        throws Exception
+    {
+        out.println(format("*** testBasic: version: %s,  async: %s, useSSL: %s, authScheme: %s, authType: %s ***",
+                version, async, useSSL, authScheme, authType));
+
+        DigestEchoServer server = EchoServers.of(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"));
+                HttpRequest.BodyPublisher reqBody = HttpRequest.BodyPublisher.fromString(body);
+                HttpRequest.Builder builder = HttpRequest.newBuilder(uri).version(version)
+                        .POST(reqBody);
+                final boolean addHeaders = authScheme == DigestEchoServer.HttpAuthSchemeType.BASICSERVER;
+                if (addHeaders) {
+                    // handle authentication ourselves
+                    assert !client.authenticator().isPresent();
+                    if (auth == null) auth = "Basic " + getBasicAuth("arthur");
+                    try {
+                        builder = builder.header(authorizationKey(authType), auth);
+                        if (isProxy(authType)) {
+                            throw new RuntimeException("Setting " + authorizationKey(authType)
+                                    + " should have failed");
+                        }
+                    } catch (IllegalArgumentException x) {
+                        if (isProxy(authType)) {
+                            System.out.println("Got expected " + x);
+                        } else 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, HttpResponse.BodyHandler.asLines()).join();
+                    } else {
+                        resp = client.send(request, HttpResponse.BodyHandler.asLines());
+                    }
+                } 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();
+                    }
+                    // If we let the stack manage the authorisation, or if we
+                    // were not authenticating with a Proxy, then there should
+                    // have been no exception.
+                    if (!addHeaders || !isProxy(authType) || client.authenticator().isPresent()) {
+                        throw new RuntimeException("Unexpected exception: " + t, t);
+                    }
+                    // In the case of Proxy authentication with Basic, we should get
+                    // an IOException complaining that we haven't set an authenticator.
+                    if (t instanceof IOException && t.getMessage().contains("No authenticator set")) {
+                        System.out.println("Got expected exception: " + t);
+                        continue;
+                    }
+                    throw new RuntimeException("Unexpected exception: " + t, t);
+                }
+                if (isProxy(authType) && addHeaders) {
+                    assert resp.statusCode() == 407;
+                    continue;
+                }
+
+                assert resp.statusCode() == 200;
+                List<String> respLines = resp.body().collect(Collectors.toList());
+                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(HttpClient.Version version, boolean async)
+            throws Exception
+    {
+        out.println(format("*** testDigest: version: %s,  async: %s, useSSL: %s, authScheme: %s, authType: %s  ***",
+                version, async, useSSL, authScheme, authType));
+        DigestEchoServer server = EchoServers.of(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.BodyPublisher.fromString(body);
+                HttpRequest.Builder reqBuilder = HttpRequest.newBuilder(uri).version(version).POST(reqBody);
+                if (challenge != null) {
+                    assert cnonceStr != null;
+                    String auth = digestResponse(uri, "POST", challenge, cnonceStr);
+                    try {
+                        reqBuilder = reqBuilder.header(authorizationKey(authType), auth);
+                        if (isProxy(authType)) {
+                            throw new RuntimeException("Setting " + authorizationKey(authType)
+                                    + " should have failed");
+                        }
+                    } catch (IllegalArgumentException x) {
+                        if (isProxy(authType)) {
+                            System.out.println("Got expected " + x);
+                        } else throw x;
+                    }
+                }
+                long start = System.nanoTime();
+                HttpRequest request = reqBuilder.build();
+                HttpResponse<Stream<String>> resp;
+                if (async) {
+                    resp = client.sendAsync(request, HttpResponse.BodyHandler.asLines()).join();
+                } else {
+                    resp = client.send(request, HttpResponse.BodyHandler.asLines());
+                }
+                System.out.println(resp);
+                assert challenge != null || resp.statusCode() == 401 || resp.statusCode() == 407;
+                if (resp.statusCode() == 401 || resp.statusCode() == 407) {
+                    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, "POST", challenge, cnonceStr);
+                    try {
+                        request = HttpRequest.newBuilder(uri).version(version)
+                            .POST(reqBody).header(authorizationKey(authType), auth).build();
+                        if (isProxy(authType)) {
+                            throw new RuntimeException("Setting " + authorizationKey(authType)
+                                    + " should have failed");
+                        }
+                    } catch (IllegalArgumentException x) {
+                        if (isProxy(authType)) {
+                            System.out.println("Got expected " + x);
+                            continue;
+                        } else throw x;
+                    }
+
+                    if (async) {
+                        resp = client.sendAsync(request, HttpResponse.BodyHandler.asLines()).join();
+                    } else {
+                        resp = client.send(request, HttpResponse.BodyHandler.asLines());
+                    }
+                    System.out.println(resp);
+                }
+                assert resp.statusCode() == 200;
+                List<String> respLines = resp.body().collect(Collectors.toList());
+                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);
+        }
+    }
+}