Created
January 5, 2023 21:08
-
-
Save alexanderankin/e46b353906152d05e18bcb71395fac5a to your computer and use it in GitHub Desktop.
simple tcp proxy server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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