Skip to content

Commit 5b7a2e4

Browse files
authored
Merge pull request #625 from nielsavonds/master
Fixed #621 - Assets don't work on HTTPS with Netty
2 parents 8e03c38 + b0b7bac commit 5b7a2e4

2 files changed

Lines changed: 129 additions & 2 deletions

File tree

jooby-netty/src/main/java/org/jooby/internal/netty/NettyResponse.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import static io.netty.channel.ChannelFutureListener.CLOSE;
2222

23+
import java.io.IOException;
2324
import java.io.InputStream;
2425
import java.nio.ByteBuffer;
2526
import java.nio.channels.FileChannel;
@@ -41,13 +42,17 @@
4142
import io.netty.handler.codec.http.DefaultFullHttpResponse;
4243
import io.netty.handler.codec.http.DefaultHttpHeaders;
4344
import io.netty.handler.codec.http.DefaultHttpResponse;
45+
import io.netty.handler.codec.http.HttpChunkedInput;
4446
import io.netty.handler.codec.http.HttpHeaderNames;
4547
import io.netty.handler.codec.http.HttpHeaderValues;
4648
import io.netty.handler.codec.http.HttpHeaders;
4749
import io.netty.handler.codec.http.HttpResponseStatus;
4850
import io.netty.handler.codec.http.HttpVersion;
4951
import io.netty.handler.codec.http.LastHttpContent;
5052
import io.netty.handler.codec.http2.HttpConversionUtil;
53+
import io.netty.handler.ssl.SslHandler;
54+
import io.netty.handler.stream.ChunkedFile;
55+
import io.netty.handler.stream.ChunkedNioFile;
5156
import io.netty.handler.stream.ChunkedStream;
5257
import io.netty.handler.stream.ChunkedWriteHandler;
5358
import io.netty.util.Attribute;
@@ -184,11 +189,28 @@ public void send(final FileChannel channel, final long offset, final long count)
184189
rsp.headers().set(headers);
185190
ChannelHandlerContext ctx = this.ctx;
186191
ctx.channel().attr(NettyRequest.NEED_FLUSH).set(false);
192+
193+
// add chunker
194+
ChannelPipeline pipeline = ctx.pipeline();
195+
if (pipeline.get("chunker") == null) {
196+
pipeline.addAfter("codec", "chunker", new ChunkedWriteHandler());
197+
}
198+
199+
// Create the cunked input here already, to properly handle the IOException
200+
final HttpChunkedInput chunkedInput = new HttpChunkedInput(new ChunkedNioFile(channel, offset, count, 8192));
201+
187202
ctx.channel().eventLoop().execute(() -> {
188203
// send headers
189204
ctx.write(rsp);
190-
ctx.write(new DefaultFileRegion(channel, offset, count));
191-
keepAlive(ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT));
205+
206+
// For SSL, we cannot send a file region
207+
if (ctx.pipeline().get(SslHandler.class) == null)
208+
{
209+
ctx.write(new DefaultFileRegion(channel, offset, count));
210+
keepAlive(ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT));
211+
} else {
212+
keepAlive(ctx.writeAndFlush(chunkedInput));
213+
}
192214
});
193215

194216
committed = true;

jooby-netty/src/test/java/org/jooby/internal/netty/NettyResponseTest.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.List;
2121
import java.util.Optional;
2222

23+
import org.apache.http.impl.bootstrap.SSLServerSetupHandler;
2324
import org.jooby.test.MockUnit;
2425
import org.jooby.test.MockUnit.Block;
2526
import org.junit.Test;
@@ -38,13 +39,17 @@
3839
import io.netty.handler.codec.http.DefaultFullHttpResponse;
3940
import io.netty.handler.codec.http.DefaultHttpHeaders;
4041
import io.netty.handler.codec.http.DefaultHttpResponse;
42+
import io.netty.handler.codec.http.HttpChunkedInput;
4143
import io.netty.handler.codec.http.HttpHeaderNames;
4244
import io.netty.handler.codec.http.HttpHeaderValues;
4345
import io.netty.handler.codec.http.HttpHeaders;
4446
import io.netty.handler.codec.http.HttpResponse;
4547
import io.netty.handler.codec.http.HttpResponseStatus;
4648
import io.netty.handler.codec.http.HttpVersion;
4749
import io.netty.handler.codec.http.LastHttpContent;
50+
import io.netty.handler.ssl.SslHandler;
51+
import io.netty.handler.stream.ChunkedInput;
52+
import io.netty.handler.stream.ChunkedNioFile;
4853
import io.netty.handler.stream.ChunkedStream;
4954
import io.netty.handler.stream.ChunkedWriteHandler;
5055
import io.netty.util.Attribute;
@@ -596,6 +601,17 @@ public void sendFileChannel() throws Exception {
596601
expect(chn.eventLoop()).andReturn(loop);
597602

598603
})
604+
.expect(unit -> {
605+
ChannelPipeline pipeline = unit.mock(ChannelPipeline.class);
606+
expect(pipeline.get("chunker")).andReturn(null);
607+
expect(pipeline.get(SslHandler.class)).andReturn(null);
608+
expect(pipeline.addAfter(eq("codec"), eq("chunker"), isA(ChunkedWriteHandler.class)))
609+
.andReturn(pipeline);
610+
611+
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
612+
expect(ctx.pipeline()).andReturn(pipeline);
613+
expect(ctx.pipeline()).andReturn(pipeline);
614+
})
599615
.expect(noKeepAliveNoLen)
600616
.run(unit -> {
601617
new NettyResponse(unit.get(ChannelHandlerContext.class), bufferSize, keepAlive)
@@ -605,6 +621,84 @@ public void sendFileChannel() throws Exception {
605621
});
606622
}
607623

624+
@Test
625+
public void sendFileChannelSSL() throws Exception {
626+
boolean keepAlive = false;
627+
FileChannel fchannel = newFileChannel(8192);
628+
new MockUnit(ChannelHandlerContext.class, ByteBuf.class, ChannelFuture.class)
629+
.expect(channel)
630+
.expect(headers)
631+
.expect(unit -> {
632+
DefaultHttpHeaders headers = unit.get(DefaultHttpHeaders.class);
633+
expect(headers.contains(HttpHeaderNames.CONTENT_LENGTH)).andReturn(false);
634+
expect(headers.remove(HttpHeaderNames.TRANSFER_ENCODING)).andReturn(headers);
635+
expect(headers.set(HttpHeaderNames.CONTENT_LENGTH, 8192L)).andReturn(headers);
636+
})
637+
.expect(unit -> {
638+
DefaultHttpResponse rsp = unit.mockConstructor(DefaultHttpResponse.class,
639+
new Class[]{HttpVersion.class,
640+
HttpResponseStatus.class },
641+
HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
642+
643+
HttpHeaders headers = unit.mock(HttpHeaders.class);
644+
expect(headers.set(unit.get(DefaultHttpHeaders.class))).andReturn(headers);
645+
646+
expect(rsp.headers()).andReturn(headers);
647+
648+
unit.registerMock(HttpResponse.class, rsp);
649+
})
650+
.expect(unit -> {
651+
ChannelFuture future = unit.get(ChannelFuture.class);
652+
653+
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
654+
expect(ctx.write(unit.get(HttpResponse.class))).andReturn(future);
655+
})
656+
.expect(
657+
unit -> {
658+
ChunkedNioFile nioFile = unit.mockConstructor(ChunkedNioFile.class,
659+
new Class[]{FileChannel.class, long.class, long.class, int.class },
660+
fchannel, 0L, fchannel.size(), 8192);
661+
662+
HttpChunkedInput chunked = unit.mockConstructor(HttpChunkedInput.class,
663+
new Class[]{ChunkedInput.class }, nioFile);
664+
665+
unit.registerMock(HttpChunkedInput.class, chunked);
666+
})
667+
.expect(unit -> {
668+
ChannelFuture future = unit.get(ChannelFuture.class);
669+
670+
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
671+
expect(ctx.writeAndFlush(unit.get(HttpChunkedInput.class))).andReturn(future);
672+
})
673+
.expect(setNeedFlush)
674+
.expect(unit -> {
675+
EventLoop loop = unit.mock(EventLoop.class);
676+
loop.execute(unit.capture(Runnable.class));
677+
678+
Channel chn = unit.get(Channel.class);
679+
expect(chn.eventLoop()).andReturn(loop);
680+
681+
})
682+
.expect(unit -> {
683+
ChannelPipeline pipeline = unit.mock(ChannelPipeline.class);
684+
expect(pipeline.get("chunker")).andReturn(null);
685+
expect(pipeline.get(SslHandler.class)).andReturn(unit.mock(SslHandler.class));
686+
expect(pipeline.addAfter(eq("codec"), eq("chunker"), isA(ChunkedWriteHandler.class)))
687+
.andReturn(pipeline);
688+
689+
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
690+
expect(ctx.pipeline()).andReturn(pipeline);
691+
expect(ctx.pipeline()).andReturn(pipeline);
692+
})
693+
.expect(noKeepAliveNoLen)
694+
.run(unit -> {
695+
new NettyResponse(unit.get(ChannelHandlerContext.class), bufferSize, keepAlive)
696+
.send(fchannel);
697+
}, unit -> {
698+
unit.captured(Runnable.class).iterator().next().run();
699+
});
700+
}
701+
608702
@Test
609703
public void sendFileChannelNoLen() throws Exception {
610704
boolean keepAlive = false;
@@ -655,6 +749,17 @@ public void sendFileChannelNoLen() throws Exception {
655749
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
656750
expect(ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)).andReturn(future);
657751
})
752+
.expect(unit -> {
753+
ChannelPipeline pipeline = unit.mock(ChannelPipeline.class);
754+
expect(pipeline.get("chunker")).andReturn(null);
755+
expect(pipeline.get(SslHandler.class)).andReturn(null);
756+
expect(pipeline.addAfter(eq("codec"), eq("chunker"), isA(ChunkedWriteHandler.class)))
757+
.andReturn(pipeline);
758+
759+
ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class);
760+
expect(ctx.pipeline()).andReturn(pipeline);
761+
expect(ctx.pipeline()).andReturn(pipeline);
762+
})
658763
.expect(setNeedFlush)
659764
.expect(noKeepAliveNoLen)
660765
.expect(unit -> {

0 commit comments

Comments
 (0)