提交 5c63b902 编写于 作者: A apetcher

8206929: Check session context for TLS 1.3 session resumption

Summary: additional checks to prevent TLS 1.3 sessions from being resumed when they shouldn't
Reviewed-by: xuelei
上级 5f93f267
......@@ -27,6 +27,7 @@ package sun.security.ssl;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
......@@ -46,6 +47,9 @@ final class PostHandshakeContext extends HandshakeContext {
"Post-handshake not supported in " + negotiatedProtocol.name);
}
this.localSupportedSignAlgs = new ArrayList<SignatureScheme>(
context.conSession.getLocalSupportedSignatureSchemes());
handshakeConsumers = new LinkedHashMap<>(consumers);
handshakeFinished = true;
}
......
......@@ -33,8 +33,11 @@ import java.util.ArrayList;
import java.util.Locale;
import java.util.Arrays;
import java.util.Optional;
import java.util.Collection;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.net.ssl.SSLPeerUnverifiedException;
import static sun.security.ssl.ClientAuthType.CLIENT_AUTH_REQUIRED;
import sun.security.ssl.ClientHello.ClientHelloMessage;
import sun.security.ssl.SSLExtension.ExtensionConsumer;
import sun.security.ssl.SSLExtension.SSLExtensionSpec;
......@@ -167,7 +170,7 @@ final class PreSharedKeyExtension {
int getIdsEncodedLength() {
int idEncodedLength = 0;
for(PskIdentity curId : identities) {
for (PskIdentity curId : identities) {
idEncodedLength += curId.getEncodedLength();
}
......@@ -190,7 +193,7 @@ final class PreSharedKeyExtension {
byte[] buffer = new byte[encodedLength];
ByteBuffer m = ByteBuffer.wrap(buffer);
Record.putInt16(m, idsEncodedLength);
for(PskIdentity curId : identities) {
for (PskIdentity curId : identities) {
curId.writeEncoded(m);
}
Record.putInt16(m, bindersEncodedLength);
......@@ -220,7 +223,7 @@ final class PreSharedKeyExtension {
String identitiesString() {
StringBuilder result = new StringBuilder();
for(PskIdentity curId : identities) {
for (PskIdentity curId : identities) {
result.append(curId.toString() + "\n");
}
......@@ -229,7 +232,7 @@ final class PreSharedKeyExtension {
String bindersString() {
StringBuilder result = new StringBuilder();
for(byte[] curBinder : binders) {
for (byte[] curBinder : binders) {
result.append("{" + Utilities.toHexString(curBinder) + "}\n");
}
......@@ -328,6 +331,7 @@ final class PreSharedKeyExtension {
public void consume(ConnectionContext context,
HandshakeMessage message,
ByteBuffer buffer) throws IOException {
ClientHelloMessage clientHello = (ClientHelloMessage) message;
ServerHandshakeContext shc = (ServerHandshakeContext)context;
// Is it a supported and enabled extension?
if (!shc.sslConfig.isAvailable(SSLExtension.CH_PRE_SHARED_KEY)) {
......@@ -367,8 +371,7 @@ final class PreSharedKeyExtension {
int idIndex = 0;
for (PskIdentity requestedId : pskSpec.identities) {
SSLSessionImpl s = sessionCache.get(requestedId.identity);
if (s != null && s.isRejoinable() &&
s.getPreSharedKey().isPresent()) {
if (s != null && canRejoin(clientHello, shc, s)) {
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
SSLLogger.fine("Resuming session: ", s);
}
......@@ -392,10 +395,68 @@ final class PreSharedKeyExtension {
// update the context
shc.handshakeExtensions.put(
SSLExtension.CH_PRE_SHARED_KEY, pskSpec);
SSLExtension.CH_PRE_SHARED_KEY, pskSpec);
}
}
private static boolean canRejoin(ClientHelloMessage clientHello,
ServerHandshakeContext shc, SSLSessionImpl s) {
boolean result = s.isRejoinable() && s.getPreSharedKey().isPresent();
// Check protocol version
if (result && s.getProtocolVersion() != shc.negotiatedProtocol) {
if (SSLLogger.isOn &&
SSLLogger.isOn("ssl,handshake,verbose")) {
SSLLogger.finest("Can't resume, incorrect protocol version");
}
result = false;
}
// Validate the required client authentication.
if (result &&
(shc.sslConfig.clientAuthType == CLIENT_AUTH_REQUIRED)) {
try {
s.getPeerPrincipal();
} catch (SSLPeerUnverifiedException e) {
if (SSLLogger.isOn &&
SSLLogger.isOn("ssl,handshake,verbose")) {
SSLLogger.finest(
"Can't resume, " +
"client authentication is required");
}
result = false;
}
// Make sure the list of supported signature algorithms matches
Collection<SignatureScheme> sessionSigAlgs =
s.getLocalSupportedSignatureSchemes();
if (result &&
!shc.localSupportedSignAlgs.containsAll(sessionSigAlgs)) {
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
SSLLogger.fine("Can't resume. Session uses different " +
"signature algorithms");
}
result = false;
}
}
// Ensure cipher suite can be negotiated
if (result && (!shc.isNegotiable(s.getSuite()) ||
!clientHello.cipherSuites.contains(s.getSuite()))) {
if (SSLLogger.isOn &&
SSLLogger.isOn("ssl,handshake,verbose")) {
SSLLogger.finest(
"Can't resume, unavailable session cipher suite");
}
result = false;
}
return result;
}
private static final
class CHPreSharedKeyUpdate implements HandshakeConsumer {
// Prevent instantiation of this class.
......@@ -547,6 +608,18 @@ final class PreSharedKeyExtension {
return null;
}
// Make sure the list of supported signature algorithms matches
Collection<SignatureScheme> sessionSigAlgs =
chc.resumingSession.getLocalSupportedSignatureSchemes();
if (!chc.localSupportedSignAlgs.containsAll(sessionSigAlgs)) {
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
SSLLogger.fine("Existing session uses different " +
"signature algorithms");
}
return null;
}
// The session must have a pre-shared key
Optional<SecretKey> pskOpt = chc.resumingSession.getPreSharedKey();
if (!pskOpt.isPresent()) {
if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) {
......@@ -658,7 +731,7 @@ final class PreSharedKeyExtension {
} catch (NoSuchAlgorithmException | InvalidKeyException ex) {
throw new IOException(ex);
}
} catch(GeneralSecurityException ex) {
} catch (GeneralSecurityException ex) {
throw new IOException(ex);
}
}
......
......@@ -96,7 +96,7 @@ final class SSLSessionImpl extends ExtendedSSLSession {
private boolean invalidated;
private X509Certificate[] localCerts;
private PrivateKey localPrivateKey;
private final String[] localSupportedSignAlgs;
private final Collection<SignatureScheme> localSupportedSignAlgs;
private String[] peerSupportedSignAlgs; // for certificate
private boolean useDefaultPeerSignAlgs = false;
private List<byte[]> statusResponses;
......@@ -144,7 +144,7 @@ final class SSLSessionImpl extends ExtendedSSLSession {
this.sessionId = new SessionId(false, null);
this.host = null;
this.port = -1;
this.localSupportedSignAlgs = new String[0];
this.localSupportedSignAlgs = Collections.emptySet();
this.serverNameIndication = null;
this.requestedServerNames = Collections.<SNIServerName>emptyList();
this.useExtendedMasterSecret = false;
......@@ -179,8 +179,9 @@ final class SSLSessionImpl extends ExtendedSSLSession {
this.sessionId = id;
this.host = hc.conContext.transport.getPeerHost();
this.port = hc.conContext.transport.getPeerPort();
this.localSupportedSignAlgs =
SignatureScheme.getAlgorithmNames(hc.localSupportedSignAlgs);
this.localSupportedSignAlgs = hc.localSupportedSignAlgs == null ?
Collections.emptySet() :
Collections.unmodifiableCollection(hc.localSupportedSignAlgs);
this.serverNameIndication = hc.negotiatedServerName;
this.requestedServerNames = Collections.<SNIServerName>unmodifiableList(
hc.getRequestedServerNames());
......@@ -969,16 +970,20 @@ final class SSLSessionImpl extends ExtendedSSLSession {
}
/**
* Gets an array of supported signature algorithms that the local side is
* willing to verify.
* Gets an array of supported signature algorithm names that the local
* side is willing to verify.
*/
@Override
public String[] getLocalSupportedSignatureAlgorithms() {
if (localSupportedSignAlgs != null) {
return localSupportedSignAlgs.clone();
}
return SignatureScheme.getAlgorithmNames(localSupportedSignAlgs);
}
return new String[0];
/**
* Gets an array of supported signature schemes that the local side is
* willing to verify.
*/
public Collection<SignatureScheme> getLocalSupportedSignatureSchemes() {
return localSupportedSignAlgs;
}
/**
......
/*
* 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.
*/
/*
* @test
* @bug 8206929
* @summary ensure that client only resumes a session if certain properties
* of the session are compatible with the new connection
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.2 ResumeChecksClient BASIC
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksClient BASIC
* @run main/othervm ResumeChecksClient BASIC
* @run main/othervm ResumeChecksClient VERSION_2_TO_3
* @run main/othervm ResumeChecksClient VERSION_3_TO_2
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksClient CIPHER_SUITE
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksClient SIGNATURE_SCHEME
*
*/
import javax.net.*;
import javax.net.ssl.*;
import java.io.*;
import java.security.*;
import java.net.*;
import java.util.*;
public class ResumeChecksClient {
static String pathToStores = "../../../../javax/net/ssl/etc";
static String keyStoreFile = "keystore";
static String trustStoreFile = "truststore";
static String passwd = "passphrase";
enum TestMode {
BASIC,
VERSION_2_TO_3,
VERSION_3_TO_2,
CIPHER_SUITE,
SIGNATURE_SCHEME
}
public static void main(String[] args) throws Exception {
TestMode mode = TestMode.valueOf(args[0]);
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);
Server server = startServer();
server.signal();
SSLContext sslContext = SSLContext.getDefault();
while (!server.started) {
Thread.yield();
}
connect(sslContext, server.port, mode, false);
server.signal();
long secondStartTime = System.currentTimeMillis();
Thread.sleep(10);
SSLSession secondSession = connect(sslContext, server.port, mode, true);
server.go = false;
server.signal();
switch (mode) {
case BASIC:
// fail if session is not resumed
if (secondSession.getCreationTime() > secondStartTime) {
throw new RuntimeException("Session was not reused");
}
break;
case VERSION_2_TO_3:
case VERSION_3_TO_2:
case CIPHER_SUITE:
case SIGNATURE_SCHEME:
// fail if a new session is not created
if (secondSession.getCreationTime() <= secondStartTime) {
throw new RuntimeException("Existing session was used");
}
break;
default:
throw new RuntimeException("unknown mode: " + mode);
}
}
private static class NoSig implements AlgorithmConstraints {
private final String alg;
NoSig(String alg) {
this.alg = alg;
}
private boolean test(String a) {
return !a.toLowerCase().contains(alg.toLowerCase());
}
public boolean permits(Set<CryptoPrimitive> primitives, Key key) {
return true;
}
public boolean permits(Set<CryptoPrimitive> primitives,
String algorithm, AlgorithmParameters parameters) {
return test(algorithm);
}
public boolean permits(Set<CryptoPrimitive> primitives,
String algorithm, Key key, AlgorithmParameters parameters) {
return test(algorithm);
}
}
private static SSLSession connect(SSLContext sslContext, int port,
TestMode mode, boolean second) {
try {
SSLSocket sock = (SSLSocket)
sslContext.getSocketFactory().createSocket();
SSLParameters params = sock.getSSLParameters();
switch (mode) {
case BASIC:
// do nothing to ensure resumption works
break;
case VERSION_2_TO_3:
if (second) {
params.setProtocols(new String[] {"TLSv1.3"});
} else {
params.setProtocols(new String[] {"TLSv1.2"});
}
break;
case VERSION_3_TO_2:
if (second) {
params.setProtocols(new String[] {"TLSv1.2"});
} else {
params.setProtocols(new String[] {"TLSv1.3"});
}
break;
case CIPHER_SUITE:
if (second) {
params.setCipherSuites(
new String[] {"TLS_AES_256_GCM_SHA384"});
} else {
params.setCipherSuites(
new String[] {"TLS_AES_128_GCM_SHA256"});
}
break;
case SIGNATURE_SCHEME:
AlgorithmConstraints constraints =
params.getAlgorithmConstraints();
if (second) {
params.setAlgorithmConstraints(new NoSig("ecdsa"));
} else {
params.setAlgorithmConstraints(new NoSig("rsa"));
}
break;
default:
throw new RuntimeException("unknown mode: " + mode);
}
sock.setSSLParameters(params);
sock.connect(new InetSocketAddress("localhost", port));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(sock.getOutputStream()));
out.println("message");
out.flush();
BufferedReader reader = new BufferedReader(
new InputStreamReader(sock.getInputStream()));
String inMsg = reader.readLine();
System.out.println("Client received: " + inMsg);
SSLSession result = sock.getSession();
sock.close();
return result;
} catch (Exception ex) {
// unexpected exception
throw new RuntimeException(ex);
}
}
private static Server startServer() {
Server server = new Server();
new Thread(server).start();
return server;
}
private static class Server implements Runnable {
public volatile boolean go = true;
private boolean signal = false;
public volatile int port = 0;
public volatile boolean started = false;
private synchronized void waitForSignal() {
while (!signal) {
try {
wait();
} catch (InterruptedException ex) {
// do nothing
}
}
signal = false;
}
public synchronized void signal() {
signal = true;
notify();
}
public void run() {
try {
SSLContext sc = SSLContext.getDefault();
ServerSocketFactory fac = sc.getServerSocketFactory();
SSLServerSocket ssock = (SSLServerSocket)
fac.createServerSocket(0);
this.port = ssock.getLocalPort();
waitForSignal();
started = true;
while (go) {
try {
System.out.println("Waiting for connection");
Socket sock = ssock.accept();
BufferedReader reader = new BufferedReader(
new InputStreamReader(sock.getInputStream()));
String line = reader.readLine();
System.out.println("server read: " + line);
PrintWriter out = new PrintWriter(
new OutputStreamWriter(sock.getOutputStream()));
out.println(line);
out.flush();
waitForSignal();
} catch (Exception ex) {
ex.printStackTrace();
}
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
}
/*
* 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.
*/
/*
* @test
* @bug 8206929
* @summary ensure that server only resumes a session if certain properties
* of the session are compatible with the new connection
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.2 ResumeChecksServer BASIC
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksServer BASIC
* @run main/othervm ResumeChecksServer BASIC
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.2 ResumeChecksServer CLIENT_AUTH
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksServer CLIENT_AUTH
* @run main/othervm ResumeChecksServer CLIENT_AUTH
* @run main/othervm ResumeChecksServer VERSION_2_TO_3
* @run main/othervm ResumeChecksServer VERSION_3_TO_2
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksServer CIPHER_SUITE
* @run main/othervm -Djdk.tls.client.protocols=TLSv1.3 ResumeChecksServer SIGNATURE_SCHEME
*
*/
import javax.net.*;
import javax.net.ssl.*;
import java.io.*;
import java.security.*;
import java.net.*;
import java.util.*;
public class ResumeChecksServer {
static String pathToStores = "../../../../javax/net/ssl/etc";
static String keyStoreFile = "keystore";
static String trustStoreFile = "truststore";
static String passwd = "passphrase";
enum TestMode {
BASIC,
CLIENT_AUTH,
VERSION_2_TO_3,
VERSION_3_TO_2,
CIPHER_SUITE,
SIGNATURE_SCHEME
}
public static void main(String[] args) throws Exception {
TestMode mode = TestMode.valueOf(args[0]);
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);
SSLSession secondSession = null;
SSLContext sslContext = SSLContext.getDefault();
ServerSocketFactory fac = sslContext.getServerSocketFactory();
SSLServerSocket ssock = (SSLServerSocket)
fac.createServerSocket(0);
Client client = startClient(ssock.getLocalPort());
try {
connect(client, ssock, mode, false);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
long secondStartTime = System.currentTimeMillis();
Thread.sleep(10);
try {
secondSession = connect(client, ssock, mode, true);
} catch (SSLHandshakeException ex) {
// this is expected
} catch (Exception ex) {
throw new RuntimeException(ex);
}
client.go = false;
client.signal();
switch (mode) {
case BASIC:
// fail if session is not resumed
if (secondSession.getCreationTime() > secondStartTime) {
throw new RuntimeException("Session was not reused");
}
break;
case CLIENT_AUTH:
// throws an exception if the client is not authenticated
secondSession.getPeerCertificates();
break;
case VERSION_2_TO_3:
case VERSION_3_TO_2:
case CIPHER_SUITE:
case SIGNATURE_SCHEME:
// fail if a new session is not created
if (secondSession.getCreationTime() <= secondStartTime) {
throw new RuntimeException("Existing session was used");
}
break;
default:
throw new RuntimeException("unknown mode: " + mode);
}
}
private static class NoSig implements AlgorithmConstraints {
private final String alg;
NoSig(String alg) {
this.alg = alg;
}
private boolean test(String a) {
return !a.toLowerCase().contains(alg.toLowerCase());
}
public boolean permits(Set<CryptoPrimitive> primitives, Key key) {
return true;
}
public boolean permits(Set<CryptoPrimitive> primitives,
String algorithm, AlgorithmParameters parameters) {
return test(algorithm);
}
public boolean permits(Set<CryptoPrimitive> primitives,
String algorithm, Key key, AlgorithmParameters parameters) {
return test(algorithm);
}
}
private static SSLSession connect(Client client, SSLServerSocket ssock,
TestMode mode, boolean second) throws Exception {
try {
client.signal();
System.out.println("Waiting for connection");
SSLSocket sock = (SSLSocket) ssock.accept();
SSLParameters params = sock.getSSLParameters();
switch (mode) {
case BASIC:
// do nothing to ensure resumption works
break;
case CLIENT_AUTH:
if (second) {
params.setNeedClientAuth(true);
} else {
params.setNeedClientAuth(false);
}
break;
case VERSION_2_TO_3:
if (second) {
params.setProtocols(new String[] {"TLSv1.3"});
} else {
params.setProtocols(new String[] {"TLSv1.2"});
}
break;
case VERSION_3_TO_2:
if (second) {
params.setProtocols(new String[] {"TLSv1.2"});
} else {
params.setProtocols(new String[] {"TLSv1.3"});
}
break;
case CIPHER_SUITE:
if (second) {
params.setCipherSuites(
new String[] {"TLS_AES_128_GCM_SHA256"});
} else {
params.setCipherSuites(
new String[] {"TLS_AES_256_GCM_SHA384"});
}
break;
case SIGNATURE_SCHEME:
params.setNeedClientAuth(true);
AlgorithmConstraints constraints =
params.getAlgorithmConstraints();
if (second) {
params.setAlgorithmConstraints(new NoSig("ecdsa"));
} else {
params.setAlgorithmConstraints(new NoSig("rsa"));
}
break;
default:
throw new RuntimeException("unknown mode: " + mode);
}
sock.setSSLParameters(params);
BufferedReader reader = new BufferedReader(
new InputStreamReader(sock.getInputStream()));
String line = reader.readLine();
System.out.println("server read: " + line);
PrintWriter out = new PrintWriter(
new OutputStreamWriter(sock.getOutputStream()));
out.println(line);
out.flush();
out.close();
SSLSession result = sock.getSession();
sock.close();
return result;
} catch (SSLHandshakeException ex) {
if (!second) {
throw ex;
}
}
return null;
}
private static Client startClient(int port) {
Client client = new Client(port);
new Thread(client).start();
return client;
}
private static class Client implements Runnable {
public volatile boolean go = true;
private boolean signal = false;
private final int port;
Client(int port) {
this.port = port;
}
private synchronized void waitForSignal() {
while (!signal) {
try {
wait();
} catch (InterruptedException ex) {
// do nothing
}
}
signal = false;
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
// do nothing
}
}
public synchronized void signal() {
signal = true;
notify();
}
public void run() {
try {
SSLContext sc = SSLContext.getDefault();
waitForSignal();
while (go) {
try {
SSLSocket sock = (SSLSocket)
sc.getSocketFactory().createSocket();
sock.connect(new InetSocketAddress("localhost", port));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(sock.getOutputStream()));
out.println("message");
out.flush();
BufferedReader reader = new BufferedReader(
new InputStreamReader(sock.getInputStream()));
String inMsg = reader.readLine();
System.out.println("Client received: " + inMsg);
out.close();
sock.close();
waitForSignal();
} catch (Exception ex) {
ex.printStackTrace();
}
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册