Skip to content

Instantly share code, notes, and snippets.

@alexanderankin
Created January 5, 2023 21:08
Show Gist options
  • Select an option

  • Save alexanderankin/e46b353906152d05e18bcb71395fac5a to your computer and use it in GitHub Desktop.

Select an option

Save alexanderankin/e46b353906152d05e18bcb71395fac5a to your computer and use it in GitHub Desktop.
simple tcp proxy server
package ps;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.TeeInputStream;
import org.apache.commons.io.output.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.stream.Stream;
@Slf4j
@Accessors(chain = true)
@Data
public class ProxyServer implements AutoCloseable {
final Config config;
final AtomicInteger id;
final ServerSocket serverSocket;
final Thread serverThread;
public static void main(String[] args) {
try (ProxyServer ignored = new ProxyServer(new ProxyServer.Config()
.setTo(new ProxyServer.IpAddress()
.setHost("127.0.0.1")
.setPort(8081)
)
.setFrom(new ProxyServer.IpAddress()
.setHost("localhost")
.setPort(8082)
)
.setCallback((client, server) -> {
System.out.println("client said: " + new String(client, StandardCharsets.UTF_8));
System.out.println("server said: " + new String(server, StandardCharsets.UTF_8));
})
)) {
}
}
@SneakyThrows
public ProxyServer(Config config) {
this.config = config;
serverSocket = new ServerSocket(
config.getFrom().getPort(),
0,
config.getFrom().toInetSocketAddress().getAddress()
);
id = new AtomicInteger();
serverThread = Stream.of(new Thread(new Runnable() {
@Override
public void run() {
while (true) acceptConnection();
}
private void acceptConnection() {
try {
Socket serverConnection = serverSocket.accept();
new Thread(() -> processConnection(serverConnection),
"serverThread-" + id.getAndIncrement())
.start();
} catch (Exception e) {
log.error("error accepting connection", e);
}
}
private void processConnection(Socket serverConnection) {
// parent thread name
String pt = Thread.currentThread().getName();
List<Thread> threads = List.of();
try (Socket clientSocket = new Socket(config.getTo().getHost(),
config.getTo().getPort());
ByteArrayOutputStream serverBuffer = new ByteArrayOutputStream();
ByteArrayOutputStream clientBuffer = new ByteArrayOutputStream()
) {
threads = Arrays.asList(new Thread(() -> {
try {
InputStream server = serverConnection.getInputStream();
server = new TeeInputStream(server, serverBuffer);
OutputStream client = clientSocket.getOutputStream();
IOUtils.copy(server, client);
} catch (Exception e) {
log.error("error streaming to client", e);
}
log.debug("done streaming to client");
}, pt + ".toClient"), new Thread(() -> {
try {
InputStream client = clientSocket.getInputStream();
client = new TeeInputStream(client, clientBuffer);
OutputStream server = serverConnection.getOutputStream();
IOUtils.copy(client, server);
} catch (Exception e) {
log.error("error streaming to server", e);
}
log.debug("done streaming to server");
}, pt + ".toServer"));
// start threads
for (Thread thread : threads) thread.start();
// wait for threads before leaving try-with-resources block
for (Thread thread : threads) thread.join();
// execute callback
config.getCallback().accept(clientBuffer.toByteArray(), serverBuffer.toByteArray());
} catch (Exception e) {
log.error("issue in processConnection", e);
for (Thread thread : threads) {
thread.interrupt();
}
}
}
}, "serverThread"))
.peek(Thread::start)
.findFirst().orElseThrow();
log.info("started");
log.debug("started with config {}", config);
}
@SneakyThrows
@Override
public void close() {
if (serverSocket != null) serverSocket.close();
}
@Accessors(chain = true)
@Data
public static class Config {
IpAddress from, to;
BiConsumer<byte[], byte[]> callback;
public BiConsumer<byte[], byte[]> getCallback() {
if (callback == null) callback = (b, b2) -> {
};
return callback;
}
}
@Accessors(chain = true)
@Data
public static class IpAddress {
String host;
int port;
InetSocketAddress toInetSocketAddress() {
return new InetSocketAddress(host, port);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment