diff options
Diffstat (limited to 'src/main/java/ganarchy/friendcode/sam/I2PSamControl.java')
-rw-r--r-- | src/main/java/ganarchy/friendcode/sam/I2PSamControl.java | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java b/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java new file mode 100644 index 0000000..f470bb0 --- /dev/null +++ b/src/main/java/ganarchy/friendcode/sam/I2PSamControl.java @@ -0,0 +1,173 @@ +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; + } +} |