TargetRequestHandler.java
/*
* Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you 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 org.apache.synapse.transport.netty.sender;
import io.netty.handler.codec.http.DefaultHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import org.apache.axiom.om.OMOutputFormat;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.transport.MessageFormatter;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.axis2.transport.http.SOAPMessageFormatter;
import org.apache.axis2.util.MessageProcessorSelector;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.protocol.HTTP;
import org.apache.synapse.transport.netty.BridgeConstants;
import org.apache.synapse.transport.netty.config.NettyConfiguration;
import org.apache.synapse.transport.netty.config.TargetConfiguration;
import org.apache.synapse.transport.netty.util.HttpUtils;
import org.apache.synapse.transport.netty.util.RequestResponseUtils;
import org.apache.synapse.transport.nhttp.NhttpConstants;
import org.apache.synapse.transport.nhttp.util.MessageFormatterDecoratorFactory;
import org.apache.synapse.transport.passthru.PassThroughConstants;
import org.apache.synapse.transport.passthru.util.PassThroughTransportUtils;
import org.apache.synapse.transport.passthru.util.RelayUtils;
import org.apache.synapse.transport.passthru.util.TargetRequestFactory;
import org.wso2.transport.http.netty.contract.Constants;
import org.wso2.transport.http.netty.contract.HttpClientConnector;
import org.wso2.transport.http.netty.contract.HttpResponseFuture;
import org.wso2.transport.http.netty.contract.HttpWsConnectorFactory;
import org.wso2.transport.http.netty.contract.config.ChunkConfig;
import org.wso2.transport.http.netty.contract.config.KeepAliveConfig;
import org.wso2.transport.http.netty.contract.config.SenderConfiguration;
import org.wso2.transport.http.netty.contractimpl.sender.channel.BootstrapConfiguration;
import org.wso2.transport.http.netty.contractimpl.sender.channel.pool.ConnectionManager;
import org.wso2.transport.http.netty.message.HttpCarbonMessage;
import org.wso2.transport.http.netty.message.HttpMessageDataStreamer;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.Objects;
/**
* {@code TargetRequestHandler} have utilities for creating and preparing an outbound request to be sent
* to the backend service.
*/
public class TargetRequestHandler {
private static final Log LOG = LogFactory.getLog(TargetRequestHandler.class);
/**
* Creates outbound request to be sent to the Backend service.
*
* @param url URL of the backend service
* @param msgContext axis2 message context
* @param targetConfiguration configurations of the Transport Sender
* @return the outbound request HttpCarbonMessage
*/
public static HttpCarbonMessage createOutboundRequestMsg(URL url, MessageContext msgContext,
TargetConfiguration targetConfiguration)
throws AxisFault {
HttpCarbonMessage outboundRequest =
new HttpCarbonMessage(new DefaultHttpRequest(new HttpVersion(BridgeConstants.HTTP_2_0, Boolean.TRUE),
HttpMethod.GET, ""));
prepareOutboundRequest(url, outboundRequest, msgContext, targetConfiguration);
return outboundRequest;
}
private static void prepareOutboundRequest(URL url, HttpCarbonMessage outboundRequest, MessageContext msgContext,
TargetConfiguration targetConfiguration) throws AxisFault {
try {
int port = getOutboundReqPort(url);
String host = url.getHost();
setOutboundReqProperties(outboundRequest, url, port, host, msgContext);
setOutboundReqHeaders(outboundRequest, port, host, msgContext, targetConfiguration);
} catch (MalformedURLException e) {
RequestResponseUtils.handleException("Malformed URL in the target EPR.", e);
} catch (IOException e) {
RequestResponseUtils.handleException("Failed to prepare the outbound request.", e);
}
}
private static void setOutboundReqProperties(HttpCarbonMessage outboundRequest, URL url, int port, String host,
MessageContext msgContext) throws IOException {
setHTTPMethod(msgContext, outboundRequest);
outboundRequest.setProperty(BridgeConstants.HTTP_HOST, host);
outboundRequest.setProperty(BridgeConstants.HTTP_PORT, port);
outboundRequest.setProperty(BridgeConstants.TO, getOutboundReqPath(url, msgContext));
outboundRequest.setProperty(BridgeConstants.PROTOCOL, url.getProtocol());
outboundRequest.setProperty(BridgeConstants.NO_ENTITY_BODY, HttpUtils.isNoEntityBodyRequest(msgContext));
}
private static void setOutboundReqHeaders(HttpCarbonMessage outboundRequest, int port, String host,
MessageContext msgContext, TargetConfiguration targetConfiguration)
throws AxisFault {
HttpHeaders headers = outboundRequest.getHeaders();
HttpUtils.removeUnwantedHeadersFromInternalTransportHeadersMap(msgContext, targetConfiguration);
HttpUtils.addTransportHeadersToTransportMessage(headers, msgContext);
setContentTypeHeaderIfApplicable(msgContext, outboundRequest, targetConfiguration);
setWSAActionIfApplicable(msgContext, headers);
HttpUtils.setHostHeader(host, port, headers, msgContext,
targetConfiguration.isPreserveHttpHeader(HTTPConstants.HEADER_HOST));
setOutboundUserAgent(headers);
}
private static void setHTTPMethod(MessageContext msgCtx, HttpCarbonMessage outboundRequest) {
String httpMethod = (String) msgCtx.getProperty(BridgeConstants.HTTP_METHOD);
if (Objects.isNull(httpMethod)) {
httpMethod = HTTPConstants.HTTP_METHOD_POST;
}
outboundRequest.setHttpMethod(httpMethod);
}
private static int getOutboundReqPort(URL url) {
int port = 80;
if (url.getPort() != -1) {
port = url.getPort();
} else if (url.getProtocol().equalsIgnoreCase(Constants.HTTPS_SCHEME)) {
port = 443;
}
return port;
}
private static String getOutboundReqPath(URL url, MessageContext msgCtx) throws IOException {
if (HttpUtils.isGETRequest(msgCtx) || (RelayUtils.isDeleteRequestWithoutPayload(msgCtx))) {
MessageFormatter formatter = MessageProcessorSelector.getMessageFormatter(msgCtx);
OMOutputFormat format = PassThroughTransportUtils.getOMOutputFormat(msgCtx);
if (formatter != null) {
URL targetURL = formatter.getTargetAddress(msgCtx, format, url);
if (targetURL != null && !targetURL.toString().isEmpty()) {
if (msgCtx.isPropertyTrue(BridgeConstants.POST_TO_URI)) {
return targetURL.toString();
} else {
return targetURL.getPath()
+ ((targetURL.getQuery() != null && !targetURL.getQuery().isEmpty())
? ("?" + targetURL.getQuery())
: "");
}
}
}
}
if (msgCtx.isPropertyTrue(BridgeConstants.POST_TO_URI)) {
return url.toString();
}
// TODO: need to check "(route.getProxyHost() != null && !route.isTunnelled())" as well
return msgCtx.isPropertyTrue(BridgeConstants.FULL_URI)
? url.toString() : url.getPath() + (url.getQuery() != null ? "?" + url.getQuery() : "");
}
private static void setOutboundUserAgent(HttpHeaders headers) {
if (!headers.contains(HttpHeaderNames.USER_AGENT)) {
headers.set(HttpHeaderNames.USER_AGENT, BridgeConstants.DEFAULT_OUTBOUND_USER_AGENT);
}
}
private static void setContentTypeHeaderIfApplicable(MessageContext msgCtx, HttpCarbonMessage outboundRequest,
TargetConfiguration targetConfiguration)
throws AxisFault {
Map transportHeaders = (Map) msgCtx.getProperty(MessageContext.TRANSPORT_HEADERS);
if (transportHeaders != null) {
String trpContentType = (String) transportHeaders.get(HTTP.CONTENT_TYPE);
if (trpContentType != null && !trpContentType.equals("")) {
if (!TargetRequestFactory.isMultipartContent(trpContentType) && !msgCtx.isDoingSwA()) {
outboundRequest.setHeader(HTTP.CONTENT_TYPE, trpContentType);
return;
}
}
}
String cType = getContentType(msgCtx,
targetConfiguration.isPreserveHttpHeader(HTTP.CONTENT_TYPE), transportHeaders);
if (cType != null
&& !HTTPConstants.HTTP_METHOD_GET.equals((String) msgCtx.getProperty(BridgeConstants.HTTP_METHOD))
&& shouldOverwriteContentType(msgCtx, outboundRequest)) {
String messageType = (String) msgCtx.getProperty(NhttpConstants.MESSAGE_TYPE);
if (messageType != null) {
// if multipart related message type and unless if message
// not get build we should
// skip of setting formatter specific content Type
if (!messageType.contains(HTTPConstants.MEDIA_TYPE_MULTIPART_RELATED)
&& !messageType.contains(HTTPConstants.MEDIA_TYPE_MULTIPART_FORM_DATA)) {
outboundRequest.setHeader(HTTP.CONTENT_TYPE, cType);
} else {
// if messageType is related to multipart and if message
// already built we need to set new
// boundary related content type at Content-Type header
boolean builderInvoked = Boolean.TRUE.equals(msgCtx
.getProperty(PassThroughConstants.MESSAGE_BUILDER_INVOKED));
if (builderInvoked) {
outboundRequest.setHeader(HTTP.CONTENT_TYPE, cType);
}
}
} else {
outboundRequest.setHeader(HTTP.CONTENT_TYPE, cType);
}
}
if ((PassThroughConstants.HTTP_GET.equals(msgCtx.getProperty(BridgeConstants.HTTP_METHOD))) ||
(RelayUtils.isDeleteRequestWithoutPayload(msgCtx))) {
MessageFormatter formatter = MessageProcessorSelector.getMessageFormatter(msgCtx);
if (formatter != null) {
outboundRequest.removeHeader(HTTP.CONTENT_TYPE);
}
}
}
public static String getContentType(MessageContext msgCtx, boolean isContentTypePreservedHeader,
Map trpHeaders) throws AxisFault {
String setEncoding = (String) msgCtx.getProperty(PassThroughConstants.SET_CHARACTER_ENCODING);
// If incoming transport isn't HTTP, transport headers can be null. Therefore null check is required
// and if headers not null check whether request comes with Content-Type header before preserving Content-Type
// Need to avoid this for multipart headers, need to add MIME Boundary property
if (trpHeaders != null
&& (trpHeaders).get(HTTPConstants.HEADER_CONTENT_TYPE) != null
&& (isContentTypePreservedHeader || PassThroughConstants.VALUE_FALSE.equals(setEncoding))
&& !RequestResponseUtils.isMultipartContent((trpHeaders).get(HTTPConstants.HEADER_CONTENT_TYPE)
.toString())) {
if (msgCtx.getProperty(org.apache.axis2.Constants.Configuration.CONTENT_TYPE) != null) {
return (String) msgCtx.getProperty(org.apache.axis2.Constants.Configuration.CONTENT_TYPE);
} else if (msgCtx.getProperty(org.apache.axis2.Constants.Configuration.MESSAGE_TYPE) != null) {
return (String) msgCtx.getProperty(org.apache.axis2.Constants.Configuration.MESSAGE_TYPE);
}
}
MessageFormatter formatter = MessageProcessorSelector.getMessageFormatter(msgCtx);
OMOutputFormat format = PassThroughTransportUtils.getOMOutputFormat(msgCtx);
if (formatter != null) {
return formatter.getContentType(msgCtx, format, msgCtx.getSoapAction());
} else {
String contentType = (String) msgCtx.getProperty(org.apache.axis2.Constants.Configuration.CONTENT_TYPE);
if (contentType != null) {
return contentType;
} else {
return new SOAPMessageFormatter().getContentType(
msgCtx, format, msgCtx.getSoapAction());
}
}
}
/**
* Check whether the we should overwrite the content type for the outgoing request.
*
* @param msgContext MessageContext
* @return whether to overwrite the content type for the outgoing request
*/
public static boolean shouldOverwriteContentType(MessageContext msgContext, HttpCarbonMessage outboundRequest) {
boolean builderInvoked = Boolean.TRUE.equals(msgContext
.getProperty(PassThroughConstants.MESSAGE_BUILDER_INVOKED));
boolean noEntityBodySet =
Boolean.TRUE.equals(msgContext.getProperty(PassThroughConstants.NO_ENTITY_BODY));
// if contentTypeInRequest is true, that means it is set from the transport headers. that means the header
// came in the source request.
boolean contentTypeInRequest = outboundRequest.getHeader("Content-Type") != null
|| outboundRequest.getHeader("content-type") != null;
boolean isDefaultContentTypeEnabled = false;
ConfigurationContext configurationContext = msgContext.getConfigurationContext();
if (configurationContext != null && configurationContext.getAxisConfiguration()
.getParameter(NhttpConstants.REQUEST_CONTENT_TYPE) != null) {
isDefaultContentTypeEnabled = true;
}
// If builder is not invoked, which means the passthrough scenario, we should overwrite the content-type
// depending on the presence of the incoming content-type.
// If builder is invoked and no entity body property is not set (which means there is a payload in the request)
// we should consider overwriting the content-type.
return (builderInvoked && !noEntityBodySet) || contentTypeInRequest || isDefaultContentTypeEnabled;
}
private static void setWSAActionIfApplicable(MessageContext msgCtx, HttpHeaders headers) {
String soapAction = msgCtx.getSoapAction();
if (soapAction == null) {
soapAction = msgCtx.getWSAAction();
}
MessageFormatter messageFormatter =
MessageFormatterDecoratorFactory.createMessageFormatterDecorator(msgCtx);
if (msgCtx.isSOAP11() && Objects.nonNull(soapAction) && !soapAction.isEmpty()
&& Objects.nonNull(messageFormatter)) {
headers.add(HTTPConstants.HEADER_SOAP_ACTION, messageFormatter.formatSOAPAction(msgCtx, null, soapAction));
}
}
public static HttpClientConnector createHttpClient(URL url, MessageContext msgContext,
HttpWsConnectorFactory httpWsConnectorFactory,
ConnectionManager connectionManager,
BootstrapConfiguration bootstrapConfiguration,
TargetConfiguration targetConfiguration) throws AxisFault {
try {
SenderConfiguration senderConfiguration = new SenderConfiguration();
populateSenderConfigurations(msgContext, senderConfiguration, targetConfiguration, url);
return httpWsConnectorFactory.createHttpClientConnector(bootstrapConfiguration, senderConfiguration,
connectionManager);
} catch (Exception ex) {
throw new AxisFault("Error while creating the HTTP Client Connector. ", ex);
}
}
public static void populateSenderConfigurations(MessageContext msgContext,
SenderConfiguration senderConfiguration,
TargetConfiguration targetConfiguration,
URL url) throws AxisFault {
String scheme = url.getProtocol() != null ? url.getProtocol() : BridgeConstants.PROTOCOL_HTTP;
senderConfiguration.setScheme(scheme);
String httpVersion = BridgeConstants.HTTP_2_0_VERSION;
String forceHttp10 = (String) msgContext.getProperty(PassThroughConstants.FORCE_HTTP_1_0);
if (BridgeConstants.VALUE_TRUE.equalsIgnoreCase(forceHttp10)) {
httpVersion = BridgeConstants.HTTP_1_0_VERSION;
}
senderConfiguration.setHttpVersion(httpVersion);
if (isClientEndpointChunkingEnabled(msgContext)) {
senderConfiguration.setChunkingConfig(ChunkConfig.ALWAYS);
} else {
senderConfiguration.setChunkingConfig(ChunkConfig.NEVER);
}
if (isClientEndpointKeepAliveDisabled(msgContext)) {
senderConfiguration.setKeepAliveConfig(KeepAliveConfig.NEVER);
} else {
senderConfiguration.setKeepAliveConfig(KeepAliveConfig.ALWAYS);
}
senderConfiguration.setHttpTraceLogEnabled(targetConfiguration.isHttpTraceLogEnabled());
// Set Request validation limits.
boolean isRequestLimitsValidationEnabled = targetConfiguration.isRequestLimitsValidationEnabled();
if (isRequestLimitsValidationEnabled) {
RequestResponseUtils.setInboundMgsSizeValidationConfig(
targetConfiguration.getClientRequestMaxStatusLineLength(),
targetConfiguration.getClientRequestMaxHeaderSize(),
targetConfiguration.getClientRequestMaxEntityBodySize(),
senderConfiguration.getMsgSizeValidationConfig());
}
senderConfiguration.setSocketIdleTimeout(targetConfiguration.getSocketTimeout() * 1000);
if (BridgeConstants.PROTOCOL_HTTPS.equals(scheme)) {
targetConfiguration.getClientSSLConfigurationBuilder().setClientSSLConfig(senderConfiguration);
}
}
private static boolean isClientEndpointChunkingEnabled(MessageContext msgContext) {
if (msgContext.isPropertyTrue(NhttpConstants.FORCE_HTTP_CONTENT_LENGTH)) {
return false;
} else {
String disableChunking = (String) msgContext.getProperty(PassThroughConstants.DISABLE_CHUNKING);
return !BridgeConstants.VALUE_TRUE.equals(disableChunking)
&& !BridgeConstants.VALUE_TRUE.equals(msgContext.getProperty(PassThroughConstants.FORCE_HTTP_1_0));
}
}
private static boolean isClientEndpointKeepAliveDisabled(MessageContext msgContext) {
String noKeepAliveProperty = (String) msgContext.getProperty(BridgeConstants.NO_KEEPALIVE);
if (Objects.nonNull(noKeepAliveProperty)) {
return msgContext.isPropertyTrue(noKeepAliveProperty);
}
Map transportHeaders = (Map) msgContext.getProperty(MessageContext.TRANSPORT_HEADERS);
if (transportHeaders != null) {
Object connectionHeader = transportHeaders.get(HTTP.CONN_DIRECTIVE);
if (Objects.nonNull(connectionHeader) && "close".equalsIgnoreCase(connectionHeader.toString())) {
return true;
}
}
return NettyConfiguration.getInstance().isKeepAliveDisabled();
}
public static void sendRequest(HttpClientConnector clientConnector, HttpCarbonMessage outboundRequestMsg,
MessageContext msgContext, TargetConfiguration targetConfiguration)
throws AxisFault {
sendOutboundRequest(clientConnector, outboundRequestMsg, msgContext, targetConfiguration);
serializeData(msgContext, outboundRequestMsg);
}
private static void sendOutboundRequest(HttpClientConnector clientConnector,
HttpCarbonMessage outboundRequestMsg,
MessageContext msgContext,
TargetConfiguration targetConfiguration) {
HttpResponseFuture future = clientConnector.send(outboundRequestMsg);
future.setHttpConnectorListener(new Axis2HttpTargetRespListener(targetConfiguration.getWorkerPool(),
msgContext, targetConfiguration));
//Set listener to receive server pushes from the backend server.
future.setPromiseAvailabilityListener(new Axis2ServerPushListener(future, msgContext,
targetConfiguration.getWorkerPool()));
}
private static void serializeData(MessageContext msgCtx, HttpCarbonMessage responseMsg)
throws AxisFault {
if (ignoreMessageBody(msgCtx)) {
OutputStream messageOutputStream = HttpUtils.getHttpMessageDataStreamer(responseMsg).getOutputStream();
HttpUtils.writeEmptyBody(messageOutputStream);
} else {
if (RequestResponseUtils.shouldInvokeFormatterToWriteBody(msgCtx)) {
HttpMessageDataStreamer outboundMsgDataStreamer = HttpUtils.getHttpMessageDataStreamer(responseMsg);
OutputStream messageOutputStream = outboundMsgDataStreamer.getOutputStream();
MessageFormatter messageFormatter =
MessageFormatterDecoratorFactory.createMessageFormatterDecorator(msgCtx);
if (Objects.nonNull(messageFormatter)) {
HttpUtils.serializeDataUsingMessageFormatter(msgCtx, messageFormatter, messageOutputStream);
} else {
LOG.warn("Could not serialize the message. No available formatter to write the "
+ "message to the backend.");
}
} else {
HttpCarbonMessage inboundCarbonMessage =
(HttpCarbonMessage) msgCtx.getProperty(BridgeConstants.HTTP_CARBON_MESSAGE);
HttpUtils.copyContentFromInboundHttpCarbonMessage(inboundCarbonMessage, responseMsg);
}
}
}
/**
* Checks if we can ignore the message body.
*
* @param msgContext axis2 message context
* @return whether we can ignore the message body
*/
private static boolean ignoreMessageBody(MessageContext msgContext) {
return HttpUtils.isGETRequest(msgContext) || RelayUtils.isDeleteRequestWithoutPayload(msgContext);
}
}