diff options
Diffstat (limited to 'src/main/java/ganarchy/friendcode/client/SamProxyThread.java')
-rw-r--r-- | src/main/java/ganarchy/friendcode/client/SamProxyThread.java | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/src/main/java/ganarchy/friendcode/client/SamProxyThread.java b/src/main/java/ganarchy/friendcode/client/SamProxyThread.java new file mode 100644 index 0000000..c7db41f --- /dev/null +++ b/src/main/java/ganarchy/friendcode/client/SamProxyThread.java @@ -0,0 +1,184 @@ +package ganarchy.friendcode.client; + +import ganarchy.friendcode.FriendCode; +import ganarchy.friendcode.sam.I2PSamControl; +import ganarchy.friendcode.sam.I2PSamStreamConnector; + +import java.io.IOException; +import java.net.Inet6Address; +import java.net.ServerSocket; +import java.net.Socket; + +public class SamProxyThread extends Thread { + private final String target; + private volatile int localPort; + private volatile Status status = Status.IDLE; + private volatile boolean running = true; + private volatile boolean isConnecting = false; + + public SamProxyThread(String target) { + this.target = target; + this.setDaemon(true); + this.setName("SAM Proxy Control Thread"); + } + + @Override + public void run() { + if (this.running) { + tryRun(true); + } + if (this.running) { + tryRun(false); + } + this.status = Status.SETUP_FAILED; + } + + private void tryRun(final boolean zeroHop) { + // this is all very inefficient but we don't particularly care + // who cares if you have 3 threads just to connect to a minecraft server/friend code + // it's only 3 threads + // the "server" is far more efficient, only requiring one + try (final I2PSamControl control = new I2PSamControl(zeroHop)) { + if (!control.connect()) { + this.running = false; + this.status = Status.CONNECTION_FAILED; + return; + } + if (!control.start()) { + this.running = false; + this.status = Status.SETUP_FAILED; + return; + } + switch (control.checkName(this.target)) { + case UNKNOWN, INVALID -> { + this.running = false; + this.status = Status.RESOLUTION_FAILED; + return; + } + case FAILED -> { + return; + } + } + // we can't distinguish if we should retry the connection + // (e.g. because building tunnels took too long, destination is known but gateway/peer is not, etc) + // vs if we should try with 1hop (because we can't reach peer from our connection, + // e.g. ipv4-only client vs ipv6-only friend, symmetric NAT, etc) + // so we just try to "warm up" the connection and hope it works. + I2PSamStreamConnector warmup = control.connectStream(this.target); + if (!warmup.connect()) { + this.running = false; + this.status = Status.CONNECTION_FAILED; + return; + } + warmup.start(); + warmup.close(); + I2PSamStreamConnector stream = control.connectStream(this.target); + if (!stream.connect()) { + this.running = false; + this.status = Status.CONNECTION_FAILED; + return; + } + this.isConnecting = true; + if (!stream.start()) { + FriendCode.LOGGER.warn("[SAM error] {}", stream.getStatus()); + this.isConnecting = false; + if (!zeroHop) { + this.running = false; + this.status = Status.SETUP_FAILED; + } + return; + } + try ( + Socket socket = stream.unwrap(); + ServerSocket server = new ServerSocket(0, 8, Inet6Address.getByAddress("localhost", new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 0)); + ) { + this.localPort = server.getLocalPort(); + server.setSoTimeout(3000); + this.status = Status.RUNNING; + try (Socket single = server.accept()) { + socket.setTcpNoDelay(true); + single.setTcpNoDelay(true); + Thread clientThread = new Thread("SAM Proxy CTS Thread") { + @Override + public void run() { + try ( + var clientRead = single.getInputStream(); + var serverWrite = socket.getOutputStream(); + ) { + byte[] buffer = new byte[128*1024]; + while (SamProxyThread.this.running) { + int read = clientRead.read(buffer); + if (read > 0) { + serverWrite.write(buffer, 0, read); + } + } + } catch (IOException ignored) { + } + } + }; + Thread serverThread = new Thread("SAM Proxy STC Thread") { + @Override + public void run() { + try ( + var serverRead = socket.getInputStream(); + var clientWrite = single.getOutputStream(); + ) { + byte[] buffer = new byte[128*1024]; + while (SamProxyThread.this.running) { + int read = serverRead.read(buffer); + if (read > 0) { + clientWrite.write(buffer, 0, read); + } + } + } catch (IOException ignored) { + } + } + }; + clientThread.setDaemon(true); + clientThread.start(); + serverThread.setDaemon(true); + serverThread.start(); + while (!this.isInterrupted() && this.running) { + if (control.sendPing() < 0) { + this.running = false; + } + try { + Thread.sleep(1500L); + } catch (InterruptedException ignored) { + } + } + } finally { + this.running = false; + } + } + } catch (IOException e) { + // don't really need to do anything + } + } + + public void stopProxy() { + this.running = false; + // FIXME this is wrong, need to actually close sockets. + this.interrupt(); + } + + public Status status() { + return this.status; + } + + public int port() { + return this.localPort; + } + + public boolean isConnecting() { + return this.isConnecting; + } + + public enum Status { + IDLE, + RUNNING, + CONNECTION_FAILED, + SETUP_FAILED, + RESOLUTION_FAILED; + } +} |