Skip to content

Commit

Permalink
feat: introduce VertxHttpClientFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
ytvnr committed Mar 6, 2024
1 parent a63ed29 commit c18ce65
Show file tree
Hide file tree
Showing 29 changed files with 1,758 additions and 0 deletions.
8 changes: 8 additions & 0 deletions gravitee-node-vertx/pom.xml
Expand Up @@ -32,6 +32,7 @@

<properties>
<mockito-inline.version>3.12.4</mockito-inline.version>
<jackson-databind.version>2.16.1</jackson-databind.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -105,5 +106,12 @@
<artifactId>bcpkix-jdk15on</artifactId>
<scope>test</scope>
</dependency>
<!-- Jackson dependencies -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson-databind.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,190 @@
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* 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.gravitee.node.vertx.client.http;

import io.gravitee.node.api.configuration.Configuration;
import io.gravitee.node.vertx.client.ssl.SslOptions;
import io.gravitee.node.vertx.proxy.VertxProxyOptionsUtils;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.http.RequestOptions;
import io.vertx.core.net.OpenSSLEngineOptions;
import io.vertx.core.net.ProxyOptions;
import io.vertx.core.net.ProxyType;
import io.vertx.rxjava3.core.Vertx;
import io.vertx.rxjava3.core.http.HttpClient;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import lombok.Builder;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Builder
public class VertxHttpClientFactory {

public static final int UNSECURE_PORT = 80;
public static final int SECURE_PORT = 443;

// Dummy {@link URLStreamHandler} implementation to avoid unknown protocol issue with default implementation (which knows how to handle only http and https protocol).
public static final URLStreamHandler URL_HANDLER = new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) {
return null;
}
};
protected static final String HTTP_SSL_OPENSSL_CONFIGURATION = "http.ssl.openssl";

@NonNull
private final Vertx vertx;

@NonNull
private final Configuration nodeConfiguration;

private String name;
private boolean shared;
private String defaultTarget;
private VertxHttpProxyOptions proxyOptions;
private VertxHttpClientOptions httpOptions;
private SslOptions sslOptions;

public HttpClient createHttpClient() {
if (httpOptions == null) {
httpOptions = new VertxHttpClientOptions();
}
return vertx.createHttpClient(createHttpClientOptions());
}

public static boolean isSecureProtocol(String protocol) {
return protocol.charAt(protocol.length() - 1) == 's' && protocol.length() > 2;
}

public static URL buildUrl(String uri) {
try {
return new URL(null, uri, URL_HANDLER);
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Target [" + uri + "] is not valid");
}
}

public static int getPort(URL target, boolean isSecured) {
final int defaultPort = isSecured ? SECURE_PORT : UNSECURE_PORT;
return target.getPort() != -1 ? target.getPort() : defaultPort;
}

public static String toAbsoluteUri(RequestOptions requestOptions, String defaultHost, int defaultPort) {
return (
(Boolean.TRUE.equals(requestOptions.isSsl()) ? "https://" : "http://") +
(
(requestOptions.getHost() != null ? requestOptions.getHost() : defaultHost) +
(requestOptions.getPort() != null ? ":" + requestOptions.getPort() : (defaultPort != -1 ? ":" + defaultPort : "")) +
requestOptions.getURI()
)
);
}

private io.vertx.core.http.HttpClientOptions createHttpClientOptions() {
io.vertx.core.http.HttpClientOptions options = new io.vertx.core.http.HttpClientOptions();

options
.setPipelining(httpOptions.isPipelining())
.setKeepAlive(httpOptions.isKeepAlive())
.setIdleTimeout((int) (httpOptions.getIdleTimeout() / 1000))
.setConnectTimeout((int) httpOptions.getConnectTimeout())
.setMaxPoolSize(httpOptions.getMaxConcurrentConnections())
.setDecompressionSupported(httpOptions.isUseCompression())
.setTryUsePerFrameWebSocketCompression(httpOptions.isUseCompression())
.setTryUsePerMessageWebSocketCompression(httpOptions.isUseCompression())
.setWebSocketCompressionAllowClientNoContext(httpOptions.isUseCompression())
.setWebSocketCompressionRequestServerNoContext(httpOptions.isUseCompression());

if (httpOptions.getVersion() == VertxHttpProtocolVersion.HTTP_2) {
options
.setProtocolVersion(HttpVersion.HTTP_2)
.setHttp2ClearTextUpgrade(httpOptions.isClearTextUpgrade())
.setHttp2MaxPoolSize(httpOptions.getMaxConcurrentConnections());
}

final URL target = buildUrl(defaultTarget);

configureHttpProxy(options);
configureSsl(options, target);

if (name != null) {
options.setName(name);
}

return options
.setShared(shared)
.setDefaultPort(getPort(target, isSecureProtocol(target.getProtocol())))
.setDefaultHost(target.getHost());
}

private void configureHttpProxy(final io.vertx.core.http.HttpClientOptions options) {
if (proxyOptions != null && proxyOptions.isEnabled()) {
if (proxyOptions.isUseSystemProxy()) {
setSystemProxy(options);
} else {
ProxyOptions vertxProxyOptions;
vertxProxyOptions = new ProxyOptions();
vertxProxyOptions.setHost(this.proxyOptions.getHost());
vertxProxyOptions.setPort(this.proxyOptions.getPort());
vertxProxyOptions.setUsername(this.proxyOptions.getUsername());
vertxProxyOptions.setPassword(this.proxyOptions.getPassword());
vertxProxyOptions.setType(ProxyType.valueOf(this.proxyOptions.getType().name()));
options.setProxyOptions(vertxProxyOptions);
}
}
}

private void configureSsl(final io.vertx.core.http.HttpClientOptions options, final URL target) {
if (isSecureProtocol(target.getProtocol())) {
// Configure SSL.
options.setSsl(true);

if (Boolean.TRUE.equals(nodeConfiguration.getProperty(HTTP_SSL_OPENSSL_CONFIGURATION, Boolean.class, false))) {
options.setSslEngineOptions(new OpenSSLEngineOptions());
}

if (sslOptions != null) {
options.setVerifyHost(sslOptions.isHostnameVerifier()).setTrustAll(sslOptions.isTrustAll());

// Client truststore configuration (trust server certificate).
sslOptions.trustStore().ifPresent(trustStore -> trustStore.configureForClient(options, defaultTarget));

// Client keystore configuration (client certificate for mtls).
sslOptions.keyStore().ifPresent(keyStore -> keyStore.configureForClient(options, defaultTarget));
}
}

options.setUseAlpn(true);
}

private void setSystemProxy(final io.vertx.core.http.HttpClientOptions options) {
try {
options.setProxyOptions(VertxProxyOptionsUtils.buildProxyOptions(nodeConfiguration));
} catch (Exception e) {
log.warn(
"HttpClient (name[{}] target[{}]) requires a system proxy to be defined but some configurations are missing or not well defined: {}",
name,
defaultTarget,
e.getMessage()
);
log.warn("Ignoring system proxy");
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* 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.gravitee.node.vertx.client.http;

import java.io.Serial;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
* @author Yann TAVERNIER (yann.tavernier at graviteesource.com)
* @author GraviteeSource Team
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class VertxHttpClientOptions implements Serializable {

@Serial
private static final long serialVersionUID = -7061411805967594667L;

public static final long DEFAULT_IDLE_TIMEOUT = 60000;
public static final long DEFAULT_CONNECT_TIMEOUT = 5000;
public static final long DEFAULT_READ_TIMEOUT = 10000;
public static final int DEFAULT_MAX_CONCURRENT_CONNECTIONS = 100;
public static final boolean DEFAULT_KEEP_ALIVE = true;
public static final boolean DEFAULT_PIPELINING = false;
public static final boolean DEFAULT_USE_COMPRESSION = true;
public static final boolean DEFAULT_PROPAGATE_CLIENT_ACCEPT_ENCODING = false;
public static final boolean DEFAULT_FOLLOW_REDIRECTS = false;
public static final boolean DEFAULT_CLEAR_TEXT_UPGRADE = true;
public static final VertxHttpProtocolVersion DEFAULT_PROTOCOL_VERSION = VertxHttpProtocolVersion.HTTP_1_1;

private long idleTimeout = DEFAULT_IDLE_TIMEOUT;
private long connectTimeout = DEFAULT_CONNECT_TIMEOUT;
private boolean keepAlive = DEFAULT_KEEP_ALIVE;
private long readTimeout = DEFAULT_READ_TIMEOUT;
private boolean pipelining = DEFAULT_PIPELINING;
private int maxConcurrentConnections = DEFAULT_MAX_CONCURRENT_CONNECTIONS;
private boolean useCompression = DEFAULT_USE_COMPRESSION;
private boolean propagateClientAcceptEncoding = DEFAULT_PROPAGATE_CLIENT_ACCEPT_ENCODING;
private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;
private boolean clearTextUpgrade = DEFAULT_CLEAR_TEXT_UPGRADE;
private VertxHttpProtocolVersion version = DEFAULT_PROTOCOL_VERSION;

public boolean isPropagateClientAcceptEncoding() {
// Propagate Accept-Encoding can only be made if useCompression is disabled.
return !useCompression && propagateClientAcceptEncoding;
}
}
@@ -0,0 +1,25 @@
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* 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.gravitee.node.vertx.client.http;

/**
* @author Yann TAVERNIER (yann.tavernier at graviteesource.com)
* @author GraviteeSource Team
*/
public enum VertxHttpProtocolVersion {
HTTP_1_1,
HTTP_2,
}
@@ -0,0 +1,47 @@
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* 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.gravitee.node.vertx.client.http;

import java.io.Serial;
import java.io.Serializable;
import lombok.Getter;
import lombok.Setter;

/**
* @author Yann TAVERNIER (yann.tavernier at graviteesource.com)
* @author GraviteeSource Team
*/
@Getter
@Setter
public class VertxHttpProxyOptions implements Serializable {

@Serial
private static final long serialVersionUID = -1133018497662951240L;

private boolean enabled;

private boolean useSystemProxy;

private String host;

private int port;

private String username;

private String password;

private VertxHttpProxyType type = VertxHttpProxyType.HTTP;
}
@@ -0,0 +1,26 @@
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* 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.gravitee.node.vertx.client.http;

/**
* @author Yann TAVERNIER (yann.tavernier at graviteesource.com)
* @author GraviteeSource Team
*/
public enum VertxHttpProxyType {
HTTP,
SOCKS4,
SOCKS5,
}

0 comments on commit c18ce65

Please sign in to comment.