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, null)) {
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;
}
}