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..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 @@ -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 = false, 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..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 @@ -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,46 @@ 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!" + } + } + + guavaServer.stopAsync().awaitTerminated() + } + feature("Max initial line length") { val serverConfig = configBlock(""" port: 0 @@ -300,14 +342,27 @@ 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 = 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 +372,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() 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!") } }