/
Http2StaticResourceCacheSpec.groovy
125 lines (117 loc) · 6.89 KB
/
Http2StaticResourceCacheSpec.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package io.micronaut.http.server.netty.resources
import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpHeaders
import io.micronaut.http.netty.AbstractNettyHttpRequest
import io.micronaut.runtime.server.EmbeddedServer
import io.netty.bootstrap.Bootstrap
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelOption
import io.netty.channel.SimpleChannelInboundHandler
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.http.DefaultFullHttpRequest
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpResponse
import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion
import io.netty.handler.codec.http2.DefaultHttp2Connection
import io.netty.handler.codec.http2.Http2SecurityUtil
import io.netty.handler.codec.http2.Http2Settings
import io.netty.handler.codec.http2.HttpConversionUtil
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder
import io.netty.handler.ssl.ApplicationProtocolConfig
import io.netty.handler.ssl.ApplicationProtocolNames
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler
import io.netty.handler.ssl.SslContextBuilder
import io.netty.handler.ssl.SupportedCipherSuiteFilter
import io.netty.handler.ssl.util.InsecureTrustManagerFactory
import org.jetbrains.annotations.NotNull
import spock.lang.Specification
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.concurrent.CompletableFuture
class Http2StaticResourceCacheSpec extends Specification {
def 'response should have proper stream id'() {
given:
def tempFile = File.createTempFile("Http2StaticResourceCacheSpec", ".html")
tempFile.write("<html><head></head><body>HTML Page from static file</body></html>")
def tempSubDir = new File(tempFile.getParentFile(), "doesntexist")
def app = ApplicationContext.run([
'micronaut.server.http-version': '2.0',
'micronaut.ssl.enabled': true,
'micronaut.ssl.port': -1,
'micronaut.ssl.buildSelfSigned': true,
'micronaut.router.static-resources.default.paths': ['classpath:public', 'file:' + tempFile.parent, 'file:' + tempSubDir.absolutePath]
])
def embeddedServer = app.getBean(EmbeddedServer)
embeddedServer.start()
def sslContext = SslContextBuilder.forClient()
.ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.applicationProtocolConfig(new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
// NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers.
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
// ACCEPT is currently the only mode supported by both OpenSsl and JDK providers.
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2))
.build()
def completion = new CompletableFuture<HttpResponse>()
def bootstrap = new Bootstrap()
.remoteAddress(embeddedServer.host, embeddedServer.port)
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.option(ChannelOption.AUTO_READ, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(@NotNull SocketChannel ch) throws Exception {
def connection = new DefaultHttp2Connection(false)
def connectionHandler = new HttpToHttp2ConnectionHandlerBuilder()
.initialSettings(Http2Settings.defaultSettings())
.frameListener(new InboundHttp2ToHttpAdapterBuilder(connection)
.maxContentLength(Integer.MAX_VALUE)
.propagateSettings(false)
.build())
.connection(connection)
.build()
ch.pipeline()
.addLast(sslContext.newHandler(ch.alloc(), embeddedServer.host, embeddedServer.port))
.addLast(new ApplicationProtocolNegotiationHandler('') {
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
if (ApplicationProtocolNames.HTTP_2 != protocol) {
throw new AssertionError((Object) protocol)
}
ctx.pipeline()
.addLast(connectionHandler)
.addLast(new SimpleChannelInboundHandler<HttpResponse>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx_, HttpResponse msg) throws Exception {
completion.complete(msg)
}
})
def request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, '/' + tempFile.getName())
request.headers().add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), 'https')
request.headers().add(AbstractNettyHttpRequest.STREAM_ID, 3)
request.headers().add(HttpHeaders.IF_MODIFIED_SINCE, Instant.ofEpochMilli(tempFile.lastModified()).atZone(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME))
ctx.channel().writeAndFlush(request)
}
})
}
})
when:
def channel = bootstrap.connect().await().channel()
def response = completion.get()
then:
response.status() == HttpResponseStatus.NOT_MODIFIED
response.headers().get(AbstractNettyHttpRequest.STREAM_ID) == '3'
cleanup:
tempFile.delete()
channel.close()
embeddedServer.close()
}
}