mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
Stub implementation of the invitation protocol (works on Android).
This commit is contained in:
121
src/net/sf/briar/invitation/AliceConnector.java
Normal file
121
src/net/sf/briar/invitation/AliceConnector.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package net.sf.briar.invitation;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static net.sf.briar.api.plugins.InvitationConstants.HASH_LENGTH;
|
||||
import static net.sf.briar.api.plugins.InvitationConstants.INVITATION_TIMEOUT;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import net.sf.briar.api.crypto.PseudoRandom;
|
||||
import net.sf.briar.api.invitation.ConnectionCallback;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
|
||||
|
||||
class AliceConnector extends Thread {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(AliceConnector.class.getName());
|
||||
|
||||
private final DuplexPlugin plugin;
|
||||
private final PseudoRandom random;
|
||||
private final ConnectionCallback callback;
|
||||
private final AtomicBoolean connected, succeeded;
|
||||
private final String pluginName;
|
||||
|
||||
AliceConnector(DuplexPlugin plugin, PseudoRandom random,
|
||||
ConnectionCallback callback, AtomicBoolean connected,
|
||||
AtomicBoolean succeeded) {
|
||||
this.plugin = plugin;
|
||||
this.random = random;
|
||||
this.callback = callback;
|
||||
this.connected = connected;
|
||||
this.succeeded = succeeded;
|
||||
pluginName = plugin.getClass().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long halfTime = System.currentTimeMillis() + INVITATION_TIMEOUT / 2;
|
||||
DuplexTransportConnection conn = makeOutgoingConnection();
|
||||
if(conn == null) conn = acceptIncomingConnection(halfTime);
|
||||
if(conn == null) return;
|
||||
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
|
||||
// Don't proceed with more than one connection
|
||||
if(connected.getAndSet(true)) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " redundant");
|
||||
tryToClose(conn, false);
|
||||
return;
|
||||
}
|
||||
// FIXME: Carry out the real invitation protocol
|
||||
InputStream in;
|
||||
try {
|
||||
in = conn.getInputStream();
|
||||
OutputStream out = conn.getOutputStream();
|
||||
byte[] hash = random.nextBytes(HASH_LENGTH);
|
||||
out.write(hash);
|
||||
out.flush();
|
||||
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " sent hash");
|
||||
int offset = 0;
|
||||
while(offset < hash.length) {
|
||||
int read = in.read(hash, offset, hash.length - offset);
|
||||
if(read == -1) break;
|
||||
offset += read;
|
||||
}
|
||||
if(offset < HASH_LENGTH) throw new EOFException();
|
||||
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " received hash");
|
||||
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " succeeded");
|
||||
succeeded.set(true);
|
||||
callback.connectionEstablished(123456, 123456,
|
||||
new ConfirmationSender(out));
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
|
||||
tryToClose(conn, true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if(in.read() == 1) callback.codesMatch();
|
||||
else callback.codesDoNotMatch();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
|
||||
tryToClose(conn, true);
|
||||
callback.codesDoNotMatch();
|
||||
}
|
||||
}
|
||||
|
||||
private DuplexTransportConnection makeOutgoingConnection() {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " making outgoing connection");
|
||||
return plugin.sendInvitation(random, INVITATION_TIMEOUT / 2);
|
||||
}
|
||||
|
||||
private DuplexTransportConnection acceptIncomingConnection(long halfTime) {
|
||||
long now = System.currentTimeMillis();
|
||||
if(now < halfTime) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " sleeping until half-time");
|
||||
try {
|
||||
Thread.sleep(halfTime - now);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Interrupted while sleeping");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " accepting incoming connection");
|
||||
return plugin.acceptInvitation(random, INVITATION_TIMEOUT / 2);
|
||||
}
|
||||
|
||||
private void tryToClose(DuplexTransportConnection conn, boolean exception) {
|
||||
try {
|
||||
conn.dispose(exception, true);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/net/sf/briar/invitation/BobConnector.java
Normal file
121
src/net/sf/briar/invitation/BobConnector.java
Normal file
@@ -0,0 +1,121 @@
|
||||
package net.sf.briar.invitation;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static net.sf.briar.api.plugins.InvitationConstants.HASH_LENGTH;
|
||||
import static net.sf.briar.api.plugins.InvitationConstants.INVITATION_TIMEOUT;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import net.sf.briar.api.crypto.PseudoRandom;
|
||||
import net.sf.briar.api.invitation.ConnectionCallback;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexPlugin;
|
||||
import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
|
||||
|
||||
class BobConnector extends Thread {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(BobConnector.class.getName());
|
||||
|
||||
private final DuplexPlugin plugin;
|
||||
private final PseudoRandom random;
|
||||
private final ConnectionCallback callback;
|
||||
private final AtomicBoolean connected, succeeded;
|
||||
private final String pluginName;
|
||||
|
||||
BobConnector(DuplexPlugin plugin, PseudoRandom random,
|
||||
ConnectionCallback callback, AtomicBoolean connected,
|
||||
AtomicBoolean succeeded) {
|
||||
this.plugin = plugin;
|
||||
this.random = random;
|
||||
this.callback = callback;
|
||||
this.connected = connected;
|
||||
this.succeeded = succeeded;
|
||||
pluginName = plugin.getClass().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long halfTime = System.currentTimeMillis() + INVITATION_TIMEOUT / 2;
|
||||
DuplexTransportConnection conn = acceptIncomingConnection();
|
||||
if(conn == null) conn = makeOutgoingConnection(halfTime);
|
||||
if(conn == null) return;
|
||||
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " connected");
|
||||
// FIXME: Carry out the real invitation protocol
|
||||
InputStream in;
|
||||
try {
|
||||
in = conn.getInputStream();
|
||||
OutputStream out = conn.getOutputStream();
|
||||
byte[] hash = new byte[HASH_LENGTH];
|
||||
int offset = 0;
|
||||
while(offset < hash.length) {
|
||||
int read = in.read(hash, offset, hash.length - offset);
|
||||
if(read == -1) break;
|
||||
offset += read;
|
||||
}
|
||||
if(offset < HASH_LENGTH) throw new EOFException();
|
||||
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " received hash");
|
||||
// Don't proceed with more than one connection
|
||||
if(connected.getAndSet(true)) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " redundant");
|
||||
tryToClose(conn, false);
|
||||
return;
|
||||
}
|
||||
out.write(hash);
|
||||
out.flush();
|
||||
if(LOG.isLoggable(INFO)) LOG.info(pluginName + " sent hash");
|
||||
succeeded.set(true);
|
||||
callback.connectionEstablished(123456, 123456,
|
||||
new ConfirmationSender(out));
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
|
||||
tryToClose(conn, true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if(in.read() == 1) callback.codesMatch();
|
||||
else callback.codesDoNotMatch();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
|
||||
tryToClose(conn, true);
|
||||
callback.codesDoNotMatch();
|
||||
}
|
||||
}
|
||||
|
||||
private DuplexTransportConnection acceptIncomingConnection() {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " accepting incoming connection");
|
||||
return plugin.acceptInvitation(random, INVITATION_TIMEOUT / 2);
|
||||
}
|
||||
|
||||
private DuplexTransportConnection makeOutgoingConnection(long halfTime) {
|
||||
long now = System.currentTimeMillis();
|
||||
if(now < halfTime) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " sleeping until half-time");
|
||||
try {
|
||||
Thread.sleep(halfTime - now);
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("Interrupted while sleeping");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info(pluginName + " making outgoing connection");
|
||||
return plugin.sendInvitation(random, INVITATION_TIMEOUT / 2);
|
||||
}
|
||||
|
||||
private void tryToClose(DuplexTransportConnection conn, boolean exception) {
|
||||
try {
|
||||
conn.dispose(exception, true);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/net/sf/briar/invitation/ConfirmationSender.java
Normal file
38
src/net/sf/briar/invitation/ConfirmationSender.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package net.sf.briar.invitation;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import net.sf.briar.api.invitation.ConfirmationCallback;
|
||||
|
||||
class ConfirmationSender implements ConfirmationCallback {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(ConfirmationSender.class.getName());
|
||||
|
||||
private final OutputStream out;
|
||||
|
||||
ConfirmationSender(OutputStream out) {
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
public void codesMatch() {
|
||||
write(1);
|
||||
}
|
||||
|
||||
public void codesDoNotMatch() {
|
||||
write(0);
|
||||
}
|
||||
|
||||
private void write(int b) {
|
||||
try {
|
||||
out.write(b);
|
||||
out.flush();
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/net/sf/briar/invitation/FailureNotifier.java
Normal file
42
src/net/sf/briar/invitation/FailureNotifier.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package net.sf.briar.invitation;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import net.sf.briar.api.invitation.ConnectionCallback;
|
||||
|
||||
class FailureNotifier extends Thread {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(FailureNotifier.class.getName());
|
||||
|
||||
private final Collection<Thread> workers;
|
||||
private final AtomicBoolean succeeded;
|
||||
private final ConnectionCallback callback;
|
||||
|
||||
FailureNotifier(Collection<Thread> workers, AtomicBoolean succeeded,
|
||||
ConnectionCallback callback) {
|
||||
this.workers = workers;
|
||||
this.succeeded = succeeded;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if(LOG.isLoggable(INFO)) LOG.info(workers.size() + " workers");
|
||||
try {
|
||||
for(Thread worker : workers) worker.join();
|
||||
if(!succeeded.get()) {
|
||||
if(LOG.isLoggable(INFO)) LOG.info("No worker succeeded");
|
||||
callback.connectionNotEstablished();
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
if(LOG.isLoggable(INFO))
|
||||
LOG.info("Interrupted while waiting for workers");
|
||||
callback.connectionNotEstablished();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
package net.sf.briar.invitation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import net.sf.briar.api.crypto.CryptoComponent;
|
||||
import net.sf.briar.api.crypto.PseudoRandom;
|
||||
import net.sf.briar.api.invitation.ConfirmationCallback;
|
||||
import net.sf.briar.api.invitation.ConnectionCallback;
|
||||
import net.sf.briar.api.invitation.InvitationManager;
|
||||
import net.sf.briar.api.plugins.PluginManager;
|
||||
@@ -27,55 +28,37 @@ class InvitationManagerImpl implements InvitationManager {
|
||||
Collection<DuplexPlugin> plugins = pluginManager.getInvitationPlugins();
|
||||
// Alice is the party with the smaller invitation code
|
||||
if(localCode < remoteCode) {
|
||||
PseudoRandom r = crypto.getPseudoRandom(localCode, remoteCode);
|
||||
startAliceInvitationWorker(plugins, r, c);
|
||||
startAliceWorkers(plugins, localCode, remoteCode, c);
|
||||
} else {
|
||||
startBobWorkers(plugins, localCode, remoteCode, c);
|
||||
}
|
||||
}
|
||||
|
||||
private void startAliceWorkers(Collection<DuplexPlugin> plugins,
|
||||
int localCode, int remoteCode, ConnectionCallback c) {
|
||||
AtomicBoolean connected = new AtomicBoolean(false);
|
||||
AtomicBoolean succeeded = new AtomicBoolean(false);
|
||||
Collection<Thread> workers = new ArrayList<Thread>();
|
||||
for(DuplexPlugin p : plugins) {
|
||||
PseudoRandom r = crypto.getPseudoRandom(localCode, remoteCode);
|
||||
Thread worker = new AliceConnector(p, r, c, connected, succeeded);
|
||||
workers.add(worker);
|
||||
worker.start();
|
||||
}
|
||||
new FailureNotifier(workers, succeeded, c).start();
|
||||
}
|
||||
|
||||
private void startBobWorkers(Collection<DuplexPlugin> plugins,
|
||||
int localCode, int remoteCode, ConnectionCallback c) {
|
||||
AtomicBoolean connected = new AtomicBoolean(false);
|
||||
AtomicBoolean succeeded = new AtomicBoolean(false);
|
||||
Collection<Thread> workers = new ArrayList<Thread>();
|
||||
for(DuplexPlugin p : plugins) {
|
||||
PseudoRandom r = crypto.getPseudoRandom(remoteCode, localCode);
|
||||
startBobInvitationWorker(plugins, r, c);
|
||||
}
|
||||
}
|
||||
|
||||
private void startAliceInvitationWorker(Collection<DuplexPlugin> plugins,
|
||||
PseudoRandom r, ConnectionCallback c) {
|
||||
// FIXME
|
||||
new FakeWorkerThread(c).start();
|
||||
}
|
||||
|
||||
private void startBobInvitationWorker(Collection<DuplexPlugin> plugins,
|
||||
PseudoRandom r, ConnectionCallback c) {
|
||||
// FIXME
|
||||
new FakeWorkerThread(c).start();
|
||||
}
|
||||
|
||||
private static class FakeWorkerThread extends Thread {
|
||||
|
||||
private final ConnectionCallback callback;
|
||||
|
||||
private FakeWorkerThread(ConnectionCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep((long) (Math.random() * 30 * 1000));
|
||||
} catch(InterruptedException ignored) {}
|
||||
if(Math.random() < 0.8) {
|
||||
callback.connectionNotEstablished();
|
||||
} else {
|
||||
callback.connectionEstablished(123456, 123456,
|
||||
new ConfirmationCallback() {
|
||||
|
||||
public void codesMatch() {}
|
||||
|
||||
public void codesDoNotMatch() {}
|
||||
});
|
||||
try {
|
||||
Thread.sleep((long) (Math.random() * 10 * 1000));
|
||||
} catch(InterruptedException ignored) {}
|
||||
if(Math.random() < 0.5) callback.codesMatch();
|
||||
else callback.codesDoNotMatch();
|
||||
}
|
||||
Thread worker = new BobConnector(p, r, c, connected, succeeded);
|
||||
workers.add(worker);
|
||||
worker.start();
|
||||
}
|
||||
new FailureNotifier(workers, succeeded, c).start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package net.sf.briar.plugins.droidtooth;
|
||||
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION;
|
||||
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
|
||||
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
|
||||
import static android.bluetooth.BluetoothAdapter.STATE_ON;
|
||||
import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
|
||||
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
@@ -265,6 +261,7 @@ class DroidtoothPlugin implements DuplexPlugin {
|
||||
// Try to connect
|
||||
try {
|
||||
BluetoothSocket s = InsecureBluetooth.createSocket(d, u);
|
||||
s.connect();
|
||||
return new DroidtoothTransportConnection(s);
|
||||
} catch(IOException e) {
|
||||
if(LOG.isLoggable(WARNING)) LOG.warning(e.toString());
|
||||
@@ -320,8 +317,6 @@ class DroidtoothPlugin implements DuplexPlugin {
|
||||
}
|
||||
// Use the same pseudo-random UUID as the contact
|
||||
UUID uuid = UUID.nameUUIDFromBytes(r.nextBytes(16));
|
||||
// Make the device discoverable if the user allows it
|
||||
makeDeviceDiscoverable();
|
||||
// Bind a new server socket to accept the invitation connection
|
||||
final BluetoothServerSocket ss;
|
||||
try {
|
||||
@@ -344,17 +339,6 @@ class DroidtoothPlugin implements DuplexPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private void makeDeviceDiscoverable() {
|
||||
synchronized(this) {
|
||||
if(!running) return;
|
||||
}
|
||||
if(adapter.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE) return;
|
||||
Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
|
||||
i.putExtra(EXTRA_DISCOVERABLE_DURATION, 120);
|
||||
i.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
appContext.startActivity(i);
|
||||
}
|
||||
|
||||
private static class BluetoothStateReceiver extends BroadcastReceiver {
|
||||
|
||||
private final CountDownLatch finished = new CountDownLatch(1);
|
||||
|
||||
@@ -65,9 +65,7 @@ class InsecureBluetooth {
|
||||
int handle = (Integer) addRfcommServiceRecord.invoke(mService, name,
|
||||
new ParcelUuid(uuid), channel, new Binder());
|
||||
if(handle == -1) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch(IOException ignored) {}
|
||||
socket.close();
|
||||
throw new IOException("Can't register SDP record for " + name);
|
||||
}
|
||||
Field f1 = adapter.getClass().getDeclaredField("mHandler");
|
||||
@@ -117,9 +115,7 @@ class InsecureBluetooth {
|
||||
Object result = bindListen.invoke(mSocket, new Object[0]);
|
||||
int errno = (Integer) result;
|
||||
if(errno != 0) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch(IOException ignored) {}
|
||||
socket.close();
|
||||
Method throwErrnoNative = mSocket.getClass().getMethod(
|
||||
"throwErrnoNative", int.class);
|
||||
throwErrnoNative.invoke(mSocket, errno);
|
||||
@@ -145,9 +141,8 @@ class InsecureBluetooth {
|
||||
@SuppressLint("NewApi")
|
||||
static BluetoothSocket createSocket(BluetoothDevice device, UUID uuid)
|
||||
throws IOException {
|
||||
if(Build.VERSION.SDK_INT >= 10) {
|
||||
if(Build.VERSION.SDK_INT >= 10)
|
||||
return device.createInsecureRfcommSocketToServiceRecord(uuid);
|
||||
}
|
||||
try {
|
||||
BluetoothSocket socket = null;
|
||||
Constructor<BluetoothSocket> constructor =
|
||||
|
||||
@@ -176,9 +176,9 @@ abstract class TcpPlugin implements DuplexPlugin {
|
||||
if(!running) return null;
|
||||
}
|
||||
SocketAddress addr = getRemoteSocketAddress(c);
|
||||
Socket s = new Socket();
|
||||
if(addr == null || s == null) return null;
|
||||
try {
|
||||
Socket s = new Socket();
|
||||
if(addr == null || s == null) return null;
|
||||
s.connect(addr);
|
||||
return new TcpTransportConnection(s);
|
||||
} catch(IOException e) {
|
||||
|
||||
Reference in New Issue
Block a user