diff --git a/.github/workflows/ci.yml b/.github/workflows/ci-4.x.yml similarity index 94% rename from .github/workflows/ci.yml rename to .github/workflows/ci-4.x.yml index ac29e09..469dfd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci-4.x.yml @@ -1,13 +1,13 @@ -name: CI +name: vertx-http-proxy (4.x) on: push: branches: - main - - '[0-9]+.[0-9]+' + - '[0-9]+.[0-9x]+' pull_request: branches: - main - - '[0-9]+.[0-9]+' + - '[0-9]+.[0-9x]+' schedule: - cron: '0 4 * * *' jobs: diff --git a/pom.xml b/pom.xml index 1895bfc..6bb643b 100644 --- a/pom.xml +++ b/pom.xml @@ -20,16 +20,21 @@ io.vertx vertx-ext-parent - 38 + 42 vertx-http-proxy - 5.0.0-SNAPSHOT + 4.5.23-SNAPSHOT Vert.x Http Proxy + + scm:git:git@github.com:eclipse-vertx/vertx-http-proxy.git + scm:git:git@github.com:eclipse-vertx/vertx-http-proxy.git + git@github.com:eclipse-vertx/vertx-http-proxy.git + + - 5.0.0-SNAPSHOT ${project.build.directory} false ${project.basedir}/src/main/resources/META-INF/MANIFEST.MF @@ -40,7 +45,7 @@ io.vertx vertx-dependencies - ${stack.version} + ${project.version} pom import diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 42bee1c..d40bdc4 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -48,16 +48,16 @@ All user-agent requests are forwarded to the *origin server* conveniently. === Origin server routing -You can create a proxy that forwards all the traffic to a single server like seen before +You can create a proxy that forwards all the traffic to a single server like seen before. -You can set an origin selector to route the traffic to a given server +You can set an origin selector to route the traffic to a given server: [source,java] ---- {@link examples.HttpProxyExamples#originSelector} ---- -You can set a function to create the client request to the origin server for ultimate flexibility +You can set a function to create the client request to the origin server for ultimate flexibility: [source,java] ---- @@ -70,21 +70,19 @@ End-to-end headers are forwarded by the proxy, hop-by-hop headers are ignored. ==== Request authority -As a transparent proxy, the request authority ({@code Host} header for HTTP/1.1, {@code :authority} pseudo header -for HTTP/2) is preserved. +By default, the proxy request authority (`Host` header for HTTP/1.1, `:authority` pseudo header for HTTP/2) is set by the HTTP client according to the origin server address. -You can override the request authority +CAUTION: The origin server may need you to set the https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Host[`x-forwarded-*`] or https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Forwarded[`forwarded`] headers on the proxied request. +For example, it might use the values to compute a created HTTP resource https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Location[location]. + +Nevertheless, you can override the request authority: [source,java] ---- {@link examples.HttpProxyExamples#overrideAuthority} ---- -When the request authority is overridden, the {@code x-forwarded-host} header is set on the request to the origin server -with the original authority value. - -WARNING: changing the request authority can have undesirable side effects and can affect the proxied web server that might -rely on the original request authority to handle cookies, URL redirects and such. +When the request authority is overridden, the `x-forwarded-host` header is automatically set on the request to the origin server with the original authority value. === WebSockets diff --git a/src/main/generated/io/vertx/httpproxy/ProxyOptionsConverter.java b/src/main/generated/io/vertx/httpproxy/ProxyOptionsConverter.java index 65fd8a6..79e9c79 100644 --- a/src/main/generated/io/vertx/httpproxy/ProxyOptionsConverter.java +++ b/src/main/generated/io/vertx/httpproxy/ProxyOptionsConverter.java @@ -17,7 +17,7 @@ public class ProxyOptionsConverter { private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER; private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER; - public static void fromJson(Iterable> json, ProxyOptions obj) { + static void fromJson(Iterable> json, ProxyOptions obj) { for (java.util.Map.Entry member : json) { switch (member.getKey()) { case "cacheOptions": @@ -34,11 +34,11 @@ public static void fromJson(Iterable> json, } } - public static void toJson(ProxyOptions obj, JsonObject json) { + static void toJson(ProxyOptions obj, JsonObject json) { toJson(obj, json.getMap()); } - public static void toJson(ProxyOptions obj, java.util.Map json) { + static void toJson(ProxyOptions obj, java.util.Map json) { if (obj.getCacheOptions() != null) { json.put("cacheOptions", obj.getCacheOptions().toJson()); } diff --git a/src/main/generated/io/vertx/httpproxy/cache/CacheOptionsConverter.java b/src/main/generated/io/vertx/httpproxy/cache/CacheOptionsConverter.java index a80be25..6a637b5 100644 --- a/src/main/generated/io/vertx/httpproxy/cache/CacheOptionsConverter.java +++ b/src/main/generated/io/vertx/httpproxy/cache/CacheOptionsConverter.java @@ -17,7 +17,7 @@ public class CacheOptionsConverter { private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER; private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER; - public static void fromJson(Iterable> json, CacheOptions obj) { + static void fromJson(Iterable> json, CacheOptions obj) { for (java.util.Map.Entry member : json) { switch (member.getKey()) { case "maxSize": @@ -29,11 +29,11 @@ public static void fromJson(Iterable> json, } } - public static void toJson(CacheOptions obj, JsonObject json) { + static void toJson(CacheOptions obj, JsonObject json) { toJson(obj, json.getMap()); } - public static void toJson(CacheOptions obj, java.util.Map json) { + static void toJson(CacheOptions obj, java.util.Map json) { json.put("maxSize", obj.getMaxSize()); } } diff --git a/src/main/java/examples/HttpProxyExamples.java b/src/main/java/examples/HttpProxyExamples.java index f1446c1..0fb1bfd 100644 --- a/src/main/java/examples/HttpProxyExamples.java +++ b/src/main/java/examples/HttpProxyExamples.java @@ -6,22 +6,17 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.RequestOptions; +import io.vertx.core.net.HostAndPort; import io.vertx.core.net.SocketAddress; -import io.vertx.httpproxy.Body; -import io.vertx.httpproxy.HttpProxy; -import io.vertx.httpproxy.ProxyContext; -import io.vertx.httpproxy.ProxyInterceptor; -import io.vertx.httpproxy.ProxyOptions; -import io.vertx.httpproxy.ProxyRequest; -import io.vertx.httpproxy.ProxyResponse; +import io.vertx.httpproxy.*; import io.vertx.httpproxy.cache.CacheOptions; /** * @author Emad Alblueshi */ +@SuppressWarnings("unused") public class HttpProxyExamples { public void origin(Vertx vertx) { @@ -45,20 +40,20 @@ public void proxy(Vertx vertx) { proxyServer.requestHandler(proxy).listen(8080); } - private SocketAddress resolveOriginAddress(HttpServerRequest request) { + private Future resolveOriginAddress(ProxyContext proxyContext) { return null; } public void originSelector(HttpProxy proxy) { - proxy.originSelector(request -> Future.succeededFuture(resolveOriginAddress(request))); + proxy.origin(OriginRequestProvider.selector(proxyContext -> resolveOriginAddress(proxyContext))); } - private RequestOptions resolveOriginOptions(HttpServerRequest request) { + private RequestOptions resolveOriginOptions(ProxyContext request) { return null; } public void originRequestProvider(HttpProxy proxy) { - proxy.originRequestProvider((request, client) -> client.request(resolveOriginOptions(request))); + proxy.origin((proxyContext) -> proxyContext.client().request(resolveOriginOptions(proxyContext))); } public void inboundInterceptor(HttpProxy proxy) { @@ -136,40 +131,12 @@ private Body filter(Body body) { return body; } - public void more(Vertx vertx, HttpClient proxyClient) { - HttpProxy proxy = HttpProxy.reverseProxy(proxyClient).originSelector( - address -> Future.succeededFuture(SocketAddress.inetSocketAddress(7070, "origin")) - ); - } - - public void lowLevel(Vertx vertx, HttpServer proxyServer, HttpClient proxyClient) { - - proxyServer.requestHandler(request -> { - ProxyRequest proxyRequest = ProxyRequest.reverseProxy(request); - - proxyClient.request(proxyRequest.getMethod(), 8080, "origin", proxyRequest.getURI()) - .compose(proxyRequest::send) - .onSuccess(proxyResponse -> { - // Send the proxy response - proxyResponse.send(); - }) - .onFailure(err -> { - // Release the request - proxyRequest.release(); - - // Send error - request.response().setStatusCode(500) - .send(); - }); - }); - } - public void overrideAuthority(HttpProxy proxy) { proxy.addInterceptor(new ProxyInterceptor() { @Override public Future handleProxyRequest(ProxyContext context) { ProxyRequest proxyRequest = context.request(); - proxyRequest.setAuthority("example.com:80"); + proxyRequest.setAuthority(HostAndPort.create("example.com", 80)); return ProxyInterceptor.super.handleProxyRequest(context); } }); diff --git a/src/main/java/io/vertx/httpproxy/HttpProxy.java b/src/main/java/io/vertx/httpproxy/HttpProxy.java index 9137dda..d06f164 100644 --- a/src/main/java/io/vertx/httpproxy/HttpProxy.java +++ b/src/main/java/io/vertx/httpproxy/HttpProxy.java @@ -18,7 +18,6 @@ import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientRequest; import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.RequestOptions; import io.vertx.core.net.SocketAddress; import io.vertx.httpproxy.impl.ReverseProxy; @@ -61,7 +60,7 @@ static HttpProxy reverseProxy(ProxyOptions options, HttpClient client) { */ @Fluent default HttpProxy origin(SocketAddress address) { - return originSelector(req -> Future.succeededFuture(address)); + return origin(OriginRequestProvider.fixedAddress(address)); } /** @@ -73,7 +72,7 @@ default HttpProxy origin(SocketAddress address) { */ @Fluent default HttpProxy origin(int port, String host) { - return origin(SocketAddress.inetSocketAddress(port, host)); + return origin(OriginRequestProvider.fixedAddress(port, host)); } /** @@ -81,12 +80,12 @@ default HttpProxy origin(int port, String host) { * * @param selector the selector * @return a reference to this, so the API can be used fluently + * @deprecated use {@link #origin(OriginRequestProvider)} instead */ + @Deprecated @Fluent default HttpProxy originSelector(Function> selector) { - return originRequestProvider((req, client) -> selector - .apply(req) - .flatMap(server -> client.request(new RequestOptions().setServer(server)))); + return origin(OriginRequestProvider.selector(proxyContext -> selector.apply(proxyContext.request().proxiedRequest()))); } /** @@ -95,10 +94,23 @@ default HttpProxy originSelector(Function> provider); + default HttpProxy originRequestProvider(BiFunction> provider) { + return origin(proxyContext -> provider.apply(proxyContext.request().proxiedRequest(), proxyContext.client())); + } + + /** + * Set a provider that creates the request to the origin server based on {@link ProxyContext}. + * + * @param provider the provider + * @return a reference to this, so the API can be used fluently + */ + @Fluent + HttpProxy origin(OriginRequestProvider provider); /** * Add an interceptor to the interceptor chain. diff --git a/src/main/java/io/vertx/httpproxy/OriginRequestProvider.java b/src/main/java/io/vertx/httpproxy/OriginRequestProvider.java new file mode 100644 index 0000000..afc3300 --- /dev/null +++ b/src/main/java/io/vertx/httpproxy/OriginRequestProvider.java @@ -0,0 +1,56 @@ +package io.vertx.httpproxy; + +import io.vertx.codegen.annotations.VertxGen; +import io.vertx.core.Future; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.RequestOptions; +import io.vertx.core.net.SocketAddress; + +import java.util.function.Function; + +/** + * A provider that creates the request to the origin server based on {@link ProxyContext}. + */ +@VertxGen +@FunctionalInterface +public interface OriginRequestProvider { + + /** + * Creates a simple provider for a fixed {@code port} and {@code host}. + */ + static OriginRequestProvider fixedAddress(int port, String host) { + return fixedAddress(SocketAddress.inetSocketAddress(port, host)); + } + + /** + * Creates a simple provider for a fixed {@link SocketAddress}. + */ + static OriginRequestProvider fixedAddress(SocketAddress address) { + return new OriginRequestProvider() { + @Override + public Future create(ProxyContext proxyContext) { + return proxyContext.client().request(new RequestOptions().setServer(address)); + } + }; + } + + /** + * Creates a provider that selects the origin server based on {@link ProxyContext}. + */ + static OriginRequestProvider selector(Function> selector) { + return new OriginRequestProvider() { + @Override + public Future create(ProxyContext proxyContext) { + return selector.apply(proxyContext).flatMap(server -> proxyContext.client().request(new RequestOptions().setServer(server))); + } + }; + } + + /** + * Create the {@link HttpClientRequest} to the origin server for a given {@link ProxyContext}. + * + * @param proxyContext the context of the proxied request and response + * @return a future, completed with the {@link HttpClientRequest} or failed + */ + Future create(ProxyContext proxyContext); +} diff --git a/src/main/java/io/vertx/httpproxy/ProxyContext.java b/src/main/java/io/vertx/httpproxy/ProxyContext.java index 8853325..acbfaad 100644 --- a/src/main/java/io/vertx/httpproxy/ProxyContext.java +++ b/src/main/java/io/vertx/httpproxy/ProxyContext.java @@ -2,6 +2,7 @@ import io.vertx.codegen.annotations.VertxGen; import io.vertx.core.Future; +import io.vertx.core.http.HttpClient; /** * A controller for proxy interception. @@ -45,4 +46,9 @@ public interface ProxyContext { * @return the attached payload */ T get(String name, Class type); + + /** + * @return the {@link HttpClient} use to interact with the origin server + */ + HttpClient client(); } diff --git a/src/main/java/io/vertx/httpproxy/ProxyOptions.java b/src/main/java/io/vertx/httpproxy/ProxyOptions.java index 4c1169e..bae6bed 100644 --- a/src/main/java/io/vertx/httpproxy/ProxyOptions.java +++ b/src/main/java/io/vertx/httpproxy/ProxyOptions.java @@ -1,13 +1,15 @@ package io.vertx.httpproxy; import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; import io.vertx.core.json.JsonObject; import io.vertx.httpproxy.cache.CacheOptions; /** * Proxy options. */ -@DataObject(generateConverter = true) +@DataObject +@JsonGen(publicConverter = false) public class ProxyOptions { /** diff --git a/src/main/java/io/vertx/httpproxy/ProxyRequest.java b/src/main/java/io/vertx/httpproxy/ProxyRequest.java index d22676c..5f7bda9 100644 --- a/src/main/java/io/vertx/httpproxy/ProxyRequest.java +++ b/src/main/java/io/vertx/httpproxy/ProxyRequest.java @@ -19,6 +19,7 @@ import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpVersion; +import io.vertx.core.net.HostAndPort; import io.vertx.httpproxy.impl.ProxiedRequest; /** @@ -113,12 +114,12 @@ static ProxyRequest reverseProxy(HttpServerRequest proxiedRequest) { * @return a reference to this, so the API can be used fluently */ @Fluent - ProxyRequest setAuthority(String authority); + ProxyRequest setAuthority(HostAndPort authority); /** * @return the request authority, for HTTP2 the {@literal :authority} pseudo header otherwise the {@literal Host} header */ - String getAuthority(); + HostAndPort getAuthority(); /** * @return the headers that will be sent to the origin server, the returned headers can be modified. The headers diff --git a/src/main/java/io/vertx/httpproxy/cache/CacheOptions.java b/src/main/java/io/vertx/httpproxy/cache/CacheOptions.java index 3c27e46..69af307 100644 --- a/src/main/java/io/vertx/httpproxy/cache/CacheOptions.java +++ b/src/main/java/io/vertx/httpproxy/cache/CacheOptions.java @@ -1,6 +1,7 @@ package io.vertx.httpproxy.cache; import io.vertx.codegen.annotations.DataObject; +import io.vertx.codegen.json.annotations.JsonGen; import io.vertx.core.impl.Arguments; import io.vertx.core.json.JsonObject; import io.vertx.httpproxy.impl.CacheImpl; @@ -9,7 +10,8 @@ /** * Cache options. */ -@DataObject(generateConverter = true) +@DataObject +@JsonGen(publicConverter = false) public class CacheOptions { public static final int DEFAULT_MAX_SIZE = 1000; diff --git a/src/main/java/io/vertx/httpproxy/impl/CachingFilter.java b/src/main/java/io/vertx/httpproxy/impl/CachingFilter.java index b248203..5d1ed52 100644 --- a/src/main/java/io/vertx/httpproxy/impl/CachingFilter.java +++ b/src/main/java/io/vertx/httpproxy/impl/CachingFilter.java @@ -11,7 +11,7 @@ import io.vertx.httpproxy.ProxyResponse; import io.vertx.httpproxy.spi.cache.Cache; -import java.util.Date; +import java.time.Instant; import java.util.function.BiFunction; class CachingFilter implements ProxyInterceptor { @@ -136,8 +136,8 @@ private Future tryHandleProxyRequestFromCache(ProxyContext contex // String ifModifiedSinceHeader = response.getHeader(HttpHeaders.IF_MODIFIED_SINCE); if ((response.method() == HttpMethod.GET || response.method() == HttpMethod.HEAD) && ifModifiedSinceHeader != null && resource.lastModified != null) { - Date ifModifiedSince = ParseUtils.parseHeaderDate(ifModifiedSinceHeader); - if (resource.lastModified.getTime() <= ifModifiedSince.getTime()) { + Instant ifModifiedSince = ParseUtils.parseHeaderDate(ifModifiedSinceHeader); + if (!ifModifiedSince.isAfter(resource.lastModified)) { response.response().setStatusCode(304).end(); return Future.succeededFuture(); } diff --git a/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java b/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java index af64626..4aa83f4 100644 --- a/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java +++ b/src/main/java/io/vertx/httpproxy/impl/HttpUtils.java @@ -13,7 +13,7 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; -import java.util.Date; +import java.time.Instant; import java.util.List; class HttpUtils { @@ -35,13 +35,13 @@ static Boolean isChunked(MultiMap headers) { } } - static Date dateHeader(MultiMap headers) { + static Instant dateHeader(MultiMap headers) { String dateHeader = headers.get(HttpHeaders.DATE); if (dateHeader == null) { List warningHeaders = headers.getAll("warning"); if (warningHeaders.size() > 0) { for (String warningHeader : warningHeaders) { - Date date = ParseUtils.parseWarningHeaderDate(warningHeader); + Instant date = ParseUtils.parseWarningHeaderDate(warningHeader); if (date != null) { return date; } diff --git a/src/main/java/io/vertx/httpproxy/impl/ParseUtils.java b/src/main/java/io/vertx/httpproxy/impl/ParseUtils.java index 22320de..b22a224 100644 --- a/src/main/java/io/vertx/httpproxy/impl/ParseUtils.java +++ b/src/main/java/io/vertx/httpproxy/impl/ParseUtils.java @@ -10,18 +10,30 @@ */ package io.vertx.httpproxy.impl; -import java.text.SimpleDateFormat; -import java.time.DayOfWeek; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.*; /** * @author Julien Viet */ public class ParseUtils { - public static Date parseHeaderDate(String value) { + public static final DateTimeFormatter RFC_850_DATE_TIME = new DateTimeFormatterBuilder() + .appendPattern("EEEE, dd-MMM-yy HH:mm:ss") + .parseLenient() + .appendLiteral(" GMT") + .toFormatter(Locale.US) + .withZone(ZoneId.of("UTC")); + + public static final DateTimeFormatter ASC_TIME = new DateTimeFormatterBuilder() + .appendPattern("EEE MMM d HH:mm:ss yyyy") + .parseLenient() + .toFormatter(Locale.US) + .withZone(ZoneId.of("UTC")); + + public static Instant parseHeaderDate(String value) { try { return parseHttpDate(value); } catch (Exception e) { @@ -29,7 +41,7 @@ public static Date parseHeaderDate(String value) { } } - public static Date parseWarningHeaderDate(String value) { + public static Instant parseWarningHeaderDate(String value) { // warn-code int index = value.indexOf(' '); if (index > 0) { @@ -43,11 +55,7 @@ public static Date parseWarningHeaderDate(String value) { int len = value.length(); if (index + 2 < len && value.charAt(index + 1) == '"' && value.charAt(len - 1) == '"') { // Space for 2 double quotes - String date = value.substring(index + 2, len - 1); - try { - return parseHttpDate(date); - } catch (Exception ignore) { - } + return parseHeaderDate(value.substring(index + 2, len - 1)); } } } @@ -55,98 +63,19 @@ public static Date parseWarningHeaderDate(String value) { return null; } - private static SimpleDateFormat RFC_1123_DATE_TIME() { - SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - return format; + public static String formatHttpDate(Instant date) { + return DateTimeFormatter.RFC_1123_DATE_TIME.format(OffsetDateTime.ofInstant(date, ZoneOffset.UTC)); } - private static SimpleDateFormat RFC_850_DATE_TIME() { - SimpleDateFormat format = new SimpleDateFormat("EEEEEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - return format; - } - - private static SimpleDateFormat ASC_TIME() { - SimpleDateFormat format = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("GMT")); - return format; - } - - public static String formatHttpDate(Date date) { - return RFC_1123_DATE_TIME().format(date); - } - - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1 - public static Date parseHttpDate(String value) throws Exception { - int sep = 0; - while (true) { - if (sep < value.length()) { - char c = value.charAt(sep); - if (c == ',') { - String s = value.substring(0, sep); - if (parseWkday(s) != null) { - // rfc1123-date - return RFC_1123_DATE_TIME().parse(value); - } else if (parseWeekday(s) != null) { - // rfc850-date - return RFC_850_DATE_TIME().parse(value); - } - return null; - } else if (c == ' ') { - String s = value.substring(0, sep); - if (parseWkday(s) != null) { - // asctime-date - return ASC_TIME().parse(value); - } - return null; - } - sep++; - } else { - return null; - } - } - } - - private static DayOfWeek parseWkday(String value) { - switch (value) { - case "Mon": - return DayOfWeek.MONDAY; - case "Tue": - return DayOfWeek.TUESDAY; - case "Wed": - return DayOfWeek.WEDNESDAY; - case "Thu": - return DayOfWeek.THURSDAY; - case "Fri": - return DayOfWeek.FRIDAY; - case "Sat": - return DayOfWeek.SATURDAY; - case "Sun": - return DayOfWeek.SUNDAY; - default: - return null; + // https://www.rfc-editor.org/rfc/rfc9110#http.date + public static Instant parseHttpDate(String value) throws Exception { + int pos = value.indexOf(','); + if (pos == 3) { // e.g. Sun, 06 Nov 1994 08:49:37 GMT + return DateTimeFormatter.RFC_1123_DATE_TIME.parse(value, Instant::from); } - } - - private static DayOfWeek parseWeekday(String value) { - switch (value) { - case "Monday": - return DayOfWeek.MONDAY; - case "Tuesday": - return DayOfWeek.TUESDAY; - case "Wednesday": - return DayOfWeek.WEDNESDAY; - case "Thursday": - return DayOfWeek.THURSDAY; - case "Friday": - return DayOfWeek.FRIDAY; - case "Saturday": - return DayOfWeek.SATURDAY; - case "Sunday": - return DayOfWeek.SUNDAY; - default: - return null; + if (pos == -1) { // e.g. Sun Nov 6 08:49:37 1994 + return ASC_TIME.parse(value, Instant::from); } + return RFC_850_DATE_TIME.parse(value, Instant::from); // e.g. Sunday, 06-Nov-94 08:49:37 GMT } } diff --git a/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java b/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java index 6f694f0..47aafd1 100644 --- a/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java +++ b/src/main/java/io/vertx/httpproxy/impl/ProxiedRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2025 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -10,19 +10,12 @@ */ package io.vertx.httpproxy.impl; -import io.vertx.core.AsyncResult; -import io.vertx.core.Future; -import io.vertx.core.Handler; -import io.vertx.core.MultiMap; -import io.vertx.core.Promise; +import io.vertx.core.*; import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpClientRequest; -import io.vertx.core.http.HttpHeaders; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpVersion; +import io.vertx.core.http.*; import io.vertx.core.http.impl.HttpServerRequestInternal; import io.vertx.core.impl.ContextInternal; +import io.vertx.core.net.HostAndPort; import io.vertx.core.streams.Pipe; import io.vertx.httpproxy.Body; import io.vertx.httpproxy.ProxyRequest; @@ -31,19 +24,21 @@ import java.util.Map; import java.util.Objects; +import static io.vertx.core.http.HttpHeaders.*; + public class ProxiedRequest implements ProxyRequest { private static final CharSequence X_FORWARDED_HOST = HttpHeaders.createOptimized("x-forwarded-host"); private static final MultiMap HOP_BY_HOP_HEADERS = MultiMap.caseInsensitiveMultiMap() - .add(HttpHeaders.CONNECTION, "whatever") - .add(HttpHeaders.KEEP_ALIVE, "whatever") - .add(HttpHeaders.PROXY_AUTHENTICATE, "whatever") - .add(HttpHeaders.PROXY_AUTHORIZATION, "whatever") + .add(CONNECTION, "whatever") + .add(KEEP_ALIVE, "whatever") + .add(PROXY_AUTHENTICATE, "whatever") + .add(PROXY_AUTHORIZATION, "whatever") .add("te", "whatever") .add("trailer", "whatever") - .add(HttpHeaders.TRANSFER_ENCODING, "whatever") - .add(HttpHeaders.UPGRADE, "whatever"); + .add(TRANSFER_ENCODING, "whatever") + .add(UPGRADE, "whatever"); final ContextInternal context; private HttpMethod method; @@ -51,7 +46,7 @@ public class ProxiedRequest implements ProxyRequest { private String uri; private final String absoluteURI; private Body body; - private String authority; + private HostAndPort authority; private final MultiMap headers; HttpClientRequest request; private final HttpServerRequest proxiedRequest; @@ -60,7 +55,7 @@ public ProxiedRequest(HttpServerRequest proxiedRequest) { // Determine content length long contentLength = -1L; - String contentLengthHeader = proxiedRequest.getHeader(HttpHeaders.CONTENT_LENGTH); + String contentLengthHeader = proxiedRequest.getHeader(CONTENT_LENGTH); if (contentLengthHeader != null) { try { contentLength = Long.parseLong(contentLengthHeader); @@ -77,7 +72,7 @@ public ProxiedRequest(HttpServerRequest proxiedRequest) { this.absoluteURI = proxiedRequest.absoluteURI(); this.proxiedRequest = proxiedRequest; this.context = (ContextInternal) ((HttpServerRequestInternal) proxiedRequest).context(); - this.authority = proxiedRequest.host(); + this.authority = null; // null is used as a signal to indicate an unchanged authority } @Override @@ -108,14 +103,14 @@ public ProxyRequest setBody(Body body) { } @Override - public ProxyRequest setAuthority(String authority) { + public ProxyRequest setAuthority(HostAndPort authority) { Objects.requireNonNull(authority); - this.authority= authority; + this.authority = authority; return this; } @Override - public String getAuthority() { + public HostAndPort getAuthority() { return authority; } @@ -154,13 +149,13 @@ public ProxyResponse response() { } void sendRequest(Handler> responseHandler) { + proxiedRequest.response().exceptionHandler(throwable -> request.reset(0L, throwable)); request.response().map(r -> { r.pause(); // Pause it return new ProxiedResponse(this, proxiedRequest.response(), r); }).onComplete(responseHandler); - request.setMethod(method); request.setURI(uri); @@ -168,56 +163,51 @@ void sendRequest(Handler> responseHandler) { for (Map.Entry header : headers) { String name = header.getKey(); String value = header.getValue(); - if (!HOP_BY_HOP_HEADERS.contains(name) && !name.equals("host")) { + if (!HOP_BY_HOP_HEADERS.contains(name) && !name.equalsIgnoreCase(HttpHeaders.HOST.toString())) { request.headers().add(name, value); } } // - String proxiedAuthority = proxiedRequest.host(); - int idx = proxiedAuthority.indexOf(':'); - String proxiedHost; - int proxiedPort; - if (idx == -1) { - proxiedHost = proxiedAuthority; - proxiedPort = -1; - } else { - proxiedHost = proxiedAuthority.substring(0, idx); - proxiedPort = Integer.parseInt(proxiedAuthority.substring(idx + 1)); + if (authority != null) { + request.authority(authority); + HostAndPort proxiedAuthority = proxiedRequest.authority(); + if (!equals(authority, proxiedAuthority)) { + // Should cope with existing forwarded host headers + request.putHeader(X_FORWARDED_HOST, proxiedAuthority.toString()); + } } - String host; - int port; - idx = authority.indexOf(':'); - if (idx == -1) { - host = authority; - port = -1; + if (body == null) { + if (proxiedRequest.headers().contains(CONTENT_LENGTH)) { + request.putHeader(CONTENT_LENGTH, "0"); + } + request.end(); } else { - host = authority.substring(0, idx); - port = Integer.parseInt(authority.substring(idx + 1)); - } - request.setHost(host); - request.setPort(port == -1 ? (request.absoluteURI().startsWith("https://") ? 443 : 80) : port); - if (!proxiedHost.equals(host) || proxiedPort != port) { - request.putHeader(X_FORWARDED_HOST, proxiedAuthority); - } + long len = body.length(); + if (len >= 0) { + request.putHeader(CONTENT_LENGTH, Long.toString(len)); + } else { + Boolean isChunked = HttpUtils.isChunked(proxiedRequest.headers()); + request.setChunked(len == -1 && Boolean.TRUE == isChunked); + } - long len = body.length(); - if (len >= 0) { - request.putHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(len)); - } else { - Boolean isChunked = HttpUtils.isChunked(proxiedRequest.headers()); - request.setChunked(len == -1 && Boolean.TRUE == isChunked); + Pipe pipe = body.stream().pipe(); + pipe.endOnComplete(true); + pipe.endOnFailure(false); + pipe.to(request, ar -> { + if (ar.failed()) { + request.reset(); + } + }); } + } - Pipe pipe = body.stream().pipe(); - pipe.endOnComplete(true); - pipe.endOnFailure(false); - pipe.to(request, ar -> { - if (ar.failed()) { - request.reset(); - } - }); + private static boolean equals(HostAndPort hp1, HostAndPort hp2) { + if (hp1 == null || hp2 == null) { + return false; + } + return hp1.host().equals(hp2.host()) && hp1.port() == hp2.port(); } @Override diff --git a/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java b/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java index 3bc88a2..e4d3a3c 100644 --- a/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java +++ b/src/main/java/io/vertx/httpproxy/impl/ProxiedResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2025 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -26,11 +26,13 @@ import io.vertx.httpproxy.ProxyRequest; import io.vertx.httpproxy.ProxyResponse; +import java.time.Instant; import java.util.ArrayList; -import java.util.Date; import java.util.Iterator; import java.util.List; +import static io.vertx.core.http.HttpHeaders.CONTENT_LENGTH; + class ProxiedResponse implements ProxyResponse { private final ProxiedRequest request; @@ -85,7 +87,7 @@ class ProxiedResponse implements ProxyResponse { String dateHeader = response.getHeader(HttpHeaders.DATE); String expiresHeader = response.getHeader(HttpHeaders.EXPIRES); if (dateHeader != null && expiresHeader != null) { - maxAge = ParseUtils.parseHeaderDate(expiresHeader).getTime() - ParseUtils.parseHeaderDate(dateHeader).getTime(); + maxAge = ParseUtils.parseHeaderDate(expiresHeader).toEpochMilli() - ParseUtils.parseHeaderDate(dateHeader).toEpochMilli(); } } } @@ -176,9 +178,9 @@ public void send(Handler> completionHandler) { } // Date header - Date date = HttpUtils.dateHeader(headers); + Instant date = HttpUtils.dateHeader(headers); if (date == null) { - date = new Date(); + date = Instant.now(); } try { proxiedResponse.putHeader("date", ParseUtils.formatHttpDate(date)); @@ -191,12 +193,12 @@ public void send(Handler> completionHandler) { if (warningHeaders.size() > 0) { warningHeaders = new ArrayList<>(warningHeaders); String dateHeader = headers.get("date"); - Date dateInstant = dateHeader != null ? ParseUtils.parseHeaderDate(dateHeader) : null; + Instant dateInstant = dateHeader != null ? ParseUtils.parseHeaderDate(dateHeader) : null; Iterator i = warningHeaders.iterator(); // Suppress incorrect warning header while (i.hasNext()) { String warningHeader = i.next(); - Date warningInstant = ParseUtils.parseWarningHeaderDate(warningHeader); + Instant warningInstant = ParseUtils.parseWarningHeaderDate(warningHeader); if (warningInstant != null && dateInstant != null && !warningInstant.equals(dateInstant)) { i.remove(); } @@ -215,9 +217,11 @@ public void send(Handler> completionHandler) { } }); - // if (body == null) { - proxiedResponse.end(); + if (response != null && response.headers().contains(CONTENT_LENGTH)) { + proxiedResponse.putHeader(CONTENT_LENGTH, "0"); + } + proxiedResponse.end().onComplete(completionHandler); return; } diff --git a/src/main/java/io/vertx/httpproxy/impl/Resource.java b/src/main/java/io/vertx/httpproxy/impl/Resource.java index 0f680cb..2b7768b 100644 --- a/src/main/java/io/vertx/httpproxy/impl/Resource.java +++ b/src/main/java/io/vertx/httpproxy/impl/Resource.java @@ -10,14 +10,13 @@ */ package io.vertx.httpproxy.impl; -import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; import io.vertx.httpproxy.Body; import io.vertx.httpproxy.ProxyResponse; -import java.util.Date; +import java.time.Instant; class Resource { @@ -27,7 +26,7 @@ class Resource { final MultiMap headers; final long timestamp; final long maxAge; - final Date lastModified; + final Instant lastModified; final String etag; final Buffer content = Buffer.buffer(); diff --git a/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java b/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java index 903070b..0058328 100644 --- a/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java +++ b/src/main/java/io/vertx/httpproxy/impl/ReverseProxy.java @@ -11,40 +11,24 @@ package io.vertx.httpproxy.impl; import io.vertx.core.Future; -import io.vertx.core.Promise; -import io.vertx.core.http.HttpClient; -import io.vertx.core.http.HttpClientRequest; -import io.vertx.core.http.HttpClientResponse; -import io.vertx.core.http.HttpHeaders; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.http.HttpVersion; -import io.vertx.core.http.RequestOptions; +import io.vertx.core.http.*; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.net.NetSocket; -import io.vertx.core.net.SocketAddress; -import io.vertx.httpproxy.HttpProxy; -import io.vertx.httpproxy.ProxyContext; -import io.vertx.httpproxy.ProxyInterceptor; -import io.vertx.httpproxy.ProxyOptions; -import io.vertx.httpproxy.ProxyRequest; -import io.vertx.httpproxy.ProxyResponse; +import io.vertx.httpproxy.*; import io.vertx.httpproxy.cache.CacheOptions; import io.vertx.httpproxy.spi.cache.Cache; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.function.BiFunction; -import java.util.function.Function; +import java.util.*; + +import static io.vertx.core.http.HttpHeaders.*; public class ReverseProxy implements HttpProxy { + private final static Logger log = LoggerFactory.getLogger(ReverseProxy.class); private final HttpClient client; private final boolean supportWebSocket; - private BiFunction> selector = (req, client) -> Future.failedFuture("No origin available"); + private OriginRequestProvider originRequestProvider = (pc) -> Future.failedFuture("No origin available"); private final List interceptors = new ArrayList<>(); public ReverseProxy(ProxyOptions options, HttpClient client) { @@ -58,8 +42,8 @@ public ReverseProxy(ProxyOptions options, HttpClient client) { } @Override - public HttpProxy originRequestProvider(BiFunction> provider) { - selector = provider; + public HttpProxy origin(OriginRequestProvider provider) { + originRequestProvider = Objects.requireNonNull(provider); return this; } @@ -81,28 +65,47 @@ public void handle(HttpServerRequest request) { return; } + Proxy proxy = new Proxy(proxyRequest); + // WebSocket upgrade tunneling - if (supportWebSocket && - request.version() == HttpVersion.HTTP_1_1 && - request.method() == HttpMethod.GET && - request.headers().contains(HttpHeaders.CONNECTION, HttpHeaders.UPGRADE, true)) { - handleWebSocketUpgrade(proxyRequest); + if (supportWebSocket && io.vertx.core.http.impl.HttpUtils.canUpgradeToWebSocket(request)) { + handleWebSocketUpgrade(proxy); return; } - Proxy proxy = new Proxy(proxyRequest); proxy.filters = interceptors.listIterator(); - proxy.sendRequest().compose(proxy::sendProxyResponse); + proxy.sendRequest() + .recover(throwable -> { + log.trace("Error in sending the request", throwable); + return Future.succeededFuture(proxyRequest.release().response().setStatusCode(502)); + }) + .compose(proxy::sendProxyResponse) + .recover(throwable -> { + log.trace("Error in sending the response", throwable); + return proxy.response().release().setStatusCode(502).send(); + }); } - private void handleWebSocketUpgrade(ProxyRequest proxyRequest) { + private void handleWebSocketUpgrade(ProxyContext proxyContext) { + ProxyRequest proxyRequest = proxyContext.request(); HttpServerRequest proxiedRequest = proxyRequest.proxiedRequest(); - resolveOrigin(proxiedRequest).onComplete(ar -> { + resolveOrigin(proxyContext).onComplete(ar -> { if (ar.succeeded()) { HttpClientRequest request = ar.result(); request.setMethod(HttpMethod.GET); request.setURI(proxiedRequest.uri()); - request.headers().addAll(proxiedRequest.headers()); + for (Map.Entry header : proxiedRequest.headers()) { + String name = header.getKey(); + if (name.equalsIgnoreCase(CONNECTION.toString())) { + // Firefox is known to send an unexpected connection header value + // Connection=keep-alive, Upgrade + // It leads to a failure in websocket proxying + // So we make sure the standard value is sent to the backend + request.headers().set(CONNECTION, UPGRADE); + } else if (!name.equalsIgnoreCase(HOST.toString())) { + request.headers().add(name, header.getValue()); + } + } Future fut2 = request.connect(); proxiedRequest.handler(request::write); proxiedRequest.endHandler(v -> request.end()); @@ -156,8 +159,8 @@ private void end(ProxyRequest proxyRequest, int sc) { .send(); } - private Future resolveOrigin(HttpServerRequest proxiedRequest) { - return selector.apply(proxiedRequest, client); + private Future resolveOrigin(ProxyContext proxyContext) { + return originRequestProvider.create(proxyContext); } private class Proxy implements ProxyContext { @@ -182,6 +185,11 @@ public T get(String name, Class type) { return type.isInstance(o) ? type.cast(o) : null; } + @Override + public HttpClient client() { + return client; + } + @Override public ProxyRequest request() { return request; @@ -213,27 +221,7 @@ public Future sendResponse() { } private Future sendProxyRequest(ProxyRequest proxyRequest) { - Future f = resolveOrigin(proxyRequest.proxiedRequest()); - f.onFailure(err -> { - // Should this be done here ? I don't think so - HttpServerRequest proxiedRequest = proxyRequest.proxiedRequest(); - proxiedRequest.resume(); - Promise promise = Promise.promise(); - proxiedRequest.exceptionHandler(promise::tryFail); - proxiedRequest.endHandler(promise::tryComplete); - promise.future().onComplete(ar2 -> { - end(proxyRequest, 502); - }); - }); - return f.compose(a -> sendProxyRequest(proxyRequest, a)); - } - - private Future sendProxyRequest(ProxyRequest proxyRequest, HttpClientRequest request) { - Future fut = proxyRequest.send(request); - fut.onFailure(err -> { - proxyRequest.proxiedRequest().response().setStatusCode(502).end(); - }); - return fut; + return resolveOrigin(this).compose(proxyRequest::send); } private Future sendProxyResponse(ProxyResponse response) { diff --git a/src/test/java/io/vertx/httpproxy/ProxyClientKeepAliveTest.java b/src/test/java/io/vertx/httpproxy/ProxyClientKeepAliveTest.java index 292bb6a..542e724 100644 --- a/src/test/java/io/vertx/httpproxy/ProxyClientKeepAliveTest.java +++ b/src/test/java/io/vertx/httpproxy/ProxyClientKeepAliveTest.java @@ -14,6 +14,7 @@ import io.vertx.core.Promise; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.*; +import io.vertx.core.net.HostAndPort; import io.vertx.core.net.NetClient; import io.vertx.core.net.SocketAddress; import io.vertx.core.streams.WriteStream; @@ -65,7 +66,8 @@ public void testNotfound(TestContext ctx) { public void testGet(TestContext ctx) { SocketAddress backend = startHttpBackend(ctx, 8081, req -> { ctx.assertEquals("/somepath", req.uri()); - ctx.assertEquals("localhost:8080", req.host()); + ctx.assertEquals("localhost", req.authority().host()); + ctx.assertEquals(8081, req.authority().port()); req.response().end("Hello World"); }); startProxy(backend); @@ -257,7 +259,7 @@ public void testFrontendCloseResponse(TestContext ctx) { @Test public void testFrontendCloseChunkedResponse(TestContext ctx) { - testBackendCloseResponse(ctx, true); + testFrontendCloseResponse(ctx, true); } private void testFrontendCloseResponse(TestContext ctx, boolean chunked) { @@ -276,11 +278,10 @@ private void testFrontendCloseResponse(TestContext ctx, boolean chunked) { }); startProxy(backend); HttpClient client = vertx.createHttpClient(); - client.request(GET, 8081, "localhost", "/", ctx.asyncAssertSuccess(req -> { + client.request(GET, 8080, "localhost", "/", ctx.asyncAssertSuccess(req -> { req.send(ctx.asyncAssertSuccess(resp -> { resp.handler(buff -> { resp.request().connection().close(); - System.out.println("closing"); }); })); })); @@ -382,7 +383,7 @@ private void checkChunkedResponse(TestContext ctx, HttpVersion version) { startProxy(backend); HttpClient client = vertx.createHttpClient(new HttpClientOptions().setProtocolVersion(version)); StringBuilder sb = new StringBuilder(); - for (int i = 0;i < num;i++) { + for (int i = 0; i < num; i++) { sb.append("chunk-").append(i); } client.request(GET, 8080, "localhost", "/", ctx.asyncAssertSuccess(req -> { @@ -416,7 +417,7 @@ public void testChunkedTransferEncodingRequest(TestContext ctx) { Async latch = ctx.async(); SocketAddress backend = startHttpBackend(ctx, 8081, req -> { StringBuilder sb = new StringBuilder(); - for (int i = 0;i < num;i++) { + for (int i = 0; i < num; i++) { sb.append("chunk-").append(i); } ctx.assertEquals("chunked", req.getHeader("transfer-encoding")); @@ -660,7 +661,6 @@ private void checkBadResponse(TestContext ctx, String... responses) throws Excep so.handler(buff -> { body.appendBuffer(buff); if (body.toString().endsWith("\r\n\r\n")) { - System.out.println(body.toString()); so.write(responseBody.get()); } }); @@ -701,8 +701,8 @@ private void streamChunkedBody(WriteStream stream, int num) { private StringBuilder randomAlphaString(int len) { Random random = new Random(); StringBuilder uri = new StringBuilder(); - for (int i = 0;i < len;i++) { - uri.append((char)('A' + random.nextInt(26))); + for (int i = 0; i < len; i++) { + uri.append((char) ('A' + random.nextInt(26))); } return uri; } @@ -729,25 +729,65 @@ public void testPropagateHeaders(TestContext ctx) { })); } + @Test + public void testIPV6Authority(TestContext ctx) { + testAuthority(ctx, HostAndPort.authority("[7a03:908:671:b520:ba27:bbff:ffff:fed2]", 1234)); + } + + @Test + public void testIPV4Authority(TestContext ctx) { + testAuthority(ctx, HostAndPort.authority("192.168.0.1", 1234)); + } + + @Test + public void testMissingPortAuthority(TestContext ctx) { + testAuthority(ctx, HostAndPort.authority("localhost", -1)); + } + + private void testAuthority(TestContext ctx, HostAndPort requestAuthority) { + SocketAddress backend = startHttpBackend(ctx, 8081, req -> { + ctx.assertEquals("/somepath", req.uri()); + ctx.assertEquals("localhost", req.authority().host()); + ctx.assertEquals(8081, req.authority().port()); + ctx.assertEquals(null, req.getHeader("x-forwarded-host")); + req.response().end("Hello World"); + }); + startProxy(proxy -> { + proxy.origin(backend); + }); + HttpClient client = vertx.createHttpClient(); + client.request(GET, 8080, "localhost", "/somepath") + .compose(req -> req + .authority(requestAuthority) + .send() + .compose(resp -> { + ctx.assertEquals(200, resp.statusCode()); + return resp.body(); + })) + .onComplete(ctx.asyncAssertSuccess(body -> { + ctx.assertEquals("Hello World", body.toString()); + })); + } + @Test public void testAuthorityOverride1(TestContext ctx) { - testAuthorityOverride(ctx, "foo:8080", "foo:8080", "localhost:8080"); + testAuthorityOverride(ctx, HostAndPort.authority("foo", 8080), "foo:8080", "localhost:8080"); } @Test public void testAuthorityOverride2(TestContext ctx) { - testAuthorityOverride(ctx, "foo", "foo", "localhost:8080"); + testAuthorityOverride(ctx, HostAndPort.authority("foo"), "foo", "localhost:8080"); } @Test public void testAuthorityOverride3(TestContext ctx) { - testAuthorityOverride(ctx, "localhost:8080", "localhost:8080", null); + testAuthorityOverride(ctx, HostAndPort.authority("localhost", 8080), "localhost:8080", null); } - private void testAuthorityOverride(TestContext ctx, String authority, String expectedAuthority, String expectedForwardedHost) { + private void testAuthorityOverride(TestContext ctx, HostAndPort authority, String expectedAuthority, String expectedForwardedHost) { SocketAddress backend = startHttpBackend(ctx, 8081, req -> { ctx.assertEquals("/somepath", req.uri()); - ctx.assertEquals(expectedAuthority, req.host()); + ctx.assertEquals(expectedAuthority, req.authority().toString()); ctx.assertEquals(expectedForwardedHost, req.getHeader("x-forwarded-host")); req.response().end("Hello World"); }); @@ -757,7 +797,8 @@ private void testAuthorityOverride(TestContext ctx, String authority, String exp @Override public Future handleProxyRequest(ProxyContext context) { ProxyRequest request = context.request(); - ctx.assertEquals("localhost:8080", request.getAuthority()); + ctx.assertEquals("localhost", request.proxiedRequest().authority().host()); + ctx.assertEquals(8080, request.proxiedRequest().authority().port()); request.setAuthority(authority); return ProxyInterceptor.super.handleProxyRequest(context); } diff --git a/src/test/java/io/vertx/httpproxy/ProxyRequestTest.java b/src/test/java/io/vertx/httpproxy/ProxyRequestTest.java index 390e267..8269905 100644 --- a/src/test/java/io/vertx/httpproxy/ProxyRequestTest.java +++ b/src/test/java/io/vertx/httpproxy/ProxyRequestTest.java @@ -99,11 +99,11 @@ public void testChunkedFrontendRequest(TestContext ctx) { req.response().end("Hello World"); }, ctx.asyncAssertSuccess()); HttpClient httpClient = vertx.createHttpClient(); - httpClient - .request(HttpMethod.GET, 8080, "localhost", "/somepath") - .compose(HttpClientRequest::send) - .compose(HttpClientResponse::body) - .onComplete(ctx.asyncAssertSuccess()); + httpClient.request(HttpMethod.GET, 8080, "localhost", "/somepath") + .compose(req -> req + .send() + .compose(HttpClientResponse::body)) + .onComplete(ctx.asyncAssertSuccess()); } @Test @@ -117,10 +117,13 @@ public void testNonChunkedFrontendRequest(TestContext ctx) { }, ctx.asyncAssertSuccess()); HttpClient httpClient = vertx.createHttpClient(); httpClient - .request(HttpMethod.POST, 8080, "localhost", "/somepath") - .compose(req -> req.setChunked(true).send("chunk")) - .compose(HttpClientResponse::body) - .onComplete(ctx.asyncAssertSuccess()); + .request(HttpMethod.POST, 8080, "localhost", "/somepath") + .compose(req -> req + .setChunked(true) + .send("chunk") + .andThen(ctx.asyncAssertSuccess(resp -> ctx.assertEquals(200, resp.statusCode()))) + .compose(HttpClientResponse::end)) + .onComplete(ctx.asyncAssertSuccess()); } @Ignore @@ -132,9 +135,14 @@ public void testIllegalTransferEncodingBackendResponse(TestContext ctx) { "connection: close\r\n" + "\r\n"), ctx.asyncAssertSuccess()); HttpClient httpClient = vertx.createHttpClient(); - httpClient.request(HttpMethod.GET, 8080, "localhost", "/somepath") - .compose(req -> req.send().compose(HttpClientResponse::body)) - .onComplete(ctx.asyncAssertSuccess()); + httpClient + .request(HttpMethod.POST, 8080, "localhost", "/somepath") + .compose(req -> req + .setChunked(true) + .send("chunk") + .andThen(ctx.asyncAssertSuccess(resp -> ctx.assertEquals(200, resp.statusCode()))) + .compose(HttpClientResponse::end)) + .onComplete(ctx.asyncAssertSuccess()); } @Test @@ -440,6 +448,7 @@ public Future handleProxyResponse(ProxyContext context) { @Test public void testUpdateRequestHeaders(TestContext ctx) throws Exception { SocketAddress backend = startHttpBackend(ctx, 8081, req -> { + ctx.assertNotEquals("example.org", req.getHeader(HttpHeaders.HOST)); ctx.assertNull(req.getHeader("header")); ctx.assertEquals("proxy_header_value", req.getHeader("proxy_header")); req.response().putHeader("header", "header_value").end(); @@ -466,6 +475,7 @@ public void testUpdateRequestHeaders(TestContext ctx) throws Exception { .compose(req -> req .putHeader("header", "header_value") + .putHeader(HttpHeaders.HOST, "example.org") .send() .compose(resp -> { ctx.assertEquals("proxy_header_value", resp.getHeader("proxy_header")); @@ -636,8 +646,8 @@ public void testProxyRequestUnresolvedTarget(TestContext ctx) { static { byte[] bytes = new byte[1024]; - for (int i = 0;i < 1024;i++) { - bytes[i] = (byte)('A' + (i % 26)); + for (int i = 0; i < 1024; i++) { + bytes[i] = (byte) ('A' + (i % 26)); } CHUNK = Buffer.buffer(bytes); } @@ -656,8 +666,8 @@ ReadStream init(ReadStream s) { stream.handler(buff -> { if (dataHandler != null) { byte[] bytes = new byte[buff.length()]; - for (int i = 0;i < bytes.length;i++) { - bytes[i] = (byte)(('a' - 'A') + buff.getByte(i)); + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) (('a' - 'A') + buff.getByte(i)); } expected.appendBytes(bytes); dataHandler.handle(Buffer.buffer(bytes)); diff --git a/src/test/java/io/vertx/httpproxy/ProxyTest.java b/src/test/java/io/vertx/httpproxy/ProxyTest.java index b51cb82..887a560 100644 --- a/src/test/java/io/vertx/httpproxy/ProxyTest.java +++ b/src/test/java/io/vertx/httpproxy/ProxyTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2020 Contributors to the Eclipse Foundation + * Copyright (c) 2011-2025 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at @@ -14,6 +14,7 @@ import io.vertx.core.http.HttpClient; import io.vertx.core.http.HttpClientResponse; import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; import io.vertx.core.net.SocketAddress; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; @@ -27,6 +28,8 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import static io.vertx.core.http.HttpHeaders.CONTENT_LENGTH; + /** * @author Julien Viet */ @@ -47,7 +50,7 @@ public void testRoundRobinSelector(TestContext ctx) { backends[i] = startHttpBackend(ctx, 8081 + value, req -> req.response().end("" + value)); } AtomicInteger count = new AtomicInteger(); - startProxy(proxy -> proxy.originSelector(req -> Future.succeededFuture(backends[count.getAndIncrement() % backends.length]))); + startProxy(proxy -> proxy.origin(OriginRequestProvider.selector(proxyContext -> Future.succeededFuture(backends[count.getAndIncrement() % backends.length])))); HttpClient client = vertx.createHttpClient(); Map result = Collections.synchronizedMap(new HashMap<>()); Async latch = ctx.async(); @@ -102,4 +105,121 @@ public Future handleProxyResponse(ProxyContext context) { latch.countDown(); }); } + + @Test + public void testFilterNullBodies(TestContext ctx) { + Async latch = ctx.async(3); + SocketAddress backend = startHttpBackend(ctx, 8081, req -> { + req.body().onComplete(ctx.asyncAssertSuccess(body -> { + ctx.assertEquals(0, body.length()); + ctx.assertEquals("0", req.getHeader(CONTENT_LENGTH)); + req.response().end("IGNORED_BACKEND_RESPONSE_BODY"); + })); + }); + startProxy(proxy -> proxy.origin(backend).addInterceptor(new ProxyInterceptor() { + @Override + public Future handleProxyRequest(ProxyContext context) { + context.request().setBody(null); + Future fut = context.sendRequest(); + fut.onComplete(ctx.asyncAssertSuccess(v -> latch.countDown())); + return fut; + } + + @Override + public Future handleProxyResponse(ProxyContext context) { + context.response().setBody(null); + Future fut = context.sendResponse(); + fut.onComplete(ctx.asyncAssertSuccess(v -> latch.countDown())); + return fut; + } + })); + HttpClient client = vertx.createHttpClient(); + client + .request(HttpMethod.POST, 8080, "localhost", "/") + .compose(req -> req + .send("IGNORED_CLIENT_REQUEST_BODY") + .compose(resp -> resp.body().map(resp)) + ).onComplete(ctx.asyncAssertSuccess(resp -> { + ctx.assertEquals(0, resp.body().result().length()); + ctx.assertEquals("0", resp.getHeader(CONTENT_LENGTH)); + latch.countDown(); + })); + } + + @Test + public void testUpstreamRefuse(TestContext ctx) { + SocketAddress backend = SocketAddress.inetSocketAddress(8081, "localhost"); + startProxy(proxy -> proxy.origin(backend)); + HttpClient client = vertx.createHttpClient(); + Async async = ctx.async(); + client.request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req.send().compose(resp -> { + ctx.assertEquals(502, resp.statusCode()); + return resp.body(); + })) + .onComplete(ctx.asyncAssertSuccess(body -> async.complete())); + } + + @Test + public void testFilterRequestFail(TestContext ctx) { + SocketAddress backend = startHttpBackend(ctx, 8081, req -> req.response().end("HOLA")); + startProxy(proxy -> proxy.origin(backend).addInterceptor(new ProxyInterceptor() { + @Override + public Future handleProxyRequest(ProxyContext context) { + return Future.failedFuture(new RuntimeException("Some error")); + } + })); + HttpClient client = vertx.createHttpClient(); + Async async = ctx.async(); + client.request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req.send().compose(resp -> { + ctx.assertEquals(502, resp.statusCode()); + return resp.body(); + })) + .onComplete(ctx.asyncAssertSuccess(body -> async.complete())); + } + + @Test + public void testFilterResponseFail(TestContext ctx) { + SocketAddress backend = startHttpBackend(ctx, 8081, req -> req.response().end("HOLA")); + startProxy(proxy -> proxy.origin(backend).addInterceptor(new ProxyInterceptor() { + @Override + public Future handleProxyResponse(ProxyContext context) { + return Future.failedFuture(new RuntimeException("Some error")); + } + })); + HttpClient client = vertx.createHttpClient(); + Async async = ctx.async(); + client.request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req.send().compose(resp -> { + ctx.assertEquals(502, resp.statusCode()); + return resp.body(); + })) + .onComplete(ctx.asyncAssertSuccess(body -> async.complete())); + } + + @Test + public void testVariableFromInterceptor(TestContext ctx) { + SocketAddress backend = startHttpBackend(ctx, 8081, req -> req.response().end("HOLA")); + ProxyInterceptor interceptor = new ProxyInterceptor() { + @Override + public Future handleProxyRequest(ProxyContext context) { + context.set("foo", "bar"); + return context.sendRequest(); + } + }; + OriginRequestProvider provider = (proxyContext) -> { + ctx.assertEquals("bar", proxyContext.get("foo", String.class)); + return proxyContext.client().request(new RequestOptions().setServer(backend)); + }; + startProxy(proxy -> proxy.origin(provider).addInterceptor(interceptor)); + HttpClient client = vertx.createHttpClient(); + client + .request(HttpMethod.GET, 8080, "localhost", "/") + .compose(req -> req + .send() + .compose(HttpClientResponse::body) + ) + .onComplete(ctx.asyncAssertSuccess(buffer -> ctx.assertEquals("HOLA", buffer.toString()))); + } } diff --git a/src/test/java/io/vertx/httpproxy/TestBase.java b/src/test/java/io/vertx/httpproxy/TestBase.java index 8593630..d868290 100644 --- a/src/test/java/io/vertx/httpproxy/TestBase.java +++ b/src/test/java/io/vertx/httpproxy/TestBase.java @@ -11,15 +11,10 @@ package io.vertx.httpproxy; import io.vertx.core.AbstractVerticle; -import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Promise; import io.vertx.core.Vertx; -import io.vertx.core.http.HttpClient; -import io.vertx.core.http.HttpClientOptions; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.*; import io.vertx.core.net.NetServer; import io.vertx.core.net.NetSocket; import io.vertx.core.net.SocketAddress; @@ -34,7 +29,7 @@ import java.io.Closeable; import java.util.concurrent.*; import java.util.function.Consumer; -import java.util.function.Function; +import java.util.function.UnaryOperator; /** * @author Julien Viet @@ -73,12 +68,16 @@ protected Closeable startProxy(SocketAddress backend) { } protected Closeable startProxy(Consumer config) { + return startProxy(UnaryOperator.identity(), config); + } + + protected Closeable startProxy(UnaryOperator proxyOptionsConfig, Consumer config) { CompletableFuture res = new CompletableFuture<>(); vertx.deployVerticle(new AbstractVerticle() { @Override public void start(Promise startFuture) { HttpClient proxyClient = vertx.createHttpClient(new HttpClientOptions(clientOptions)); - HttpServer proxyServer = vertx.createHttpServer(new HttpServerOptions(serverOptions)); + HttpServer proxyServer = vertx.createHttpServer(new HttpServerOptions(proxyOptionsConfig.apply(new HttpServerOptions(serverOptions)))); HttpProxy proxy = HttpProxy.reverseProxy(proxyOptions, proxyClient); config.accept(proxy); proxyServer.requestHandler(proxy); diff --git a/src/test/java/io/vertx/httpproxy/WebSocketTest.java b/src/test/java/io/vertx/httpproxy/WebSocketTest.java index f86c070..75f7659 100644 --- a/src/test/java/io/vertx/httpproxy/WebSocketTest.java +++ b/src/test/java/io/vertx/httpproxy/WebSocketTest.java @@ -12,28 +12,13 @@ import io.vertx.core.Future; import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpClient; -import io.vertx.core.http.HttpClientResponse; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.ServerWebSocket; -import io.vertx.core.http.UpgradeRejectedException; -import io.vertx.core.http.WebSocketConnectOptions; -import io.vertx.core.http.WebsocketVersion; +import io.vertx.core.http.*; import io.vertx.core.net.SocketAddress; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; -import io.vertx.ext.unit.junit.VertxUnitRunnerWithParametersFactory; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import static io.vertx.core.http.HttpMethod.GET; /** * @author Julien Viet @@ -128,4 +113,103 @@ public void testInboundClose(TestContext ctx) { async.complete(); })); } + + @Test + public void testWebSocketFirefox(TestContext ctx) { + Async async = ctx.async(); + SocketAddress backend = startHttpBackend(ctx, 8081, req -> { + Future fut = req.toWebSocket(); + fut.onComplete(ctx.asyncAssertSuccess(ws -> { + ws.closeHandler(v -> { + async.complete(); + }); + })); + }); + startProxy(backend); + HttpClient httpClient = vertx.createHttpClient(); + RequestOptions options = new RequestOptions() + .setPort(8080) + .setHost("localhost") + .setURI("/ws") + .putHeader("Origin", "http://localhost:8080/") + .putHeader("Connection", "keep-alive, Upgrade") + .putHeader("Upgrade", "Websocket") + .putHeader("Sec-WebSocket-Version", "13") + .putHeader("Sec-WebSocket-Key", "xy6UoM3l3TcREmAeAhZuYQ=="); + httpClient.request(options).onComplete(ctx.asyncAssertSuccess(clientRequest -> { + clientRequest.connect().onComplete(ctx.asyncAssertSuccess(response -> { + ctx.assertEquals(101, response.statusCode()); + response.netSocket().close(); + })); + })); + } + + @Test + public void testWebSocketHostHeader(TestContext ctx) { + Async async = ctx.async(); + SocketAddress backend = startHttpBackend(ctx, 8081, req -> { + ctx.assertEquals("localhost:8081", req.headers().get("Host")); + Future fut = req.toWebSocket(); + fut.onComplete(ctx.asyncAssertSuccess(ws -> { + ws.closeHandler(v -> { + async.complete(); + }); + })); + }); + startProxy(backend); + HttpClient httpClient = vertx.createHttpClient(); + RequestOptions options = new RequestOptions() + .setPort(8080) + .setHost("localhost") + .setURI("/ws") + .putHeader("Origin", "http://localhost:8080/") + .putHeader("Connection", "Upgrade") + .putHeader("Upgrade", "Websocket") + .putHeader("Sec-WebSocket-Version", "13") + .putHeader("Sec-WebSocket-Key", "xy6UoM3l3TcREmAeAhZuYQ=="); + httpClient.request(options).onComplete(ctx.asyncAssertSuccess(clientRequest -> { + clientRequest.connect().onComplete(ctx.asyncAssertSuccess(response -> { + ctx.assertEquals(101, response.statusCode()); + response.netSocket().close(); + })); + })); + } + + @Test + public void testWebSocketExtensionsNegotiatedBetweenClientAndBackend(TestContext ctx) { + Async async = ctx.async(); + HttpServerOptions backendOptions = new HttpServerOptions().setPort(8081).setHost("localhost") + .setPerFrameWebSocketCompressionSupported(false) // Disable extension in the backend + .setPerMessageWebSocketCompressionSupported(false); // Disable extension in the backend + SocketAddress backend = startHttpBackend(ctx, backendOptions, req -> { + ctx.assertTrue(req.headers().contains("sec-websocket-extensions")); + Future fut = req.toWebSocket(); + fut.onComplete(ctx.asyncAssertSuccess(ws -> { + ws.handler(buff -> ws.writeTextMessage(buff.toString())); + ws.closeHandler(v -> { + async.complete(); + }); + })); + }); + startProxy(proxyServerOptions -> { + return proxyServerOptions + .setPerFrameWebSocketCompressionSupported(true) // Enable extension in the proxy + .setPerMessageWebSocketCompressionSupported(true); // Enable extension in the proxy + }, httpProxy -> httpProxy.origin(backend)); + WebSocketClient wsClient = vertx.createWebSocketClient(new WebSocketClientOptions() + .setTryUsePerFrameCompression(true) // Enable extension in the client + .setTryUsePerMessageCompression(true)); // Enable extension in the client + WebSocketConnectOptions options = new WebSocketConnectOptions() + .setPort(8080) + .setHost("localhost") + .setURI("/ws"); + wsClient.connect(options).onComplete(ctx.asyncAssertSuccess(ws -> { + ctx.assertFalse(ws.headers().contains("sec-websocket-extensions"), "Expected extensions to be declined"); + ws.textMessageHandler(msg -> { + ctx.assertEquals("hello", msg); + ws.close(); + }); + ws.writeTextMessage("hello"); + })); + } } diff --git a/src/test/java/io/vertx/httpproxy/cache/CacheConditionalGetTest.java b/src/test/java/io/vertx/httpproxy/cache/CacheConditionalGetTest.java index 072ebaa..e919227 100644 --- a/src/test/java/io/vertx/httpproxy/cache/CacheConditionalGetTest.java +++ b/src/test/java/io/vertx/httpproxy/cache/CacheConditionalGetTest.java @@ -21,7 +21,8 @@ import org.junit.Rule; import org.junit.Test; -import java.util.Date; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.concurrent.atomic.AtomicInteger; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; @@ -58,9 +59,9 @@ public void testIfModifiedSinceRespondsNotModified(TestContext ctx) throws Excep .withStatus(200) .withHeader("Cache-Control", "public") .withHeader("ETag", "tag0") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(now))) - .withHeader("Last-Modified", ParseUtils.formatHttpDate(new Date(now - 5000))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(now + 5000))) + .withHeader("Date", ParseUtils.formatHttpDate(Instant.ofEpochMilli(now))) + .withHeader("Last-Modified", ParseUtils.formatHttpDate(Instant.ofEpochMilli(now).minus(5000, ChronoUnit.MILLIS))) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.ofEpochMilli(now).plus(5000, ChronoUnit.MILLIS))) .withBody("content"))); startProxy(new SocketAddressImpl(8081, "localhost")); Async latch = ctx.async(); @@ -74,7 +75,7 @@ public void testIfModifiedSinceRespondsNotModified(TestContext ctx) throws Excep vertx.setTimer(3000, id -> { client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg") .compose(req2 -> req2 - .putHeader(HttpHeaders.IF_MODIFIED_SINCE, ParseUtils.formatHttpDate(new Date(now - 5000))) + .putHeader(HttpHeaders.IF_MODIFIED_SINCE, ParseUtils.formatHttpDate(Instant.ofEpochMilli(now).minus(5000, ChronoUnit.MILLIS))) .send() .compose(resp2 -> { ctx.assertEquals(304, resp2.statusCode()); diff --git a/src/test/java/io/vertx/httpproxy/cache/CacheExpires2Test.java b/src/test/java/io/vertx/httpproxy/cache/CacheExpires2Test.java index 6c01fa3..22c4ee5 100644 --- a/src/test/java/io/vertx/httpproxy/cache/CacheExpires2Test.java +++ b/src/test/java/io/vertx/httpproxy/cache/CacheExpires2Test.java @@ -24,7 +24,8 @@ import org.junit.Rule; import org.junit.Test; -import java.util.Date; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.concurrent.atomic.AtomicInteger; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; @@ -52,10 +53,8 @@ public void setUp() { } protected void setCacheControl(MultiMap headers, long now, long delaySeconds) { - Date tomorrow = new Date(); - tomorrow.setTime(now + delaySeconds * 1000); headers.set(HttpHeaders.CACHE_CONTROL, "public"); - headers.set(HttpHeaders.EXPIRES, ParseUtils.formatHttpDate(tomorrow)); + headers.set(HttpHeaders.EXPIRES, ParseUtils.formatHttpDate(Instant.now().plus(delaySeconds, ChronoUnit.SECONDS))); } @Test @@ -66,8 +65,8 @@ public void testPublicInvalidClientMaxAgeRevalidation(TestContext ctx) throws Ex .withStatus(200) .withHeader("Cache-Control", "public") .withHeader("ETag", "tag0") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) + .withHeader("Date", ParseUtils.formatHttpDate(Instant.now())) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.now().plus(5000, ChronoUnit.MILLIS))) .withBody("content"))); stubFor(get(urlEqualTo("/img.jpg")).withHeader("If-None-Match", equalTo("tag0")).inScenario("s") .willReturn( @@ -75,8 +74,8 @@ public void testPublicInvalidClientMaxAgeRevalidation(TestContext ctx) throws Ex .withStatus(200) .withHeader("Cache-Control", "public") .withHeader("Etag", "tag1") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) + .withHeader("Date", ParseUtils.formatHttpDate(Instant.now())) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.now().plus(5000, ChronoUnit.MILLIS))) .withBody("content2"))); startProxy(new SocketAddressImpl(8081, "localhost")); Async latch = ctx.async(); @@ -133,8 +132,8 @@ private void testUncacheableRequestInvalidatesEntryOnOk(TestContext ctx, HttpMet .withStatus(200) .withHeader("Cache-Control", "public") .withHeader("ETag", "tag0") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) + .withHeader("Date", ParseUtils.formatHttpDate(Instant.now())) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.now().plus(5000, ChronoUnit.MILLIS))) .withBody("content")) .willSetStateTo("abc")); stubFor(get(urlEqualTo("/img.jpg")).inScenario("s").whenScenarioStateIs("abc") @@ -143,8 +142,8 @@ private void testUncacheableRequestInvalidatesEntryOnOk(TestContext ctx, HttpMet .withStatus(200) .withHeader("Cache-Control", "public") .withHeader("Etag", "tag1") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) + .withHeader("Date", ParseUtils.formatHttpDate(Instant.now())) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.now().plus(5000, ChronoUnit.MILLIS))) .withBody("content2"))); stubFor(head(urlEqualTo("/img.jpg")).inScenario("s").whenScenarioStateIs("abc") .willReturn( @@ -152,8 +151,8 @@ private void testUncacheableRequestInvalidatesEntryOnOk(TestContext ctx, HttpMet .withStatus(200) .withHeader("Cache-Control", "public") .withHeader("Etag", "tag1") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))))); + .withHeader("Date", ParseUtils.formatHttpDate(Instant.now())) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.now().plus(5000, ChronoUnit.MILLIS))))); startProxy(new SocketAddressImpl(8081, "localhost")); Async latch = ctx.async(); client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg") @@ -216,8 +215,8 @@ private void testUncacheableHeadRevalidatesEntry(TestContext ctx, int status) th .withStatus(200) .withHeader("Cache-Control", "public") .withHeader("ETag", "tag0") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) + .withHeader("Date", ParseUtils.formatHttpDate(Instant.now())) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.now().plus(5000, ChronoUnit.MILLIS))) .withBody("content"))); stubFor(head(urlEqualTo("/img.jpg")) .willReturn( @@ -225,8 +224,8 @@ private void testUncacheableHeadRevalidatesEntry(TestContext ctx, int status) th .withStatus(status) .withHeader("Cache-Control", "public") .withHeader("ETag", "tag0") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))))); + .withHeader("Date", ParseUtils.formatHttpDate(Instant.now())) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.now().plus(5000, ChronoUnit.MILLIS))))); startProxy(new SocketAddressImpl(8081, "localhost")); Async latch = ctx.async(); client.request(HttpMethod.GET, 8080, "localhost", "/img.jpg") @@ -267,8 +266,8 @@ public void testHeadDoesNotPopulateCache(TestContext ctx) throws Exception { .withStatus(200) .withHeader("Cache-Control", "public") .withHeader("ETag", "tag0") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))) + .withHeader("Date", ParseUtils.formatHttpDate(Instant.now())) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.now().plus(5000, ChronoUnit.MILLIS))) .withBody("content"))); stubFor(head(urlEqualTo("/img.jpg")) .willReturn( @@ -276,8 +275,8 @@ public void testHeadDoesNotPopulateCache(TestContext ctx) throws Exception { .withStatus(200) .withHeader("Cache-Control", "public") .withHeader("ETag", "tag0") - .withHeader("Date", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) - .withHeader("Expires", ParseUtils.formatHttpDate(new Date(System.currentTimeMillis() + 5000))))); + .withHeader("Date", ParseUtils.formatHttpDate(Instant.now())) + .withHeader("Expires", ParseUtils.formatHttpDate(Instant.now().plus(5000, ChronoUnit.MILLIS))))); startProxy(new SocketAddressImpl(8081, "localhost")); Async latch = ctx.async(); client.request(HttpMethod.HEAD, 8080, "localhost", "/img.jpg") diff --git a/src/test/java/io/vertx/httpproxy/cache/CacheExpiresTest.java b/src/test/java/io/vertx/httpproxy/cache/CacheExpiresTest.java index 258d2d0..5c7c8c6 100644 --- a/src/test/java/io/vertx/httpproxy/cache/CacheExpiresTest.java +++ b/src/test/java/io/vertx/httpproxy/cache/CacheExpiresTest.java @@ -21,7 +21,8 @@ import io.vertx.httpproxy.impl.ParseUtils; import org.junit.Test; -import java.util.Date; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.concurrent.atomic.AtomicInteger; /** @@ -40,10 +41,8 @@ public void setUp() { } protected void setCacheControl(MultiMap headers, long now, long delaySeconds) { - Date tomorrow = new Date(); - tomorrow.setTime(now + delaySeconds * 1000); headers.set(HttpHeaders.CACHE_CONTROL, "public"); - headers.set(HttpHeaders.EXPIRES, ParseUtils.formatHttpDate(tomorrow)); + headers.set(HttpHeaders.EXPIRES, ParseUtils.formatHttpDate(Instant.now().plus(delaySeconds, ChronoUnit.SECONDS))); } @Test @@ -146,8 +145,8 @@ private void testPublic(TestContext ctx, Handler respHandler) throws E SocketAddress backend = startHttpBackend(ctx, 8081, req -> { hits.incrementAndGet(); ctx.assertEquals(HttpMethod.GET, req.method()); - Date now = new Date(); - setCacheControl(req.response().headers(), now.getTime(), 5); + Instant now = Instant.now(); + setCacheControl(req.response().headers(), now.toEpochMilli(), 5); req.response() .putHeader(HttpHeaders.LAST_MODIFIED, ParseUtils.formatHttpDate(now)) .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(now)) @@ -185,8 +184,8 @@ private void testPublicInvalidClientMaxAge(TestContext ctx, long maxAge) throws case 0: ctx.assertEquals(null, req.getHeader(HttpHeaders.ETAG)); req.response() - .putHeader(HttpHeaders.LAST_MODIFIED, ParseUtils.formatHttpDate(new Date(now))) - .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(new Date(now))) + .putHeader(HttpHeaders.LAST_MODIFIED, ParseUtils.formatHttpDate(Instant.ofEpochMilli(now))) + .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(Instant.ofEpochMilli(now))) .putHeader(HttpHeaders.ETAG, "" + now) .end("content"); break; @@ -195,12 +194,12 @@ private void testPublicInvalidClientMaxAge(TestContext ctx, long maxAge) throws if (System.currentTimeMillis() < now + maxAge * 1000) { req.response() .setStatusCode(304) - .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) + .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(Instant.ofEpochMilli(System.currentTimeMillis()))) .putHeader(HttpHeaders.ETAG, "" + now) .end(); } else { req.response() - .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(new Date(System.currentTimeMillis()))) + .putHeader(HttpHeaders.DATE, ParseUtils.formatHttpDate(Instant.ofEpochMilli(System.currentTimeMillis()))) .putHeader(HttpHeaders.ETAG, "" + now + "2") .end("content2"); } diff --git a/src/test/java/io/vertx/httpproxy/impl/ParseUtilsTest.java b/src/test/java/io/vertx/httpproxy/impl/ParseUtilsTest.java new file mode 100644 index 0000000..75bf728 --- /dev/null +++ b/src/test/java/io/vertx/httpproxy/impl/ParseUtilsTest.java @@ -0,0 +1,50 @@ +package io.vertx.httpproxy.impl; + +import org.junit.Test; + +import java.time.Instant; +import java.time.format.DateTimeFormatter; + +import static junit.framework.TestCase.assertEquals; + +public class ParseUtilsTest { + + private final Instant RESULT_DATE = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse("Tue, 2 Jan 2024 12:34:56 GMT")); + + /** + * Test parse RFC_1123_DATE_TIME : EEE, dd MMM yyyy HH:mm:ss z + * + * @throws Exception + */ + @Test + public void testParseHttpDateRFC_1123_DATE_TIME() throws Exception { + assertEquals(RESULT_DATE, ParseUtils.parseHttpDate("Tue, 2 Jan 2024 12:34:56 GMT")); + assertEquals(RESULT_DATE, ParseUtils.parseHttpDate("Tue, 02 Jan 2024 13:34:56 +0100")); + } + + @Test + public void testFormatHttpDateRFC_1123_DATE_TIME() { + assertEquals("Tue, 2 Jan 2024 12:34:56 GMT", ParseUtils.formatHttpDate(RESULT_DATE)); + } + + /** + * Test parse RFC_850_DATE_TIME : EEEEEEEEE, dd-MMM-yy HH:mm:ss zzz + * + * @throws Exception + */ + @Test + public void testParseHttpDateRFC_850_DATE_TIME() throws Exception { + assertEquals(RESULT_DATE, ParseUtils.parseHttpDate("Tuesday, 02-Jan-24 12:34:56 GMT")); + } + + /** + * Test parse ASC_TIME : EEE MMM d HH:mm:ss yyyy + * + * @throws Exception + */ + @Test + public void testParseHttpDateASC_TIME() throws Exception { + assertEquals(RESULT_DATE, ParseUtils.parseHttpDate("Tue Jan 2 12:34:56 2024")); + } + +}