From 0951ea30d953e0e049f8f01c5374f94880f1449f Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Sat, 18 Jan 2020 14:35:14 +0000 Subject: [PATCH 1/3] Add support for compressed responses --- .../com/hotels/styx/servers/StyxHttpServer.kt | 3 + .../hotels/styx/servers/StyxHttpServerTest.kt | 85 ++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt b/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt index 122c1e0dc4..317f23e142 100644 --- a/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt +++ b/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt @@ -40,6 +40,7 @@ object StyxHttpServer { val SCHEMA = `object`( field("port", integer()), field("handler", string()), + optional("compressResponses", bool()), optional("tlsSettings", `object`( optional("sslProvider", string()), optional("certificateFile", string()), @@ -79,6 +80,7 @@ private data class StyxHttpServerTlsSettings( private data class StyxHttpServerConfiguration( val port: Int, val handler: String, + val compressResponses: Boolean, val tlsSettings: StyxHttpServerTlsSettings?, val maxInitialLength: Int = 4096, @@ -101,6 +103,7 @@ internal class StyxHttpServerFactory : StyxServerFactory { val environment = context.environment() val proxyServerConfig = ProxyServerConfig.Builder() + .setCompressResponses(config.compressResponses) .setMaxInitialLength(config.maxInitialLength) .setMaxHeaderSize(config.maxHeaderSize) .setMaxChunkSize(config.maxChunkSize) diff --git a/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt b/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt index 94de70eb5c..3426b8113b 100644 --- a/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt +++ b/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt @@ -49,7 +49,9 @@ import io.kotlintest.specs.FeatureSpec import reactor.core.publisher.Flux import reactor.core.publisher.toFlux import reactor.core.publisher.toMono +import java.nio.charset.Charset import java.nio.charset.StandardCharsets.UTF_8 +import java.util.zip.GZIPInputStream class StyxHttpServerTest : FeatureSpec({ feature("HTTP request handling") { @@ -109,6 +111,72 @@ class StyxHttpServerTest : FeatureSpec({ } } + feature("Response compression") { + + val serverConfig = configBlock(""" + port: 0 + handler: aHandler + compressResponses: true + """.trimIndent()) + + val server = StyxHttpServerFactory().create("test-01", routingContext, serverConfig, db) + val guavaServer = toGuavaService(server) + guavaServer.startAsync().awaitRunning() + + scenario("Responses are compressed if accept-encoding is set to gzip") { + StyxHttpClient.Builder().build().send(get("/blah") + .header(HOST, "localhost:${server.inetAddress().port}") + .header("accept-encoding", "7z, gzip") + .build()) + .wait()!! + .let { + it.status() shouldBe OK + it.header("content-encoding").get() shouldBe "gzip" + ungzip(it.body(), UTF_8) shouldBe "Hello, test!" + } + } + + scenario("Does not compress response if accept-encoding not sent") { + StyxHttpClient.Builder().build().send(get("/blah") + .header(HOST, "localhost:${server.inetAddress().port}") + .build()) + .wait()!! + .let { + it.status() shouldBe OK + it.header("content-encoding").isPresent shouldBe false + it.bodyAs(UTF_8) shouldBe "Hello, test!" + } + } + + scenario("Does not compress response if accept-encoding unsupported") { + StyxHttpClient.Builder().build().send(get("/blah") + .header(HOST, "localhost:${server.inetAddress().port}") + .header("accept-encoding", "7z") + .build()) + .wait()!! + .let { + it.status() shouldBe OK + it.header("content-encoding").isPresent shouldBe false + it.bodyAs(UTF_8) shouldBe "Hello, test!" + } + } + + scenario("Does not compress response if response content-encoding is already set") { + StyxHttpClient.Builder().build().send(get("/compressed") + .header(HOST, "localhost:${server.inetAddress().port}") + .header("accept-encoding", "gzip") + .build()) + .wait()!! + .let { + it.status() shouldBe OK + it.header("content-encoding").get() shouldBe "gzip" + it.bodyAs(UTF_8) shouldBe "Hello, test!" // Just as the server sent it + } + } + + guavaServer.stopAsync().awaitTerminated() + } + feature("Max initial line length") { val serverConfig = configBlock(""" port: 0 @@ -302,12 +370,25 @@ private fun createConnection(port: Int) = NettyConnectionFactory.Builder() private val response = HttpResponse.response(OK) .header("source", "secure") + .header("content-type", "text/plain") .body("Hello, test!", UTF_8) .build() +private val compressedResponse = HttpResponse.response(OK) + .header("source", "secure") + .header("content-type", "text/plain") + .header("content-encoding", "gzip") + .body("Hello, test!", UTF_8) // Not actually compressed, just claims to be, which is all we want. + .build() + private val routingContext = RoutingObjectFactoryContext( routeRefLookup = routeLookup { - ref("aHandler" to RoutingObject { _, _ -> Eventual.of(response.stream()) }) + ref("aHandler" to RoutingObject { request, _ -> + when(request.url().toString()) { + "/compressed" -> Eventual.of(compressedResponse.stream()) + else -> Eventual.of(response.stream()) + } + }) ref("aggregator" to RoutingObject { request, _ -> request @@ -317,6 +398,8 @@ private val routingContext = RoutingObjectFactoryContext( }) .get() +private fun ungzip(content: ByteArray, charset: Charset): String = GZIPInputStream(content.inputStream()).bufferedReader(charset).use { it.readText() } + private val db = StyxObjectStore>() private val crtFile = fixturesHome(StyxHttpServerTest::class.java, "/ssl/testCredentials.crt").toString() From 1ac87b9d6f4f3e9ab99d767d9e8852a69f9f8324 Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Tue, 21 Jan 2020 06:25:27 +0000 Subject: [PATCH 2/3] Remove duplicate tests (from HttpCompressionSpec), leaving only basic ones --- .../hotels/styx/servers/StyxHttpServerTest.kt | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt b/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt index 3426b8113b..8d1b3add1b 100644 --- a/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt +++ b/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt @@ -148,32 +148,6 @@ class StyxHttpServerTest : FeatureSpec({ } } - scenario("Does not compress response if accept-encoding unsupported") { - StyxHttpClient.Builder().build().send(get("/blah") - .header(HOST, "localhost:${server.inetAddress().port}") - .header("accept-encoding", "7z") - .build()) - .wait()!! - .let { - it.status() shouldBe OK - it.header("content-encoding").isPresent shouldBe false - it.bodyAs(UTF_8) shouldBe "Hello, test!" - } - } - - scenario("Does not compress response if response content-encoding is already set") { - StyxHttpClient.Builder().build().send(get("/compressed") - .header(HOST, "localhost:${server.inetAddress().port}") - .header("accept-encoding", "gzip") - .build()) - .wait()!! - .let { - it.status() shouldBe OK - it.header("content-encoding").get() shouldBe "gzip" - it.bodyAs(UTF_8) shouldBe "Hello, test!" // Just as the server sent it - } - } - guavaServer.stopAsync().awaitTerminated() } From e359044b760eef2ca4fa409bd95cbac022bfa69a Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Thu, 23 Jan 2020 12:14:44 +0000 Subject: [PATCH 3/3] Test improvements --- .../com/hotels/styx/servers/StyxHttpServer.kt | 2 +- .../hotels/styx/servers/StyxHttpServerTest.kt | 6 ++-- .../com/hotels/styx/proxy/CompressionSpec.kt | 33 ++++++++++--------- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt b/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt index 317f23e142..d957d23dd7 100644 --- a/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt +++ b/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt @@ -80,7 +80,7 @@ private data class StyxHttpServerTlsSettings( private data class StyxHttpServerConfiguration( val port: Int, val handler: String, - val compressResponses: Boolean, + val compressResponses: Boolean = false, val tlsSettings: StyxHttpServerTlsSettings?, val maxInitialLength: Int = 4096, diff --git a/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt b/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt index 8d1b3add1b..73bd449ac6 100644 --- a/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt +++ b/components/proxy/src/test/kotlin/com/hotels/styx/servers/StyxHttpServerTest.kt @@ -24,7 +24,7 @@ import com.hotels.styx.api.HttpHeaderNames.CONNECTION import com.hotels.styx.api.HttpHeaderNames.CONTENT_LENGTH import com.hotels.styx.api.HttpHeaderNames.HOST import com.hotels.styx.api.HttpRequest.get -import com.hotels.styx.api.HttpResponse +import com.hotels.styx.api.HttpResponse.response import com.hotels.styx.api.HttpResponseStatus.OK import com.hotels.styx.api.HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE import com.hotels.styx.api.HttpResponseStatus.REQUEST_TIMEOUT @@ -342,13 +342,13 @@ private fun createConnection(port: Int) = NettyConnectionFactory.Builder() .createConnection(newOriginBuilder("localhost", port).build(), ConnectionSettings(250)) .block()!! -private val response = HttpResponse.response(OK) +private val response = response(OK) .header("source", "secure") .header("content-type", "text/plain") .body("Hello, test!", UTF_8) .build() -private val compressedResponse = HttpResponse.response(OK) +private val compressedResponse = response(OK) .header("source", "secure") .header("content-type", "text/plain") .header("content-encoding", "gzip") diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/proxy/CompressionSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/proxy/CompressionSpec.kt index 3e2e1cbf8f..f01d3d3eed 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/proxy/CompressionSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/proxy/CompressionSpec.kt @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import com.hotels.styx.api.HttpResponseStatus.OK import com.hotels.styx.client.StyxHttpClient import com.hotels.styx.support.StyxServerProvider import com.hotels.styx.support.proxyHttpHostHeader +import com.hotels.styx.support.wait import io.kotlintest.Spec import io.kotlintest.shouldBe import io.kotlintest.specs.FeatureSpec @@ -39,10 +40,10 @@ class CompressionSpec : FeatureSpec() { .build(); client.send(request) - .toMono() - .block() + .wait()!! .let { - it!!.status() shouldBe (OK) + it.status() shouldBe (OK) + it.header("content-encoding").get() shouldBe "gzip" ungzip(it.body()) shouldBe ("Hello from http server!") } } @@ -52,10 +53,10 @@ class CompressionSpec : FeatureSpec() { .build(); client.send(request) - .toMono() - .block() + .wait()!! .let { - it!!.status() shouldBe (OK) + it.status() shouldBe (OK) + it.header("content-encoding").isPresent shouldBe false it.bodyAs(UTF_8) shouldBe ("Hello from http server!") } } @@ -67,10 +68,10 @@ class CompressionSpec : FeatureSpec() { .build(); client.send(request) - .toMono() - .block() + .wait()!! .let { - it!!.status() shouldBe (OK) + it.status() shouldBe (OK) + it.header("content-encoding").isPresent shouldBe false it.bodyAs(UTF_8) shouldBe ("Hello from http server!") } } @@ -82,10 +83,10 @@ class CompressionSpec : FeatureSpec() { .build(); client.send(request) - .toMono() - .block() + .wait()!! .let { - it!!.status() shouldBe (OK) + it.status() shouldBe (OK) + it.header("content-encoding").get() shouldBe "gzip" it.bodyAs(UTF_8) shouldBe ("Hello from http server!") } } @@ -99,10 +100,10 @@ class CompressionSpec : FeatureSpec() { .build(); client.send(request) - .toMono() - .block() + .wait()!! .let { - it!!.status() shouldBe (OK) + it.status() shouldBe (OK) + it.header("content-encoding").isPresent shouldBe false it.bodyAs(UTF_8) shouldBe ("Hello from http server!") } }