-
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
- Loading branch information
Showing
8 changed files
with
584 additions
and
152 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
36 changes: 36 additions & 0 deletions
36
...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,36 @@ | ||
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; | ||
|
||
/** | ||
* TODO | ||
*/ | ||
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.
276 changes: 276 additions & 0 deletions
276
...o/quarkus/rest/client/reactive/deployment/devservices/DevServicesRestClientHttpProxy.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,276 @@ | ||
package io.quarkus.rest.client.reactive.deployment.devservices; | ||
|
||
import java.io.Closeable; | ||
import java.io.IOException; | ||
import java.io.UncheckedIOException; | ||
import java.net.ServerSocket; | ||
import java.net.URI; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
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.restclient.config.RestClientBuildConfig; | ||
import io.quarkus.restclient.config.RestClientConfig; | ||
import io.quarkus.restclient.config.RestClientsBuildTimeConfig; | ||
import io.vertx.core.Future; | ||
import io.vertx.core.MultiMap; | ||
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.httpproxy.HttpProxy; | ||
import io.vertx.httpproxy.ProxyContext; | ||
import io.vertx.httpproxy.ProxyInterceptor; | ||
import io.vertx.httpproxy.ProxyRequest; | ||
import io.vertx.httpproxy.ProxyResponse; | ||
|
||
@BuildSteps(onlyIfNot = IsNormal.class) | ||
public class DevServicesRestClientHttpProxy { | ||
|
||
private static final Logger log = Logger.getLogger(DevServicesRestClientHttpProxy.class); | ||
|
||
private static final Set<RestClientHttpProxyBuildItem> runningBIs = new HashSet<>(); | ||
|
||
private static final AtomicReference<Vertx> vertx = new AtomicReference<>(); | ||
|
||
@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())); | ||
} 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())); | ||
} | ||
} | ||
} | ||
|
||
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, | ||
BuildProducer<DevServicesResultBuildItem> devServicePropertiesProducer, | ||
CuratedApplicationShutdownBuildItem closeBuildItem) { | ||
if (restClientHttpProxyBuildItems.isEmpty()) { | ||
return; | ||
} | ||
|
||
Set<RestClientHttpProxyBuildItem> currentBIs = new HashSet<>(restClientHttpProxyBuildItems); | ||
|
||
Set<RestClientHttpProxyBuildItem> inRunningAndNotCurrent = new HashSet<>(runningBIs); | ||
inRunningAndNotCurrent.removeAll(currentBIs); | ||
|
||
// we need to remove the running ones that should no longer be running | ||
for (var running : inRunningAndNotCurrent) { | ||
try { | ||
log.debug("Attempting to close HTTP proxy server for REST Client '" + running.getClassName() + "'"); | ||
running.getCloseable().close(); | ||
log.debug("Closed HTTP proxy server for REST Client '" + running.getClassName() + "'"); | ||
} catch (IOException e) { | ||
throw new UncheckedException(e); | ||
} | ||
} | ||
runningBIs.removeAll(inRunningAndNotCurrent); | ||
|
||
Set<RestClientHttpProxyBuildItem> inCurrentAndNotRunning = new HashSet<>(currentBIs); | ||
inCurrentAndNotRunning.removeAll(runningBIs); | ||
|
||
if (vertx.get() == null) { | ||
// TODO: what settings do we need here in order to minimize the footprint? | ||
vertx.set(Vertx.vertx()); | ||
} | ||
|
||
for (var current : inCurrentAndNotRunning) { | ||
URI baseUri = URI.create(current.getBaseUri()); | ||
|
||
var clientOptions = new HttpClientOptions(); | ||
if (baseUri.getScheme().equals("https")) { | ||
clientOptions.setSsl(true); | ||
} | ||
HttpClient proxyClient = vertx.get().createHttpClient(clientOptions); | ||
HttpProxy proxy = HttpProxy.reverseProxy(proxyClient); | ||
proxy.origin(determineOriginPort(baseUri), baseUri.getHost()) | ||
.addInterceptor(new HostSettingInterceptor(baseUri.getHost())); | ||
|
||
HttpServer proxyServer = vertx.get().createHttpServer(); | ||
Integer port = findRandomPort(); | ||
proxyServer.requestHandler(proxy).listen(port); | ||
|
||
log.info("Started HTTP proxy server on http://localhost:" + port + " for REST Client '" + current.getClassName() | ||
+ "'"); | ||
|
||
current.attachClosable(new HttpServerClosable(proxyServer)); | ||
runningBIs.add(current); | ||
closeBuildItem.addCloseTask(new VertxAndRunningBIsClosingRunnable(vertx, runningBIs), true); | ||
|
||
String urlKeyName = String.format("quarkus.rest-client.\"%s\".override-uri", current.getClassName()); | ||
String urlKeyValue = String.format("http://localhost:%d", port); | ||
if (baseUri.getPath() != null) { | ||
if (!"/".equals(baseUri.getPath()) && !baseUri.getPath().isEmpty()) { | ||
urlKeyValue = urlKeyValue + "/" + baseUri.getPath(); | ||
} | ||
} | ||
|
||
devServicePropertiesProducer.produce( | ||
new DevServicesResultBuildItem("rest-client-" + current.getClassName() + "-proxy", | ||
null, | ||
Map.of(urlKeyName, urlKeyValue))); | ||
} | ||
} | ||
|
||
private int determineOriginPort(URI baseUri) { | ||
if (baseUri.getPort() != -1) { | ||
return baseUri.getPort(); | ||
} | ||
if (baseUri.getScheme().equals("https")) { | ||
return 443; | ||
} | ||
return 80; | ||
} | ||
|
||
private Integer findRandomPort() { | ||
try (ServerSocket socket = new ServerSocket(0)) { | ||
return socket.getLocalPort(); | ||
} catch (IOException e) { | ||
throw new UncheckedIOException(e); | ||
} | ||
} | ||
|
||
/** | ||
* This class sets the Host HTTP Header in order to avoid having services being blocked | ||
* for presenting a wrong value | ||
*/ | ||
private static class HostSettingInterceptor implements ProxyInterceptor { | ||
|
||
private final String host; | ||
|
||
private HostSettingInterceptor(String host) { | ||
this.host = host; | ||
} | ||
|
||
@Override | ||
public Future<ProxyResponse> handleProxyRequest(ProxyContext context) { | ||
ProxyRequest request = context.request(); | ||
MultiMap headers = request.headers(); | ||
headers.set("Host", host); | ||
|
||
return context.sendRequest(); | ||
} | ||
} | ||
|
||
private static class HttpServerClosable implements Closeable { | ||
private final HttpServer server; | ||
|
||
public HttpServerClosable(HttpServer server) { | ||
this.server = server; | ||
} | ||
|
||
@Override | ||
public void close() throws IOException { | ||
// TODO: do we need to wait for the future to complete? | ||
server.close(); | ||
} | ||
} | ||
|
||
private static class VertxAndRunningBIsClosingRunnable implements Runnable { | ||
private final AtomicReference<Vertx> vertx; | ||
private final Set<RestClientHttpProxyBuildItem> runningBIs; | ||
|
||
public VertxAndRunningBIsClosingRunnable(AtomicReference<Vertx> vertx, Set<RestClientHttpProxyBuildItem> runningBIs) { | ||
this.vertx = vertx; | ||
this.runningBIs = runningBIs; | ||
} | ||
|
||
@Override | ||
public void run() { | ||
for (RestClientHttpProxyBuildItem runningBI : runningBIs) { | ||
try { | ||
runningBI.getCloseable().close(); | ||
} catch (IOException e) { | ||
throw new UncheckedException(e); | ||
} | ||
} | ||
// TODO: do we need to wait for this? | ||
vertx.get().close(); | ||
vertx.set(null); | ||
} | ||
} | ||
|
||
@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(); | ||
} | ||
} |
Oops, something went wrong.