diff --git a/tools/http-fault-injector/sample-clients/java/http-fault-injector-client/src/main/java/httpfaultinjectorclient/App.java b/tools/http-fault-injector/sample-clients/java/http-fault-injector-client/src/main/java/httpfaultinjectorclient/App.java index 393ac694ccf..67c88b623c5 100644 --- a/tools/http-fault-injector/sample-clients/java/http-fault-injector-client/src/main/java/httpfaultinjectorclient/App.java +++ b/tools/http-fault-injector/sample-clients/java/http-fault-injector-client/src/main/java/httpfaultinjectorclient/App.java @@ -13,13 +13,13 @@ public class App { public static void main(String[] args) throws Exception { HttpClient httpClient = HttpClient.create(); - // You must either add the .NET developer certiifcate to the Java cacerts keystore, or uncomment the following + // You must either add the .NET developer certificate to the Java cacerts keystore, or uncomment the following // lines to disable SSL validation. // // io.netty.handler.ssl.SslContext sslContext = io.netty.handler.ssl.SslContextBuilder // .forClient().trustManager(io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE).build(); // httpClient = httpClient.secure(sslContextBuilder -> sslContextBuilder.sslContext(sslContext)); - + System.out.println("Sending request..."); HttpClientResponse response = get(httpClient, "https://www.example.org").block(); diff --git a/tools/http-fault-injector/sample-clients/java/storage-blobs/pom.xml b/tools/http-fault-injector/sample-clients/java/storage-blobs/pom.xml new file mode 100644 index 00000000000..3af80ed95a1 --- /dev/null +++ b/tools/http-fault-injector/sample-clients/java/storage-blobs/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + + org.example + storage-blobs + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + + com.azure + azure-storage-blob + 12.12.0 + + + + \ No newline at end of file diff --git a/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/App.java b/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/App.java new file mode 100644 index 00000000000..30607f82e6e --- /dev/null +++ b/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/App.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.azure.core.http.HttpClient; +import com.azure.core.util.BinaryData; +import com.azure.core.util.Configuration; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobClientBuilder; +import com.azure.storage.common.policy.RequestRetryOptions; +import com.azure.storage.common.policy.RetryPolicyType; + +import java.time.Duration; + +/** + * This is a sample application using azure-storage-blob to send requests to the HTTP fault injector. All concepts + * presented here are applicable to all Azure SDKs which use HTTP as its network transport. + */ +public class App { + public static void main(String[] args) { + HttpClient httpClient = HttpClient.createDefault(); + + // You must either add the .NET developer certificate to the Java cacerts keystore, or uncomment the following + // lines to disable SSL validation if using Netty/Reactor Netty as the underlying HttpClient. + // + // io.netty.handler.ssl.SslContext sslContext = io.netty.handler.ssl.SslContextBuilder + // .forClient().trustManager(io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE).build(); + // httpClient = httpClient.secure(sslContextBuilder -> sslContextBuilder.sslContext(sslContext)); + + BlobClient blobClient = new BlobClientBuilder() + .connectionString(Configuration.getGlobalConfiguration().get("STORAGE_CONNECTION_STRING")) + .containerName("sample") + .blobName("sample.txt") + .retryOptions(new RequestRetryOptions(RetryPolicyType.FIXED, 3, Duration.ofMinutes(1), + Duration.ofSeconds(1), Duration.ofSeconds(1), null)) + // .httpClient(new FaultInjectorHttpClient(httpClient)) // Using an HttpClient is also a valid option. + .addPolicy(new FaultInjectorUrlRewriterPolicy()) + .buildClient(); + + System.out.println("Sending request..."); + BinaryData content = blobClient.downloadContent(); + System.out.printf("Content: %s%n", content); + } +} diff --git a/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/FaultInjectorHttpClient.java b/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/FaultInjectorHttpClient.java new file mode 100644 index 00000000000..40759b2743e --- /dev/null +++ b/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/FaultInjectorHttpClient.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.util.Context; +import reactor.core.publisher.Mono; + +import java.util.Objects; + +/** + * General purpose {@link HttpClient} which re-writes request URLs to use the HTTP fault injector before using an + * underlying {@link HttpClient} to send the network request. + */ +public final class FaultInjectorHttpClient implements HttpClient { + private final HttpClient httpClient; + private final String host; + private final int port; + + /** + * Default constructor for {@link FaultInjectorHttpClient} which expects the HTTP fault injector to use {@link + * Utils#DEFAULT_HTTP_FAULT_INJECTOR_HOST} and running on port {@link Utils#DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT}. + * + * @param httpClient The underlying {@link HttpClient} used to make network requests. + */ + public FaultInjectorHttpClient(HttpClient httpClient) { + this(httpClient, Utils.DEFAULT_HTTP_FAULT_INJECTOR_HOST, Utils.DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT); + } + + /** + * Constructor for {@link FaultInjectorHttpClient} which allows for the configuration of which {@code host} and + * {@code port} the HTTP fault injector is using. + * + * @param httpClient The underlying {@link HttpClient} used to make network requests. + * @param host The host HTTP fault injector is running on. + * @param port The port HTTP fault injector is using. + * @throws NullPointerException If {@code httpClient} or {@code host} is null. + * @throws IllegalArgumentException If {@code host} is an empty string or {@code port} is an invalid port. + */ + public FaultInjectorHttpClient(HttpClient httpClient, String host, int port) { + this.httpClient = Objects.requireNonNull(httpClient, "'httpClient' cannot be null."); + Utils.validateHostAndPort(host, port); + + this.host = host; + this.port = port; + } + + @Override + public Mono send(HttpRequest request) { + return send(request, Context.NONE); + } + + @Override + public Mono send(HttpRequest request, Context context) { + return Mono.defer(() -> Mono.fromCallable(() -> Utils.rewriteUrlToUseFaultInjector(request, host, port))) + .flatMap(rewrittenHttpRequest -> httpClient.send(rewrittenHttpRequest, context)); + } +} diff --git a/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/FaultInjectorUrlRewriterPolicy.java b/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/FaultInjectorUrlRewriterPolicy.java new file mode 100644 index 00000000000..c376f7fc625 --- /dev/null +++ b/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/FaultInjectorUrlRewriterPolicy.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.azure.core.http.HttpPipelineCallContext; +import com.azure.core.http.HttpPipelineNextPolicy; +import com.azure.core.http.HttpPipelinePosition; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.policy.HttpPipelinePolicy; +import reactor.core.publisher.Mono; + +/** + * General purpose {@link HttpPipelinePolicy} which re-writes the request URL to send it to the HTTP fault injector. + */ +public final class FaultInjectorUrlRewriterPolicy implements HttpPipelinePolicy { + private final String host; + private final int port; + + /** + * Default constructor for {@link FaultInjectorUrlRewriterPolicy} which expects the HTTP fault injector to use + * {@link Utils#DEFAULT_HTTP_FAULT_INJECTOR_HOST} and running on port + * {@link Utils#DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT}. + */ + public FaultInjectorUrlRewriterPolicy() { + this(Utils.DEFAULT_HTTP_FAULT_INJECTOR_HOST, Utils.DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT); + } + + /** + * Constructor used to configure re-writing the request URL to the non-default HTTP fault injector host and port. + * + * @param host The host HTTP fault injector is running on. + * @param port The port HTTP fault injector is using. + * @throws NullPointerException If {@code host} is null. + * @throws IllegalArgumentException If {@code host} is an empty string or {@code port} is an invalid port. + */ + public FaultInjectorUrlRewriterPolicy(String host, int port) { + Utils.validateHostAndPort(host, port); + + this.host = host; + this.port = port; + } + + @Override + public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { + context.setHttpRequest(Utils.rewriteUrlToUseFaultInjector(context.getHttpRequest(), host, port)); + + return next.process(); + } + + @Override + public HttpPipelinePosition getPipelinePosition() { + // The policy should be ran per retry in case calls are made to a secondary, fail-over host. + return HttpPipelinePosition.PER_RETRY; + } +} diff --git a/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/Utils.java b/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/Utils.java new file mode 100644 index 00000000000..f0d93a6c5f4 --- /dev/null +++ b/tools/http-fault-injector/sample-clients/java/storage-blobs/src/main/java/Utils.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import com.azure.core.http.HttpRequest; + +import java.net.URI; +import java.util.Objects; + +/** + * Utility class containing constants and methods for re-writing {@link HttpRequest HttpRequests} to use the HTTP fault + * injector. + */ +public final class Utils { + /** + * The default host used by HTTP fault injector. + */ + public static final String DEFAULT_HTTP_FAULT_INJECTOR_HOST = "localhost"; + + /** + * The default HTTP port used by HTTP fault injector. + */ + public static final int DEFAULT_HTTP_FAULT_INJECTOR_HTTP_PORT = 7777; + + /** + * The default HTTPS port used by HTTP fault injector. + */ + public static final int DEFAULT_HTTP_FAULT_INJECTOR_HTTPS_PORT = 7778; + + /** + * The HTTP header used by HTTP fault injector to determine where it needs to forward a request. + */ + public static final String HTTP_FAULT_INJECTOR_UPSTREAM_HOST_HEADER = "X-Upstream-Host"; + + /** + * Utility method which re-writes the {@link HttpRequest HttpRequest's} URL to use the HTTP fault injector. + *

+ * This will set the HTTP header {@link #HTTP_FAULT_INJECTOR_UPSTREAM_HOST_HEADER} to the request URL used by the + * HTTP request before re-writing and will update the request URL to use the HTTP fault injector. + * + * @param request The {@link HttpRequest} having its URL re-written. + * @param host The HTTP fault injector host. + * @param port The HTTP fault injector port. + * @return The updated {@link HttpRequest} with its URL re-written. + * @throws NullPointerException If {@code request} or {@code host} are null. + * @throws IllegalArgumentException If {@code host} is an empty string or {@code port} is + * an invalid port. + * @throws IllegalStateException If the request URL isn't valid or the HTTP fault injector URL isn't valid. + */ + public static HttpRequest rewriteUrlToUseFaultInjector(HttpRequest request, String host, int port) { + validateHostAndPort(host, port); + + try { + URI requestUri = request.getUrl().toURI(); + URI faultInjectorUri = new URI(requestUri.getScheme(), requestUri.getUserInfo(), host, + port, requestUri.getPath(), requestUri.getQuery(), requestUri.getFragment()); + + String xUpstreamHost = (requestUri.getPort() < 0) + ? requestUri.getHost() + : requestUri.getHost() + ":" + requestUri.getPort(); + + return request.setHeader(HTTP_FAULT_INJECTOR_UPSTREAM_HOST_HEADER, xUpstreamHost) + .setUrl(faultInjectorUri.toURL()); + } catch (Exception exception) { + throw new IllegalStateException(exception); + } + } + + /* + * Helper method for validating the HTTP fault injector host and port. + */ + static void validateHostAndPort(String host, int port) { + Objects.requireNonNull(host, "'host' cannot be null."); + + if (host.isEmpty()) { + throw new IllegalArgumentException("'host' must be a non-empty string."); + } + + if (port < 1 || port > 65535) { + throw new IllegalArgumentException("'port' must be a valid port number."); + } + } + + private Utils() { } +}