refactor(core): remove out-of-scope HTTP/2 client/proxy, reorganize docs, refresh README
HttpProxy and Http2Client (719 LOC) shipped a reverse-proxy adapter and outbound HTTP/2 client from flash core with zero callers anywhere in the server itself — only each other and their own tests. An HTTP/1.1+2 server framework has no business bundling an outbound client; that capability belongs in its own flash-extensions/flash-ext-* module if/when it's needed. Removed, along with the now-dead src/bench load driver that depended on Http2Client (no replacement client written here — flagged as follow-up work, not silently dropped). docs/http2/ had accumulated core, cross-protocol documentation alongside genuine HTTP/2-protocol internals: HTTP1-HARDENING, TRANSPORT, MESSAGE-MODEL, TRAILERS-AND-STREAMING and BYTES all describe machinery HTTP/1.1 and HTTP/2 share, not HTTP/2 specifically. Moved to a new docs/core/, leaving docs/http2/ to the protocol layers, wire internals and operational docs that are actually HTTP/2-specific. CLEARTEXT-AND-PROXY.md renamed to CLEARTEXT.md and its now-removed upstream-client section cut, matching the source removal above. README.md: removed the "HTTP/2 upstream proxy" section (documented the deleted HttpProxy/Http2Client), the flash-bench module row and build command (not a module that exists in this repo), and fixed every doc link to the new docs/core/ paths. Added the new FlashConfiguration.maxConnections field to the configuration reference. src/bench/ (a load-test harness distinct from the JMH suite, not wired into any Maven profile or CI) is committed here for the first time.
This commit is contained in:
@@ -1,131 +0,0 @@
|
||||
package dev.relism.flash.http2;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import dev.relism.flash.extension.FlashApp;
|
||||
import dev.relism.flash.extension.FlashConfiguration;
|
||||
import dev.relism.flash.http.HttpMethod;
|
||||
import dev.relism.flash.http.proxy.HttpProxy;
|
||||
import dev.relism.flash.http2.client.Http2Client;
|
||||
import dev.relism.flash.http2.client.Http2ClientResponse;
|
||||
import dev.relism.flash.models.MutableHeaderMap;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ProxyTrailerRelayTest {
|
||||
private FlashApp upstream;
|
||||
private FlashApp proxy;
|
||||
private Http2Client proxyUpstream;
|
||||
|
||||
@AfterEach
|
||||
void stop() {
|
||||
if (proxyUpstream != null) proxyUpstream.close();
|
||||
if (proxy != null) proxy.stop().join();
|
||||
if (upstream != null) upstream.stop().join();
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestAndResponseTrailersSurviveH2AndH1DownstreamProxyHops() throws Exception {
|
||||
int upstreamPort = freePort();
|
||||
upstream =
|
||||
FlashApp.create(
|
||||
FlashConfiguration.builder()
|
||||
.host("127.0.0.1")
|
||||
.port(upstreamPort)
|
||||
.http2CleartextEnabled(true)
|
||||
.build());
|
||||
upstream.post(
|
||||
"/relay",
|
||||
(request, response) ->
|
||||
response
|
||||
.header("x-query", request.query("mode"))
|
||||
.header("x-private-seen", String.valueOf(request.header("x-private") != null))
|
||||
.body(request.body().bytes())
|
||||
.trailer("x-relayed-trailer", request.trailers().first("x-request-trailer")));
|
||||
upstream.start();
|
||||
|
||||
int proxyPort = freePort();
|
||||
proxyUpstream = new Http2Client();
|
||||
proxy =
|
||||
FlashApp.create(
|
||||
FlashConfiguration.builder()
|
||||
.host("127.0.0.1")
|
||||
.port(proxyPort)
|
||||
.http2CleartextEnabled(true)
|
||||
.build());
|
||||
proxy.post(
|
||||
"/relay",
|
||||
HttpProxy.toHttp2(URI.create("http://127.0.0.1:" + upstreamPort), proxyUpstream));
|
||||
proxy.start();
|
||||
|
||||
MutableHeaderMap h2Headers = fields("connection", "x-private");
|
||||
add(h2Headers, "x-private", "must-not-cross");
|
||||
MutableHeaderMap h2Trailers = fields("x-request-trailer", "from-h2");
|
||||
try (Http2Client downstream = new Http2Client()) {
|
||||
Http2ClientResponse response =
|
||||
downstream.exchange(
|
||||
URI.create("http://127.0.0.1:" + proxyPort + "/relay?mode=h2"),
|
||||
HttpMethod.POST,
|
||||
h2Headers,
|
||||
"hello-h2".getBytes(StandardCharsets.UTF_8),
|
||||
h2Trailers);
|
||||
assertEquals("hello-h2", new String(response.body(), StandardCharsets.UTF_8));
|
||||
assertEquals("h2", response.headers().first("x-query"));
|
||||
assertEquals("false", response.headers().first("x-private-seen"));
|
||||
assertEquals("from-h2", response.trailers().first("x-relayed-trailer"));
|
||||
}
|
||||
|
||||
String h1 = h1Exchange(proxyPort);
|
||||
assertTrue(h1.contains("hello-h1"), h1);
|
||||
assertTrue(h1.toLowerCase().contains("x-query: h1"), h1);
|
||||
assertTrue(h1.toLowerCase().contains("x-private-seen: false"), h1);
|
||||
assertTrue(h1.toLowerCase().contains("x-relayed-trailer: from-h1"), h1);
|
||||
assertFalse(h1.contains("must-not-cross"), h1);
|
||||
}
|
||||
|
||||
private static String h1Exchange(int port) throws Exception {
|
||||
try (Socket socket = new Socket("127.0.0.1", port)) {
|
||||
socket.setSoTimeout(2_000);
|
||||
socket
|
||||
.getOutputStream()
|
||||
.write(
|
||||
("POST /relay?mode=h1 HTTP/1.1\r\n"
|
||||
+ "Host: 127.0.0.1\r\n"
|
||||
+ "Connection: x-private, close\r\n"
|
||||
+ "X-Private: must-not-cross\r\n"
|
||||
+ "Transfer-Encoding: chunked\r\n"
|
||||
+ "Trailer: x-request-trailer\r\n\r\n"
|
||||
+ "8\r\nhello-h1\r\n"
|
||||
+ "0\r\nX-Request-Trailer: from-h1\r\n\r\n")
|
||||
.getBytes(StandardCharsets.US_ASCII));
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
socket.getInputStream().transferTo(bytes);
|
||||
return bytes.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static MutableHeaderMap fields(String name, String value) {
|
||||
MutableHeaderMap headers = new MutableHeaderMap();
|
||||
add(headers, name, value);
|
||||
return headers;
|
||||
}
|
||||
|
||||
private static void add(MutableHeaderMap headers, String name, String value) {
|
||||
byte[] nameBytes = name.getBytes(StandardCharsets.US_ASCII);
|
||||
byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
headers.add(nameBytes, 0, nameBytes.length, valueBytes, 0, valueBytes.length);
|
||||
}
|
||||
|
||||
private static int freePort() throws Exception {
|
||||
try (ServerSocket socket = new ServerSocket(0)) {
|
||||
return socket.getLocalPort();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package dev.relism.flash.http2.client;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import dev.relism.flash.extension.FlashApp;
|
||||
import dev.relism.flash.extension.FlashConfiguration;
|
||||
import dev.relism.flash.http.HttpMethod;
|
||||
import dev.relism.flash.models.MutableHeaderMap;
|
||||
import dev.relism.flash.tls.TestKeystores;
|
||||
import dev.relism.flash.tls.TlsConfig;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
class Http2ClientTest {
|
||||
private FlashApp app;
|
||||
|
||||
@AfterEach
|
||||
void stop() {
|
||||
if (app != null) app.stop().join();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reusesOriginConnectionAndExchangesFlowControlledBodiesAndTrailers() throws Exception {
|
||||
int port = freePort();
|
||||
app =
|
||||
FlashApp.create(
|
||||
FlashConfiguration.builder()
|
||||
.host("127.0.0.1")
|
||||
.port(port)
|
||||
.http2CleartextEnabled(true)
|
||||
.build());
|
||||
app.post(
|
||||
"/relay",
|
||||
(request, response) -> {
|
||||
byte[] body = request.body().bytes();
|
||||
String checksum = request.trailers().first("x-request-checksum");
|
||||
return response
|
||||
.header("x-upstream", request.header("x-forwarded-test"))
|
||||
.body(body)
|
||||
.trailer("x-response-checksum", checksum);
|
||||
});
|
||||
app.start();
|
||||
|
||||
byte[] body = new byte[2 * 1024 * 1024 + 31];
|
||||
for (int i = 0; i < body.length; i++) body[i] = (byte) (i * 29);
|
||||
MutableHeaderMap requestHeaders = fields("x-forwarded-test", "yes");
|
||||
MutableHeaderMap requestTrailers = fields("x-request-checksum", "valid");
|
||||
|
||||
try (Http2Client client = new Http2Client()) {
|
||||
URI uri = URI.create("http://127.0.0.1:" + port + "/relay");
|
||||
Http2ClientResponse first =
|
||||
client.exchange(uri, HttpMethod.POST, requestHeaders, body, requestTrailers);
|
||||
Http2ClientResponse second =
|
||||
client.exchange(
|
||||
uri,
|
||||
HttpMethod.POST,
|
||||
requestHeaders,
|
||||
"again".getBytes(StandardCharsets.UTF_8),
|
||||
requestTrailers);
|
||||
|
||||
assertEquals(200, first.statusCode());
|
||||
assertEquals("yes", first.headers().first("x-upstream"));
|
||||
assertArrayEquals(body, first.body());
|
||||
assertEquals("valid", first.trailers().first("x-response-checksum"));
|
||||
assertArrayEquals("again".getBytes(StandardCharsets.UTF_8), second.body());
|
||||
assertEquals(1, client.pooledConnectionCount());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void negotiatesTlsAlpnAndVerifiesTheUpstreamHostname(@TempDir Path directory) throws Exception {
|
||||
int port = freePort();
|
||||
Path keystore =
|
||||
TestKeystores.build(
|
||||
directory,
|
||||
"http2-client.p12",
|
||||
"changeit",
|
||||
TestKeystores.Entry.of("server", "localhost", "localhost"));
|
||||
app =
|
||||
FlashApp.create(
|
||||
FlashConfiguration.builder()
|
||||
.host("127.0.0.1")
|
||||
.port(port)
|
||||
.tls(TlsConfig.keystore(keystore, "changeit"))
|
||||
.http2Enabled(true)
|
||||
.build());
|
||||
app.get("/secure", (request, response) -> "tls-h2");
|
||||
app.start();
|
||||
|
||||
try (Http2Client client = new Http2Client(TestKeystores.trustAllClientContext())) {
|
||||
Http2ClientResponse response =
|
||||
client.get(URI.create("https://localhost:" + port + "/secure"));
|
||||
assertEquals(200, response.statusCode());
|
||||
assertEquals("tls-h2", new String(response.body(), StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void configuresConnectionsForRequestResponseLatency() throws Exception {
|
||||
try (Socket socket = new Socket()) {
|
||||
assertFalse(socket.getTcpNoDelay());
|
||||
Http2Client.configureLowLatency(socket);
|
||||
assertTrue(socket.getTcpNoDelay());
|
||||
}
|
||||
}
|
||||
|
||||
private static MutableHeaderMap fields(String name, String value) {
|
||||
MutableHeaderMap headers = new MutableHeaderMap();
|
||||
byte[] nameBytes = name.getBytes(StandardCharsets.US_ASCII);
|
||||
byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
|
||||
headers.add(nameBytes, 0, nameBytes.length, valueBytes, 0, valueBytes.length);
|
||||
return headers;
|
||||
}
|
||||
|
||||
private static int freePort() throws Exception {
|
||||
try (ServerSocket socket = new ServerSocket(0)) {
|
||||
return socket.getLocalPort();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user