Skip to content

Commit e4eb048

Browse files
authored
Add Apache HttpClient 5 transport (#1358)
Adds a new, Apache HttpClient 5 based transport (without Jersey) that implements `DockerHttpClient` and supports input hijacking
1 parent 77103e3 commit e4eb048

File tree

8 files changed

+925
-0
lines changed

8 files changed

+925
-0
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<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">
2+
<modelVersion>4.0.0</modelVersion>
3+
4+
<parent>
5+
<groupId>com.github.docker-java</groupId>
6+
<artifactId>docker-java-parent</artifactId>
7+
<version>3.2.2-SNAPSHOT</version>
8+
<relativePath>../pom.xml</relativePath>
9+
</parent>
10+
11+
<artifactId>docker-java-transport-httpclient5</artifactId>
12+
<packaging>jar</packaging>
13+
14+
<name>docker-java-transport-httpclient5</name>
15+
<url>https://github.com/docker-java/docker-java</url>
16+
<description>Java API Client for Docker</description>
17+
18+
<dependencies>
19+
<dependency>
20+
<groupId>${project.groupId}</groupId>
21+
<artifactId>docker-java-core</artifactId>
22+
<version>${project.version}</version>
23+
</dependency>
24+
25+
<dependency>
26+
<groupId>org.apache.httpcomponents.client5</groupId>
27+
<artifactId>httpclient5</artifactId>
28+
<version>5.0</version>
29+
<exclusions>
30+
<exclusion>
31+
<groupId>org.apache.httpcomponents.core5</groupId>
32+
<artifactId>httpcore5-h2</artifactId>
33+
</exclusion>
34+
</exclusions>
35+
</dependency>
36+
37+
<dependency>
38+
<groupId>net.java.dev.jna</groupId>
39+
<artifactId>jna-platform</artifactId>
40+
<version>5.5.0</version>
41+
</dependency>
42+
</dependencies>
43+
44+
<build>
45+
<plugins>
46+
<plugin>
47+
<groupId>com.github.siom79.japicmp</groupId>
48+
<artifactId>japicmp-maven-plugin</artifactId>
49+
<configuration>
50+
<!-- TODO remove once this module is released -->
51+
<skip>true</skip>
52+
</configuration>
53+
</plugin>
54+
55+
<plugin>
56+
<groupId>org.apache.felix</groupId>
57+
<artifactId>maven-bundle-plugin</artifactId>
58+
<extensions>true</extensions>
59+
<configuration>
60+
<instructions>
61+
<Export-Package>com.github.dockerjava.httpclient5.*</Export-Package>
62+
</instructions>
63+
</configuration>
64+
</plugin>
65+
</plugins>
66+
</build>
67+
</project>
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package com.github.dockerjava.httpclient5;
2+
3+
import com.github.dockerjava.core.DockerClientConfig;
4+
import com.github.dockerjava.core.DockerHttpClient;
5+
import com.github.dockerjava.core.SSLConfig;
6+
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
7+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
8+
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
9+
import org.apache.hc.client5.http.impl.classic.HttpClients;
10+
import org.apache.hc.client5.http.impl.io.ManagedHttpClientConnectionFactory;
11+
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
12+
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
13+
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
14+
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
15+
import org.apache.hc.core5.http.ConnectionClosedException;
16+
import org.apache.hc.core5.http.ContentLengthStrategy;
17+
import org.apache.hc.core5.http.Header;
18+
import org.apache.hc.core5.http.HttpHeaders;
19+
import org.apache.hc.core5.http.HttpHost;
20+
import org.apache.hc.core5.http.NameValuePair;
21+
import org.apache.hc.core5.http.config.Registry;
22+
import org.apache.hc.core5.http.config.RegistryBuilder;
23+
import org.apache.hc.core5.http.impl.DefaultContentLengthStrategy;
24+
import org.apache.hc.core5.http.impl.io.EmptyInputStream;
25+
import org.apache.hc.core5.http.io.entity.InputStreamEntity;
26+
import org.apache.hc.core5.http.protocol.BasicHttpContext;
27+
import org.apache.hc.core5.http.protocol.HttpContext;
28+
import org.apache.hc.core5.net.URIAuthority;
29+
import org.slf4j.Logger;
30+
import org.slf4j.LoggerFactory;
31+
32+
import javax.net.ssl.SSLContext;
33+
import java.io.IOException;
34+
import java.io.InputStream;
35+
import java.net.Socket;
36+
import java.net.URI;
37+
import java.util.List;
38+
import java.util.Map;
39+
import java.util.Objects;
40+
import java.util.stream.Collectors;
41+
import java.util.stream.Stream;
42+
43+
public final class ApacheDockerHttpClient implements DockerHttpClient {
44+
45+
public static final class Factory {
46+
47+
private DockerClientConfig dockerClientConfig = null;
48+
49+
public Factory dockerClientConfig(DockerClientConfig value) {
50+
this.dockerClientConfig = value;
51+
return this;
52+
}
53+
54+
public ApacheDockerHttpClient build() {
55+
Objects.requireNonNull(dockerClientConfig, "dockerClientConfig");
56+
return new ApacheDockerHttpClient(dockerClientConfig);
57+
}
58+
}
59+
60+
private final CloseableHttpClient httpClient;
61+
62+
private final HttpHost host;
63+
64+
private ApacheDockerHttpClient(DockerClientConfig dockerClientConfig) {
65+
Registry<ConnectionSocketFactory> socketFactoryRegistry = createConnectionSocketFactoryRegistry(dockerClientConfig);
66+
67+
URI dockerHost = dockerClientConfig.getDockerHost();
68+
69+
switch (dockerHost.getScheme()) {
70+
case "unix":
71+
case "npipe":
72+
host = new HttpHost(dockerHost.getScheme(), "localhost", 2375);
73+
break;
74+
case "tcp":
75+
host = new HttpHost(
76+
socketFactoryRegistry.lookup("https") != null ? "https" : "http",
77+
dockerHost.getHost(),
78+
dockerHost.getPort()
79+
);
80+
break;
81+
default:
82+
host = HttpHost.create(dockerHost);
83+
}
84+
85+
httpClient = HttpClients.custom()
86+
.setRequestExecutor(new HijackingHttpRequestExecutor(null))
87+
.setConnectionManager(new PoolingHttpClientConnectionManager(
88+
socketFactoryRegistry,
89+
new ManagedHttpClientConnectionFactory(
90+
null,
91+
null,
92+
null,
93+
null,
94+
message -> {
95+
Header transferEncodingHeader = message.getFirstHeader(HttpHeaders.TRANSFER_ENCODING);
96+
if (transferEncodingHeader != null) {
97+
if ("identity".equalsIgnoreCase(transferEncodingHeader.getValue())) {
98+
return ContentLengthStrategy.UNDEFINED;
99+
}
100+
}
101+
return DefaultContentLengthStrategy.INSTANCE.determineLength(message);
102+
},
103+
null
104+
)
105+
))
106+
.build();
107+
}
108+
109+
private Registry<ConnectionSocketFactory> createConnectionSocketFactoryRegistry(DockerClientConfig dockerClientConfig) {
110+
RegistryBuilder<ConnectionSocketFactory> socketFactoryRegistryBuilder = RegistryBuilder.create();
111+
112+
SSLConfig sslConfig = dockerClientConfig.getSSLConfig();
113+
if (sslConfig != null) {
114+
try {
115+
SSLContext sslContext = sslConfig.getSSLContext();
116+
if (sslContext != null) {
117+
socketFactoryRegistryBuilder.register("https", new SSLConnectionSocketFactory(sslContext));
118+
}
119+
} catch (Exception e) {
120+
throw new RuntimeException(e);
121+
}
122+
}
123+
124+
return socketFactoryRegistryBuilder
125+
.register("tcp", PlainConnectionSocketFactory.INSTANCE)
126+
.register("http", PlainConnectionSocketFactory.INSTANCE)
127+
.register("unix", new PlainConnectionSocketFactory() {
128+
@Override
129+
public Socket createSocket(HttpContext context) throws IOException {
130+
URI dockerHost = dockerClientConfig.getDockerHost();
131+
132+
return new UnixDomainSocket(dockerHost.getPath());
133+
}
134+
})
135+
.register("npipe", new PlainConnectionSocketFactory() {
136+
@Override
137+
public Socket createSocket(HttpContext context) {
138+
URI dockerHost = dockerClientConfig.getDockerHost();
139+
140+
return new NamedPipeSocket(dockerHost.getPath());
141+
}
142+
})
143+
.build();
144+
}
145+
146+
@Override
147+
public Response execute(Request request) {
148+
HttpContext context = new BasicHttpContext();
149+
HttpUriRequestBase httpUriRequest = new HttpUriRequestBase(request.method(), URI.create(request.path()));
150+
httpUriRequest.setScheme(host.getSchemeName());
151+
httpUriRequest.setAuthority(new URIAuthority(host.getHostName(), host.getPort()));
152+
153+
request.headers().forEach(httpUriRequest::addHeader);
154+
155+
InputStream body = request.body();
156+
if (body != null) {
157+
httpUriRequest.setEntity(new InputStreamEntity(body, null));
158+
}
159+
160+
if (request.hijackedInput() != null) {
161+
context.setAttribute(HijackingHttpRequestExecutor.HIJACKED_INPUT_ATTRIBUTE, request.hijackedInput());
162+
httpUriRequest.setHeader("Upgrade", "tcp");
163+
httpUriRequest.setHeader("Connection", "Upgrade");
164+
}
165+
166+
try {
167+
CloseableHttpResponse response = httpClient.execute(host, httpUriRequest, context);
168+
169+
return new ApacheResponse(httpUriRequest, response);
170+
} catch (IOException e) {
171+
throw new RuntimeException(e);
172+
}
173+
}
174+
175+
@Override
176+
public void close() throws IOException {
177+
httpClient.close();
178+
}
179+
180+
static class ApacheResponse implements Response {
181+
182+
private static final Logger LOGGER = LoggerFactory.getLogger(ApacheResponse.class);
183+
184+
private final HttpUriRequestBase request;
185+
186+
private final CloseableHttpResponse response;
187+
188+
ApacheResponse(HttpUriRequestBase httpUriRequest, CloseableHttpResponse response) {
189+
this.request = httpUriRequest;
190+
this.response = response;
191+
}
192+
193+
@Override
194+
public int getStatusCode() {
195+
return response.getCode();
196+
}
197+
198+
@Override
199+
public Map<String, List<String>> getHeaders() {
200+
return Stream.of(response.getHeaders()).collect(Collectors.groupingBy(
201+
NameValuePair::getName,
202+
Collectors.mapping(NameValuePair::getValue, Collectors.toList())
203+
));
204+
}
205+
206+
@Override
207+
public String getHeader(String name) {
208+
Header firstHeader = response.getFirstHeader(name);
209+
return firstHeader != null ? firstHeader.getValue() : null;
210+
}
211+
212+
@Override
213+
public InputStream getBody() {
214+
try {
215+
return response.getEntity() != null
216+
? response.getEntity().getContent()
217+
: EmptyInputStream.INSTANCE;
218+
} catch (IOException e) {
219+
throw new RuntimeException(e);
220+
}
221+
}
222+
223+
@Override
224+
public void close() {
225+
try {
226+
request.abort();
227+
} catch (Exception e) {
228+
LOGGER.debug("Failed to abort the request", e);
229+
}
230+
231+
try {
232+
response.close();
233+
} catch (ConnectionClosedException e) {
234+
LOGGER.trace("Failed to close the response", e);
235+
} catch (Exception e) {
236+
LOGGER.debug("Failed to close the response", e);
237+
}
238+
}
239+
}
240+
}

0 commit comments

Comments
 (0)