# HttpWebSocketInboundEndpointHandler.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.api;

    import org.apache.axiom.om.OMElement;
    import org.apache.axis2.AxisFault;
    import org.apache.axis2.context.ConfigurationContext;
    import org.apache.axis2.description.Parameter;
    import org.apache.axis2.description.TransportInDescription;
    import org.apache.axis2.transport.TransportListener;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.synapse.transport.netty.BridgeConstants;
    import org.apache.synapse.transport.netty.api.config.HttpWebSocketInboundEndpointConfiguration;
    import org.apache.synapse.transport.netty.api.config.SSLConfiguration;
    import org.apache.synapse.transport.netty.listener.Axis2HttpSSLTransportListener;
    import org.apache.synapse.transport.netty.listener.Axis2HttpTransportListener;

    import java.io.IOException;
    import java.net.ServerSocket;
    import java.util.HashMap;
    import java.util.Objects;

    /**
     * API class to access Pass-Through (HTTP) and WebSocket Core Inbound Endpoint management classes.
     */
    public class HttpWebSocketInboundEndpointHandler {

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

        private static HashMap<Integer, TransportListener> transportListenerMap = new HashMap<>();

        /**
         * Default value for verification timeout to validate whether port is closed successfully.
         */
        private static final int DEFAULT_PORT_CLOSE_VERIFY_TIMEOUT = 10;

        /**
         * Start Endpoint Listener.
         *
         * @param configurationContext configuration context
         * @param httpWebSocketConfig  configuration that represents http and websocket inbound endpoint
         * @return whether the Endpoint started successfully or not
         */
        public static boolean startListener(ConfigurationContext configurationContext,
                                            HttpWebSocketInboundEndpointConfiguration httpWebSocketConfig) {

            Axis2HttpTransportListener transportListener = new Axis2HttpTransportListener();

            return startServer(transportListener, BridgeConstants.TRANSPORT_HTTPWS, httpWebSocketConfig,
                    configurationContext);
        }

        /**
         * Start SSL Endpoint Listener.
         *
         * @param configurationContext configuration context
         * @param httpWebSocketConfig  configuration that represents http and websocket inbound endpoint
         * @return whether the Endpoint started successfully or not
         */
        public static boolean startSSLListener(ConfigurationContext configurationContext,
                                               HttpWebSocketInboundEndpointConfiguration httpWebSocketConfig) {

            Axis2HttpTransportListener transportListener = new Axis2HttpSSLTransportListener();

            return startServer(transportListener, BridgeConstants.TRANSPORT_HTTPSWSS, httpWebSocketConfig,
                    configurationContext);
        }

        private static boolean startServer(Axis2HttpTransportListener transportListener, String transportName,
                                           HttpWebSocketInboundEndpointConfiguration httpWebSocketConfig,
                                           ConfigurationContext configurationContext) {

            TransportInDescription transportInDescription;
            try {
                // Generate the TransportInDescription and add it to the ConfigurationContext
                transportInDescription = createTransportInDescription(transportName, transportListener,
                        httpWebSocketConfig);
            } catch (AxisFault e) {
                logStartupError(e, "Error occurred while generating TransportInDescription. Hence, ",
                        httpWebSocketConfig, transportName);
                return false;
            }

            try {
                // Initialize the Axis2HttpTransportListener and set MessagingHandlers
                transportListener.init(configurationContext, transportInDescription);
                transportListener.setMessagingHandlers(httpWebSocketConfig.getInboundEndpointHandlers());
            } catch (AxisFault e) {
                logStartupError(e, "Error occurred while initializing the " + transportName
                        + " transport listener. Hence, ", httpWebSocketConfig, transportName);
                return false;
            }

            try {
                transportListener.start();
                // If the transport it started successfully, store it in the transportListenerMap against the port. This
                // map will be used when stopping the listener.
                transportListenerMap.put(httpWebSocketConfig.getPort(), transportListener);
            } catch (AxisFault e) {
                logStartupError(e, "Error occurred while generating TransportInDescription. Hence, ",
                        httpWebSocketConfig, transportName);
                return false;
            }
            return true;
        }

        private static void logStartupError(AxisFault e, String msg,
                                            HttpWebSocketInboundEndpointConfiguration httpWebSocketConfig,
                                            String transportName) {

            LOG.error(msg + " Could not start the " + transportName + " transport listener for endpoint : "
                    + httpWebSocketConfig.getEndpointName() + " on port " + httpWebSocketConfig.getPort(), e);
        }

        private static TransportInDescription createTransportInDescription(
                String transportName, TransportListener transportListener,
                HttpWebSocketInboundEndpointConfiguration httpWebSocketConfig)
                throws AxisFault {

            TransportInDescription transportInDescription = new TransportInDescription(transportName);
            transportInDescription.setReceiver(transportListener);

            // populate parameters
            addParameter(transportInDescription, BridgeConstants.PORT_PARAM, httpWebSocketConfig.getPort());
            addParameter(transportInDescription, BridgeConstants.HOSTNAME_PARAM, httpWebSocketConfig.getHostname());
            addParameter(transportInDescription, BridgeConstants.HTTP_PROTOCOL_VERSIONS_PARAM,
                    httpWebSocketConfig.getHttpProtocolVersion());

            SSLConfiguration sslConfiguration = httpWebSocketConfig.getSslConfiguration();
            // SSLConfiguration will be null for non-secured transport. Hence, need to do the null check here.
            if (Objects.nonNull(sslConfiguration)) {
                populateSSLParameters(sslConfiguration, transportInDescription);
            }

            return transportInDescription;
        }

        private static void populateSSLParameters(SSLConfiguration sslConfig, TransportInDescription transportIn)
                throws AxisFault {

            addParameter(transportIn, BridgeConstants.KEY_STORE, sslConfig.getKeyStoreElement());
            addParameter(transportIn, BridgeConstants.TRUST_STORE, sslConfig.getTrustStoreElement());
            addParameter(transportIn, BridgeConstants.SSL_VERIFY_CLIENT, sslConfig.getClientAuthElement());
            addParameter(transportIn, BridgeConstants.HTTPS_PROTOCOL, sslConfig.getHttpsProtocolElement());
            addParameter(transportIn, BridgeConstants.SSL_PROTOCOL, sslConfig.getSslProtocolElement());
            addParameter(transportIn, BridgeConstants.CLIENT_REVOCATION, sslConfig.getRevocationVerifierElement());
            addParameter(transportIn, BridgeConstants.PREFERRED_CIPHERS, sslConfig.getPreferredCiphersElement());
            addParameter(transportIn, BridgeConstants.SSL_SESSION_TIMEOUT, sslConfig.getSessionTimeoutElement());
            addParameter(transportIn, BridgeConstants.SSL_HANDSHAKE_TIMEOUT, sslConfig.getHandshakeTimeoutElement());
        }

        private static void addParameter(TransportInDescription transportInDescription, String name,
                                         OMElement parameterElement)
                throws AxisFault {

            Parameter parameter = new Parameter();
            parameter.setName(name);
            parameter.setParameterElement(parameterElement);
            transportInDescription.addParameter(parameter);
        }

        private static void addParameter(TransportInDescription transportInDescription, String name, Object value)
                throws AxisFault {

            Parameter parameter = new Parameter();
            parameter.setName(name);
            parameter.setValue(value);
            transportInDescription.addParameter(parameter);
        }

        /**
         * Close ListeningEndpoint running on the given port.
         *
         * @param port Port of  ListeningEndpoint to be closed
         * @return IS successfully closed
         */
        public static boolean closeEndpoint(int port) {

            LOG.info("Closing Endpoint Listener for port " + port);
            TransportListener listener = transportListenerMap.get(port);
            if (Objects.nonNull(listener)) {
                try {
                    listener.stop();
                } catch (AxisFault e) {
                    LOG.error("Cannot close Endpoint relevant to port " + port, e);
                    return false;
                } finally {
                    int portCloseVerifyTimeout = DEFAULT_PORT_CLOSE_VERIFY_TIMEOUT;
                    if (isPortCloseSuccess(port, portCloseVerifyTimeout)) {
                        LOG.info("Successfully closed Endpoint Listener for port " + port);
                    } else {
                        LOG.warn("Port close verify timeout " + portCloseVerifyTimeout + "s exceeded. "
                                + "Endpoint Listener for port " + port + " still bound to the ListenerEndpoint.");
                    }
                }
            }
            return true;
        }

        private static boolean isPortCloseSuccess(int port, int portCloseVerifyTimeout) {

            boolean portCloseSuccess = false;

            for (int i = 0; i < portCloseVerifyTimeout; i++) {
                try {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Verify port [" + port + "] close status. Attempt: " + i);
                    }

                    ServerSocket ss = new ServerSocket(port);
                    ss.close();
                    ss = null;
                    portCloseSuccess = true;
                    break;
                } catch (IOException e) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("The port " + port + " is not closed yet, verify again after waiting 1s", e);
                    }

                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException ex) {
                        //ignore
                    }
                }
            }

            return portCloseSuccess;
        }

    }