Skip to content

Commit

Permalink
Enhance RedirectingHttpRequesterFilter: redirect headers and messag…
Browse files Browse the repository at this point in the history
…e body (#1792)

Motivation:

With pre-existing implementation of `RedirectingHttpRequesterFilter` its
hard for users to redirect headers and a message body. There is an
opportunity to improve API for request customization and automatically
redirect those components for relative redirects.

Modifications:

- Introduce `RedirectConfig` and `RedirectConfigBuilder` for extensive
redirect configuration;
- Automatically redirect headers and message body for relative locations;
- Allow configuring what methods should be redirected;
- Let users provide a custom predicate to avoid redirects in some
conditions;
- Make "change POST -> GET" behavior opt-in;
- Give users access to make any changes for the redirecting request,
keeping the context of the original request and redirect response;
- Deprecate `MultiAddressHttpClientBuilder#maxRedirects`;
- Add `MultiAddressHttpClientBuilder#followRedirects(RedirectConfig)`;
- Add `RedirectingHttpRequesterFilter(RedirectConfig)` ctor;
- Deprecate unnecessary ctors for `RedirectingHttpRequesterFilter`;
- Update tests and improve test coverage;
- Update redirect examples;

Result:

Full automatic handling for relative redirects, extensible API to
configure non-relative redirects, simplified user experience.
  • Loading branch information
idelpivnitskiy committed Oct 4, 2021
1 parent 759f589 commit 5cc648c
Show file tree
Hide file tree
Showing 23 changed files with 1,735 additions and 722 deletions.
12 changes: 6 additions & 6 deletions servicetalk-examples/docs/modules/ROOT/pages/http/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,13 @@ same for all API styles.

* link:{source-root}/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/RedirectingServer.java[RedirectingServer] -
Starts two servers, one of them (HTTP) redirects to another (HTTPS).
* link:{source-root}/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/ManualRedirectClient.java[ManualRedirectClient.java] -
Async `Hello World` example that demonstrates how redirects can be handled manually when single-address clients are used.
* link:{source-root}/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/SimpleRedirectUrlClient.java[SimpleRedirectUrlClient.java] -
Async `Hello World` example that demonstrates how redirects can be handled automatically by a multi-address client.
* link:{source-root}/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/RedirectWithStateUrlClient.java[RedirectWithStateUrlClient.java] -
* link:{source-root}/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/SingleAddressRedirectClient.java[SingleAddressRedirectClient.java] -
Async `Hello World` example that demonstrates how relative redirects can be handled automatically by a single-address client.
* link:{source-root}/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/MultiAddressUrlRedirectClient.java[MultiAddressUrlRedirectClient.java] -
Async `Hello World` example that demonstrates how redirects can be handled automatically by a multi-address client.
It demonstrates how users can preserve headers and payload body of the original request while redirecting.
It demonstrates how users can preserve headers and payload body of the original request while redirecting to non-relative locations.
* link:{source-root}/servicetalk-examples/http/redirects/src/main/java/io/servicetalk/examples/http/redirects/ManualRedirectClient.java[ManualRedirectClient.java] -
Async `Hello World` example that demonstrates how redirects can be handled manually between multiple single-address clients.

[#HTTP2]
== HTTP/2
Expand Down
3 changes: 3 additions & 0 deletions servicetalk-examples/http/redirects/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ apply plugin: "java"
apply from: "../../gradle/idea.gradle"

dependencies {
implementation project(":servicetalk-annotations")
implementation project(":servicetalk-http-utils") // gives access to RedirectingHttpRequesterFilter
implementation project(":servicetalk-http-netty")
implementation "com.google.code.findbugs:jsr305:$jsr305Version"

// This dependency brings self-signed TLS certificates for demonstration purposes.
// Users have to use their own certificates instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
import io.servicetalk.http.netty.HttpClients;
import io.servicetalk.test.resources.DefaultTestCerts;
import io.servicetalk.transport.api.ClientSslConfigBuilder;
import io.servicetalk.transport.api.HostAndPort;

import java.util.concurrent.CountDownLatch;
import javax.annotation.Nullable;

import static io.servicetalk.concurrent.api.Single.succeeded;
import static io.servicetalk.examples.http.redirects.RedirectingServer.CUSTOM_HEADER;
Expand All @@ -32,16 +33,18 @@
import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer;

/**
* Async "Hello World" example that demonstrates how redirects can be handled manually when single-address clients are
* used with possibilities to:
* Async "Hello World" example that demonstrates how redirects can be handled manually between multiple
* {@link HttpClients#forSingleAddress(HostAndPort) single-address} clients with possibilities to:
* <ol>
* <li>Change the target server or perform a relative redirect.</li>
* <li>Preserve headers while redirecting.</li>
* <li>Preserve payload body while redirecting.</li>
* </ol>
* This is a specialized use-case. For simplification, consider using one
* {@link HttpClients#forMultiAddressUrl() multi-address} client, demonstrated in {@link MultiAddressUrlRedirectClient}
* example.
*/
public final class ManualRedirectClient {

public static void main(String... args) throws Exception {
try (HttpClient secureClient = HttpClients.forSingleAddress("localhost", SECURE_SERVER_PORT)
// The custom SSL configuration here is necessary only because this example uses self-signed
Expand All @@ -50,13 +53,8 @@ public static void main(String... args) throws Exception {
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem).build()).build()) {

try (HttpClient client = HttpClients.forSingleAddress("localhost", NON_SECURE_SERVER_PORT).build()) {
// This example is demonstrating asynchronous execution, but needs to prevent the main thread from exiting
// before the response has been processed. This isn't typical usage for a streaming API but is useful for
// demonstration purposes.
CountDownLatch responseProcessedLatch = new CountDownLatch(1);

// Redirect of a GET request with a custom header:
HttpRequest originalGet = client.get("http://localhost:8080/sayHello")
System.out.println("- Redirect of a GET request with a custom header:");
HttpRequest originalGet = client.get("/non-relative")
.addHeader(CUSTOM_HEADER, "value");
client.request(originalGet)
.flatMap(response -> {
Expand All @@ -70,18 +68,18 @@ public static void main(String... args) throws Exception {
// Decided not to follow redirect, return the original response or an error:
return succeeded(response);
})
.afterFinally(responseProcessedLatch::countDown)
.subscribe(resp -> {
.whenOnSuccess(resp -> {
System.out.println(resp.toString((name, value) -> value));
System.out.println(resp.payloadBody(textDeserializer()));
System.out.println();
});

responseProcessedLatch.await();
})
// This example is demonstrating asynchronous execution, but needs to prevent the main thread
// from exiting before the response has been processed. This isn't typical usage for an
// asynchronous API but is useful for demonstration purposes.
.toFuture().get();

// Redirect of a POST request with a payload body:
responseProcessedLatch = new CountDownLatch(1);
HttpRequest originalPost = client.post("http://localhost:8080/sayHello")
System.out.println("- Redirect of a POST request with a payload body:");
HttpRequest originalPost = client.post("/non-relative")
.payloadBody(client.executionContext().bufferAllocator().fromAscii("some_content"));
client.request(originalPost)
.flatMap(response -> {
Expand All @@ -95,18 +93,20 @@ public static void main(String... args) throws Exception {
// Decided not to follow redirect, return the original response or an error:
return succeeded(response);
})
.afterFinally(responseProcessedLatch::countDown)
.subscribe(resp -> {
.whenOnSuccess(resp -> {
System.out.println(resp.toString((name, value) -> value));
System.out.println(resp.payloadBody(textDeserializer()));
});

responseProcessedLatch.await();
})
// This example is demonstrating asynchronous execution, but needs to prevent the main thread
// from exiting before the response has been processed. This isn't typical usage for an
// asynchronous API but is useful for demonstration purposes.
.toFuture().get();
}
}
}

private static HttpClient lookupClient(CharSequence location, HttpClient sameClient, HttpClient secureClient) {
private static HttpClient lookupClient(@Nullable CharSequence location, HttpClient sameClient,
HttpClient secureClient) {
if (location == null || location.length() < 1) {
throw new IllegalArgumentException("Response does not contain redirect location");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright © 2021 Apple Inc. and the ServiceTalk project authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.servicetalk.examples.http.redirects;

import io.servicetalk.http.api.HttpClient;
import io.servicetalk.http.api.HttpRequestMethod;
import io.servicetalk.http.api.MultiAddressHttpClientBuilder;
import io.servicetalk.http.api.RedirectConfig;
import io.servicetalk.http.api.RedirectConfigBuilder;
import io.servicetalk.http.netty.HttpClients;
import io.servicetalk.test.resources.DefaultTestCerts;
import io.servicetalk.transport.api.ClientSslConfigBuilder;

import static io.servicetalk.examples.http.redirects.RedirectingServer.CUSTOM_HEADER;
import static io.servicetalk.examples.http.redirects.RedirectingServer.NON_SECURE_SERVER_PORT;
import static io.servicetalk.examples.http.redirects.RedirectingServer.SECURE_SERVER_PORT;
import static io.servicetalk.http.api.HttpHeaderNames.LOCATION;
import static io.servicetalk.http.api.HttpRequestMethod.GET;
import static io.servicetalk.http.api.HttpRequestMethod.POST;
import static io.servicetalk.http.api.HttpSerializationProviders.textDeserializer;

/**
* Async `Hello World` example that demonstrates how redirects can be handled automatically by a
* {@link HttpClients#forMultiAddressUrl() multi-address} client. It demonstrates how users can preserve headers and
* payload body of the original request while redirecting to non-relative locations.
* <p>
* For security reasons, request methods other than {@link HttpRequestMethod#GET GET} or
* {@link HttpRequestMethod#HEAD HEAD}, headers and message body are not automatically redirected for non-relative
* locations. Users have to explicitly configure what should be redirected when they are sure that redirect does not
* forward to a malicious target server. Relative redirects always carry forward headers and message body. For more
* information, see {@link MultiAddressHttpClientBuilder#followRedirects(RedirectConfig)} and
* {@link RedirectConfigBuilder}.
*/
public final class MultiAddressUrlRedirectClient {

public static void main(String... args) throws Exception {
try (HttpClient client = HttpClients.forMultiAddressUrl()
// Enables redirection:
.followRedirects(new RedirectConfigBuilder()
// All following config options are optional:
.maxRedirects(3)
// by default, only relative redirects are allowed
.allowNonRelativeRedirects(true)
// by default, POST requests don't follow redirects:
.allowedMethods(GET, POST)
// apply additional restrictions which redirects to follow:
.redirectPredicate((relative, redirectCount, prevRequest, redirectResponse) ->
relative // allow only relative redirects
// OR non-relative redirects to a trusted server:
|| redirectResponse.headers().get(LOCATION, "").toString()
.startsWith("https://localhost:" + SECURE_SERVER_PORT))
// explicitly specify what headers should be redirected to non-relative locations:
.headersToRedirect(CUSTOM_HEADER)
// explicitly specify that payload body should be redirected to non-relative locations:
.redirectPayloadBody(true)
// custom modifications for a redirected request:
.redirectRequestTransformer((relative, prevRequest, redirectResponse, redirectedRequest) -> {
// if necessary, apply addition modifications for redirectedRequest based on the context of
// prevRequest and redirectResponse: check/copy other headers, modify request method, etc.
return redirectedRequest;
})
.build())
.initializer((scheme, address, builder) -> {
// The custom SSL configuration here is necessary only because this example uses self-signed
// certificates. For cases when it's enough to use the local trust store, MultiAddressUrl client
// already provides default SSL configuration and this step may be skipped.
if ("https".equalsIgnoreCase(scheme)) {
builder.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem).build());
}
})
.build()) {

final String serverThatRedirects = "http://localhost:" + NON_SECURE_SERVER_PORT;
System.out.println("- Simple GET request:");
client.request(client.get(serverThatRedirects + "/relative"))
.whenOnSuccess(resp -> {
System.out.println(resp.toString((name, value) -> value));
System.out.println(resp.payloadBody(textDeserializer()));
System.out.println();
})
// This example is demonstrating asynchronous execution, but needs to prevent the main thread from
// exiting before the response has been processed. This isn't typical usage for an asynchronous API
// but is useful for demonstration purposes.
.toFuture().get();

System.out.println("- Relative redirect for POST request with headers and payload body:");
client.request(client.post(serverThatRedirects + "/relative")
.addHeader(CUSTOM_HEADER, "value")
.payloadBody(client.executionContext().bufferAllocator().fromAscii("some_content")))
.whenOnSuccess(resp -> {
System.out.println(resp.toString((name, value) -> value));
System.out.println(resp.payloadBody(textDeserializer()));
System.out.println();
})
// This example is demonstrating asynchronous execution, but needs to prevent the main thread from
// exiting before the response has been processed. This isn't typical usage for an asynchronous API
// but is useful for demonstration purposes.
.toFuture().get();

System.out.println("- Non-relative redirect for POST request with headers and payload body:");
client.request(client.post(serverThatRedirects + "/non-relative")
.addHeader(CUSTOM_HEADER, "value")
.payloadBody(client.executionContext().bufferAllocator().fromAscii("some_content")))
.whenOnSuccess(resp -> {
System.out.println(resp.toString((name, value) -> value));
System.out.println(resp.payloadBody(textDeserializer()));
System.out.println();
})
// This example is demonstrating asynchronous execution, but needs to prevent the main thread from
// exiting before the response has been processed. This isn't typical usage for an asynchronous API
// but is useful for demonstration purposes.
.toFuture().get();
}
}
}
Loading

0 comments on commit 5cc648c

Please sign in to comment.