8144566: Custom HostnameVerifier disables SNI extension
Reviewed-by: mullan, wetmore
--- a/jdk/src/java.base/share/classes/sun/security/ssl/SSLSocketImpl.java Fri Apr 22 17:07:54 2016 +0800
+++ b/jdk/src/java.base/share/classes/sun/security/ssl/SSLSocketImpl.java Fri Apr 22 09:27:35 2016 +0000
@@ -209,6 +209,12 @@
Collection<SNIMatcher> sniMatchers =
Collections.<SNIMatcher>emptyList();
+ // Is the serverNames set to empty with SSLParameters.setServerNames()?
+ private boolean noSniExtension = false;
+
+ // Is the sniMatchers set to empty with SSLParameters.setSNIMatchers()?
+ private boolean noSniMatcher = false;
+
// Configured application protocol values
String[] applicationProtocols = new String[0];
@@ -642,6 +648,11 @@
}
super.connect(endpoint, timeout);
+
+ if (host == null || host.length() == 0) {
+ useImplicitHost(false);
+ }
+
doneConnect();
}
@@ -2098,41 +2109,62 @@
outputRecord.setVersion(protocolVersion);
}
+ //
+ // ONLY used by ClientHandshaker for the server hostname during handshaking
+ //
synchronized String getHost() {
// Note that the host may be null or empty for localhost.
if (host == null || host.length() == 0) {
- if (!trustNameService) {
- // If the local name service is not trustworthy, reverse host
- // name resolution should not be performed for endpoint
- // identification. Use the application original specified
- // hostname or IP address instead.
- host = getOriginalHostname(getInetAddress());
- } else {
- host = getInetAddress().getHostName();
- }
+ useImplicitHost(true);
}
return host;
}
/*
- * Get the original application specified hostname.
+ * Try to set and use the implicit specified hostname
*/
- private static String getOriginalHostname(InetAddress inetAddress) {
- /*
- * Get the original hostname via jdk.internal.misc.SharedSecrets.
- */
- JavaNetInetAddressAccess jna = SharedSecrets.getJavaNetInetAddressAccess();
- String originalHostname = jna.getOriginalHostName(inetAddress);
+ private synchronized void useImplicitHost(boolean noSniUpdate) {
- /*
- * If no application specified hostname, use the IP address.
- */
- if (originalHostname == null || originalHostname.length() == 0) {
- originalHostname = inetAddress.getHostAddress();
+ // Note: If the local name service is not trustworthy, reverse
+ // host name resolution should not be performed for endpoint
+ // identification. Use the application original specified
+ // hostname or IP address instead.
+
+ // Get the original hostname via jdk.internal.misc.SharedSecrets
+ InetAddress inetAddress = getInetAddress();
+ if (inetAddress == null) { // not connected
+ return;
}
- return originalHostname;
+ JavaNetInetAddressAccess jna =
+ SharedSecrets.getJavaNetInetAddressAccess();
+ String originalHostname = jna.getOriginalHostName(inetAddress);
+ if ((originalHostname != null) &&
+ (originalHostname.length() != 0)) {
+
+ host = originalHostname;
+ if (!noSniUpdate && serverNames.isEmpty() && !noSniExtension) {
+ serverNames =
+ Utilities.addToSNIServerNameList(serverNames, host);
+
+ if (!roleIsServer &&
+ (handshaker != null) && !handshaker.started()) {
+ handshaker.setSNIServerNames(serverNames);
+ }
+ }
+
+ return;
+ }
+
+ // No explicitly specified hostname, no server name indication.
+ if (!trustNameService) {
+ // The local name service is not trustworthy, use IP address.
+ host = inetAddress.getHostAddress();
+ } else {
+ // Use the underlying reverse host name resolution service.
+ host = getInetAddress().getHostName();
+ }
}
// ONLY used by HttpsClient to setup the URI specified hostname
@@ -2144,6 +2176,10 @@
this.host = host;
this.serverNames =
Utilities.addToSNIServerNameList(this.serverNames, this.host);
+
+ if (!roleIsServer && (handshaker != null) && !handshaker.started()) {
+ handshaker.setSNIServerNames(serverNames);
+ }
}
/**
@@ -2533,8 +2569,21 @@
// the super implementation does not handle the following parameters
params.setEndpointIdentificationAlgorithm(identificationProtocol);
params.setAlgorithmConstraints(algorithmConstraints);
- params.setSNIMatchers(sniMatchers);
- params.setServerNames(serverNames);
+
+ if (sniMatchers.isEmpty() && !noSniMatcher) {
+ // 'null' indicates none has been set
+ params.setSNIMatchers(null);
+ } else {
+ params.setSNIMatchers(sniMatchers);
+ }
+
+ if (serverNames.isEmpty() && !noSniExtension) {
+ // 'null' indicates none has been set
+ params.setServerNames(null);
+ } else {
+ params.setServerNames(serverNames);
+ }
+
params.setUseCipherSuitesOrder(preferLocalCipherSuites);
params.setMaximumPacketSize(maximumPacketSize);
params.setApplicationProtocols(applicationProtocols);
@@ -2568,11 +2617,13 @@
List<SNIServerName> sniNames = params.getServerNames();
if (sniNames != null) {
+ noSniExtension = sniNames.isEmpty();
serverNames = sniNames;
}
Collection<SNIMatcher> matchers = params.getSNIMatchers();
if (matchers != null) {
+ noSniMatcher = matchers.isEmpty();
sniMatchers = matchers;
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/net/ssl/ServerName/BestEffortOnLazyConnected.java Fri Apr 22 09:27:35 2016 +0000
@@ -0,0 +1,337 @@
+/*
+ * 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.
+ */
+
+//
+// SunJSSE does not support dynamic system properties, no way to re-use
+// system properties in samevm/agentvm mode.
+//
+
+/**
+ * @test
+ * @bug 8144566
+ * @summary Custom HostnameVerifier disables SNI extension
+ * @run main/othervm BestEffortOnLazyConnected
+ */
+
+import java.io.*;
+import java.nio.*;
+import java.nio.channels.*;
+import java.util.*;
+import java.net.*;
+import javax.net.ssl.*;
+
+public class BestEffortOnLazyConnected {
+
+ /*
+ * =============================================================
+ * Set the various variables needed for the tests, then
+ * specify what tests to run on each side.
+ */
+
+ /*
+ * Should we run the client or server in a separate thread?
+ * Both sides can throw exceptions, but do you have a preference
+ * as to which side should be the main thread.
+ */
+ private static final boolean separateServerThread = true;
+
+ /*
+ * Where do we find the keystores?
+ */
+ private static final String pathToStores = "../etc";
+ private static final String keyStoreFile = "keystore";
+ private static final String trustStoreFile = "truststore";
+ private static final String passwd = "passphrase";
+
+ /*
+ * Is the server ready to serve?
+ */
+ private static volatile boolean serverReady = false;
+
+ /*
+ * Turn on SSL debugging?
+ */
+ private static final boolean debug = false;
+
+ /*
+ * the fully qualified domain name of localhost
+ */
+ private static String hostname = null;
+
+ /*
+ * If the client or server is doing some kind of object creation
+ * that the other side depends on, and that thread prematurely
+ * exits, you may experience a hang. The test harness will
+ * terminate all hung threads after its timeout has expired,
+ * currently 3 minutes by default, but you might try to be
+ * smart about it....
+ */
+
+ /*
+ * Define the server side of the test.
+ *
+ * If the server prematurely exits, serverReady will be set to true
+ * to avoid infinite hangs.
+ */
+ private void doServerSide() throws Exception {
+ SSLServerSocketFactory sslssf =
+ (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
+ try (SSLServerSocket sslServerSocket =
+ (SSLServerSocket) sslssf.createServerSocket(serverPort)) {
+
+ serverPort = sslServerSocket.getLocalPort();
+
+ /*
+ * Signal Client, we're ready for his connect.
+ */
+ serverReady = true;
+
+ try (SSLSocket sslSocket = (SSLSocket)sslServerSocket.accept()) {
+ InputStream sslIS = sslSocket.getInputStream();
+ OutputStream sslOS = sslSocket.getOutputStream();
+
+ sslIS.read();
+ sslOS.write(85);
+ sslOS.flush();
+
+ ExtendedSSLSession session =
+ (ExtendedSSLSession)sslSocket.getSession();
+ if (session.getRequestedServerNames().isEmpty()) {
+ throw new Exception("No expected Server Name Indication");
+ }
+ }
+ }
+ }
+
+ /*
+ * Define the client side of the test.
+ *
+ * If the server prematurely exits, serverReady will be set to true
+ * to avoid infinite hangs.
+ */
+ private void doClientSide() throws Exception {
+
+ /*
+ * Wait for server to get started.
+ */
+ while (!serverReady) {
+ Thread.sleep(50);
+ }
+
+ SSLSocketFactory sslsf =
+ (SSLSocketFactory) SSLSocketFactory.getDefault();
+
+ try (SSLSocket sslSocket = (SSLSocket)sslsf.createSocket()) {
+
+ sslSocket.connect(new InetSocketAddress(hostname, serverPort), 0);
+
+ InputStream sslIS = sslSocket.getInputStream();
+ OutputStream sslOS = sslSocket.getOutputStream();
+
+ sslOS.write(280);
+ sslOS.flush();
+ sslIS.read();
+ }
+ }
+
+
+ /*
+ * =============================================================
+ * The remainder is just support stuff
+ */
+
+ // use any free port by default
+ private volatile int serverPort = 0;
+
+ private volatile Exception serverException = null;
+ private volatile Exception clientException = null;
+
+ public static void main(String[] args) throws Exception {
+ String keyFilename =
+ System.getProperty("test.src", ".") + "/" + pathToStores +
+ "/" + keyStoreFile;
+ String trustFilename =
+ System.getProperty("test.src", ".") + "/" + pathToStores +
+ "/" + trustStoreFile;
+
+ System.setProperty("javax.net.ssl.keyStore", keyFilename);
+ System.setProperty("javax.net.ssl.keyStorePassword", passwd);
+ System.setProperty("javax.net.ssl.trustStore", trustFilename);
+ System.setProperty("javax.net.ssl.trustStorePassword", passwd);
+
+ if (debug) {
+ System.setProperty("javax.net.debug", "all");
+ }
+
+ try {
+ hostname = InetAddress.getLocalHost().getCanonicalHostName();
+ } catch (UnknownHostException uhe) {
+ System.out.println(
+ "Ignore the test as the local hostname cannot be determined");
+
+ return;
+ }
+
+ System.out.println(
+ "The fully qualified domain name of the local host is " +
+ hostname);
+ // Ignore the test if the hostname does not sound like a domain name.
+ if ((hostname == null) || hostname.isEmpty() ||
+ hostname.startsWith("localhost") ||
+ Character.isDigit(hostname.charAt(hostname.length() - 1))) {
+
+ System.out.println("Ignore the test as the local hostname " +
+ "cannot be determined as fully qualified domain name");
+
+ return;
+ }
+
+ /*
+ * Start the tests.
+ */
+ new BestEffortOnLazyConnected();
+ }
+
+ private Thread clientThread = null;
+ private Thread serverThread = null;
+
+ /*
+ * Primary constructor, used to drive remainder of the test.
+ *
+ * Fork off the other side, then do your work.
+ */
+ BestEffortOnLazyConnected() throws Exception {
+ try {
+ if (separateServerThread) {
+ startServer(true);
+ startClient(false);
+ } else {
+ startClient(true);
+ startServer(false);
+ }
+ } catch (Exception e) {
+ // swallow for now. Show later
+ }
+
+ /*
+ * Wait for other side to close down.
+ */
+ if (separateServerThread) {
+ serverThread.join();
+ } else {
+ clientThread.join();
+ }
+
+ /*
+ * When we get here, the test is pretty much over.
+ * Which side threw the error?
+ */
+ Exception local;
+ Exception remote;
+ String whichRemote;
+
+ if (separateServerThread) {
+ remote = serverException;
+ local = clientException;
+ whichRemote = "server";
+ } else {
+ remote = clientException;
+ local = serverException;
+ whichRemote = "client";
+ }
+
+ /*
+ * If both failed, return the curthread's exception, but also
+ * print the remote side Exception
+ */
+ if ((local != null) && (remote != null)) {
+ System.out.println(whichRemote + " also threw:");
+ remote.printStackTrace();
+ System.out.println();
+ throw local;
+ }
+
+ if (remote != null) {
+ throw remote;
+ }
+
+ if (local != null) {
+ throw local;
+ }
+ }
+
+ private void startServer(boolean newThread) throws Exception {
+ if (newThread) {
+ serverThread = new Thread() {
+ public void run() {
+ try {
+ doServerSide();
+ } catch (Exception e) {
+ /*
+ * Our server thread just died.
+ *
+ * Release the client, if not active already...
+ */
+ System.err.println("Server died...");
+ serverReady = true;
+ serverException = e;
+ }
+ }
+ };
+ serverThread.start();
+ } else {
+ try {
+ doServerSide();
+ } catch (Exception e) {
+ serverException = e;
+ } finally {
+ serverReady = true;
+ }
+ }
+ }
+
+ private void startClient(boolean newThread) throws Exception {
+ if (newThread) {
+ clientThread = new Thread() {
+ public void run() {
+ try {
+ doClientSide();
+ } catch (Exception e) {
+ /*
+ * Our client thread just died.
+ */
+ System.err.println("Client died...");
+ clientException = e;
+ }
+ }
+ };
+ clientThread.start();
+ } else {
+ try {
+ doClientSide();
+ } catch (Exception e) {
+ clientException = e;
+ }
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/sun/net/www/protocol/https/HttpsURLConnection/ImpactOnSNI.java Fri Apr 22 09:27:35 2016 +0000
@@ -0,0 +1,390 @@
+/*
+ * 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.
+ */
+
+//
+// SunJSSE does not support dynamic system properties, no way to re-use
+// system properties in samevm/agentvm mode.
+//
+
+/*
+ * @test
+ * @bug 8144566
+ * @summary Custom HostnameVerifier disables SNI extension
+ * @run main/othervm ImpactOnSNI
+ */
+
+import java.io.*;
+import java.net.*;
+import javax.net.ssl.*;
+
+public class ImpactOnSNI {
+
+ /*
+ * =============================================================
+ * Set the various variables needed for the tests, then
+ * specify what tests to run on each side.
+ */
+
+ /*
+ * Should we run the client or server in a separate thread?
+ * Both sides can throw exceptions, but do you have a preference
+ * as to which side should be the main thread.
+ */
+ private static final boolean separateServerThread = true;
+
+ /*
+ * Where do we find the keystores?
+ */
+ private static final String pathToStores =
+ "../../../../../../javax/net/ssl/etc";
+ private static final String keyStoreFile = "keystore";
+ private static final String trustStoreFile = "truststore";
+ private static final String passwd = "passphrase";
+
+ /*
+ * Is the server ready to serve?
+ */
+ private static volatile boolean serverReady = false;
+
+ /*
+ * Is the connection ready to close?
+ */
+ private static volatile boolean closeReady = false;
+
+ /*
+ * Turn on SSL debugging?
+ */
+ private static final boolean debug = false;
+
+ /*
+ * Message posted
+ */
+ private static final String postMsg = "HTTP post on a https server";
+
+ /*
+ * the fully qualified domain name of localhost
+ */
+ private static String hostname = null;
+
+ /*
+ * If the client or server is doing some kind of object creation
+ * that the other side depends on, and that thread prematurely
+ * exits, you may experience a hang. The test harness will
+ * terminate all hung threads after its timeout has expired,
+ * currently 3 minutes by default, but you might try to be
+ * smart about it....
+ */
+
+ /*
+ * Define the server side of the test.
+ *
+ * If the server prematurely exits, serverReady will be set to true
+ * to avoid infinite hangs.
+ */
+ private void doServerSide() throws Exception {
+ SSLServerSocketFactory sslssf =
+ (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
+ try (SSLServerSocket sslServerSocket =
+ (SSLServerSocket)sslssf.createServerSocket(serverPort)) {
+
+ serverPort = sslServerSocket.getLocalPort();
+
+ /*
+ * Signal Client, we're ready for his connect.
+ */
+ serverReady = true;
+
+ /*
+ * Accept connections
+ */
+ try (SSLSocket sslSocket = (SSLSocket)sslServerSocket.accept()) {
+ InputStream sslIS = sslSocket.getInputStream();
+ OutputStream sslOS = sslSocket.getOutputStream();
+ BufferedReader br =
+ new BufferedReader(new InputStreamReader(sslIS));
+ PrintStream ps = new PrintStream(sslOS);
+
+ // process HTTP POST request from client
+ System.out.println("status line: " + br.readLine());
+ String msg = null;
+ while ((msg = br.readLine()) != null && msg.length() > 0);
+
+ msg = br.readLine();
+ if (msg.equals(postMsg)) {
+ ps.println("HTTP/1.1 200 OK\n\n");
+ } else {
+ ps.println("HTTP/1.1 500 Not OK\n\n");
+ }
+ ps.flush();
+
+ ExtendedSSLSession session =
+ (ExtendedSSLSession)sslSocket.getSession();
+ if (session.getRequestedServerNames().isEmpty()) {
+ throw new Exception("No expected Server Name Indication");
+ }
+
+ // close the socket
+ while (!closeReady) {
+ Thread.sleep(50);
+ }
+ }
+ }
+ }
+
+ /*
+ * Define the client side of the test.
+ *
+ * If the server prematurely exits, serverReady will be set to true
+ * to avoid infinite hangs.
+ */
+ private void doClientSide() throws Exception {
+ /*
+ * Wait for server to get started.
+ */
+ while (!serverReady) {
+ Thread.sleep(50);
+ }
+
+ // Send HTTP POST request to server
+ URL url = new URL("https://" + hostname + ":" + serverPort);
+
+ HttpsURLConnection.setDefaultHostnameVerifier(new NameVerifier());
+ HttpsURLConnection http = (HttpsURLConnection)url.openConnection();
+ http.setDoOutput(true);
+
+ http.setRequestMethod("POST");
+ PrintStream ps = new PrintStream(http.getOutputStream());
+ try {
+ ps.println(postMsg);
+ ps.flush();
+ if (http.getResponseCode() != 200) {
+ throw new RuntimeException("test Failed");
+ }
+ } finally {
+ ps.close();
+ http.disconnect();
+ closeReady = true;
+ }
+ }
+
+ private static class NameVerifier implements HostnameVerifier {
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ }
+
+ /*
+ * =============================================================
+ * The remainder is just support stuff
+ */
+
+ // use any free port by default
+ private volatile int serverPort = 0;
+
+ private volatile Exception serverException = null;
+ private volatile Exception clientException = null;
+
+ public static void main(String[] args) throws Exception {
+ String keyFilename =
+ System.getProperty("test.src", "./") + "/" + pathToStores +
+ "/" + keyStoreFile;
+ String trustFilename =
+ System.getProperty("test.src", "./") + "/" + pathToStores +
+ "/" + trustStoreFile;
+
+ System.setProperty("javax.net.ssl.keyStore", keyFilename);
+ System.setProperty("javax.net.ssl.keyStorePassword", passwd);
+ System.setProperty("javax.net.ssl.trustStore", trustFilename);
+ System.setProperty("javax.net.ssl.trustStorePassword", passwd);
+
+ if (debug) {
+ System.setProperty("javax.net.debug", "all");
+ }
+
+ try {
+ hostname = InetAddress.getLocalHost().getCanonicalHostName();
+ } catch (UnknownHostException uhe) {
+ System.out.println(
+ "Ignore the test as the local hostname cannot be determined");
+
+ return;
+ }
+
+ System.out.println(
+ "The fully qualified domain name of the local host is " +
+ hostname);
+ // Ignore the test if the hostname does not sound like a domain name.
+ if ((hostname == null) || hostname.isEmpty() ||
+ hostname.startsWith("localhost") ||
+ Character.isDigit(hostname.charAt(hostname.length() - 1))) {
+
+ System.out.println("Ignore the test as the local hostname " +
+ "cannot be determined as fully qualified domain name");
+
+ return;
+ }
+
+ /*
+ * Start the tests.
+ */
+ new ImpactOnSNI();
+ }
+
+ private Thread clientThread = null;
+ private Thread serverThread = null;
+
+ /*
+ * Primary constructor, used to drive remainder of the test.
+ *
+ * Fork off the other side, then do your work.
+ */
+ ImpactOnSNI() throws Exception {
+ Exception startException = null;
+ try {
+ if (separateServerThread) {
+ startServer(true);
+ startClient(false);
+ } else {
+ startClient(true);
+ startServer(false);
+ }
+ } catch (Exception e) {
+ startException = e;
+ }
+
+ /*
+ * Wait for other side to close down.
+ */
+ if (separateServerThread) {
+ if (serverThread != null) {
+ serverThread.join();
+ }
+ } else {
+ if (clientThread != null) {
+ clientThread.join();
+ }
+ }
+
+ /*
+ * When we get here, the test is pretty much over.
+ * Which side threw the error?
+ */
+ Exception local;
+ Exception remote;
+
+ if (separateServerThread) {
+ remote = serverException;
+ local = clientException;
+ } else {
+ remote = clientException;
+ local = serverException;
+ }
+
+ Exception exception = null;
+
+ /*
+ * Check various exception conditions.
+ */
+ if ((local != null) && (remote != null)) {
+ // If both failed, return the curthread's exception.
+ local.initCause(remote);
+ exception = local;
+ } else if (local != null) {
+ exception = local;
+ } else if (remote != null) {
+ exception = remote;
+ } else if (startException != null) {
+ exception = startException;
+ }
+
+ /*
+ * If there was an exception *AND* a startException,
+ * output it.
+ */
+ if (exception != null) {
+ if (exception != startException && startException != null) {
+ exception.addSuppressed(startException);
+ }
+ throw exception;
+ }
+
+ // Fall-through: no exception to throw!
+ }
+
+ private void startServer(boolean newThread) throws Exception {
+ if (newThread) {
+ serverThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ doServerSide();
+ } catch (Exception e) {
+ /*
+ * Our server thread just died.
+ *
+ * Release the client, if not active already...
+ */
+ System.err.println("Server died...");
+ serverReady = true;
+ serverException = e;
+ }
+ }
+ };
+ serverThread.start();
+ } else {
+ try {
+ doServerSide();
+ } catch (Exception e) {
+ serverException = e;
+ } finally {
+ serverReady = true;
+ }
+ }
+ }
+
+ private void startClient(boolean newThread) throws Exception {
+ if (newThread) {
+ clientThread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ doClientSide();
+ } catch (Exception e) {
+ /*
+ * Our client thread just died.
+ */
+ System.err.println("Client died...");
+ clientException = e;
+ }
+ }
+ };
+ clientThread.start();
+ } else {
+ try {
+ doClientSide();
+ } catch (Exception e) {
+ clientException = e;
+ }
+ }
+ }
+}