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"));
+ }
+
+}