# SourceResponseHandler.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.DefaultHttpResponse;
    import io.netty.handler.codec.http.HttpHeaderNames;
    import io.netty.handler.codec.http.HttpResponseStatus;
    import io.netty.handler.codec.http.HttpVersion;
    import org.apache.axiom.om.OMOutputFormat;
    import org.apache.axis2.AxisFault;
    import org.apache.axis2.Constants;
    import org.apache.axis2.context.MessageContext;
    import org.apache.axis2.transport.MessageFormatter;
    import org.apache.axis2.transport.http.HTTPConstants;
    import org.apache.axis2.util.MessageProcessorSelector;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.http.HttpStatus;
    import org.apache.synapse.transport.netty.BridgeConstants;
    import org.apache.synapse.transport.netty.config.SourceConfiguration;
    import org.apache.synapse.transport.netty.util.CacheUtils;
    import org.apache.synapse.transport.netty.util.HttpUtils;
    import org.apache.synapse.transport.netty.util.MessageUtils;
    import org.apache.synapse.transport.netty.util.RequestResponseUtils;
    import org.apache.synapse.transport.passthru.util.PassThroughTransportUtils;
    import org.wso2.caching.CachingConstants;
    import org.wso2.transport.http.netty.contract.config.ChunkConfig;
    import org.wso2.transport.http.netty.message.Http2PushPromise;
    import org.wso2.transport.http.netty.message.HttpCarbonMessage;

    import java.io.IOException;
    import java.io.OutputStream;
    import java.util.Objects;

    /**
     * {@code SourceResponseHandler} have utilities for creating and preparing an outbound response to be sent
     * to the HTTP client.
     */
    public class SourceResponseHandler {

        private static final Log LOG = LogFactory.getLog(SourceResponseHandler.class);

        /**
         * Creates outbound response to be sent back to the HTTP client using the axis2 message context and
         * the original client request.
         *
         * @param msgCtx        axis2 message context
         * @param clientRequest original HTTP client request
         * @return the outbound response HttpCarbonMessage
         * @throws AxisFault if something goes wrong when creating the outbound response
         */
        public static HttpCarbonMessage createOutboundResponseMsg(MessageContext msgCtx, HttpCarbonMessage clientRequest)
                throws AxisFault {

            HttpVersion version = new HttpVersion(BridgeConstants.HTTP_2_0, true);
            if (BridgeConstants.HTTP_1_1_VERSION.equals(((SourceConfiguration) msgCtx.getProperty(BridgeConstants.HTTP_SOURCE_CONFIGURATION)).getProtocol())) {
                version = HttpVersion.HTTP_1_1;
            }
            HttpCarbonMessage outboundResponseMsg = new HttpCarbonMessage(new DefaultHttpResponse(version,
                    HttpResponseStatus.OK));
            try {
                handleMTOM(msgCtx);
                handleETAGCaching(clientRequest, outboundResponseMsg, msgCtx);
                if (isValidCacheResponse(msgCtx)) {
                    return outboundResponseMsg;
                }
                prepareOutboundResponse(clientRequest, outboundResponseMsg, msgCtx);
            } catch (AxisFault e) {
                RequestResponseUtils.handleException("Error while creating the outbound response!", e);
            }
            return outboundResponseMsg;
        }

        /**
         * Retrieve the push promise received from the backend server.
         *
         * @param msgCtx axis2 message context
         * @return htttp2PushPromise Http2PushPromise
         * @throws AxisFault if push promise is not available
         */
        public static Http2PushPromise getPushPromise(MessageContext msgCtx) throws AxisFault {

            Http2PushPromise http2PushPromise = (Http2PushPromise) msgCtx.getProperty(BridgeConstants.PUSH_PROMISE);
            if (http2PushPromise == null) {
                RequestResponseUtils.handleException("Error while retrieving push promise.");
            }
            return http2PushPromise;
        }

        private static void handleMTOM(MessageContext msgCtx) throws AxisFault {

            if (isMTOMEnabled(msgCtx)) {
                try {
                    MessageUtils.buildMessage(msgCtx);
                } catch (IOException e) {
                    RequestResponseUtils.handleException("IO Error occurred while building the message", e);
                }
            }
        }

        private static void prepareOutboundResponse(HttpCarbonMessage inboundRequestMsg,
                                                    HttpCarbonMessage outboundResponseMsg, MessageContext msgContext) {

            setOutboundResponseProperties(outboundResponseMsg, msgContext);
            setOutboundResponseHeaders(inboundRequestMsg, outboundResponseMsg, msgContext);
        }

        private static void setOutboundResponseProperties(HttpCarbonMessage outboundResponseMsg,
                                                          MessageContext msgContext) {

            setHttpVersion(outboundResponseMsg, msgContext);

            int statusCode = determineHttpStatusCode(msgContext);
            outboundResponseMsg.setHttpStatusCode(statusCode);

            setReasonPhrase(outboundResponseMsg, msgContext, statusCode);
        }

        private static void setOutboundResponseHeaders(HttpCarbonMessage inboundRequestMsg,
                                                       HttpCarbonMessage outboundResponseMsg, MessageContext msgContext) {

            HttpUtils.removeUnwantedHeadersFromInternalTransportHeadersMap(msgContext,
                    (SourceConfiguration) msgContext.getProperty(BridgeConstants.HTTP_SOURCE_CONFIGURATION));
            HttpUtils.addTransportHeadersToTransportMessage(outboundResponseMsg.getHeaders(), msgContext);
            setChunkingHeader(inboundRequestMsg, outboundResponseMsg, msgContext);
            setContentTypeHeader(outboundResponseMsg, msgContext);
        }

        private static void setContentTypeHeader(HttpCarbonMessage outboundResponseMsg, MessageContext msgContext) {

            if (!canOutboundResponseHaveContentTypeHeader(msgContext)) {
                outboundResponseMsg.removeHeader(BridgeConstants.CONTENT_TYPE_HEADER);
                return;
            }

            if (shouldOverwriteOutboundResponseContentTypeHeader(msgContext)) {
                try {
                    String contentType = getContentTypeForOutboundResponse(msgContext);
                    outboundResponseMsg.setHeader(BridgeConstants.CONTENT_TYPE_HEADER, contentType);
                } catch (AxisFault axisFault) {
                    LOG.error("Error occurred while setting the Content-Type header. Hence, not overwriting the "
                            + "outbound response Content-Type Header");
                }
            }
        }

        private static boolean shouldOverwriteOutboundResponseContentTypeHeader(MessageContext msgContext) {

            // the below null check is to decide whether the last call is an http request or not.
            // If it is an http and if the request has been built, we need to overwrite the Content-Type header.
            // Or else, if the http request was a pass through one, we do not need to modify the
            // Content-Type header.
            if (RequestResponseUtils.isHttpCarbonMessagePresent(msgContext)) {
                return !isPassThroughMessage(msgContext);
            }
            return true;
        }

        public static boolean canOutboundResponseHaveContentTypeHeader(MessageContext msgContext) {
            //TODO: verify this -> if we do not have a content to be sent in the source response, can we just
            // skip setting the content-type header?
            return true;
        }

        public static String getContentTypeForOutboundResponse(MessageContext msgContext) throws AxisFault {
            //This is to support MTOM in response path for requests sent without a SOAPAction. The reason is
            //axis2 selects application/xml formatter as the formatter for formatting the ESB to client response
            //when there is no SOAPAction.
            if (msgContext.isPropertyTrue(Constants.Configuration.ENABLE_MTOM)
                    || msgContext.isPropertyTrue(Constants.Configuration.ENABLE_SWA)) {
                Object contentType = msgContext.getProperty(Constants.Configuration.CONTENT_TYPE);
                // The following condition will allow us to set the content-type as multipart/related if and only if
                // the content type is null or not starts with multipart/related. We cannot blindly set the content-type
                // as multipart/related as it would replace the multipart content-type with MIME boundary and the cause the
                // issue of response dropping the MIME boundary.
                if (Objects.isNull(contentType)
                        || !((String) contentType).trim().startsWith(HTTPConstants.MEDIA_TYPE_MULTIPART_RELATED)) {
                    msgContext.setProperty(Constants.Configuration.CONTENT_TYPE,
                            HTTPConstants.MEDIA_TYPE_MULTIPART_RELATED);
                }
                msgContext.setProperty(Constants.Configuration.MESSAGE_TYPE,
                        HTTPConstants.MEDIA_TYPE_MULTIPART_RELATED);
            }

            Object contentTypeInMsgCtx = msgContext.getProperty(Constants.Configuration.CONTENT_TYPE);
            OMOutputFormat format = PassThroughTransportUtils.getOMOutputFormat(msgContext);

            // If ContentType header is set in the axis2 message context, use it.
            if (contentTypeInMsgCtx != null) {
                String contentTypeValueInMsgCtx = contentTypeInMsgCtx.toString();
                // Skip multipart/related as it should be taken from formatter.
                if (!(contentTypeValueInMsgCtx.contains(HTTPConstants.MEDIA_TYPE_MULTIPART_RELATED)
                        || contentTypeValueInMsgCtx.contains(HTTPConstants.MEDIA_TYPE_MULTIPART_FORM_DATA))) {

                    // adding charset only if charset is not available,
                    if (!contentTypeValueInMsgCtx.contains(HTTPConstants.CHAR_SET_ENCODING)
                            && msgContext.isPropertyTrue(BridgeConstants.SET_CHARACTER_ENCODING, true)) {
                        String encoding = format.getCharSetEncoding();
                        if (encoding != null) {
                            contentTypeValueInMsgCtx += "; charset=" + encoding;
                        }
                    }
                    return contentTypeValueInMsgCtx;
                }
            }

            // If ContentType is not set from msg context, get the formatter ContentType
            MessageFormatter formatter = null;
            try {
                formatter = MessageProcessorSelector.getMessageFormatter(msgContext);
            } catch (AxisFault e) {
                RequestResponseUtils.handleException("Cannot find a suitable MessageFormatter.", e);
            }
            return formatter.getContentType(msgContext, format, msgContext.getSoapAction());
        }

        /**
         * The pass through (when message body is not built) status of the message.
         *
         * @return true if it is a pass through.
         */
        public static boolean isPassThroughMessage(MessageContext msgContext) {

            boolean builderInvoked = Boolean.TRUE.equals(
                    msgContext.getProperty(BridgeConstants.MESSAGE_BUILDER_INVOKED));
            return !builderInvoked;
        }

        private static void setHttpVersion(HttpCarbonMessage outboundResponseMsg, MessageContext msgContext) {

            String version = determineHttpVersion(msgContext);
            outboundResponseMsg.setHttpVersion(version);
        }

        private static void setReasonPhrase(HttpCarbonMessage outboundResponseMsg, MessageContext msgContext,
                                            int statusCode) {

            String reasonPhrase = determineResponseReasonPhrase(msgContext, statusCode);
            // Whenever the reason phrase is null, the transport-http will infer the correct reason phrase based on the
            // provided status code.
            outboundResponseMsg.setProperty(org.wso2.transport.http.netty.contract.Constants.HTTP_REASON_PHRASE,
                    reasonPhrase);
        }

        private static void setChunkingHeader(HttpCarbonMessage inboundRequestMsg, HttpCarbonMessage outboundResponseMsg,
                                              MessageContext msgContext) {

            boolean canHaveContentLengthOrTransferEncodingHeader =
                    checkContentLengthAndTransferEncodingHeaderAllowance(inboundRequestMsg.getHttpMethod(),
                            outboundResponseMsg.getHttpStatusCode());
            if (canHaveContentLengthOrTransferEncodingHeader) {
                if (disableChunking(msgContext)) {
                    outboundResponseMsg.setProperty(BridgeConstants.CHUNKING_CONFIG, ChunkConfig.NEVER);
                } else {
                    outboundResponseMsg.setProperty(BridgeConstants.CHUNKING_CONFIG, ChunkConfig.ALWAYS);
                }
            }
        }

        private static boolean checkContentLengthAndTransferEncodingHeaderAllowance(String httpMethod, int statusCode) {
            // According to RFC 7230 - HTTP/1.1 Message Syntax and Routing - Message Body Length, the following logic
            // was implemented.
            if (BridgeConstants.HTTP_HEAD.equalsIgnoreCase(httpMethod)) {
                // Any response to a HEAD request
                return false;
            } else if (BridgeConstants.HTTP_CONNECT.equals(httpMethod)) {
                // Any 2xx (Successful) response to a CONNECT request
                return (statusCode / 100 != 2);
            }

            // Any response with a 1xx (Informational), 204 (No Content), or 304 (Not Modified) status code
            return statusCode >= HttpStatus.SC_OK
                    && statusCode != HttpStatus.SC_NO_CONTENT
                    && statusCode != HttpStatus.SC_NOT_MODIFIED
                    && statusCode != HttpStatus.SC_RESET_CONTENT;
        }

        private static boolean disableChunking(MessageContext msgContext) {

            if (msgContext.isPropertyTrue(BridgeConstants.FORCE_HTTP_CONTENT_LENGTH)) {
                return true;
            } else {
                String disableChunking = (String) msgContext.getProperty(BridgeConstants.DISABLE_CHUNKING);
                return Constants.VALUE_TRUE.equals(disableChunking)
                        || Constants.VALUE_TRUE.equals(msgContext.getProperty(BridgeConstants.FORCE_HTTP_1_0));
            }
        }

        private static String determineHttpVersion(MessageContext msgContext) {

            if (msgContext.isPropertyTrue(BridgeConstants.FORCE_HTTP_1_0)) {
                return BridgeConstants.HTTP_1_0_VERSION;
            } else if (BridgeConstants.HTTP_2_0_VERSION.equals(((HttpCarbonMessage) msgContext.getProperty(BridgeConstants.HTTP_CARBON_MESSAGE)).getHttpVersion())) {
                return BridgeConstants.HTTP_2_0_VERSION;
            }
            return BridgeConstants.HTTP_1_1_VERSION;
        }

        /**
         * Determine the Http Status Code depending on the message type processed <br>
         * (normal response versus fault response) as well as Axis2 message context properties set
         * via Synapse configuration or MessageBuilders.
         *
         * @param msgContext the Axis2 message context
         * @return the HTTP status code to set in the HTTP response object
         * @see BridgeConstants#SC_ACCEPTED
         * @see BridgeConstants#ERROR_CODE
         */
        private static int determineHttpStatusCode(MessageContext msgContext) {

            int httpStatus = HttpStatus.SC_OK;

            Integer errorCode = (Integer) msgContext.getProperty(BridgeConstants.ERROR_CODE);
            if (errorCode != null) {
                return HttpStatus.SC_BAD_GATEWAY;
            }

            // if this is a dummy message to handle http 202 case with non-blocking IO
            // set the status code to 202
            if (msgContext.isPropertyTrue(BridgeConstants.SC_ACCEPTED)) {
                return HttpStatus.SC_ACCEPTED;
            } else {
                Object statusCode = msgContext.getProperty(BridgeConstants.HTTP_SC);
                if (statusCode != null) {
                    try {
                        httpStatus = Integer.parseInt(statusCode.toString());
                        return httpStatus;
                    } catch (NumberFormatException e) {
                        LOG.warn("Unable to set the HTTP status code from the property "
                                + BridgeConstants.HTTP_SC + " with value: " + statusCode);
                    }
                }

                // Is this a fault message?
                boolean handleFault = HttpUtils.isFaultMessage(msgContext);
                boolean faultsAsHttp200 = HttpUtils.sendFaultAsHTTP200(msgContext);

                // Set HTTP status code to 500 if this is a fault case and we shall not use HTTP 200
                if (handleFault && !faultsAsHttp200) {
                    httpStatus = HttpStatus.SC_INTERNAL_SERVER_ERROR;
                }
            }
            return httpStatus;
        }

        /**
         * Determine the Http Status Message depending on the message type processed <br>
         * (normal response versus fault response) as well as Axis2 message context properties set
         * via Synapse configuration or MessageBuilders.
         *
         * @param msgContext the Axis2 message context
         * @return the HTTP status message string or null
         * @see BridgeConstants#HTTP_SC_DESC
         * @see BridgeConstants#HTTP_STATUS_CODE_SENT_FROM_BACKEND
         * @see BridgeConstants#HTTP_REASON_PHRASE_SENT_FROM_BACKEND
         */
        private static String determineResponseReasonPhrase(MessageContext msgContext, int statusCode) {

            String statusLine = null;
            Object statusLineProperty = msgContext.getProperty(BridgeConstants.HTTP_SC_DESC);
            if (statusLineProperty != null) {
                statusLine = (String) statusLineProperty;
            }

            Object httpReasonPhraseFromBackend =
                    msgContext.getProperty(BridgeConstants.HTTP_REASON_PHRASE_SENT_FROM_BACKEND);
            Object httpStatusCodeFromBackend = msgContext.getProperty(BridgeConstants.HTTP_STATUS_CODE_SENT_FROM_BACKEND);

            if (Objects.isNull(httpStatusCodeFromBackend) || Objects.isNull(httpReasonPhraseFromBackend)) {
                return statusLine;
            }

            if (statusCode != (Integer) httpStatusCodeFromBackend && httpReasonPhraseFromBackend.equals(statusLine)) {
                // make the statusLine null so that the proper status code will be by the Netty server.
                statusLine = null;
            }
            return statusLine;
        }

        private static void handleETAGCaching(HttpCarbonMessage inboundRequestMsg, HttpCarbonMessage outboundResponseMsg,
                                              MessageContext msgCtx) throws AxisFault {

            if (isEtagEnabled(msgCtx)) {
                try {
                    MessageUtils.buildMessage(msgCtx);
                } catch (IOException e) {
                    RequestResponseUtils.handleException("IO Error occurred while building the message", e);
                }
                String hash = CachingConstants.DEFAULT_XML_IDENTIFIER.getDigest(msgCtx);
                outboundResponseMsg.setHeader(BridgeConstants.ETAG_HEADER, "\"" + hash + "\"");
            }

            if (CacheUtils.isValidCachedResponse(outboundResponseMsg, inboundRequestMsg)) {
                // A 304 Not Modified message is an HTTP response status code indicating that the requested resource
                // has not been modified since the previous transmission, so there is no need to retransmit the
                // requested resource to the client. In effect, a 304 Not Modified response code acts as an
                // implicit redirection to a cached version of the requested resource.
                outboundResponseMsg.setHttpStatusCode(HttpResponseStatus.NOT_MODIFIED.code());
                outboundResponseMsg.setProperty(org.wso2.transport.http.netty.contract.Constants.HTTP_REASON_PHRASE,
                        HttpResponseStatus.NOT_MODIFIED.reasonPhrase());
                setHttpVersion(outboundResponseMsg, msgCtx);
                outboundResponseMsg.removeHeader(HttpHeaderNames.CONTENT_LENGTH.toString());
                outboundResponseMsg.removeHeader(HttpHeaderNames.CONTENT_TYPE.toString());
                msgCtx.setProperty(BridgeConstants.VALID_CACHED_RESPONSE, true);
            }
        }

        /**
         * Writes the response headers and the response body to the client.
         *
         * @param msgCtx              axis2 message context
         * @param inboundRequestMsg   inbound request carbon message
         * @param outboundResponseMsg outbound response carbon message
         * @throws AxisFault if something goes wrong when sending out the response
         */
        public static void sendResponse(MessageContext msgCtx, HttpCarbonMessage inboundRequestMsg,
                                        HttpCarbonMessage outboundResponseMsg) throws AxisFault {

            HttpUtils.sendOutboundResponse(inboundRequestMsg, outboundResponseMsg);
            serializeData(msgCtx, outboundResponseMsg);
        }

        /**
         * Send the server push promise to the client.
         *
         * @param http2PushPromise Http2PushPromise
         * @param clientRequest    HttpCarbonMessage
         * @throws AxisFault if error occurred while pushing responses.
         */
        public static void pushPromise(Http2PushPromise http2PushPromise, HttpCarbonMessage clientRequest) throws AxisFault {

            HttpUtils.pushPromise(http2PushPromise, clientRequest);
        }

        /**
         * Send the server pushes to the client.
         *
         * @param msgCtx           axis2 message context
         * @param http2PushPromise Http2PushPromise
         * @param outboundPushMsg  HttpCarbonMessage
         * @param clientRequest    HttpCarbonMessage
         * @throws AxisFault if error occurred while pushing responses.
         */
        public static void pushResponse(MessageContext msgCtx, Http2PushPromise http2PushPromise,
                                        HttpCarbonMessage outboundPushMsg, HttpCarbonMessage clientRequest) throws AxisFault {

            HttpUtils.pushResponse(http2PushPromise, outboundPushMsg, clientRequest);
            serializeData(msgCtx, outboundPushMsg);
        }

        private static void serializeData(MessageContext msgCtx, HttpCarbonMessage responseMsg)
                throws AxisFault {

            if (hasNoResponseBodyToSend(msgCtx)) {
                OutputStream messageOutputStream = HttpUtils.getHttpMessageDataStreamer(responseMsg).getOutputStream();
                HttpUtils.writeEmptyBody(messageOutputStream);
            } else {
                if (RequestResponseUtils.shouldInvokeFormatterToWriteBody(msgCtx)) {
                    OutputStream messageOutputStream = HttpUtils.getHttpMessageDataStreamer(responseMsg).getOutputStream();
                    MessageFormatter messageFormatter = MessageUtils.getMessageFormatter(msgCtx);
                    HttpUtils.serializeDataUsingMessageFormatter(msgCtx, messageFormatter, messageOutputStream);
                } else {
                    HttpCarbonMessage inboundCarbonMessage =
                            (HttpCarbonMessage) msgCtx.getProperty(BridgeConstants.HTTP_CARBON_MESSAGE);
                    HttpUtils.copyContentFromInboundHttpCarbonMessage(inboundCarbonMessage, responseMsg);
                }
            }
        }

        private static boolean hasNoResponseBodyToSend(MessageContext msgCtx) {

            return msgCtx.isPropertyTrue(BridgeConstants.NO_ENTITY_BODY)
                    || msgCtx.isPropertyTrue(BridgeConstants.VALID_CACHED_RESPONSE);
        }

        private static boolean isEtagEnabled(MessageContext msgCtx) {

            return msgCtx.isPropertyTrue(BridgeConstants.HTTP_ETAG_ENABLED);
        }

        private static boolean isMTOMEnabled(MessageContext msgCtx) {

            return Objects.nonNull(msgCtx.getProperty(org.apache.axis2.Constants.Configuration.ENABLE_MTOM));
        }

        private static boolean isValidCacheResponse(MessageContext msgCtx) {

            return msgCtx.isPropertyTrue(BridgeConstants.VALID_CACHED_RESPONSE);
        }
    }