/* * Copyright (c) 2016, 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 com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; import java.io.InputStream; import java.net.Authenticator; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; import java.net.URL; import java.net.URLConnection; import java.util.List; /* * @test * @bug 8138990 * @summary Tests for HTTP Digest auth * The impl maintains a cache for auth info, * the testcases run in a separate JVM to avoid cache hits * @modules jdk.httpserver * @run main/othervm DigestAuth good * @run main/othervm DigestAuth only_nonce * @run main/othervm DigestAuth sha1 * @run main/othervm DigestAuth no_header * @run main/othervm DigestAuth no_nonce * @run main/othervm DigestAuth no_qop * @run main/othervm DigestAuth invalid_alg * @run main/othervm DigestAuth validate_server * @run main/othervm DigestAuth validate_server_no_qop */ public class DigestAuth { static final String LOCALHOST = "localhost"; static final String EXPECT_FAILURE = null; static final String EXPECT_DIGEST = "Digest"; static final String REALM = "testrealm@host.com"; static final String NEXT_NONCE = "40f2e879449675f288476d772627370a"; static final String GOOD_WWW_AUTH_HEADER = "Digest " + "realm=\"testrealm@host.com\", " + "qop=\"auth,auth-int\", " + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; static final String GOOD_WWW_AUTH_HEADER_NO_QOP = "Digest " + "realm=\"testrealm@host.com\", " + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; static final String WWW_AUTH_HEADER_NO_NONCE = "Digest " + "realm=\"testrealm@host.com\", " + "qop=\"auth,auth-int\", " + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; static final String WWW_AUTH_HEADER_NO_QOP = "Digest " + "realm=\"testrealm@host.com\", " + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""; static final String WWW_AUTH_HEADER_ONLY_NONCE = "Digest " + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\""; static final String WWW_AUTH_HEADER_SHA1 = "Digest " + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + "algorithm=\"SHA1\""; static final String WWW_AUTH_HEADER_INVALID_ALGORITHM = "Digest " + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + "algorithm=\"SHA123\""; static final String AUTH_INFO_HEADER_NO_QOP_FIRST = "nextnonce=\"" + NEXT_NONCE + "\", " + "rspauth=\"ee85bc4315d8b18757809f1a8b9382d8\""; static final String AUTH_INFO_HEADER_NO_QOP_SECOND = "rspauth=\"12f2fa12841b3775b6054576722446b2\""; static final String AUTH_INFO_HEADER_WRONG_DIGEST = "nextnonce=\"" + NEXT_NONCE + "\", " + "rspauth=\"7327570c586207eca2afae94fc20903d\", " + "cnonce=\"0a4f113b\", " + "nc=00000001, " + "qop=auth"; public static void main(String[] args) throws Exception { if (args.length == 0) { throw new RuntimeException("No testcase specified"); } String testcase = args[0]; // start a local HTTP server try (LocalHttpServer server = LocalHttpServer.startServer()) { // set authenticator AuthenticatorImpl auth = new AuthenticatorImpl(); Authenticator.setDefault(auth); String url = String.format("http://%s:%d/test/", LOCALHOST, server.getPort()); boolean success = true; switch (testcase) { case "good": // server returns a good WWW-Authenticate header server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER); success = testAuth(url, auth, EXPECT_DIGEST); if (auth.lastRequestedPrompt == null || !auth.lastRequestedPrompt.equals(REALM)) { System.out.println("Unexpected realm: " + auth.lastRequestedPrompt); success = false; } break; case "validate_server": // enable processing Authentication-Info headers System.setProperty("http.auth.digest.validateServer", "true"); /* Server returns good WWW-Authenticate * and Authentication-Info headers with wrong digest */ server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER); server.setAuthInfoHeader(AUTH_INFO_HEADER_WRONG_DIGEST); success = testAuth(url, auth, EXPECT_FAILURE); if (auth.lastRequestedPrompt == null || !auth.lastRequestedPrompt.equals(REALM)) { System.out.println("Unexpected realm: " + auth.lastRequestedPrompt); success = false; } break; case "validate_server_no_qop": // enable processing Authentication-Info headers System.setProperty("http.auth.digest.validateServer", "true"); /* Server returns good both WWW-Authenticate * and Authentication-Info headers without any qop field, * so that client-nonce should not be taked into account, * and connection should succeed. */ server.setWWWAuthHeader(GOOD_WWW_AUTH_HEADER_NO_QOP); server.setAuthInfoHeader(AUTH_INFO_HEADER_NO_QOP_FIRST); success = testAuth(url, auth, EXPECT_DIGEST); if (auth.lastRequestedPrompt == null || !auth.lastRequestedPrompt.equals(REALM)) { System.out.println("Unexpected realm: " + auth.lastRequestedPrompt); success = false; } // connect again and check if nextnonce was used server.setAuthInfoHeader(AUTH_INFO_HEADER_NO_QOP_SECOND); success &= testAuth(url, auth, EXPECT_DIGEST); if (!NEXT_NONCE.equals(server.lastRequestedNonce)) { System.out.println("Unexpected next nonce: " + server.lastRequestedNonce); success = false; } break; case "only_nonce": /* Server returns a good WWW-Authenticate header * which contains only nonce (no realm set). * * Realm from WWW-Authenticate header is passed to * authenticator which can use it as a prompt * when it asks a user for credentials. * * It's fine if an HTTP client doesn't fail if no realm set, * and delegates making a decision to authenticator/user. */ server.setWWWAuthHeader(WWW_AUTH_HEADER_ONLY_NONCE); success = testAuth(url, auth, EXPECT_DIGEST); if (auth.lastRequestedPrompt != null && !auth.lastRequestedPrompt.trim().isEmpty()) { System.out.println("Unexpected realm: " + auth.lastRequestedPrompt); success = false; } break; case "sha1": // server returns a good WWW-Authenticate header with SHA-1 server.setWWWAuthHeader(WWW_AUTH_HEADER_SHA1); success = testAuth(url, auth, EXPECT_DIGEST); break; case "no_header": // server returns no WWW-Authenticate header success = testAuth(url, auth, EXPECT_FAILURE); if (auth.lastRequestedScheme != null) { System.out.println("Unexpected scheme: " + auth.lastRequestedScheme); success = false; } break; case "no_nonce": // server returns a wrong WWW-Authenticate header (no nonce) server.setWWWAuthHeader(WWW_AUTH_HEADER_NO_NONCE); success = testAuth(url, auth, EXPECT_FAILURE); break; case "invalid_alg": // server returns a wrong WWW-Authenticate header // (invalid hash algorithm) server.setWWWAuthHeader(WWW_AUTH_HEADER_INVALID_ALGORITHM); success = testAuth(url, auth, EXPECT_FAILURE); break; case "no_qop": // server returns a good WWW-Authenticate header // without QOPs server.setWWWAuthHeader(WWW_AUTH_HEADER_NO_QOP); success = testAuth(url, auth, EXPECT_DIGEST); break; default: throw new RuntimeException("Unexpected testcase: " + testcase); } if (!success) { throw new RuntimeException("Test failed"); } } System.out.println("Test passed"); } static boolean testAuth(String url, AuthenticatorImpl auth, String expectedScheme) { try { System.out.printf("Connect to %s, expected auth scheme is '%s'%n", url, expectedScheme); load(url); if (expectedScheme == null) { System.out.println("Unexpected successful connection"); return false; } System.out.printf("Actual auth scheme is '%s'%n", auth.lastRequestedScheme); if (!expectedScheme.equalsIgnoreCase(auth.lastRequestedScheme)) { System.out.println("Unexpected auth scheme"); return false; } } catch (IOException e) { if (expectedScheme != null) { System.out.println("Unexpected exception: " + e); e.printStackTrace(System.out); return false; } System.out.println("Expected exception: " + e); } return true; } static void load(String url) throws IOException { URLConnection conn = new URL(url).openConnection(); conn.setUseCaches(false); try (BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream()))) { String line = reader.readLine(); if (line == null) { throw new IOException("Couldn't read response"); } do { System.out.println(line); } while ((line = reader.readLine()) != null); } } private static class AuthenticatorImpl extends Authenticator { private String lastRequestedScheme; private String lastRequestedPrompt; @Override public PasswordAuthentication getPasswordAuthentication() { lastRequestedScheme = getRequestingScheme(); lastRequestedPrompt = getRequestingPrompt(); System.out.println("AuthenticatorImpl: requested " + lastRequestedScheme); return new PasswordAuthentication("Mufasa", "Circle Of Life".toCharArray()); } } // local HTTP server which pretends to support HTTP Digest auth static class LocalHttpServer implements HttpHandler, AutoCloseable { private final HttpServer server; private volatile String wwwAuthHeader = null; private volatile String authInfoHeader = null; private volatile String lastRequestedNonce; private LocalHttpServer(HttpServer server) { this.server = server; } void setWWWAuthHeader(String wwwAuthHeader) { this.wwwAuthHeader = wwwAuthHeader; } void setAuthInfoHeader(String authInfoHeader) { this.authInfoHeader = authInfoHeader; } static LocalHttpServer startServer() throws IOException { HttpServer httpServer = HttpServer.create( new InetSocketAddress(0), 0); LocalHttpServer localHttpServer = new LocalHttpServer(httpServer); localHttpServer.start(); return localHttpServer; } void start() { server.createContext("/test", this); server.start(); System.out.println("HttpServer: started on port " + getPort()); } void stop() { server.stop(0); System.out.println("HttpServer: stopped"); } int getPort() { return server.getAddress().getPort(); } @Override public void handle(HttpExchange t) throws IOException { System.out.println("HttpServer: handle connection"); // read a request try (InputStream is = t.getRequestBody()) { while (is.read() > 0); } try { List<String> headers = t.getRequestHeaders() .get("Authorization"); String header = ""; if (headers != null && !headers.isEmpty()) { header = headers.get(0).trim().toLowerCase(); } if (header.startsWith("digest")) { if (authInfoHeader != null) { t.getResponseHeaders().add("Authentication-Info", authInfoHeader); } lastRequestedNonce = findParameter(header, "nonce"); byte[] output = "hello".getBytes(); t.sendResponseHeaders(200, output.length); t.getResponseBody().write(output); System.out.println("HttpServer: return 200"); } else { if (wwwAuthHeader != null) { t.getResponseHeaders().add( "WWW-Authenticate", wwwAuthHeader); } byte[] output = "forbidden".getBytes(); t.sendResponseHeaders(401, output.length); t.getResponseBody().write(output); System.out.println("HttpServer: return 401"); } } catch (IOException e) { System.out.println("HttpServer: exception: " + e); System.out.println("HttpServer: return 500"); t.sendResponseHeaders(500, 0); } finally { t.close(); } } private static String findParameter(String header, String name) { name = name.toLowerCase(); if (header != null) { String[] params = header.split("\\s"); for (String param : params) { param = param.trim().toLowerCase(); if (param.startsWith(name)) { String[] parts = param.split("="); if (parts.length > 1) { return parts[1] .replaceAll("\"", "").replaceAll(",", ""); } } } } return null; } @Override public void close() { stop(); } } }