summary refs log tree commit diff stats
path: root/src/main/java/ganarchy/friendcode/client/SamProxyThread.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/ganarchy/friendcode/client/SamProxyThread.java')
-rw-r--r--src/main/java/ganarchy/friendcode/client/SamProxyThread.java184
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;
+    }
+}