Initial commit
This commit is contained in:
		
				commit
				
					
						58e0d56051
					
				
			
		
					 16 changed files with 632 additions and 0 deletions
				
			
		
							
								
								
									
										38
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										3
									
								
								.idea/.gitignore
									
										
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
# Default ignored files
 | 
			
		||||
/shelf/
 | 
			
		||||
/workspace.xml
 | 
			
		||||
							
								
								
									
										7
									
								
								.idea/encodings.xml
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.idea/encodings.xml
									
										
									
										generated
									
									
									
										Normal 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
									
								
							
							
						
						
									
										14
									
								
								.idea/misc.xml
									
										
									
										generated
									
									
									
										Normal 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
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
										
									
										generated
									
									
									
										Normal 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
									
								
							
							
						
						
									
										4
									
								
								README.md
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										25
									
								
								pom.xml
									
										
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										65
									
								
								src/main/java/eu/m724/Main.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/main/java/eu/m724/Main.java
									
										
									
									
									
										Normal 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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								src/main/java/eu/m724/docker/DockerEngine.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/main/java/eu/m724/docker/DockerEngine.java
									
										
									
									
									
										Normal 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										203
									
								
								src/main/java/eu/m724/docker/DockerEngineDAO.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/main/java/eu/m724/docker/DockerEngineDAO.java
									
										
									
									
									
										Normal 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();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/main/java/eu/m724/docker/JsonBodyHandler.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/main/java/eu/m724/docker/JsonBodyHandler.java
									
										
									
									
									
										Normal 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
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										73
									
								
								src/main/java/eu/m724/docker/proxy/ConnectionThread.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/main/java/eu/m724/docker/proxy/ConnectionThread.java
									
										
									
									
									
										Normal 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								src/main/java/eu/m724/docker/proxy/RemoteSocketFactory.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/main/java/eu/m724/docker/proxy/RemoteSocketFactory.java
									
										
									
									
									
										Normal 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								src/main/java/eu/m724/docker/proxy/ServerRunnable.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/main/java/eu/m724/docker/proxy/ServerRunnable.java
									
										
									
									
									
										Normal 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());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								src/main/java/eu/m724/docker/proxy/TcpSocketProxy.java
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/main/java/eu/m724/docker/proxy/TcpSocketProxy.java
									
										
									
									
									
										Normal 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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue