-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add HTTP Proxy Dev Service for REST Client
The quarkus-rest-client starts a simple pass-through proxy by default which can be used as a target for Wireshark (or similar tools) in order to capture all the traffic originating from the REST Client (this really makes sense when the REST Client is used against HTTPS services). However, advances integrations that build on the REST Client can register their own proxies by using the DevServicesRestClientProxyProvider SPI
- Loading branch information
Showing
13 changed files
with
1,239 additions
and
153 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
37 changes: 37 additions & 0 deletions
37
...c/main/java/io/quarkus/rest/client/reactive/deployment/RegisteredRestClientBuildItem.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package io.quarkus.rest.client.reactive.deployment; | ||
|
||
import java.util.Objects; | ||
import java.util.Optional; | ||
|
||
import org.jboss.jandex.ClassInfo; | ||
|
||
import io.quarkus.builder.item.MultiBuildItem; | ||
|
||
/** | ||
* Contains information about the REST Clients that have been discovered via | ||
* {@link org.eclipse.microprofile.rest.client.inject.RegisterRestClient} | ||
*/ | ||
public final class RegisteredRestClientBuildItem extends MultiBuildItem { | ||
|
||
private final ClassInfo classInfo; | ||
private final Optional<String> configKey; | ||
private final Optional<String> defaultBaseUri; | ||
|
||
public RegisteredRestClientBuildItem(ClassInfo classInfo, Optional<String> configKey, Optional<String> defaultBaseUri) { | ||
this.classInfo = Objects.requireNonNull(classInfo); | ||
this.configKey = Objects.requireNonNull(configKey); | ||
this.defaultBaseUri = Objects.requireNonNull(defaultBaseUri); | ||
} | ||
|
||
public ClassInfo getClassInfo() { | ||
return classInfo; | ||
} | ||
|
||
public Optional<String> getConfigKey() { | ||
return configKey; | ||
} | ||
|
||
public Optional<String> getDefaultBaseUri() { | ||
return defaultBaseUri; | ||
} | ||
} |
316 changes: 165 additions & 151 deletions
316
...src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java
Large diffs are not rendered by default.
Oops, something went wrong.
291 changes: 291 additions & 0 deletions
291
.../rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxyProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
package io.quarkus.rest.client.reactive.deployment.devservices; | ||
|
||
import java.io.Closeable; | ||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.IdentityHashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
import java.util.stream.Collectors; | ||
|
||
import org.apache.commons.lang3.exception.UncheckedException; | ||
import org.jboss.jandex.ClassInfo; | ||
import org.jboss.jandex.IndexView; | ||
import org.jboss.logging.Logger; | ||
|
||
import io.quarkus.deployment.IsNormal; | ||
import io.quarkus.deployment.annotations.BuildProducer; | ||
import io.quarkus.deployment.annotations.BuildStep; | ||
import io.quarkus.deployment.annotations.BuildSteps; | ||
import io.quarkus.deployment.builditem.CombinedIndexBuildItem; | ||
import io.quarkus.deployment.builditem.CuratedApplicationShutdownBuildItem; | ||
import io.quarkus.deployment.builditem.DevServicesResultBuildItem; | ||
import io.quarkus.rest.client.reactive.deployment.RegisteredRestClientBuildItem; | ||
import io.quarkus.rest.client.reactive.spi.DevServicesRestClientProxyProvider; | ||
import io.quarkus.rest.client.reactive.spi.RestClientHttpProxyBuildItem; | ||
import io.quarkus.restclient.config.RestClientBuildConfig; | ||
import io.quarkus.restclient.config.RestClientConfig; | ||
import io.quarkus.restclient.config.RestClientsBuildTimeConfig; | ||
|
||
@BuildSteps(onlyIfNot = IsNormal.class) | ||
public class DevServicesRestClientHttpProxyProcessor { | ||
|
||
private static final Logger log = Logger.getLogger(DevServicesRestClientHttpProxyProcessor.class); | ||
|
||
// the following fields are needed for state management as proxied can come and go | ||
private static final AtomicReference<Set<RestClientHttpProxyBuildItem>> runningProxies = new AtomicReference<>( | ||
new HashSet<>()); | ||
private static final AtomicReference<Set<DevServicesRestClientProxyProvider>> runningProviders = new AtomicReference<>( | ||
Collections.newSetFromMap(new IdentityHashMap<>())); | ||
private static final AtomicReference<Set<Closeable>> providerCloseables = new AtomicReference<>( | ||
Collections.newSetFromMap(new IdentityHashMap<>())); | ||
|
||
@BuildStep | ||
public DevServicesRestClientProxyProvider.BuildItem registerDefaultProvider() { | ||
return new DevServicesRestClientProxyProvider.BuildItem(VertxHttpProxyDevServicesRestClientProxyProvider.INSTANCE); | ||
} | ||
|
||
@BuildStep | ||
public void determineRequiredProxies(RestClientsBuildTimeConfig restClientsBuildTimeConfig, | ||
CombinedIndexBuildItem combinedIndexBuildItem, | ||
List<RegisteredRestClientBuildItem> registeredRestClientBuildItems, | ||
BuildProducer<RestClientHttpProxyBuildItem> producer) { | ||
if (restClientsBuildTimeConfig.configs.isEmpty()) { | ||
return; | ||
} | ||
|
||
IndexView index = combinedIndexBuildItem.getIndex(); | ||
|
||
Map<String, RestClientBuildConfig> configs = restClientsBuildTimeConfig.configs; | ||
for (var configEntry : configs.entrySet()) { | ||
if (!configEntry.getValue().enableLocalProxy) { | ||
log.trace("Ignoring config key: '" + configEntry.getKey() + "' because enableLocalProxy is false"); | ||
break; | ||
} | ||
|
||
String configKey = sanitizeKey(configEntry.getKey()); | ||
|
||
RegisteredRestClientBuildItem matchingBI = null; | ||
// check if the configKey matches one of the @RegisterRestClient values | ||
for (RegisteredRestClientBuildItem bi : registeredRestClientBuildItems) { | ||
if (bi.getConfigKey().isPresent() && configKey.equals(bi.getConfigKey().get())) { | ||
matchingBI = bi; | ||
break; | ||
} | ||
} | ||
if (matchingBI != null) { | ||
Optional<String> baseUri = oneOf( | ||
RestClientConfig.getConfigValue(configKey, "uri", String.class), | ||
RestClientConfig.getConfigValue(configKey, "url", String.class), | ||
matchingBI.getDefaultBaseUri()); | ||
|
||
if (baseUri.isEmpty()) { | ||
log.debug("Unable to determine uri or url for config key '" + configKey + "'"); | ||
break; | ||
} | ||
producer.produce(new RestClientHttpProxyBuildItem(matchingBI.getClassInfo().name().toString(), baseUri.get(), | ||
configEntry.getValue().localProxyProvider)); | ||
} else { | ||
// now we check if the configKey was actually a class name | ||
ClassInfo classInfo = index.getClassByName(configKey); | ||
if (classInfo == null) { | ||
log.debug( | ||
"Key '" + configKey + "' could not be matched to either a class name or a REST Client's configKey"); | ||
break; | ||
} | ||
Optional<String> baseUri = oneOf( | ||
RestClientConfig.getConfigValue(configKey, "uri", String.class), | ||
RestClientConfig.getConfigValue(configKey, "url", String.class)); | ||
if (baseUri.isEmpty()) { | ||
log.debug("Unable to determine uri or url for config key '" + configKey + "'"); | ||
break; | ||
} | ||
producer.produce(new RestClientHttpProxyBuildItem(classInfo.name().toString(), baseUri.get(), | ||
configEntry.getValue().localProxyProvider)); | ||
} | ||
} | ||
} | ||
|
||
private String sanitizeKey(String key) { | ||
if (key.startsWith("\"") && key.endsWith("\"")) { | ||
return key.substring(1, key.length() - 1); | ||
} | ||
return key; | ||
} | ||
|
||
@BuildStep | ||
public void start(List<RestClientHttpProxyBuildItem> restClientHttpProxyBuildItems, | ||
List<DevServicesRestClientProxyProvider.BuildItem> restClientProxyProviderBuildItems, | ||
BuildProducer<DevServicesResultBuildItem> devServicePropertiesProducer, | ||
CuratedApplicationShutdownBuildItem closeBuildItem) { | ||
if (restClientHttpProxyBuildItems.isEmpty()) { | ||
return; | ||
} | ||
|
||
Set<RestClientHttpProxyBuildItem> requestedProxies = new HashSet<>(restClientHttpProxyBuildItems); | ||
|
||
Set<RestClientHttpProxyBuildItem> proxiesToClose = new HashSet<>(runningProxies.get()); | ||
proxiesToClose.removeAll(requestedProxies); | ||
|
||
// we need to remove the running ones that should no longer be running | ||
for (var running : proxiesToClose) { | ||
closeRunningProxy(running); | ||
} | ||
runningProxies.get().removeAll(proxiesToClose); | ||
|
||
// we need to figure out which ones to start | ||
Set<RestClientHttpProxyBuildItem> proxiesToRun = new HashSet<>(requestedProxies); | ||
proxiesToRun.removeAll(runningProxies.get()); | ||
|
||
// determine which providers to use for each of the new proxies to start | ||
Map<RestClientHttpProxyBuildItem, DevServicesRestClientProxyProvider> biToProviderMap = new HashMap<>(); | ||
for (var toStart : proxiesToRun) { | ||
DevServicesRestClientProxyProvider provider; | ||
if (toStart.getProvider().isPresent()) { | ||
String requestedProviderName = toStart.getProvider().get(); | ||
|
||
var maybeProviderBI = restClientProxyProviderBuildItems | ||
.stream() | ||
.filter(pbi -> requestedProviderName.equals(pbi.getProvider().name())) | ||
.findFirst(); | ||
if (maybeProviderBI.isEmpty()) { | ||
throw new RuntimeException("Unable to find provider for REST Client '" + toStart.getClassName() | ||
+ "' with name '" + requestedProviderName + "'"); | ||
} | ||
|
||
provider = maybeProviderBI.get().getProvider(); | ||
} else { | ||
// the algorithm is the following: | ||
// if only the default is around, use it | ||
// if there is only one besides the default, use it | ||
// if there are multiple ones, fail | ||
|
||
List<DevServicesRestClientProxyProvider.BuildItem> nonDefault = restClientProxyProviderBuildItems.stream() | ||
.filter(pib -> !pib.getProvider().name().equals(VertxHttpProxyDevServicesRestClientProxyProvider.NAME)) | ||
.toList(); | ||
if (nonDefault.isEmpty()) { | ||
provider = VertxHttpProxyDevServicesRestClientProxyProvider.INSTANCE; | ||
} else if (nonDefault.size() == 1) { | ||
// TODO: this part of the algorithm is questionable... | ||
provider = nonDefault.iterator().next().getProvider(); | ||
} else { | ||
String availableProviders = restClientProxyProviderBuildItems.stream().map(bi -> bi.getProvider().name()) | ||
.collect( | ||
Collectors.joining(",")); | ||
throw new RuntimeException("Multiple providers found for REST Client '" + toStart.getClassName() | ||
+ "'. Please specify one by setting 'quarkus.rest-client.\"" + toStart.getClassName() | ||
+ "\".local-proxy-provider' to one the following providers: " + availableProviders); | ||
} | ||
} | ||
|
||
biToProviderMap.put(toStart, provider); | ||
} | ||
|
||
// here is where we set up providers | ||
var providersToRun = new HashSet<>(biToProviderMap.values()); | ||
providersToRun.removeAll(runningProviders.get()); | ||
for (var provider : providersToRun) { | ||
Closeable closeable = provider.setup(); | ||
if (closeable != null) { | ||
providerCloseables.get().add(closeable); | ||
} | ||
runningProviders.get().add(provider); | ||
} | ||
|
||
// this is where we actually start proxies | ||
for (var bi : proxiesToRun) { | ||
URI baseUri = URI.create(bi.getBaseUri()); | ||
|
||
var provider = biToProviderMap.get(bi); | ||
var createResult = provider.create(bi); | ||
var proxyServerClosable = createResult.closeable(); | ||
bi.attachClosable(proxyServerClosable); | ||
runningProxies.get().add(bi); | ||
|
||
var urlKeyName = String.format("quarkus.rest-client.\"%s\".override-uri", bi.getClassName()); | ||
var urlKeyValue = String.format("http://%s:%d", createResult.host(), createResult.port()); | ||
if (baseUri.getPath() != null) { | ||
if (!"/".equals(baseUri.getPath()) && !baseUri.getPath().isEmpty()) { | ||
urlKeyValue = urlKeyValue + "/" + baseUri.getPath(); | ||
} | ||
} | ||
|
||
devServicePropertiesProducer.produce( | ||
new DevServicesResultBuildItem("rest-client-" + bi.getClassName() + "-proxy", | ||
null, | ||
Map.of(urlKeyName, urlKeyValue))); | ||
} | ||
|
||
closeBuildItem.addCloseTask(new CloseTask(runningProxies, providerCloseables, runningProviders), true); | ||
} | ||
|
||
private static void closeRunningProxy(RestClientHttpProxyBuildItem running) { | ||
try { | ||
Closeable closeable = running.getCloseable(); | ||
if (closeable != null) { | ||
log.debug("Attempting to close HTTP proxy server for REST Client '" + running.getClassName() + "'"); | ||
closeable.close(); | ||
log.debug("Closed HTTP proxy server for REST Client '" + running.getClassName() + "'"); | ||
} | ||
} catch (IOException e) { | ||
throw new UncheckedException(e); | ||
} | ||
} | ||
|
||
@SafeVarargs | ||
private static <T> Optional<T> oneOf(Optional<T>... optionals) { | ||
for (Optional<T> o : optionals) { | ||
if (o != null && o.isPresent()) { | ||
return o; | ||
} | ||
} | ||
return Optional.empty(); | ||
} | ||
|
||
private static class CloseTask implements Runnable { | ||
|
||
private final AtomicReference<Set<RestClientHttpProxyBuildItem>> runningProxiesRef; | ||
private final AtomicReference<Set<Closeable>> providerCloseablesRef; | ||
private final AtomicReference<Set<DevServicesRestClientProxyProvider>> runningProvidersRef; | ||
|
||
public CloseTask(AtomicReference<Set<RestClientHttpProxyBuildItem>> runningProxiesRef, | ||
AtomicReference<Set<Closeable>> providerCloseablesRef, | ||
AtomicReference<Set<DevServicesRestClientProxyProvider>> runningProvidersRef) { | ||
|
||
this.runningProxiesRef = runningProxiesRef; | ||
this.providerCloseablesRef = providerCloseablesRef; | ||
this.runningProvidersRef = runningProvidersRef; | ||
} | ||
|
||
@Override | ||
public void run() { | ||
Set<RestClientHttpProxyBuildItem> restClientHttpProxyBuildItems = runningProxiesRef.get(); | ||
for (var bi : restClientHttpProxyBuildItems) { | ||
closeRunningProxy(bi); | ||
} | ||
runningProxiesRef.set(new HashSet<>()); | ||
|
||
Set<Closeable> providerCloseables = providerCloseablesRef.get(); | ||
for (Closeable closeable : providerCloseables) { | ||
try { | ||
if (closeable != null) { | ||
log.debug("Attempting to close provider"); | ||
closeable.close(); | ||
log.debug("Closed provider"); | ||
} | ||
} catch (IOException e) { | ||
throw new UncheckedException(e); | ||
} | ||
} | ||
providerCloseablesRef.set(Collections.newSetFromMap(new IdentityHashMap<>())); | ||
|
||
runningProvidersRef.set(Collections.newSetFromMap(new IdentityHashMap<>())); | ||
} | ||
} | ||
} |
Oops, something went wrong.