summary refs log blame commit diff stats
path: root/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java
blob: f470bb042968f4d615d2e0b5136af4bee58c948d (plain) (tree)












































































































































































                                                                                                                                                          
package ganarchy.friendcode.sam;

import com.google.common.collect.ImmutableMap;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.Socket;

public class I2PSamControl extends I2PSamStateMachine {
    private final boolean zeroHop;
    private String pubkey;
    private String id;

    public I2PSamControl(boolean zeroHop) {
        this.zeroHop = zeroHop;
    }

    @Override
    public boolean connect() {
        try {
            // try IPv6 first
            // there's no Inet6Address.getLoopbackAddress() which is unfortunate.
            final Socket samSocket = new Socket();
            samSocket.connect(new InetSocketAddress(Inet6Address.getByAddress("localhost", new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}, 0), 7656), 3000);
            return this.connect(samSocket);
        } catch (IOException e) {
            try {
                final Socket samSocket = new Socket();
                samSocket.connect(new InetSocketAddress(Inet4Address.getByAddress("localhost", new byte[] {127,0,0,1}), 7656), 3000);
                return this.connect(samSocket);
            } catch (IOException ex) {
                return false;
            }
        }
    }

    @Override
    public boolean start() {
        if (!super.start()) {
            return false;
        }
        try {
            // try to enable auth
            this.enableAuth();
            // FIXME "Friend Code Type: World" vs "Friend Code Type: Session"
            // generate our keys
            this.sendCommand(new I2PSamCommand(
                "DEST", "GENERATE",
                ImmutableMap.of("SIGNATURE_TYPE", "EdDSA_SHA512_Ed25519")
            ));
            var dest = this.getCommand("DEST", "REPLY");
            this.pubkey = dest.parameters().get("PUB");
            var privkey = dest.parameters().get("PRIV");
            // setup our session
            int i = 0;
            String status;
            do {
                i++;
                // we want a session with the given privkey and zero hops for maximum performance
                // this isn't anonymous but then it's not meant to be - we're using I2P as a free STUN/TURN service
                // the connecting clients handle the need for TURN by setting their hop count to 1 as needed
                this.id = "minecraft_friendcode_" + i;
                final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
                this.sendCommand(new I2PSamCommand(
                    "SESSION", "CREATE",
                    builder.put(
                        "STYLE", "STREAM"
                    ).put(
                        "ID", this.id
                    ).put(
                        "DESTINATION", privkey
                    ).put(
                        "inbound.length", this.zeroHop ? "0" : "1"
                    ).put(
                        "outbound.length", this.zeroHop ? "0" : "1"
                    ).put(
                        "inbound.allowZeroHop", "true"
                    ).put(
                        "outbound.allowZeroHop", "true"
                    ).put(
                        "shouldBundleReplyInfo", "true"
                    ).put(
                        "i2cp.dontPublishLeaseSet", "false"
                    ).put(
                        "streaming.maxWindowSize", "1024"
                    ).build()
                ));
            } while ("DUPLICATED_ID".equals(status = this.getCommand("SESSION", "STATUS").parameters().get("RESULT")));
            return "OK".equals(status);
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Creates a stream forwarder.
     * @return A stream forwarder.
     */
    public I2PSamStreamForwarder forwardStream(String port) {
        return new I2PSamStreamForwarder(this.getSamBridgeAddress(), id, port);
    }

    /**
     * Creates a stream forwarder.
     * @return A stream connector.
     */
    public I2PSamStreamConnector connectStream(String b32) {
        return new I2PSamStreamConnector(this.getSamBridgeAddress(), id, b32);
    }

    /**
     * Returns the session pubkey.
     */
    public String pubkey() {
        return this.pubkey;
    }

    /**
     * Set up and enable auth.
     *
     * @throws IOException If an I/O error occurs.
     */
    private void enableAuth() throws IOException {
        // enable auth (if it hasn't been explicitly disabled by the user), for the user's sake
        // (ugh i2p you really fucked up by not making this the default)
        // btw tor does this with a filesystem path to an auth cookie (which is much more secure) but we digress
        this.sendCommand(new I2PSamCommand(
            "AUTH", "ADD",
            ImmutableMap.of("USER", "minecraft_friendcode", "PASSWORD", "friendcode")
        ));
        // if the user already exists, don't enable auth
        if ("OK".equals(this.getCommand("AUTH", "STATUS").parameters().get("RESULT"))) {
            this.sendCommand(new I2PSamCommand("AUTH", "ENABLE"));
            // ignore the response on this one, just get it off the queue
            this.getCommand("AUTH", "STATUS");
        }
    }

    public NameStatus checkName(String target) {
        try {
            this.sendCommand(new I2PSamCommand("NAMING", "LOOKUP", ImmutableMap.of("NAME", target)));
            I2PSamCommand result = this.getCommand("NAMING", "REPLY");
            if (result.parameters().get("RESULT") == null) {
                return NameStatus.FAILED;
            }
            switch (result.parameters().get("RESULT")) {
                case "OK" -> {
                    return NameStatus.OK;
                }
                case "INVALID_KEY" -> {
                    return NameStatus.INVALID;
                }
                case "KEY_NOT_FOUND" -> {
                    return NameStatus.UNKNOWN;
                }
                default -> {
                    return NameStatus.FAILED;
                }
            }
        } catch (IOException e) {
            return NameStatus.FAILED;
        }
    }

    public enum NameStatus {
        OK,
        INVALID,
        UNKNOWN,
        FAILED;
    }
}