Skip to content

Commit f760f0c

Browse files
authored
Merge pull request #1220 from werehuman/ssl-close-notify-to-tcp-fin
Netty factory: Close TCP connection when receive TLS close notify
2 parents 9b1a8fe + 350a806 commit f760f0c

File tree

2 files changed

+91
-1
lines changed

2 files changed

+91
-1
lines changed

src/main/java/com/github/dockerjava/netty/NettyDockerCmdExecFactory.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
import com.github.dockerjava.core.AbstractDockerCmdExecFactory;
1717
import com.github.dockerjava.core.WebTarget;
18+
import io.netty.util.concurrent.Future;
19+
import io.netty.util.concurrent.GenericFutureListener;
1820
import org.apache.commons.lang.SystemUtils;
1921

2022
import com.github.dockerjava.api.command.DockerCmdExecFactory;
@@ -232,12 +234,34 @@ public DuplexChannel connect(Bootstrap bootstrap) throws InterruptedException {
232234
throw new RuntimeException("no port configured for " + host);
233235
}
234236

235-
DuplexChannel channel = (DuplexChannel) bootstrap.connect(host, port).sync().channel();
237+
final DuplexChannel channel = (DuplexChannel) bootstrap.connect(host, port).sync().channel();
236238

237239
final SslHandler ssl = initSsl(dockerClientConfig);
238240

239241
if (ssl != null) {
240242
channel.pipeline().addFirst(ssl);
243+
244+
// https://tools.ietf.org/html/rfc5246#section-7.2.1
245+
// TLS has its own special message about connection termination. Because TLS is a
246+
// session-level protocol, it can be covered by any transport-level protocol like
247+
// TCP, UTP and so on. But we know exactly that data being transferred over TCP and
248+
// that other side will never send any byte into this TCP connection, so this
249+
// channel should be closed.
250+
// RFC says that we must notify opposite side about closing. This could be done only
251+
// in sun.security.ssl.SSLEngineImpl and unfortunately it does not send this
252+
// message. On the other hand RFC does not enforce the opposite side to wait for
253+
// such message.
254+
ssl.sslCloseFuture().addListener(new GenericFutureListener<Future<? super Channel>>() {
255+
@Override
256+
public void operationComplete(Future<? super Channel> future) {
257+
channel.eventLoop().execute(new Runnable() {
258+
@Override
259+
public void run() {
260+
channel.close();
261+
}
262+
});
263+
}
264+
});
241265
}
242266

243267
return channel;

src/test/java/com/github/dockerjava/cmd/AttachContainerCmdIT.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
import org.slf4j.LoggerFactory;
1414

1515
import java.io.ByteArrayInputStream;
16+
import java.io.Closeable;
1617
import java.io.File;
1718
import java.io.InputStream;
1819
import java.io.PipedInputStream;
1920
import java.io.PipedOutputStream;
21+
import java.util.concurrent.CountDownLatch;
2022
import java.util.concurrent.TimeUnit;
23+
import java.util.concurrent.atomic.AtomicLong;
2124

2225
import static com.github.dockerjava.junit.DockerRule.DEFAULT_IMAGE;
2326
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -28,6 +31,7 @@
2831
import static org.hamcrest.Matchers.isEmptyString;
2932
import static org.hamcrest.Matchers.not;
3033
import static org.junit.Assert.assertEquals;
34+
import static org.junit.Assert.assertTrue;
3135
import static org.junit.Assume.assumeThat;
3236

3337
/**
@@ -205,6 +209,68 @@ public void onNext(Frame frame) {
205209
callback.close();
206210
}
207211

212+
/**
213+
* {@link AttachContainerResultCallback#onComplete()} should be called immediately after
214+
* container exit. It was broken for Netty and TLS connection.
215+
*/
216+
@Test
217+
public void attachContainerClosesStdoutWhenContainerExits() throws Exception {
218+
DockerClient dockerClient = dockerRule.getClient();
219+
220+
CreateContainerResponse container = dockerClient.createContainerCmd(DEFAULT_IMAGE)
221+
.withCmd("echo", "hello world")
222+
.withTty(false)
223+
.exec();
224+
LOG.info("Created container: {}", container.toString());
225+
226+
final CountDownLatch started = new CountDownLatch(1);
227+
final AtomicLong startedAtNanos = new AtomicLong();
228+
final CountDownLatch gotLine = new CountDownLatch(1);
229+
final CountDownLatch completed = new CountDownLatch(1);
230+
final AtomicLong gotLineAtNanos = new AtomicLong();
231+
AttachContainerTestCallback callback = new AttachContainerTestCallback() {
232+
@Override
233+
public void onStart(Closeable stream) {
234+
startedAtNanos.set(System.nanoTime());
235+
started.countDown();
236+
super.onStart(stream);
237+
}
238+
239+
@Override
240+
public void onNext(Frame item) {
241+
if (item.getStreamType() == StreamType.STDOUT) {
242+
gotLineAtNanos.set(System.nanoTime());
243+
gotLine.countDown();
244+
}
245+
super.onNext(item);
246+
}
247+
248+
@Override
249+
public void onComplete() {
250+
completed.countDown();
251+
super.onComplete();
252+
}
253+
};
254+
255+
try (Closeable ignored = callback) {
256+
dockerClient.attachContainerCmd(container.getId())
257+
.withStdOut(true)
258+
.withFollowStream(true)
259+
.exec(callback);
260+
261+
dockerClient.startContainerCmd(container.getId()).exec();
262+
263+
assertTrue("Should start in a reasonable time", started.await(30, SECONDS));
264+
assertTrue("Should get first line quickly after the start", gotLine.await(15, SECONDS));
265+
266+
long gotLineDurationSeconds = (gotLineAtNanos.get() - startedAtNanos.get()) / 1_000_000_000L;
267+
LOG.info("Got line from {} for {} seconds", container.getId(), gotLineDurationSeconds);
268+
269+
boolean finished = completed.await(1L + gotLineDurationSeconds, SECONDS);
270+
assertTrue("Should get EOF in a time close to time of getting the first line", finished);
271+
}
272+
}
273+
208274
public static class AttachContainerTestCallback extends AttachContainerResultCallback {
209275
private StringBuffer log = new StringBuffer();
210276

0 commit comments

Comments
 (0)