# ProxyConfig.java

    /*
     * Copyright (c) 2015, 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.http.conn;

    import java.util.Collections;
    import java.util.LinkedHashSet;
    import java.util.Locale;
    import java.util.Set;
    import java.util.Map;
    import java.util.HashMap;
    import java.util.HashSet;

    import org.apache.axis2.AxisFault;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.http.HttpHost;
    import org.apache.http.auth.MalformedChallengeException;
    import org.apache.http.auth.UsernamePasswordCredentials;

    public class ProxyConfig {

        private static final Log log = LogFactory.getLog(ProxyConfig.class);
        private final HttpHost proxy;
        private final UsernamePasswordCredentials creds;
        private final Set<String> proxyBypass;

        // The set of known hosts to bypass proxy
        private Set<String> knownDirectHosts = new HashSet<String>();

         // The set of known hosts to go via proxy
        private Set<String> knownProxyHosts = new HashSet<String>();

        // Map to hold the known proxy profile configuration
        private Map<String, ProxyProfileConfig> knownProxyConfigMap = new HashMap<String,ProxyProfileConfig>();

        // Map to hold the custom proxy profile details
        private Map<String, ProxyProfileConfig> proxyProfileMap = new HashMap<String, ProxyProfileConfig>();

        public ProxyConfig(
                final HttpHost proxy,
                final UsernamePasswordCredentials creds,
                final String[] proxyBypass,
                final Map<String, ProxyProfileConfig> proxyProfileMap) {
            super();
            this.proxy = proxy;
            this.creds = creds;
            if (proxyBypass != null) {
                this.proxyBypass = new LinkedHashSet<String>(proxyBypass.length);
                for (String s: proxyBypass) {
                    this.proxyBypass.add(s.trim().toLowerCase(Locale.US));
                }
            } else {
                this.proxyBypass = Collections.<String>emptySet();
            }

            if (proxyProfileMap != null) {
                this.proxyProfileMap = proxyProfileMap;
            } else {
                this.proxyProfileMap = Collections.emptyMap();
            }
        }

        public HttpHost getProxy() {
            return proxy;
        }

        public UsernamePasswordCredentials getCreds() {
            return creds;
        }

        public Set<String> getProxyBypass() {
            return proxyBypass;
        }

        /**
         * Selects the configured proxy server
         * @param target request endpoint
         * @return proxy host based on the proxy profile or http.proxyHost,
         *         null when no proxy is configured or if the target is matched with proxy bypass
         */
        public HttpHost selectProxy(final HttpHost target) {
            if (isProxyProfileConfigured()) {
                return getProxyForTargetHost(target.getHostName());
            }

            if (this.proxy != null) {
                if (knownProxyHosts.contains(target.getHostName().toLowerCase(Locale.US))) {
                    return this.proxy;
                } else if (knownDirectHosts.contains(target.getHostName().toLowerCase(Locale.US))) {
                    return null;
                } else {
                    // we are encountering this host for the first time
                    if (isBypass(target.getHostName().toLowerCase(Locale.US))) {
                        return null;
                    } else {
                        return this.proxy;
                    }
                }
            }
            return this.proxy;
        }

        /**
         * checks weather the proxy profile map is empty
         *
         * @return true if proxy profile map is not empty, false otherwise
         */
        public boolean isProxyProfileConfigured() {
            return !this.proxyProfileMap.isEmpty();
        }

        /**
         * select the appropriate proxy for the given targetHost
         *
         * @param targetHost targeted end point
         * @return proxy mapped for the end point, if not returns null
         */
        private HttpHost getProxyForTargetHost(String targetHost) {
            HttpHost proxy = null;

            ProxyProfileConfig proxyProfileForTargetHost = getProxyProfileForTargetHost(targetHost);
            if (proxyProfileForTargetHost != null) {
                proxy = proxyProfileForTargetHost.getProxy();
            }

            return proxy;
        }

        /**
         * Selects the appropriate proxyProfileConfiguration for the given targetHost
         *
         * First, checks in the knownProxyConfigMap and returns the proxyProfile. If the profile is not in the
         * knowProxyConfigMap checks the knowDirectHosts and returns null (since the targetHost is not associated with
         * any proxy).
         *
         * If the request hits the ESB for the first time, checks whether the default profile is configured. If so, a flag
         * is set true. Then the targetHost is matched against the proxyProfileMaps key set
         * i.e check any of the key patten is matching with the targetHost. If the targetHost is matched against a key then
         * calls getProxyProfileConfig(String, String) and returns the proxyProfileConfig.
         *
         * If the targetHost is not matched against the proxyProfileMap key set and default profile flag is set true
         * then calls getProxyProfileConfig(String, String) and returns the defaultProfile.
         *
         * @param targetHost request's targeted host
         * @return ProxyProfileConfig for the given targetHost
         */
        private ProxyProfileConfig getProxyProfileForTargetHost(String targetHost) {
            if (knownProxyConfigMap.containsKey(targetHost)) {
                return knownProxyConfigMap.get(targetHost);
            }

            if (knownDirectHosts.contains(targetHost)) {
                return null;
            }

            boolean defaultProfile = false;
            for (String key : proxyProfileMap.keySet()) {
                if ("*".equals(key)) {
                    log.debug("Default proxy profile found");
                    defaultProfile = true;
                    continue;
                }
                if (targetHost.matches(key)) {
                    return getProxyProfileConfig(targetHost, key);
                }
            }

            if (defaultProfile) {
                return getProxyProfileConfig(targetHost, "*");
            }

            return null;
        }

        /**
         * Selects the proxyProfile for the given key and gets the bypass set. Matches the targetHost against the
         * bypass set. If it is matched then adds the targetHost to the knownDirectHosts List and returns null.
         * Otherwise (i.e the targetHost is not matched in the bypass proxy) puts the proxyProfile against the targetHost
         * into the knownProxyConfigMap and returns the proxyProfileConfig
         *
         * @param targetHost request's targeted host
         * @param key proxyProfileMap's key, if default profile then the key is "*"
         * @return proxyProfileConfig
         */
        private ProxyProfileConfig getProxyProfileConfig(String targetHost, String key) {
            ProxyProfileConfig proxyProfileConfig = proxyProfileMap.get(key);
            Set<String> proxyByPass = proxyProfileConfig.getProxyByPass();
            for (String bypass : proxyByPass) {
                if (targetHost.matches(bypass)) {
                    knownDirectHosts.add(targetHost);
                    return null;
                }
            }
            knownProxyConfigMap.put(targetHost, proxyProfileConfig);
            return proxyProfileConfig;
        }

        /**
         * select the proxy credential for the targetHost
         *
         * @param targetHost targeted host
         * @return proxy credential for the given end point, if not returns null
         */
        public UsernamePasswordCredentials getCredentialsForTargetHost(String targetHost) {
            UsernamePasswordCredentials credentials = null;
            ProxyProfileConfig proxyProfileForTargetHost = getProxyProfileForTargetHost(targetHost);

            if (proxyProfileForTargetHost != null) {
                credentials = proxyProfileForTargetHost.getCredentials();
            }

            return credentials;
        }

        /**
         * returns appropriate log message based on the proxy configuration
         * whether loading proxy profile or single proxy server or no proxy configured
         *
         * @return log message
         */
        public String logProxyConfig() {
            if (isProxyProfileConfigured()) {
                return "HTTP Sender using proxy profile";
            }

            if (this.proxy != null) {
                return "HTTP Sender using Proxy " + getProxy() + " and  bypassing " + getProxyBypass();
            } else {
                return "No proxy configuration found";
            }
        }

        /**
         * checks whether proxy configured (either proxy profile or single server)
         *
         * @return true when either proxy profile is configure or default proxy server is configure, false otherwise
         */
        private boolean isProxyConfigured() {
            return proxy != null || isProxyProfileConfigured();
        }

        /**
         * checks the proxy configuration and profile configuration whether proxy is configured with credential
         *
         * @return true when at least one proxy has configured with credential, false otherwise
         */
        private boolean isProxyHasCredential() {
            if (!isProxyConfigured()) {
                return false;
            }

            if (!isProxyProfileConfigured()) {
                return getCreds() != null;
            }

            for (Map.Entry<String, ProxyProfileConfig> proxyProfile : this.proxyProfileMap.entrySet()) {
                if (proxyProfile.getValue().getCredentials() != null) {
                    return true;
                }
            }
            return false;
        }

        /**
         * returns DefaultProxyAuthenticator if single proxy server is configured
         * ProfileProxyAuthenticator if proxy profile is configured
         *
         * @return ProxyAuthenticator, if proxy is not configured null
         * @throws AxisFault
         */
        public ProxyAuthenticator createProxyAuthenticator() throws AxisFault {
            if (!isProxyHasCredential()) {
                return null;
            }

            ProxyAuthenticator proxyAuthenticator;

            try {
                if (isProxyProfileConfigured()) {
                    proxyAuthenticator = new ProfileProxyAuthenticator(this);
                } else {
                    proxyAuthenticator = new DefaultProxyAuthenticator(getCreds());
                }

            } catch (MalformedChallengeException e) {
                throw new AxisFault("Error while creating proxy authenticator", e);
            }

            return proxyAuthenticator;
        }

        @Override
        public String toString() {
            return "[proxy=" + proxy + ", proxyCredential=" + creds + ", proxyBypass=" + proxyBypass +
                    ", proxyProfileMap=" + proxyProfileMap + "]";
        }

        private boolean isBypass(String hostName) {
            for (String entry : this.proxyBypass) {
                if (hostName.matches(entry)) {
                    knownDirectHosts.add(hostName);
                    return true;
                }
            }
            knownProxyHosts.add(hostName);
            return false;
        }
    }