| /* | |
| * Copyright (c) 2017, 2021, 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 static java.lang.System.out; | |
| import static java.net.http.HttpResponse.BodyHandlers.discarding; | |
| import java.io.DataInputStream; | |
| import java.io.IOException; | |
| import java.io.UncheckedIOException; | |
| import java.net.InetAddress; | |
| import java.net.InetSocketAddress; | |
| import java.net.ServerSocket; | |
| import java.net.Socket; | |
| import java.net.URI; | |
| import java.net.http.HttpClient; | |
| import java.net.http.HttpClient.Version; | |
| import java.net.http.HttpRequest; | |
| import java.net.http.HttpResponse; | |
| import java.util.List; | |
| import java.util.Locale; | |
| import java.util.concurrent.CompletableFuture; | |
| import java.util.concurrent.CompletionException; | |
| import javax.net.ServerSocketFactory; | |
| import javax.net.ssl.SSLContext; | |
| import javax.net.ssl.SSLHandshakeException; | |
| import javax.net.ssl.SSLParameters; | |
| import javax.net.ssl.SSLSocket; | |
| /** | |
| * @test | |
| * @bug 8238990 8258951 | |
| * @run main/othervm -Djdk.internal.httpclient.debug=true HandshakeFailureTest TLSv1.2 | |
| * @run main/othervm -Djdk.internal.httpclient.debug=true HandshakeFailureTest TLSv1.3 | |
| * @summary Verify SSLHandshakeException is received when the handshake fails, | |
| * either because the server closes (EOF) the connection during handshaking, | |
| * or no cipher suite can be negotiated (TLSv1.2) or no available authentication | |
| * scheme (TLSv1.3). | |
| */ | |
| // To switch on debugging use: | |
| // @run main/othervm -Djdk.internal.httpclient.debug=true HandshakeFailureTest | |
| public class HandshakeFailureTest { | |
| // The number of iterations each testXXXClient performs. Can be increased | |
| // when running standalone testing. | |
| static final int TIMES = 10; | |
| private static String tlsProtocol; | |
| private static int maxWsaeConnAborted; | |
| // On Microsoft Windows, a WSAECONNABORTED error could be raised | |
| // if the client side fails to retransmit a TCP packet. | |
| // This could happen if for instance, the server stops reading and | |
| // close the socket while the client is still trying to push | |
| // data through. | |
| // With TLSv1.3, and our dummy SSLServer implementation below, | |
| // this can occur quite often. | |
| // Our HTTP stack should automatically wrap such exceptions | |
| // in SSLHandshakeException if they are raised while the handshake | |
| // in progress. So it would be an error to receive WSAECONNABORTED | |
| // here. This test has some special code to handle WSAECONNABORTED | |
| // and fail if they reach the test code. | |
| public static final String WSAECONNABORTED_MSG = | |
| "An established connection was aborted by the software in your host machine"; | |
| public static final boolean isWindows = System.getProperty("os.name", "") | |
| .toLowerCase(Locale.ROOT).contains("win"); | |
| public enum ExpectedExceptionType { | |
| HANDSHAKE_FAILURE, | |
| WSAECONNABORTED | |
| } | |
| // The exception checker is used to record how many WSAECONNABORTED | |
| // have reached the test code. There should be none: | |
| // (usually max should be 0) | |
| static final class ExceptionChecker { | |
| int count; | |
| Throwable aborted = null; | |
| public void check(Throwable expected) { | |
| if (ExpectedExceptionType.WSAECONNABORTED == checkExceptionOrCause(expected)) { | |
| count++; | |
| aborted = expected; | |
| } | |
| } | |
| public void check(int max) { | |
| if (count > max) { | |
| out.println("WSAECONNABORTED received too many times: " + count); | |
| aborted.printStackTrace(out); | |
| throw new AssertionError("WSAECONNABORTED received too many times: " + count, aborted); | |
| } | |
| } | |
| } | |
| public static void main(String[] args) throws Exception { | |
| tlsProtocol = args[0]; | |
| // At this time, all WSAECONNABORTED exception raised during | |
| // the handshake should have been wrapped in SSLHandshakeException, | |
| // so we allow none to reach here. | |
| maxWsaeConnAborted = 0; | |
| HandshakeFailureTest test = new HandshakeFailureTest(); | |
| List<AbstractServer> servers = List.of(new PlainServer(), new SSLServer()); | |
| for (AbstractServer server : servers) { | |
| try (server) { | |
| out.format("%n%n------ Testing with server:%s ------%n", server); | |
| URI uri = new URI("https://" + server.getAuthority() + "/"); | |
| test.testSyncSameClient(uri, Version.HTTP_1_1); | |
| test.testSyncSameClient(uri, Version.HTTP_2); | |
| test.testSyncDiffClient(uri, Version.HTTP_1_1); | |
| test.testSyncDiffClient(uri, Version.HTTP_2); | |
| test.testAsyncSameClient(uri, Version.HTTP_1_1); | |
| test.testAsyncSameClient(uri, Version.HTTP_2); | |
| test.testAsyncDiffClient(uri, Version.HTTP_1_1); | |
| test.testAsyncDiffClient(uri, Version.HTTP_2); | |
| } | |
| } | |
| } | |
| static HttpClient getClient() { | |
| SSLParameters params = new SSLParameters(); | |
| params.setProtocols(new String[] { tlsProtocol }); | |
| return HttpClient.newBuilder() | |
| .sslParameters(params) | |
| .build(); | |
| } | |
| void testSyncSameClient(URI uri, Version version) throws Exception { | |
| out.printf("%n--- testSyncSameClient %s ---%n", version); | |
| HttpClient client = getClient(); | |
| ExceptionChecker exceptionChecker = new ExceptionChecker(); | |
| for (int i = 0; i < TIMES; i++) { | |
| out.printf("iteration %d%n", i); | |
| HttpRequest request = HttpRequest.newBuilder(uri) | |
| .version(version) | |
| .build(); | |
| try { | |
| HttpResponse<Void> response = client.send(request, discarding()); | |
| String msg = String.format("UNEXPECTED response=%s%n", response); | |
| throw new RuntimeException(msg); | |
| } catch (IOException expected) { | |
| out.printf("Client: caught expected exception: %s%n", expected); | |
| exceptionChecker.check(expected); | |
| } | |
| } | |
| exceptionChecker.check(maxWsaeConnAborted); | |
| } | |
| void testSyncDiffClient(URI uri, Version version) throws Exception { | |
| out.printf("%n--- testSyncDiffClient %s ---%n", version); | |
| ExceptionChecker exceptionChecker = new ExceptionChecker(); | |
| for (int i = 0; i < TIMES; i++) { | |
| out.printf("iteration %d%n", i); | |
| // a new client each time | |
| HttpClient client = getClient(); | |
| HttpRequest request = HttpRequest.newBuilder(uri) | |
| .version(version) | |
| .build(); | |
| try { | |
| HttpResponse<Void> response = client.send(request, discarding()); | |
| String msg = String.format("UNEXPECTED response=%s%n", response); | |
| throw new RuntimeException(msg); | |
| } catch (IOException expected) { | |
| out.printf("Client: caught expected exception: %s%n", expected); | |
| exceptionChecker.check(expected); | |
| } | |
| } | |
| exceptionChecker.check(maxWsaeConnAborted); | |
| } | |
| void testAsyncSameClient(URI uri, Version version) throws Exception { | |
| out.printf("%n--- testAsyncSameClient %s ---%n", version); | |
| HttpClient client = getClient(); | |
| ExceptionChecker exceptionChecker = new ExceptionChecker(); | |
| for (int i = 0; i < TIMES; i++) { | |
| out.printf("iteration %d%n", i); | |
| HttpRequest request = HttpRequest.newBuilder(uri) | |
| .version(version) | |
| .build(); | |
| CompletableFuture<HttpResponse<Void>> response = | |
| client.sendAsync(request, discarding()); | |
| try { | |
| response.join(); | |
| String msg = String.format("UNEXPECTED response=%s%n", response); | |
| throw new RuntimeException(msg); | |
| } catch (CompletionException ce) { | |
| Throwable expected = ce.getCause(); | |
| out.printf("Client: caught expected exception: %s%n", expected); | |
| exceptionChecker.check(expected); | |
| } | |
| } | |
| exceptionChecker.check(maxWsaeConnAborted); | |
| } | |
| void testAsyncDiffClient(URI uri, Version version) throws Exception { | |
| out.printf("%n--- testAsyncDiffClient %s ---%n", version); | |
| ExceptionChecker exceptionChecker = new ExceptionChecker(); | |
| for (int i = 0; i < TIMES; i++) { | |
| out.printf("iteration %d%n", i); | |
| // a new client each time | |
| HttpClient client = getClient(); | |
| HttpRequest request = HttpRequest.newBuilder(uri) | |
| .version(version) | |
| .build(); | |
| CompletableFuture<HttpResponse<Void>> response = | |
| client.sendAsync(request, discarding()); | |
| try { | |
| response.join(); | |
| String msg = String.format("UNEXPECTED response=%s%n", response); | |
| throw new RuntimeException(msg); | |
| } catch (CompletionException ce) { | |
| ce.printStackTrace(out); | |
| Throwable expected = ce.getCause(); | |
| out.printf("Client: caught expected exception: %s%n", expected); | |
| exceptionChecker.check(expected); | |
| } | |
| } | |
| exceptionChecker.check(maxWsaeConnAborted); | |
| } | |
| // Tells whether this exception was raised from a WSAECONNABORTED | |
| // error raised in the native code. | |
| static boolean isWsaeConnAborted(Throwable t) { | |
| return t instanceof IOException && WSAECONNABORTED_MSG.equalsIgnoreCase(t.getMessage()); | |
| } | |
| // We might allow some spurious WSAECONNABORTED exception. | |
| // The decision whether to allow such errors or not is taken by | |
| // the ExceptionChecker | |
| static ExpectedExceptionType checkExceptionOrCause(Throwable t) { | |
| final Throwable original = t; | |
| do { | |
| if (SSLHandshakeException.class.isInstance(t)) { | |
| // For TLSv1.3, possibly the server is (being) closed when | |
| // the client read the input alert. In this case, the client | |
| // just gets SocketException instead of SSLHandshakeException. | |
| System.out.println("Found expected exception/cause: " + t); | |
| return ExpectedExceptionType.HANDSHAKE_FAILURE; | |
| } | |
| if (isWindows && isWsaeConnAborted(t)) { | |
| System.out.println("Found WSAECONNABORTED: " + t); | |
| return ExpectedExceptionType.WSAECONNABORTED; | |
| } | |
| } while ((t = t.getCause()) != null); | |
| original.printStackTrace(System.out); | |
| throw new RuntimeException( | |
| "Not found expected SSLHandshakeException in " | |
| + original, original); | |
| } | |
| /** Common super type for PlainServer and SSLServer. */ | |
| static abstract class AbstractServer extends Thread implements AutoCloseable { | |
| protected final ServerSocket ss; | |
| protected volatile boolean closed; | |
| AbstractServer(String name, ServerSocket ss) throws IOException { | |
| super(name); | |
| ss.setReuseAddress(false); | |
| ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); | |
| this.ss = ss; | |
| this.start(); | |
| } | |
| int getPort() { return ss.getLocalPort(); } | |
| String getAuthority() { | |
| String address = ss.getInetAddress().getHostAddress(); | |
| if (address.contains(":")) address = "[" + address + "]"; | |
| return address + ":" + ss.getLocalPort(); | |
| } | |
| @Override | |
| public void close() { | |
| if (closed) | |
| return; | |
| closed = true; | |
| try { | |
| ss.close(); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException("Unexpected", e); | |
| } | |
| } | |
| } | |
| /** Emulates a server-side, using plain cleartext Sockets, that just closes | |
| * the connection, after a small variable delay. */ | |
| static class PlainServer extends AbstractServer { | |
| private volatile int count; | |
| PlainServer() throws IOException { | |
| super("PlainServer", new ServerSocket()); | |
| } | |
| @Override | |
| public void run() { | |
| while (!closed) { | |
| try (Socket s = ss.accept()) { | |
| count++; | |
| /* SSL record layer - contains the client hello | |
| struct { | |
| uint8 major, minor; | |
| } ProtocolVersion; | |
| enum { | |
| change_cipher_spec(20), alert(21), handshake(22), | |
| application_data(23), (255) | |
| } ContentType; | |
| struct { | |
| ContentType type; | |
| ProtocolVersion version; | |
| uint16 length; | |
| opaque fragment[SSLPlaintext.length]; | |
| } SSLPlaintext; */ | |
| DataInputStream din = new DataInputStream(s.getInputStream()); | |
| int contentType = din.read(); | |
| out.println("ContentType:" + contentType); | |
| int majorVersion = din.read(); | |
| out.println("Major:" + majorVersion); | |
| int minorVersion = din.read(); | |
| out.println("Minor:" + minorVersion); | |
| int length = din.readShort(); | |
| out.println("length:" + length); | |
| byte[] ba = new byte[length]; | |
| din.readFully(ba); | |
| // simulate various delays in response | |
| Thread.sleep(10 * (count % 10)); | |
| s.close(); // close without giving any reply | |
| } catch (IOException e) { | |
| if (!closed) { | |
| out.println("PlainServer: unexpected " + e); | |
| e.printStackTrace(out); | |
| } | |
| } catch (InterruptedException e) { | |
| if (!closed) { | |
| out.println("PlainServer: unexpected " + e); | |
| e.printStackTrace(out); | |
| throw new RuntimeException(e); | |
| } | |
| break; | |
| } catch (Error | RuntimeException e) { | |
| if (!closed) { | |
| out.println("PlainServer: unexpected " + e); | |
| e.printStackTrace(out); | |
| throw new RuntimeException(e); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| /** Emulates a server-side, using SSL Sockets, that will fail during | |
| * handshaking, as there are no cipher suites in common (TLSv1.2) | |
| * or no available authentication scheme (TLSv1.3). */ | |
| static class SSLServer extends AbstractServer { | |
| static final SSLContext sslContext = createUntrustingContext(); | |
| static final ServerSocketFactory factory = sslContext.getServerSocketFactory(); | |
| static SSLContext createUntrustingContext() { | |
| try { | |
| SSLContext sslContext = SSLContext.getInstance("TLS"); | |
| sslContext.init(null, null, null); | |
| return sslContext; | |
| } catch (Throwable t) { | |
| throw new AssertionError(t); | |
| } | |
| } | |
| SSLServer() throws IOException { | |
| super("SSLServer", factory.createServerSocket()); | |
| } | |
| @Override | |
| public void run() { | |
| while (!closed) { | |
| try (SSLSocket s = (SSLSocket)ss.accept()) { | |
| s.getInputStream().read(); // will throw SHE here | |
| throw new AssertionError("Should not reach here"); | |
| } catch (SSLHandshakeException expected) { | |
| // Expected: SSLHandshakeException: no cipher suites in common (TLSv1.2) | |
| // or no available authentication scheme (TLSv1.3) | |
| out.printf("SSLServer: caught expected exception: %s%n", expected); | |
| } catch (IOException e) { | |
| if (!closed) { | |
| out.println("SSLServer: unexpected " + e); | |
| e.printStackTrace(out); | |
| } | |
| } catch (Error | RuntimeException e) { | |
| if (!closed) { | |
| out.println("SSLServer: unexpected " + e); | |
| e.printStackTrace(out); | |
| throw new RuntimeException(e); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| } |