Initial commit

This commit is contained in:
Minecon724 2025-02-14 08:20:25 +01:00
commit 58e0d56051
No known key found for this signature in database
GPG key ID: 3CCC4D267742C8E8
16 changed files with 632 additions and 0 deletions

38
.gitignore vendored Normal file
View file

@ -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

3
.idea/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

7
.idea/encodings.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

14
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

4
README.md Normal file
View file

@ -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)

25
pom.xml Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>eu.m724</groupId>
<artifactId>dcdn</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20250107</version>
</dependency>
</dependencies>
</project>

View file

@ -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<mounts.length(); i++) {
var j = mounts.getJSONObject(i);
System.out.printf("%s %s -> %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();
}
}

View file

@ -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<Void> ping() {
return dao.requestString("_ping").thenAccept(b -> {
if (!b.equals("OK")) {
throw new CompletionException(new Exception("Not OK")); // TODO the right Exception?
}
});
}
public CompletableFuture<JSONObject> info() {
return dao.requestJson("info");
}
public CompletableFuture<JSONObject> inspectContainer(String id) {
return dao.requestJson("containers/" + id + "/json");
}
public CompletableFuture<JSONObject> createContainer(String name, JSONObject data) {
return dao.requestJson("containers/create", Map.of("name", name), data);
}
}

View file

@ -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.<br>
* 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<JSONObject> requestJson(String path) {
return requestJson(path, null, null);
}
/**
* Create a GET request that expects a JSON from the server.<br>
* 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<JSONObject> requestJson(String path, Map<String, Object> queryParams) {
return requestJson(path, queryParams, null);
}
/**
* Create a POST request with a JSON body that expects a JSON from the server.<br>
* 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<JSONObject> 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.<br>
* 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<JSONObject> requestJson(String path, Map<String, Object> queryParams, JSONObject data) {
return commonRequest(path, queryParams, data, new JsonBodyHandler());
}
/**
* Create a GET request that returns the response as String.<br>
* 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<String> requestString(String path) {
return requestString(path, null, null);
}
/**
* Create a GET request that returns the response as String.<br>
* 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<String> requestString(String path, Map<String, Object> queryParams) {
return requestString(path, queryParams, null);
}
/**
* Create a POST request with a JSON body that returns the response as String.<br>
* 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<String> requestString(String path, JSONObject data) {
return requestString(path, null, data);
}
/**
* Create a POST request with a JSON body that returns the response as String.<br>
* 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<String> requestString(String path, Map<String, Object> queryParams, JSONObject data) {
return commonRequest(path, queryParams, data, HttpResponse.BodyHandlers.ofString());
}
/**
* Create a GET request that ignores response body.<br>
* 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<Void> requestNoContent(String path) {
return requestNoContent(path, null, null);
}
/**
* Create a GET request that ignores response body.<br>
* 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<Void> requestNoContent(String path, Map<String, Object> queryParams) {
return requestNoContent(path, queryParams, null);
}
/**
* Create a POST request with a JSON body that ignores response body.<br>
* 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<Void> requestNoContent(String path, JSONObject data) {
return requestNoContent(path, null, data);
}
/**
* Create a POST request with a JSON body that ignores response body.<br>
* 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<Void> requestNoContent(String path, Map<String, Object> queryParams, JSONObject data) {
return commonRequest(path, queryParams, data, HttpResponse.BodyHandlers.discarding());
}
private HttpRequest createRequest(String path) {
return HttpRequest.newBuilder(socketUri.resolve(path)).build();
}
private <T> CompletableFuture<T> commonRequest(String path, Map<String, Object> queryParams, JSONObject data, HttpResponse.BodyHandler<T> 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();
});
}
}

View file

@ -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<JSONObject> {
@Override
public HttpResponse.BodySubscriber<JSONObject> 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
);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}
}
}

View file

@ -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());
}
}