/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.kerby.has.server.web;

import org.apache.hadoop.HadoopIllegalArgumentException;
import org.apache.hadoop.http.HttpConfig;
import org.apache.hadoop.http.HttpServer2;
import org.apache.hadoop.net.NetUtils;
import org.apache.hadoop.security.SecurityUtil;
import org.apache.hadoop.security.authentication.server.AuthenticationFilter;
import org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler;
import org.apache.kerby.has.common.HasConfig;
import org.apache.kerby.has.common.HasException;
import org.apache.kerby.has.server.HasServer;
import org.apache.kerby.has.server.web.rest.AsRequestApi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContext;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;

public class WebServer {
    public static final Logger LOG = LoggerFactory.getLogger(WebServer.class);

    private HttpServer2 httpServer;
    private final HasConfig conf;

    private InetSocketAddress httpAddress;
    private InetSocketAddress httpsAddress;

    protected static final String HAS_SERVER_ATTRIBUTE_KEY = "hasserver";

    public WebServer(HasConfig conf) {
        this.conf = conf;
    }

    public HasConfig getConf() {
        return conf;
    }

    private void init() {

        final String pathSpec = "/has/v1/*";

        // add has packages
        httpServer.addJerseyResourcePackage(AsRequestApi.class
                .getPackage().getName(),
            pathSpec);
    }

    public void defineFilter() {
        String authType = conf.getString(WebConfigKey.HAS_AUTHENTICATION_FILTER_AUTH_TYPE);
        if (authType.equals("kerberos")) {
            // add authentication filter for webhdfs
            final String className = conf.getString(
                WebConfigKey.HAS_AUTHENTICATION_FILTER_KEY,
                WebConfigKey.HAS_AUTHENTICATION_FILTER_DEFAULT);

            final String name = className;

            Map<String, String> params = getAuthFilterParams(conf);

            String kadminPathSpec = "/has/v1/kadmin/*";
            String hadminPathSpec = "/has/v1/hadmin/*";
            HttpServer2.defineFilter(httpServer.getWebAppContext(), name, className,
                params, new String[]{kadminPathSpec, hadminPathSpec});
            HttpServer2.LOG.info("Added filter '" + name + "' (class=" + className
                + ")");
        }
    }

    public void defineConfFilter() {
        String confFilterName = ConfFilter.class.getName();
        String confPath = "/has/v1/conf/*";
        HttpServer2.defineFilter(httpServer.getWebAppContext(), confFilterName, confFilterName,
                getAuthFilterParams(conf), new String[]{confPath});
        HttpServer2.LOG.info("Added filter '" + confFilterName + "' (class=" + confFilterName
                + ")");
    }

    private Map<String, String> getAuthFilterParams(HasConfig conf) {
        Map<String, String> params = new HashMap<>();

        String authType = conf.getString(WebConfigKey.HAS_AUTHENTICATION_FILTER_AUTH_TYPE);
        if (authType != null && !authType.isEmpty()) {
            params.put(AuthenticationFilter.AUTH_TYPE, authType);
        }
        String principal = conf.getString(WebConfigKey.HAS_AUTHENTICATION_KERBEROS_PRINCIPAL_KEY);
        if (principal != null && !principal.isEmpty()) {
            try {
                principal = SecurityUtil.getServerPrincipal(principal,
                    getHttpsAddress().getHostName());
            } catch (IOException e) {
                LOG.warn("Errors occurred when get server principal. " + e.getMessage());
            }
            params.put(KerberosAuthenticationHandler.PRINCIPAL, principal);
        }
        String keytab = conf.getString(WebConfigKey.HAS_AUTHENTICATION_KERBEROS_KEYTAB_KEY);
        if (keytab != null && !keytab.isEmpty()) {
            params.put(KerberosAuthenticationHandler.KEYTAB, keytab);
        }
        String rule = conf.getString(WebConfigKey.HAS_AUTHENTICATION_KERBEROS_NAME_RULES);
        if (rule != null && !rule.isEmpty()) {
            params.put(KerberosAuthenticationHandler.NAME_RULES, rule);
        } else {
            params.put(KerberosAuthenticationHandler.NAME_RULES, "DEFAULT");
        }
        return params;
    }

    public InetSocketAddress getBindAddress() {
        if (httpAddress != null) {
            return httpAddress;
        } else if (httpsAddress != null) {
            return httpsAddress;
        } else {
            return null;
        }
    }

    /**
     * for information related to the different configuration options and
     * Http Policy is decided.
     *
     * @throws HasException HAS exception when starting web server
     */
    public void start() throws HasException {

        HttpConfig.Policy policy = getHttpPolicy(conf);

        final String bindHost =
            conf.getString(WebConfigKey.HAS_HTTPS_BIND_HOST_KEY);
        InetSocketAddress httpAddr = null;
        if (policy.isHttpEnabled()) {
            final String httpAddrString = conf.getString(
                WebConfigKey.HAS_HTTP_ADDRESS_KEY,
                WebConfigKey.HAS_HTTP_ADDRESS_DEFAULT);
            httpAddr = NetUtils.createSocketAddr(httpAddrString);
            if (bindHost != null && !bindHost.isEmpty()) {
                httpAddr = new InetSocketAddress(bindHost, httpAddr.getPort());
            }
            LOG.info("Get the http address: " + httpAddr);
        }

        InetSocketAddress httpsAddr = null;
        if (policy.isHttpsEnabled()) {
            final String httpsAddrString = conf.getString(
                WebConfigKey.HAS_HTTPS_ADDRESS_KEY,
                WebConfigKey.HAS_HTTPS_ADDRESS_DEFAULT);
            httpsAddr = NetUtils.createSocketAddr(httpsAddrString);

            if (bindHost != null && !bindHost.isEmpty()) {
                httpsAddr = new InetSocketAddress(bindHost, httpsAddr.getPort());
            }
            LOG.info("Get the https address: " + httpsAddr);
        }

        HttpServer2.Builder builder = httpServerTemplateForHAS(conf, httpAddr, httpsAddr, "has");

        try {
            httpServer = builder.build();
        } catch (IOException e) {
            throw new HasException("Errors occurred when building http server. " + e.getMessage());
        }

        init();

        try {
            httpServer.start();
        } catch (IOException e) {
            throw new HasException("Errors occurred when starting http server. " + e.getMessage());
        }
        int connIdx = 0;
        if (policy.isHttpEnabled()) {
            httpAddress = httpServer.getConnectorAddress(connIdx++);
            if (httpAddress != null) {
                conf.setString(WebConfigKey.HAS_HTTP_ADDRESS_KEY,
                    NetUtils.getHostPortString(httpAddress));
            }
        }

        if (policy.isHttpsEnabled()) {
            httpsAddress = httpServer.getConnectorAddress(connIdx);
            if (httpsAddress != null) {
                conf.setString(WebConfigKey.HAS_HTTPS_ADDRESS_KEY,
                    NetUtils.getHostPortString(httpsAddress));
            }
        }
    }

    public void setWebServerAttribute(HasServer hasServer) {
        httpServer.setAttribute(HAS_SERVER_ATTRIBUTE_KEY, hasServer);
    }

    public static HasServer getHasServerFromContext(ServletContext context) {
        return (HasServer) context.getAttribute(HAS_SERVER_ATTRIBUTE_KEY);
    }

    /**
     * Get http policy.
     *
     * @param conf the HAS config
     * @return HttpConfig.Policy the policy
     */
    public HttpConfig.Policy getHttpPolicy(HasConfig conf) {
        String policyStr = conf.getString(WebConfigKey.HAS_HTTP_POLICY_KEY,
            WebConfigKey.HAS_HTTP_POLICY_DEFAULT);
        HttpConfig.Policy policy = HttpConfig.Policy.fromString(policyStr);
        if (policy == null) {
            throw new HadoopIllegalArgumentException("Unrecognized value '"
                + policyStr + "' for " + WebConfigKey.HAS_HTTP_POLICY_KEY);
        }

        conf.setString(WebConfigKey.HAS_HTTP_POLICY_KEY, policy.name());
        return policy;
    }

    /**
     * Return a HttpServer.Builder that the HAS can use to
     * initialize their HTTP / HTTPS server.
     *
     * @param conf the HAS config
     * @param httpAddr the InetSocketAddress of http
     * @param httpsAddr the InetSocketAddress of https
     * @param name the host name
     * @return HttpServer2.Builder the builder
     * @throws HasException HAS exception
     */
    public HttpServer2.Builder httpServerTemplateForHAS(
        HasConfig conf, final InetSocketAddress httpAddr, final InetSocketAddress httpsAddr,
        String name) throws HasException {
        HttpConfig.Policy policy = getHttpPolicy(conf);

        HttpServer2.Builder builder = new HttpServer2.Builder().setName(name);

        if (policy.isHttpEnabled()) {
            if (httpAddr != null && httpAddr.getPort() == 0) {
                builder.setFindPort(true);
            }

            URI uri = URI.create("http://" + NetUtils.getHostPortString(httpAddr));
            builder.addEndpoint(uri);
            LOG.info("Starting Web-server for " + name + " at: " + uri);
        }

        if (policy.isHttpsEnabled() && httpsAddr != null) {
            HasConfig sslConf = loadSslConfiguration(conf);
            loadSslConfToHttpServerBuilder(builder, sslConf);

            if (httpsAddr.getPort() == 0) {
                builder.setFindPort(true);
            }

            URI uri = URI.create("https://" + NetUtils.getHostPortString(httpsAddr));
            builder.addEndpoint(uri);
            LOG.info("Starting Web-server for " + name + " at: " + uri);
        }

        return builder;
    }

    /**
     * Load HTTPS-related configuration.
     *
     * @param conf HAS config
     * @return HasConfig after loading ssl configuration
     * @throws HasException HAS exception when loading HTTPS related configuration
     */
    public HasConfig loadSslConfiguration(HasConfig conf) throws HasException {
        HasConfig sslConf = new HasConfig();

        String sslConfigString = conf.getString(
            WebConfigKey.HAS_SERVER_HTTPS_KEYSTORE_RESOURCE_KEY,
            WebConfigKey.HAS_SERVER_HTTPS_KEYSTORE_RESOURCE_DEFAULT);
        LOG.info("Get the ssl config file: " + sslConfigString);
        File sslConfig = new File(sslConfigString);
        if (!sslConfig.exists()) {
            throw new HasException("The ssl server config file "
                + sslConfigString + " does not exist.");
        }
        try {
            sslConf.addIniConfig(sslConfig);
        } catch (IOException e) {
            throw new HasException("Errors occurred when adding config. " + e.getMessage());
        }

        final String[] reqSslProps = {
            WebConfigKey.HAS_SERVER_HTTPS_TRUSTSTORE_LOCATION_KEY,
            WebConfigKey.HAS_SERVER_HTTPS_KEYSTORE_LOCATION_KEY,
            WebConfigKey.HAS_SERVER_HTTPS_KEYSTORE_PASSWORD_KEY,
            WebConfigKey.HAS_SERVER_HTTPS_KEYPASSWORD_KEY
        };

        // Check if the required properties are included
        for (String sslProp : reqSslProps) {
            if (sslConf.getString(sslProp) == null) {
                LOG.warn("SSL config " + sslProp + " is missing. If "
                    + WebConfigKey.HAS_SERVER_HTTPS_KEYSTORE_RESOURCE_KEY
                    + " is specified, make sure it is a relative path");
            }
        }

        boolean requireClientAuth = conf.getBoolean(WebConfigKey.HAS_CLIENT_HTTPS_NEED_AUTH_KEY,
            WebConfigKey.HAS_CLIENT_HTTPS_NEED_AUTH_DEFAULT);
        sslConf.setBoolean(WebConfigKey.HAS_CLIENT_HTTPS_NEED_AUTH_KEY, requireClientAuth);
        return sslConf;
    }

    public HttpServer2.Builder loadSslConfToHttpServerBuilder(HttpServer2.Builder builder,
                                                              HasConfig sslConf) {
        return builder
            .needsClientAuth(
                sslConf.getBoolean(WebConfigKey.HAS_CLIENT_HTTPS_NEED_AUTH_KEY,
                    WebConfigKey.HAS_CLIENT_HTTPS_NEED_AUTH_DEFAULT))
            .keyPassword(getPassword(sslConf, WebConfigKey.HAS_SERVER_HTTPS_KEYPASSWORD_KEY))
            .keyStore(sslConf.getString("ssl.server.keystore.location"),
                getPassword(sslConf, WebConfigKey.HAS_SERVER_HTTPS_KEYSTORE_PASSWORD_KEY),
                sslConf.getString("ssl.server.keystore.type", "jks"))
            .trustStore(sslConf.getString("ssl.server.truststore.location"),
                getPassword(sslConf, WebConfigKey.HAS_SERVER_HTTPS_TRUSTSTORE_PASSWORD_KEY),
                sslConf.getString("ssl.server.truststore.type", "jks"))
            .excludeCiphers(
                sslConf.getString("ssl.server.exclude.cipher.list"));
    }

    /**
     * Leverages the Configuration.getPassword method to attempt to get
     * passwords from the CredentialProvider API before falling back to
     * clear text in config - if falling back is allowed.
     *
     * @param conf  Configuration instance
     * @param alias name of the credential to retreive
     * @return String credential value or null
     */
    public String getPassword(HasConfig conf, String alias) {

        return conf.getString(alias);
    }

    public void stop() throws Exception {
        if (httpServer != null) {
            httpServer.stop();
        }
    }

    public InetSocketAddress getHttpAddress() {
        return httpAddress;
    }

    public InetSocketAddress getHttpsAddress() {
        return httpsAddress;
    }
}
