/*
* Copyright 2014 JBoss Inc
*
* 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.apiman.gateway.platforms.vertx3.connector;
import io.apiman.common.config.options.BasicAuthOptions;
import io.apiman.common.util.ApimanPathUtils;
import io.apiman.common.util.Basic;
import io.apiman.gateway.engine.IApiConnection;
import io.apiman.gateway.engine.IApiConnectionResponse;
import io.apiman.gateway.engine.async.AsyncResultImpl;
import io.apiman.gateway.engine.async.IAsyncHandler;
import io.apiman.gateway.engine.async.IAsyncResultHandler;
import io.apiman.gateway.engine.auth.RequiredAuthType;
import io.apiman.gateway.engine.beans.Api;
import io.apiman.gateway.engine.beans.ApiRequest;
import io.apiman.gateway.engine.beans.ApiResponse;
import io.apiman.gateway.engine.beans.exceptions.ConnectorException;
import io.apiman.gateway.engine.beans.util.QueryMap;
import io.apiman.gateway.engine.io.IApimanBuffer;
import io.apiman.gateway.engine.io.ISignalReadStream;
import io.apiman.gateway.engine.io.ISignalWriteStream;
import io.apiman.gateway.platforms.vertx3.http.HttpApiFactory;
import io.apiman.gateway.platforms.vertx3.i18n.Messages;
import io.apiman.gateway.platforms.vertx3.io.VertxApimanBuffer;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpClientResponse;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
/**
* A vert.x-based HTTP connector; implementing both {@link ISignalReadStream} and {@link ISignalWriteStream}.
*
* Its {@link ISignalWriteStream} elements are valid immediately and its {@link ISignalReadStream} is sent as
* an event to the provided {@link #resultHandler} when once it has reached a valid state. Hence, it is safe
* to return instances immediately after the constructor has returned.
*
* @author Marc Savy {@literal <msavy@redhat.com>}
*/
@SuppressWarnings("nls")
class HttpConnector implements IApiConnectionResponse, IApiConnection {
private static final Set<String> SUPPRESSED_HEADERS = new HashSet<>();
static {
SUPPRESSED_HEADERS.add("Transfer-Encoding");
SUPPRESSED_HEADERS.add("X-API-Key");
SUPPRESSED_HEADERS.add("Host");
}
private Logger logger = LoggerFactory.getLogger(this.getClass());
private ApiRequest apiRequest;
private ApiResponse apiResponse;
private IAsyncResultHandler<IApiConnectionResponse> resultHandler;
private IAsyncHandler<Void> drainHandler;
private IAsyncHandler<IApimanBuffer> bodyHandler;
private IAsyncHandler<Void> endHandler;
private ExceptionHandler exceptionHandler;
private boolean inboundFinished = false;
private boolean outboundFinished = false;
private Api api;
private String apiPath;
private String apiHost;
private String destination;
private int apiPort;
private boolean isHttps;
private BasicAuthOptions basicOptions;
private HttpClient client;
private HttpClientRequest clientRequest;
private HttpClientResponse clientResponse;
private URI apiEndpoint;
private HttpConnectorOptions options;
/**
* Construct an {@link HttpConnector} instance. The {@link #resultHandler} must remain exclusive to a
* given instance.
*
* @param vertx a vertx
* @param client the vertx http client
* @param api an API
* @param request a request with fields filled
* @param options the connector options
* @param resultHandler a handler, called when reading is permitted
*/
public HttpConnector(Vertx vertx, HttpClient client, ApiRequest request, Api api, HttpConnectorOptions options,
IAsyncResultHandler<IApiConnectionResponse> resultHandler) {
this.client = client;
this.api = api;
this.apiRequest = request;
this.resultHandler = resultHandler;
this.exceptionHandler = new ExceptionHandler();
this.apiEndpoint = options.getUri();
this.options = options;
isHttps = apiEndpoint.getScheme().equals("https");
apiHost = apiEndpoint.getHost();
apiPort = getPort();
apiPath = apiEndpoint.getPath().isEmpty() || apiEndpoint.getPath().equals("/") ? "" : apiEndpoint.getPath();
destination = apiRequest.getDestination() == null ? "" : apiRequest.getDestination();
verifyConnection();
doConnection();
}
private int getPort() {
if (apiEndpoint.getPort() != -1)
return apiEndpoint.getPort();
return isHttps ? 443 : 80;
}
private void verifyConnection() {
switch (options.getRequiredAuthType()) {
case BASIC:
basicOptions = new BasicAuthOptions(api.getEndpointProperties());
if (!isHttps && basicOptions.isRequireSSL())
throw new ConnectorException("Endpoint security requested (BASIC auth) but endpoint is not secure (SSL).");
break;
case MTLS:
if (!isHttps)
throw new ConnectorException("Mutual TLS specified, but endpoint is not HTTPS.");
break;
case DEFAULT:
break;
}
}
private void doConnection() {
String endpoint = ApimanPathUtils.join(apiPath, destination + queryParams(apiRequest.getQueryParams()));
logger.debug(String.format("Connecting to %s | port: %d verb: %s path: %s", apiHost, apiPort,
HttpMethod.valueOf(apiRequest.getType()), endpoint));
clientRequest = client.request(HttpMethod.valueOf(apiRequest.getType()),
apiPort,
apiHost,
endpoint,
(HttpClientResponse vxClientResponse) -> {
clientResponse = vxClientResponse;
// Pause until we're given permission to xfer the response.
vxClientResponse.pause();
apiResponse = HttpApiFactory.buildResponse(vxClientResponse, SUPPRESSED_HEADERS);
vxClientResponse.handler((Handler<Buffer>) chunk -> {
bodyHandler.handle(new VertxApimanBuffer(chunk));
});
vxClientResponse.endHandler((Handler<Void>) v -> {
endHandler.handle((Void) null);
});
vxClientResponse.exceptionHandler(exceptionHandler);
// The response is only ever returned when vxClientResponse is valid.
resultHandler.handle(AsyncResultImpl
.create((IApiConnectionResponse) HttpConnector.this));
});
clientRequest.exceptionHandler(exceptionHandler);
if (options.hasDataPolicy() || !apiRequest.getHeaders().containsKey("Content-Length")) {
clientRequest.headers().remove("Content-Length");
clientRequest.setChunked(true);
}
apiRequest.getHeaders().forEach(e -> clientRequest.headers().add(e.getKey(), e.getValue()));
addMandatoryRequestHeaders(clientRequest.headers());
if (options.getRequiredAuthType() == RequiredAuthType.BASIC) {
clientRequest.putHeader("Authorization", Basic.encode(basicOptions.getUsername(), basicOptions.getPassword()));
}
}
private void addMandatoryRequestHeaders(MultiMap headers) {
String port = apiEndpoint.getPort() == -1 ? "" : ":" + apiEndpoint.getPort();
headers.add("Host", apiEndpoint.getHost() + port);
}
@Override
public ApiResponse getHead() {
return apiResponse;
}
@Override
public void transmit() {
logger.debug("Resuming");
clientResponse.resume();
}
@Override
public void abort(Throwable t) {
bodyHandler(null);
if(clientRequest != null) {
clientRequest.end();
}
if(clientResponse != null) {
clientResponse.netSocket().close(); //TODO verify
}
}
@Override
public void bodyHandler(IAsyncHandler<IApimanBuffer> bodyHandler) {
this.bodyHandler = bodyHandler;
}
@Override
public void endHandler(IAsyncHandler<Void> endHandler) {
this.endHandler = endHandler;
}
@Override
public void write(IApimanBuffer chunk) {
if (inboundFinished) {
throw new IllegalStateException(Messages.getString("HttpConnector.0"));
}
if (chunk.getNativeBuffer() instanceof Buffer) {
clientRequest.write((Buffer) chunk.getNativeBuffer());
// When write queue has diminished sufficiently, drain handler will be invoked.
if (clientRequest.writeQueueFull() && drainHandler != null) {
clientRequest.drainHandler(drainHandler::handle);
}
} else {
throw new IllegalArgumentException(Messages.getString("HttpConnector.1"));
}
}
@Override
public void end() {
clientRequest.end();
inboundFinished = true;
}
@Override
public boolean isFinished() {
return inboundFinished && outboundFinished;
}
@Override
public boolean isConnected() {
return !isFinished();
}
@Override
public void drainHandler(IAsyncHandler<Void> drainHandler) {
this.drainHandler = drainHandler;
}
@Override
public boolean isFull() {
return clientRequest.writeQueueFull();
}
private String queryParams(QueryMap queryParams) {
if (queryParams == null || queryParams.isEmpty())
return "";
StringBuilder sb = new StringBuilder(queryParams.size() * 2 * 10);
String joiner = "?";
try {
for (Entry<String, String> entry : queryParams) {
sb.append(joiner);
sb.append(entry.getKey());
if (entry.getValue() != null) {
sb.append("=");
sb.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
}
joiner = "&";
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return sb.toString();
}
private class ExceptionHandler implements Handler<Throwable> {
@Override
public void handle(Throwable error) {
resultHandler.handle(AsyncResultImpl
.<IApiConnectionResponse> create(error));
}
}
}