From 58e0d56051fddac91b9bb26b19436b5c885b8e58 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Fri, 14 Feb 2025 08:20:25 +0100 Subject: [PATCH] Initial commit --- .gitignore | 38 ++++ .idea/.gitignore | 3 + .idea/encodings.xml | 7 + .idea/misc.xml | 14 ++ .idea/vcs.xml | 6 + README.md | 4 + pom.xml | 25 +++ src/main/java/eu/m724/Main.java | 65 ++++++ .../java/eu/m724/docker/DockerEngine.java | 37 ++++ .../java/eu/m724/docker/DockerEngineDAO.java | 203 ++++++++++++++++++ .../java/eu/m724/docker/JsonBodyHandler.java | 21 ++ .../exception/FailedRequestException.java | 21 ++ .../m724/docker/proxy/ConnectionThread.java | 73 +++++++ .../docker/proxy/RemoteSocketFactory.java | 22 ++ .../eu/m724/docker/proxy/ServerRunnable.java | 33 +++ .../eu/m724/docker/proxy/TcpSocketProxy.java | 60 ++++++ 16 files changed, 632 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/eu/m724/Main.java create mode 100644 src/main/java/eu/m724/docker/DockerEngine.java create mode 100644 src/main/java/eu/m724/docker/DockerEngineDAO.java create mode 100644 src/main/java/eu/m724/docker/JsonBodyHandler.java create mode 100644 src/main/java/eu/m724/docker/exception/FailedRequestException.java create mode 100644 src/main/java/eu/m724/docker/proxy/ConnectionThread.java create mode 100644 src/main/java/eu/m724/docker/proxy/RemoteSocketFactory.java create mode 100644 src/main/java/eu/m724/docker/proxy/ServerRunnable.java create mode 100644 src/main/java/eu/m724/docker/proxy/TcpSocketProxy.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..7ace097 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c38f773 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +### Supply chain +- `com.github.docker-java:docker-java:3.4.1` [GitHub](https://github.com/docker-java/docker-java) \ + Warning: [Numerous CVEs.](https://mvnrepository.com/artifact/com.github.docker-java/docker-java/3.4.1) [Project seems dead](https://github.com/docker-java/docker-java/issues) + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..217a19c --- /dev/null +++ b/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + eu.m724 + dcdn + 1.0-SNAPSHOT + + + 21 + 21 + UTF-8 + + + + + org.json + json + 20250107 + + + + \ No newline at end of file diff --git a/src/main/java/eu/m724/Main.java b/src/main/java/eu/m724/Main.java new file mode 100644 index 0000000..fac62e9 --- /dev/null +++ b/src/main/java/eu/m724/Main.java @@ -0,0 +1,65 @@ +package eu.m724; + +import eu.m724.docker.DockerEngine; +import eu.m724.docker.exception.FailedRequestException; +import eu.m724.docker.proxy.TcpSocketProxy; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.util.concurrent.ExecutionException; + +public class Main { + public static void main(String[] args) throws IOException, InterruptedException { + var proxy = new TcpSocketProxy(StandardProtocolFamily.UNIX, UnixDomainSocketAddress.of("/var/run/docker.sock")); + proxy.start(); + System.out.println("Started"); + + System.out.printf("Tested in %d ms %n", proxy.test() / 1000000); + + var engine = new DockerEngine(proxy.getURI()); + engine.ping().join(); + + var info = engine.info().join(); + System.out.println("Docker runtime:"); + System.out.println("- " + info.getString("ID")); + System.out.println("- " + info.getString("OSType") + " " + info.getString("Architecture")); + System.out.println("- OS: " + info.getString("OperatingSystem") + " " + info.getString("OSVersion")); + System.out.println("- Hostname: " + info.getString("Name")); + + JSONObject containerInfo = null; + try { + containerInfo = engine.inspectContainer("dcdn_nginx").get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof FailedRequestException fre && fre.getResponse().statusCode() == 404) { + System.out.println("Container doesn't exist"); + } else { + throw new RuntimeException("Exception getting container info", e); + } + } + + if (containerInfo != null) { + var mounts = containerInfo.getJSONArray("Mounts"); + System.out.printf("Detected %d mounts:%n", mounts.length()); + for (int i=0; i %s %n", j.getString("Name"), j.getString("Source"), j.getString("Destination")); + } + } else { + System.out.println("Creating it"); + + var data = new JSONObject() + .put("Image", "nginx:1.27"); + try { + containerInfo = engine.createContainer("dcdn_nginx", data).get(); + // TODO + } catch (ExecutionException e) { + throw new RuntimeException("Exception creating container", e); + } + } + + proxy.close(); + } +} \ No newline at end of file diff --git a/src/main/java/eu/m724/docker/DockerEngine.java b/src/main/java/eu/m724/docker/DockerEngine.java new file mode 100644 index 0000000..dc94dc3 --- /dev/null +++ b/src/main/java/eu/m724/docker/DockerEngine.java @@ -0,0 +1,37 @@ +package eu.m724.docker; + +import org.json.JSONObject; + +import java.net.URI; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; + +public class DockerEngine { + private final DockerEngineDAO dao; + + + public DockerEngine(URI socketUri) { + this.dao = new DockerEngineDAO(socketUri); + } + + public CompletableFuture ping() { + return dao.requestString("_ping").thenAccept(b -> { + if (!b.equals("OK")) { + throw new CompletionException(new Exception("Not OK")); // TODO the right Exception? + } + }); + } + + public CompletableFuture info() { + return dao.requestJson("info"); + } + + public CompletableFuture inspectContainer(String id) { + return dao.requestJson("containers/" + id + "/json"); + } + + public CompletableFuture createContainer(String name, JSONObject data) { + return dao.requestJson("containers/create", Map.of("name", name), data); + } +} diff --git a/src/main/java/eu/m724/docker/DockerEngineDAO.java b/src/main/java/eu/m724/docker/DockerEngineDAO.java new file mode 100644 index 0000000..d3151b3 --- /dev/null +++ b/src/main/java/eu/m724/docker/DockerEngineDAO.java @@ -0,0 +1,203 @@ +package eu.m724.docker; + +import eu.m724.docker.exception.FailedRequestException; +import org.json.JSONObject; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +public class DockerEngineDAO { + private final URI socketUri; + private final HttpClient httpClient; + + DockerEngineDAO(URI socketUri) { + this.socketUri = socketUri; + this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).connectTimeout(Duration.ofSeconds(30)).build(); + } + + public void close() { + httpClient.close(); + } + + + /** + * Create a GET request that expects a JSON from the server.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @return the completable future that returns + */ + public CompletableFuture requestJson(String path) { + return requestJson(path, null, null); + } + + /** + * Create a GET request that expects a JSON from the server.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @param queryParams URL query parameters + * @return the completable future that returns + */ + public CompletableFuture requestJson(String path, Map queryParams) { + return requestJson(path, queryParams, null); + } + + /** + * Create a POST request with a JSON body that expects a JSON from the server.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @param data the JSON request body + * @return the completable future that returns + */ + public CompletableFuture requestJson(String path, JSONObject data) { + return requestJson(path, null, data); + } + + /** + * Create a POST request with a JSON body that expects a JSON from the server.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @param queryParams URL query parameters + * @param data the JSON request body + * @return the completable future that returns + */ + public CompletableFuture requestJson(String path, Map queryParams, JSONObject data) { + return commonRequest(path, queryParams, data, new JsonBodyHandler()); + } + + + /** + * Create a GET request that returns the response as String.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @return the completable future that returns + */ + public CompletableFuture requestString(String path) { + return requestString(path, null, null); + } + + /** + * Create a GET request that returns the response as String.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @param queryParams URL query parameters + * @return the completable future that returns + */ + public CompletableFuture requestString(String path, Map queryParams) { + return requestString(path, queryParams, null); + } + + /** + * Create a POST request with a JSON body that returns the response as String.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @param data the JSON request body + * @return the completable future that returns + */ + public CompletableFuture requestString(String path, JSONObject data) { + return requestString(path, null, data); + } + + /** + * Create a POST request with a JSON body that returns the response as String.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @param queryParams URL query parameters + * @param data the JSON request body + * @return the completable future that returns + */ + public CompletableFuture requestString(String path, Map queryParams, JSONObject data) { + return commonRequest(path, queryParams, data, HttpResponse.BodyHandlers.ofString()); + } + + /** + * Create a GET request that ignores response body.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @return the completable future that returns + */ + public CompletableFuture requestNoContent(String path) { + return requestNoContent(path, null, null); + } + + /** + * Create a GET request that ignores response body.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @param queryParams URL query parameters + * @return the completable future that returns + */ + public CompletableFuture requestNoContent(String path, Map queryParams) { + return requestNoContent(path, queryParams, null); + } + + /** + * Create a POST request with a JSON body that ignores response body.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @param data the JSON request body + * @return the completable future that returns + */ + public CompletableFuture requestNoContent(String path, JSONObject data) { + return requestNoContent(path, null, data); + } + + /** + * Create a POST request with a JSON body that ignores response body.
+ * A {@link FailedRequestException} is thrown by the future if a non-200 status code is returned. + * + * @param path request path + * @param queryParams URL query parameters + * @param data the JSON request body + * @return the completable future that returns + */ + public CompletableFuture requestNoContent(String path, Map queryParams, JSONObject data) { + return commonRequest(path, queryParams, data, HttpResponse.BodyHandlers.discarding()); + } + + + private HttpRequest createRequest(String path) { + return HttpRequest.newBuilder(socketUri.resolve(path)).build(); + } + + private CompletableFuture commonRequest(String path, Map queryParams, JSONObject data, HttpResponse.BodyHandler bodyHandler) { + if (queryParams != null && !queryParams.isEmpty()) { + path += '?'; + path += queryParams.entrySet().stream() + .map(e -> e.getKey() + '=' + e.getValue()) + .collect(Collectors.joining("&")); + } + + System.out.println(path); + var builder = HttpRequest.newBuilder(socketUri.resolve(path)); + + if (data != null) { + builder.POST(HttpRequest.BodyPublishers.ofString(data.toString())) + .header("Content-Type", "application/json"); + } + + var request = builder.build(); + + return httpClient.sendAsync(request, bodyHandler).thenApply(response -> { + if (response.statusCode() / 100 != 2) + throw new FailedRequestException(response); + return response.body(); + }); + } +} diff --git a/src/main/java/eu/m724/docker/JsonBodyHandler.java b/src/main/java/eu/m724/docker/JsonBodyHandler.java new file mode 100644 index 0000000..747ff85 --- /dev/null +++ b/src/main/java/eu/m724/docker/JsonBodyHandler.java @@ -0,0 +1,21 @@ +package eu.m724.docker; + +import org.json.JSONObject; + +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; + +public class JsonBodyHandler implements HttpResponse.BodyHandler { + @Override + public HttpResponse.BodySubscriber apply(HttpResponse.ResponseInfo responseInfo) { + if (responseInfo.statusCode() == 204) { + return HttpResponse.BodySubscribers.replacing(new JSONObject()); + } + + + return HttpResponse.BodySubscribers.mapping( + HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8), + JSONObject::new + ); + } +} diff --git a/src/main/java/eu/m724/docker/exception/FailedRequestException.java b/src/main/java/eu/m724/docker/exception/FailedRequestException.java new file mode 100644 index 0000000..10a9635 --- /dev/null +++ b/src/main/java/eu/m724/docker/exception/FailedRequestException.java @@ -0,0 +1,21 @@ +package eu.m724.docker.exception; + +import java.net.http.HttpResponse; +import java.util.concurrent.CompletionException; + +public class FailedRequestException extends CompletionException { + private final HttpResponse response; + + public FailedRequestException(HttpResponse response) { + this.response = response; + } + + @Override + public String getMessage() { + return "Code: " + response.statusCode() + " | Body: " + response.body(); + } + + public HttpResponse getResponse() { + return response; + } +} \ No newline at end of file diff --git a/src/main/java/eu/m724/docker/proxy/ConnectionThread.java b/src/main/java/eu/m724/docker/proxy/ConnectionThread.java new file mode 100644 index 0000000..958ef73 --- /dev/null +++ b/src/main/java/eu/m724/docker/proxy/ConnectionThread.java @@ -0,0 +1,73 @@ +package eu.m724.docker.proxy; + +import java.io.IOException; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +public class ConnectionThread extends Thread { + private final ExecutorService executorService; + private final RemoteSocketFactory remoteSocketFactory; + private final Socket clientSocket; + + ConnectionThread(ExecutorService executorService, RemoteSocketFactory remoteSocketFactory, Socket clientSocket) { + this.executorService = executorService; + this.remoteSocketFactory = remoteSocketFactory; + this.clientSocket = clientSocket; + } + + @Override + public void run() { + try ( + clientSocket; + var remoteSocket = remoteSocketFactory.newChannel() + ) { + System.out.println("connected"); + inner(remoteSocket); + System.out.println("disconnected"); + } catch (IOException e) { + System.err.println("Exception handling client: " + e.getMessage()); + } + } + + private void inner(SocketChannel remoteSocket) throws IOException { + ByteBuffer clientBuffer = ByteBuffer.allocate(8192); + ByteBuffer remoteBuffer = ByteBuffer.allocate(8192); + + try ( + var clientIn = clientSocket.getInputStream(); + var clientOut = clientSocket.getOutputStream() + ) { + // Use virtual threads for bidirectional forwarding + Future clientToRemote = executorService.submit(() -> { + while (clientSocket.isConnected() && remoteSocket.isConnected()) { + int bytesRead = clientIn.read(clientBuffer.array()); + if (bytesRead == -1) break; + clientBuffer.limit(bytesRead); + remoteSocket.write(clientBuffer); + clientBuffer.clear(); + } + return null; + }); + + Future remoteToClient = executorService.submit(() -> { + while (clientSocket.isConnected() && remoteSocket.isConnected()) { + int bytesRead = remoteSocket.read(remoteBuffer); + if (bytesRead == -1) break; + clientOut.write(remoteBuffer.array(), 0, bytesRead); + remoteBuffer.clear(); + } + return null; + }); + + // Wait for both directions to complete + clientToRemote.get(); + remoteToClient.get(); + } catch (InterruptedException | ExecutionException e) { + throw new IOException("Transfer interrupted", e); + } + } +} diff --git a/src/main/java/eu/m724/docker/proxy/RemoteSocketFactory.java b/src/main/java/eu/m724/docker/proxy/RemoteSocketFactory.java new file mode 100644 index 0000000..4d725a1 --- /dev/null +++ b/src/main/java/eu/m724/docker/proxy/RemoteSocketFactory.java @@ -0,0 +1,22 @@ +package eu.m724.docker.proxy; + +import java.io.IOException; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.nio.channels.SocketChannel; + +public class RemoteSocketFactory { + private final StandardProtocolFamily protocolFamily; + private final SocketAddress address; + + public RemoteSocketFactory(StandardProtocolFamily protocolFamily, SocketAddress address) { + this.protocolFamily = protocolFamily; + this.address = address; + } + + public SocketChannel newChannel() throws IOException { + var channel = SocketChannel.open(protocolFamily); + channel.connect(address); + return channel; + } +} diff --git a/src/main/java/eu/m724/docker/proxy/ServerRunnable.java b/src/main/java/eu/m724/docker/proxy/ServerRunnable.java new file mode 100644 index 0000000..98a402f --- /dev/null +++ b/src/main/java/eu/m724/docker/proxy/ServerRunnable.java @@ -0,0 +1,33 @@ +package eu.m724.docker.proxy; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.concurrent.ExecutorService; + +public class ServerRunnable implements Runnable { + private final ExecutorService executorService; + private final ServerSocket serverSocket; + private final RemoteSocketFactory remoteSocketFactory; + + public ServerRunnable(ExecutorService executorService, ServerSocket serverSocket, RemoteSocketFactory remoteSocketFactory) { + this.executorService = executorService; + this.serverSocket = serverSocket; + this.remoteSocketFactory = remoteSocketFactory; + } + + @Override + public void run() { + while (!serverSocket.isClosed() && !Thread.currentThread().isInterrupted()) { + try { + var client = serverSocket.accept(); + client.setSoTimeout(60_000); + + executorService.execute(new ConnectionThread(executorService, remoteSocketFactory, client)); + } catch (IOException e) { + if (serverSocket.isClosed()) break; + + System.err.println("Exception handling client request: " + e.getMessage()); + } + } + } +} diff --git a/src/main/java/eu/m724/docker/proxy/TcpSocketProxy.java b/src/main/java/eu/m724/docker/proxy/TcpSocketProxy.java new file mode 100644 index 0000000..400d7b3 --- /dev/null +++ b/src/main/java/eu/m724/docker/proxy/TcpSocketProxy.java @@ -0,0 +1,60 @@ +package eu.m724.docker.proxy; + +import java.io.IOException; +import java.net.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class TcpSocketProxy implements AutoCloseable { + private final RemoteSocketFactory remoteSocketFactory; + + private ServerSocket serverSocket; + private ExecutorService executorService; + + public TcpSocketProxy(StandardProtocolFamily protocolFamily, SocketAddress address) { + this.remoteSocketFactory = new RemoteSocketFactory(protocolFamily, address); + } + + public void start() throws IOException { + start(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + } + + public void start(SocketAddress bindAddress) throws IOException { + if (serverSocket != null) return; + + this.serverSocket = new ServerSocket(); + serverSocket.bind(bindAddress); + + this.executorService = Executors.newVirtualThreadPerTaskExecutor(); + executorService.execute(new ServerRunnable(executorService, serverSocket, remoteSocketFactory)); + + System.out.println("Proxy bound on " + getURI()); + } + + public long test() throws IOException { + var start = System.nanoTime(); + + var channel = remoteSocketFactory.newChannel(); + channel.close(); + + var end = System.nanoTime(); + + return end - start; + } + + @Override + public void close() throws IOException { + if (serverSocket != null) serverSocket.close(); + if (executorService != null) executorService.shutdownNow(); + } + + public URI getURI() { + var address = serverSocket.getInetAddress().getHostAddress(); + + if (serverSocket.getInetAddress() instanceof Inet6Address) { + address = '[' + address + ']'; + } + + return URI.create("http://" + address + ":" + serverSocket.getLocalPort()); + } +}