diff options
Diffstat (limited to 'src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java')
-rw-r--r-- | src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java b/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java new file mode 100644 index 0000000..d05717d --- /dev/null +++ b/src/main/java/ganarchy/friendcode/sam/I2PSamStateMachine.java @@ -0,0 +1,220 @@ +package ganarchy.friendcode.sam; + +import com.google.common.collect.ImmutableMap; +import ganarchy.friendcode.FriendCode; +import net.minecraft.util.Util; + +import java.io.*; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.Iterator; +import java.util.Objects; + +public abstract class I2PSamStateMachine implements Closeable { + // FIXME user/password from config + private static final I2PSamCommand HELLO_MESSAGE = new I2PSamCommand( + "HELLO", "VERSION", + ImmutableMap.of( + "MIN", "3.2", + "USER", "minecraft_friendcode", + "PASSWORD", "friendcode" + ) + ); + private Socket samSocket; + private PushbackReader reader; + private OutputStreamWriter writer; + private final ArrayDeque<I2PSamCommand> queue = new ArrayDeque<>(); + private boolean started; + private boolean connected; + private String id; + + protected I2PSamStateMachine() { + } + + /** + * Connects to the SAM socket. + * + * @return Whether the connection was successful. + */ + public abstract boolean connect(); + + protected boolean connect(Socket samSocket) { + this.samSocket = samSocket; + if (this.connected) { + throw new IllegalStateException(); + } + this.connected = true; + return true; + } + + /** + * Starts the SAM session. + * + * @return Whether we were able to start the SAM session. + */ + public boolean start() { + if (!this.connected) { + throw new IllegalStateException(); + } + if (this.started) { + throw new IllegalStateException(); + } + this.started = true; + try { + // 8 KiC limit + this.reader = new PushbackReader(new InputStreamReader(this.samSocket.getInputStream(), StandardCharsets.UTF_8), 8192); + this.writer = new OutputStreamWriter(this.samSocket.getOutputStream(), StandardCharsets.UTF_8); + + + // send HELLO + // FIXME let the user change the credentials + this.sendCommand(HELLO_MESSAGE); + return "OK".equals(this.getCommand("HELLO", "REPLY").parameters().get("RESULT")); + } catch (IOException e) { + return false; + } + } + + /** + * Attempts to step the SAM session. + * + * This will generally not block, unless something went wrong. + */ + public void tryStep() { + try { + if (this.reader != null && this.reader.ready()) { + this.step(); + } + } catch (IOException ignored) { + } + } + + /** + * Steps the SAM session. + * + * @throws IOException If an I/O error occurs. + */ + public void step() throws IOException { + this.writer.flush(); + final String line; + // 8 KiC limit + final var buffer = new char[8192]; + final var builder = new StringBuilder(); + int cursor = 0; + while (true) { + final int read = this.reader.read(buffer, cursor, buffer.length - cursor); + if (read < 0) { + line = null; + break; + } + builder.append(buffer, cursor, read); + final int target; + if ((target = builder.indexOf("\n", cursor) + 1) > 0) { + this.reader.unread(buffer, target, cursor + read - target); + builder.setLength(target - 1); + line = builder.toString(); + break; + } + cursor += read; + } + if (line == null) { + throw new EOFException(); + } + // NOTE \r will result in fuckery, but the bridge should not generally send them + FriendCode.LOGGER.debug("[SAM (RAW)] {}", line); + final I2PSamCommand command; + try { + command = I2PSamCommand.parse(line); + } catch (IllegalArgumentException e) { + FriendCode.LOGGER.error("[SAM Error] Closing SAM bridge.", e); + this.samSocket.close(); + return; + } + if (command == null) { + // empty line + return; + } + if (command.name().equals("PING")) { + this.sendCommand(new I2PSamCommand("PONG", command.opcode(), command.parameters())); + } else { + queue.push(command); + } + } + + /** + * Reads a name from the SAM session. + * + * @param name The command name. + * @param opcode The subcommand name. + * @return The name line, or null if something went wrong. + * @throws IOException If an I/O error occurs. + */ + protected I2PSamCommand getCommand(String name, String opcode) throws IOException { + final boolean checkPing = name.equals("PING") || name.equals("PONG"); + while (true) { + // check queue for lines + for (Iterator<I2PSamCommand> iterator = this.queue.iterator(); iterator.hasNext(); ) { + I2PSamCommand queued = iterator.next(); + // special check for PING/PONG + if (queued.name().equals(name) && (checkPing || Objects.equals(queued.opcode(), opcode))) { + iterator.remove(); + return queued; + } + } + // if that failed, read more lines + this.step(); + } + } + + protected void sendCommand(I2PSamCommand command) throws IOException { + String line = command.buildCommandLine() + "\n"; + if (line.length() >= 8192) { + throw new IOException("command too long - max 8 KiC"); + } + this.writer.write(line); + } + + /** + * Sends a PING. + * + * @return The round-trip-time in milliseconds. + */ + public long sendPing() { + try { + long time = Util.getMeasuringTimeMs(); + this.sendCommand(new I2PSamCommand("PING", "" + time)); + return Util.getMeasuringTimeMs() - Long.parseLong(this.getCommand("PONG", null).opcode()); + } catch (IOException e) { + return -1; + } + } + + /** + * Returns the SAM bridge address. + * @return The SAM bridge address. + */ + protected SocketAddress getSamBridgeAddress() { + if (!this.connected) { + throw new IllegalStateException("not connected"); + } + return this.samSocket.getRemoteSocketAddress(); + } + + /** + * Closes this connection. + * @throws IOException As per {@link Socket#close()}. + */ + public void close() throws IOException { + this.samSocket.close(); + } + + protected Socket unwrap() { + Socket socket = this.samSocket; + this.samSocket = null; + this.reader = null; + this.writer = null; + return socket; + } +} |