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);
    }
}