Skip to content

Commit

Permalink
Add Apache HttpClient 5 transport (#1358)
Browse files Browse the repository at this point in the history
Adds a new, Apache HttpClient 5 based transport (without Jersey) that implements `DockerHttpClient` and supports input hijacking
  • Loading branch information
bsideup committed Apr 7, 2020
1 parent 77103e3 commit e4eb048
Show file tree
Hide file tree
Showing 8 changed files with 925 additions and 0 deletions.
67 changes: 67 additions & 0 deletions docker-java-transport-httpclient5/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<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>

<parent>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-parent</artifactId>
<version>3.2.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>docker-java-transport-httpclient5</artifactId>
<packaging>jar</packaging>

<name>docker-java-transport-httpclient5</name>
<url>https://github.com/docker-java/docker-java</url>
<description>Java API Client for Docker</description>

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>docker-java-core</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.0</version>
<exclusions>
<exclusion>

This comment has been minimized.

Copy link
@delanym

delanym Aug 14, 2023

What is this exclusion for? I'm getting

java.lang.NoClassDefFoundError: org/apache/hc/core5/http2/HttpVersionPolicy

 

    at org.apache.hc.client5.http.config.TlsConfig$Builder.build(TlsConfig.java:211)
    at org.apache.hc.client5.http.config.TlsConfig.<clinit>(TlsConfig.java:47)
    at org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager.resolveTlsConfig(PoolingHttpClientConnectionManager.java:276)
    at org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:441)
    at org.apache.hc.client5.http.impl.classic.InternalExecRuntime.connectEndpoint(InternalExecRuntime.java:162)
    at org.apache.hc.client5.http.impl.classic.InternalExecRuntime.connectEndpoint(InternalExecRuntime.java:172)
    at org.apache.hc.client5.http.impl.classic.ConnectExec.execute(ConnectExec.java:142)
    at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
    at org.apache.hc.client5.http.impl.classic.ProtocolExec.execute(ProtocolExec.java:192)
    at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
    at org.apache.hc.client5.http.impl.classic.HttpRequestRetryExec.execute(HttpRequestRetryExec.java:96)
    at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
    at org.apache.hc.client5.http.impl.classic.ContentCompressionExec.execute(ContentCompressionExec.java:152)
    at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
    at org.apache.hc.client5.http.impl.classic.RedirectExec.execute(RedirectExec.java:115)
    at org.apache.hc.client5.http.impl.classic.ExecChainElement.execute(ExecChainElement.java:51)
    at org.apache.hc.client5.http.impl.classic.InternalHttpClient.doExecute(InternalHttpClient.java:170)
    at org.apache.hc.client5.http.impl.classic.CloseableHttpClient.execute(CloseableHttpClient.java:87)
    at com.github.dockerjava.httpclient5.ApacheDockerHttpClientImpl.execute(ApacheDockerHttpClientImpl.java:191)
    at com.github.dockerjava.httpclient5.ApacheDockerHttpClient.execute(ApacheDockerHttpClient.java:9)
    at com.github.dockerjava.core.DefaultInvocationBuilder.execute(DefaultInvocationBuilder.java:228)
    at com.github.dockerjava.core.DefaultInvocationBuilder.get(DefaultInvocationBuilder.java:202)
    at com.github.dockerjava.core.DefaultInvocationBuilder.get(DefaultInvocationBuilder.java:74)
    at com.github.dockerjava.core.exec.ListImagesCmdExec.execute(ListImagesCmdExec.java:41)
    at com.github.dockerjava.core.exec.ListImagesCmdExec.execute(ListImagesCmdExec.java:16)
    at com.github.dockerjava.core.exec.AbstrSyncDockerCmdExec.exec(AbstrSyncDockerCmdExec.java:21)
    at com.github.dockerjava.core.command.AbstrDockerCmd.exec(AbstrDockerCmd.java:33)
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna-platform</artifactId>
<version>5.5.0</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>com.github.siom79.japicmp</groupId>
<artifactId>japicmp-maven-plugin</artifactId>
<configuration>
<!-- TODO remove once this module is released -->
<skip>true</skip>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<Export-Package>com.github.dockerjava.httpclient5.*</Export-Package>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package com.github.dockerjava.httpclient5;

import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.DockerHttpClient;
import com.github.dockerjava.core.SSLConfig;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.ConnectionClosedException;
import org.apache.hc.core5.http.ContentLengthStrategy;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.impl.DefaultContentLengthStrategy;
import org.apache.hc.core5.http.impl.io.EmptyInputStream;
import org.apache.hc.core5.http.io.entity.InputStreamEntity;
import org.apache.hc.core5.http.protocol.BasicHttpContext;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.net.URIAuthority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public final class ApacheDockerHttpClient implements DockerHttpClient {

public static final class Factory {

private DockerClientConfig dockerClientConfig = null;

public Factory dockerClientConfig(DockerClientConfig value) {
this.dockerClientConfig = value;
return this;
}

public ApacheDockerHttpClient build() {
Objects.requireNonNull(dockerClientConfig, "dockerClientConfig");
return new ApacheDockerHttpClient(dockerClientConfig);
}
}

private final CloseableHttpClient httpClient;

private final HttpHost host;

private ApacheDockerHttpClient(DockerClientConfig dockerClientConfig) {
Registry<ConnectionSocketFactory> socketFactoryRegistry = createConnectionSocketFactoryRegistry(dockerClientConfig);

URI dockerHost = dockerClientConfig.getDockerHost();

switch (dockerHost.getScheme()) {
case "unix":
case "npipe":
host = new HttpHost(dockerHost.getScheme(), "localhost", 2375);
break;
case "tcp":
host = new HttpHost(
socketFactoryRegistry.lookup("https") != null ? "https" : "http",
dockerHost.getHost(),
dockerHost.getPort()
);
break;
default:
host = HttpHost.create(dockerHost);
}

httpClient = HttpClients.custom()
.setRequestExecutor(new HijackingHttpRequestExecutor(null))
.setConnectionManager(new PoolingHttpClientConnectionManager(
socketFactoryRegistry,
new ManagedHttpClientConnectionFactory(
null,
null,
null,
null,
message -> {
Header transferEncodingHeader = message.getFirstHeader(HttpHeaders.TRANSFER_ENCODING);
if (transferEncodingHeader != null) {
if ("identity".equalsIgnoreCase(transferEncodingHeader.getValue())) {
return ContentLengthStrategy.UNDEFINED;
}
}
return DefaultContentLengthStrategy.INSTANCE.determineLength(message);
},
null
)
))
.build();
}

private Registry<ConnectionSocketFactory> createConnectionSocketFactoryRegistry(DockerClientConfig dockerClientConfig) {
RegistryBuilder<ConnectionSocketFactory> socketFactoryRegistryBuilder = RegistryBuilder.create();

SSLConfig sslConfig = dockerClientConfig.getSSLConfig();
if (sslConfig != null) {
try {
SSLContext sslContext = sslConfig.getSSLContext();
if (sslContext != null) {
socketFactoryRegistryBuilder.register("https", new SSLConnectionSocketFactory(sslContext));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}

return socketFactoryRegistryBuilder
.register("tcp", PlainConnectionSocketFactory.INSTANCE)
.register("http", PlainConnectionSocketFactory.INSTANCE)
.register("unix", new PlainConnectionSocketFactory() {
@Override
public Socket createSocket(HttpContext context) throws IOException {
URI dockerHost = dockerClientConfig.getDockerHost();

return new UnixDomainSocket(dockerHost.getPath());
}
})
.register("npipe", new PlainConnectionSocketFactory() {
@Override
public Socket createSocket(HttpContext context) {
URI dockerHost = dockerClientConfig.getDockerHost();

return new NamedPipeSocket(dockerHost.getPath());
}
})
.build();
}

@Override
public Response execute(Request request) {
HttpContext context = new BasicHttpContext();
HttpUriRequestBase httpUriRequest = new HttpUriRequestBase(request.method(), URI.create(request.path()));
httpUriRequest.setScheme(host.getSchemeName());
httpUriRequest.setAuthority(new URIAuthority(host.getHostName(), host.getPort()));

request.headers().forEach(httpUriRequest::addHeader);

InputStream body = request.body();
if (body != null) {
httpUriRequest.setEntity(new InputStreamEntity(body, null));
}

if (request.hijackedInput() != null) {
context.setAttribute(HijackingHttpRequestExecutor.HIJACKED_INPUT_ATTRIBUTE, request.hijackedInput());
httpUriRequest.setHeader("Upgrade", "tcp");
httpUriRequest.setHeader("Connection", "Upgrade");
}

try {
CloseableHttpResponse response = httpClient.execute(host, httpUriRequest, context);

return new ApacheResponse(httpUriRequest, response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public void close() throws IOException {
httpClient.close();
}

static class ApacheResponse implements Response {

private static final Logger LOGGER = LoggerFactory.getLogger(ApacheResponse.class);

private final HttpUriRequestBase request;

private final CloseableHttpResponse response;

ApacheResponse(HttpUriRequestBase httpUriRequest, CloseableHttpResponse response) {
this.request = httpUriRequest;
this.response = response;
}

@Override
public int getStatusCode() {
return response.getCode();
}

@Override
public Map<String, List<String>> getHeaders() {
return Stream.of(response.getHeaders()).collect(Collectors.groupingBy(
NameValuePair::getName,
Collectors.mapping(NameValuePair::getValue, Collectors.toList())
));
}

@Override
public String getHeader(String name) {
Header firstHeader = response.getFirstHeader(name);
return firstHeader != null ? firstHeader.getValue() : null;
}

@Override
public InputStream getBody() {
try {
return response.getEntity() != null
? response.getEntity().getContent()
: EmptyInputStream.INSTANCE;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public void close() {
try {
request.abort();
} catch (Exception e) {
LOGGER.debug("Failed to abort the request", e);
}

try {
response.close();
} catch (ConnectionClosedException e) {
LOGGER.trace("Failed to close the response", e);
} catch (Exception e) {
LOGGER.debug("Failed to close the response", e);
}
}
}
}

0 comments on commit e4eb048

Please sign in to comment.