summary refs log blame commit diff stats
path: root/src/main/java/ganarchy/friendcode/client/SamProxyThread.java
blob: c7db41f392c142d8c83fe9ee28caa63f582a14b8 (plain) (tree)























































































































































































                                                                                                                                                                        
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;
    }
}