Skip to content

Commit

Permalink
Filter headers for netty HTTP redirect and CONNECT requests
Browse files Browse the repository at this point in the history
Signed-off-by: jansupol <[email protected]>
  • Loading branch information
jansupol authored and senivam committed Oct 11, 2023
1 parent 065584b commit cbda4fc
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;

import javax.ws.rs.HttpMethod;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.client.ClientProperties;
Expand Down Expand Up @@ -125,6 +131,7 @@ protected void notifyResponse() {
} else {
ClientRequest newReq = new ClientRequest(jerseyRequest);
newReq.setUri(newUri);
restrictRedirectRequest(newReq, cr);
connector.execute(newReq, redirectUriHistory, responseAvailable);
}
} catch (IllegalArgumentException e) {
Expand Down Expand Up @@ -217,4 +224,62 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
super.userEventTriggered(ctx, evt);
}
}

/*
* RFC 9110 Section 15.4
* https://httpwg.org/specs/rfc9110.html#rfc.section.15.4
*/
private void restrictRedirectRequest(ClientRequest newRequest, ClientResponse response) {
final MultivaluedMap<String, Object> headers = newRequest.getHeaders();
final Boolean keepMethod = newRequest.resolveProperty(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, Boolean.TRUE);

if (Boolean.FALSE.equals(keepMethod) && newRequest.getMethod().equals(HttpMethod.POST)) {
switch (response.getStatus()) {
case 301 /* MOVED PERMANENTLY */:
case 302 /* FOUND */:
removeContentHeaders(headers);
newRequest.setMethod(HttpMethod.GET);
newRequest.setEntity(null);
break;
}
}

for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
final Map.Entry<String, List<Object>> entry = it.next();
if (ProxyHeaders.INSTANCE.test(entry.getKey())) {
it.remove();
}
}

headers.remove(HttpHeaders.IF_MATCH);
headers.remove(HttpHeaders.IF_NONE_MATCH);
headers.remove(HttpHeaders.IF_MODIFIED_SINCE);
headers.remove(HttpHeaders.IF_UNMODIFIED_SINCE);
headers.remove(HttpHeaders.AUTHORIZATION);
headers.remove(HttpHeaderNames.REFERER.toString());
headers.remove(HttpHeaders.COOKIE);
}

private void removeContentHeaders(MultivaluedMap<String, Object> headers) {
for (final Iterator<Map.Entry<String, List<Object>>> it = headers.entrySet().iterator(); it.hasNext(); ) {
final Map.Entry<String, List<Object>> entry = it.next();
final String lowName = entry.getKey().toLowerCase(Locale.ROOT);
if (lowName.startsWith("content-")) {
it.remove();
}
}
headers.remove(HttpHeaders.LAST_MODIFIED);
headers.remove(HttpHeaderNames.TRANSFER_ENCODING.toString());
}

/* package */ static class ProxyHeaders implements Predicate<String> {
static final ProxyHeaders INSTANCE = new ProxyHeaders();
private static final String HOST = HttpHeaders.HOST.toLowerCase(Locale.ROOT);

@Override
public boolean test(String headerName) {
String lowName = headerName.toLowerCase(Locale.ROOT);
return lowName.startsWith("proxy-") || lowName.equals(HOST);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,32 @@ public class NettyClientProperties {

/**
* <p>
* This property determines the maximum number of idle connections that will be simultaneously kept alive
* in total, rather than per destination. The default is 60. Specify 0 to disable.
* Sets the endpoint identification algorithm to HTTPS.
* </p>
* <p>
* The default value is {@code true} (for HTTPS uri scheme).
* </p>
* <p>
* The name of the configuration property is <tt>{@value}</tt>.
* </p>
* @since 2.35
* @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)
*/
public static final String MAX_CONNECTIONS_TOTAL = "jersey.config.client.maxTotalConnections";
public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = "jersey.config.client.tls.enableHostnameVerification";

/**
* <p>
* Filter the HTTP headers for requests (CONNECT) towards the proxy except for PROXY-prefixed and HOST headers when {@code true}.
* </p>
* <p>
* The default value is {@code true} and the headers are filtered out.
* </p>
* <p>
* The name of the configuration property is <tt>{@value}</tt>.
* </p>
* @since 2.41
*/
public static final String FILTER_HEADERS_FOR_PROXY = "jersey.config.client.filter.headers.proxy";

/**
* <p>
Expand All @@ -56,18 +77,11 @@ public class NettyClientProperties {

/**
* <p>
* Sets the endpoint identification algorithm to HTTPS.
* </p>
* <p>
* The default value is {@code true} (for HTTPS uri scheme).
* </p>
* <p>
* The name of the configuration property is <tt>{@value}</tt>.
* This property determines the maximum number of idle connections that will be simultaneously kept alive
* in total, rather than per destination. The default is 60. Specify 0 to disable.
* </p>
* @since 2.35
* @see javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)
*/
public static final String ENABLE_SSL_HOSTNAME_VERIFICATION = "jersey.config.client.tls.enableHostnameVerification";
public static final String MAX_CONNECTIONS_TOTAL = "jersey.config.client.maxTotalConnections";

/**
* The maximal number of redirects during single request.
Expand All @@ -82,4 +96,20 @@ public class NettyClientProperties {
* @see org.glassfish.jersey.netty.connector.internal.RedirectException
*/
public static final String MAX_REDIRECTS = "jersey.config.client.NettyConnectorProvider.maxRedirects";

/**
* <p>
* Sets the HTTP POST method to be preserved on HTTP status 301 (MOVED PERMANENTLY) or status 302 (FOUND) when {@code true}
* or redirected as GET when {@code false}.
* </p>
* <p>
* The default value is {@code true} and the HTTP POST request is not redirected as GET.
* </p>
* <p>
* The name of the configuration property is <tt>{@value}</tt>.
* </p>
* @since 2.41
*/
public static final String PRESERVE_METHOD_ON_REDIRECT = "jersey.config.client.redirect.preserve.method";

}
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ protected void initChannel(SocketChannel ch) throws Exception {
}

// headers
setHeaders(jerseyRequest, nettyRequest.headers());
setHeaders(jerseyRequest, nettyRequest.headers(), false);

// host header - http 1.1
if (!nettyRequest.headers().contains(HttpHeaderNames.HOST)) {
Expand Down Expand Up @@ -538,7 +538,8 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc

private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, SocketAddress proxyAddr,
String userName, String password, long connectTimeout) {
HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders());
final Boolean filter = jerseyRequest.resolveProperty(NettyClientProperties.FILTER_HEADERS_FOR_PROXY, Boolean.TRUE);
HttpHeaders httpHeaders = setHeaders(jerseyRequest, new DefaultHttpHeaders(), Boolean.TRUE.equals(filter));

ProxyHandler proxy = userName == null ? new HttpProxyHandler(proxyAddr, httpHeaders)
: new HttpProxyHandler(proxyAddr, userName, password, httpHeaders);
Expand All @@ -549,9 +550,11 @@ private static ProxyHandler createProxyHandler(ClientRequest jerseyRequest, Sock
return proxy;
}

private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers) {
private static HttpHeaders setHeaders(ClientRequest jerseyRequest, HttpHeaders headers, boolean proxyOnly) {
for (final Map.Entry<String, List<String>> e : jerseyRequest.getStringHeaders().entrySet()) {
headers.add(e.getKey(), e.getValue());
if (!proxyOnly || JerseyClientHandler.ProxyHeaders.INSTANCE.test(e.getKey())) {
headers.add(e.getKey(), e.getValue());
}
}
return headers;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/

package org.glassfish.jersey.netty.connector;

import io.netty.handler.codec.http.HttpHeaderNames;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.net.URI;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;

public class RedirectHeadersTest extends JerseyTest {

private static final Logger LOGGER = Logger.getLogger(RedirectHeadersTest.class.getName());
private static final String TEST_URL = "http://localhost:%d/test";
private static final AtomicReference<String> TEST_URL_REF = new AtomicReference<>();
private static final String ENTITY = "entity";

@BeforeEach
public void before() {
final String url = String.format(TEST_URL, getPort());
TEST_URL_REF.set(url);
}

@Path("/test")
public static class RedirectResource {
@GET
public String get(@QueryParam("value") String value) {
return "GET" + value;
}

@POST
public String echo(@QueryParam("value") String value, String entity) {
return entity + value;
}

@GET
@Path("headers2")
public String headers(@Context HttpHeaders headers) {
String encoding = headers.getHeaderString(HttpHeaders.CONTENT_ENCODING);
String auth = headers.getHeaderString(HttpHeaderNames.PROXY_AUTHORIZATION.toString());
return encoding + ":" + auth;
}

@POST
@Path("301")
public Response redirect301(String entity) {
return Response.status(Response.Status.MOVED_PERMANENTLY)
.header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=301"))
.build();
}

@POST
@Path("302")
public Response redirect302(String entity) {
return Response.status(Response.Status.FOUND)
.header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=302"))
.build();
}

@POST
@Path("307")
public Response redirect307(String entity) {
return Response.status(Response.Status.TEMPORARY_REDIRECT)
.header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=307"))
.build();
}

@POST
@Path("308")
public Response redirectHeaders(String entity) {
return Response.status(308)
.header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "?value=308"))
.build();
}


@POST
@Path("headers1")
public Response redirect308(String whatever) {
return Response.status(301).header(HttpHeaders.LOCATION, URI.create(TEST_URL_REF.get() + "/headers2")).build();
}
}

@Override
protected Application configure() {
ResourceConfig config = new ResourceConfig(RedirectResource.class);
config.register(new LoggingFeature(LOGGER, LoggingFeature.Verbosity.PAYLOAD_ANY));
return config;
}

@Override
protected void configureClient(ClientConfig config) {
config.property(ClientProperties.FOLLOW_REDIRECTS, true);
config.connectorProvider(new NettyConnectorProvider());
}

@Test
void testPost() {
testPost("301");
testPost("302");
testPost("307");
testPost("308");
}

@Test
void testGet() {
Assertions.assertEquals("GET301", testGet("301"));
Assertions.assertEquals("GET302", testGet("302"));
Assertions.assertEquals(ENTITY + "307", testGet("307"));
Assertions.assertEquals(ENTITY + "308", testGet("308"));
}

@Test
void testHeaders() {
MultivaluedMap<String, Object> headers = new MultivaluedHashMap<>();
headers.add(HttpHeaders.CONTENT_ENCODING, "gzip");
headers.add(HttpHeaderNames.PROXY_AUTHORIZATION.toString(), "basic aGVsbG86d29ybGQ=");
try (Response response = target("test")
.property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)
.path("headers1").request().headers(headers).post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) {
Assertions.assertEquals(200, response.getStatus());
Assertions.assertEquals("null:null", response.readEntity(String.class));
}

}

void testPost(String status) {
try (Response response = target("test").path(status).request().post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) {
Assertions.assertEquals(200, response.getStatus());
Assertions.assertEquals(ENTITY + status, response.readEntity(String.class));
}
}

String testGet(String status) {
try (Response response = target("test")
.property(NettyClientProperties.PRESERVE_METHOD_ON_REDIRECT, false)
.path(status).request().post(Entity.entity(ENTITY, MediaType.TEXT_PLAIN_TYPE))) {
Assertions.assertEquals(200, response.getStatus());
return response.readEntity(String.class);
}
}
}
Loading

0 comments on commit cbda4fc

Please sign in to comment.