/*
 * 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
 *
 *      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.catalina.valves.rewrite;

import java.io.IOException;
import java.nio.charset.Charset;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.TimeUnit;

import org.apache.catalina.WebResource;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.connector.Request;
import org.apache.tomcat.util.http.FastHttpDateFormat;
import org.apache.tomcat.util.net.SSLSupport;
import org.apache.tomcat.util.net.jsse.PEMFile;
import org.apache.tomcat.util.net.openssl.ciphers.Cipher;
import org.apache.tomcat.util.net.openssl.ciphers.EncryptionLevel;
import org.apache.tomcat.util.net.openssl.ciphers.OpenSSLCipherConfigurationParser;

public class ResolverImpl extends Resolver {

    protected Request request = null;

    public ResolverImpl(Request request) {
        this.request = request;
    }

    /**
     * The following are not implemented:
     * <ul>
     * <li>SERVER_ADMIN</li>
     * <li>API_VERSION</li>
     * <li>IS_SUBREQ</li>
     * </ul>
     */
    @Override
    public String resolve(String key) {
        if (key.equals("HTTP_USER_AGENT")) {
            return request.getHeader("user-agent");
        } else if (key.equals("HTTP_REFERER")) {
            return request.getHeader("referer");
        } else if (key.equals("HTTP_COOKIE")) {
            return request.getHeader("cookie");
        } else if (key.equals("HTTP_FORWARDED")) {
            return request.getHeader("forwarded");
        } else if (key.equals("HTTP_HOST")) {
            // Don't look directly at the host header to handle:
            // - Host name in HTTP/1.1 request line
            // - HTTP/0.9 & HTTP/1.0 requests
            // - HTTP/2 :authority pseudo header
            return request.getServerName();
        } else if (key.equals("HTTP_PROXY_CONNECTION")) {
            return request.getHeader("proxy-connection");
        } else if (key.equals("HTTP_ACCEPT")) {
            return request.getHeader("accept");
        } else if (key.equals("REMOTE_ADDR")) {
            return request.getRemoteAddr();
        } else if (key.equals("REMOTE_HOST")) {
            return request.getRemoteHost();
        } else if (key.equals("REMOTE_PORT")) {
            return String.valueOf(request.getRemotePort());
        } else if (key.equals("REMOTE_USER")) {
            return request.getRemoteUser();
        } else if (key.equals("REMOTE_IDENT")) {
            return request.getRemoteUser();
        } else if (key.equals("REQUEST_METHOD")) {
            return request.getMethod();
        } else if (key.equals("SCRIPT_FILENAME")) {
            return request.getServletContext().getRealPath(request.getServletPath());
        } else if (key.equals("REQUEST_PATH")) {
            return request.getRequestPathMB().toString();
        } else if (key.equals("CONTEXT_PATH")) {
            return request.getContextPath();
        } else if (key.equals("SERVLET_PATH")) {
            return emptyStringIfNull(request.getServletPath());
        } else if (key.equals("PATH_INFO")) {
            return emptyStringIfNull(request.getPathInfo());
        } else if (key.equals("QUERY_STRING")) {
            return emptyStringIfNull(request.getQueryString());
        } else if (key.equals("AUTH_TYPE")) {
            return request.getAuthType();
        } else if (key.equals("DOCUMENT_ROOT")) {
            return request.getServletContext().getRealPath("/");
        } else if (key.equals("SERVER_NAME")) {
            return request.getLocalName();
        } else if (key.equals("SERVER_ADDR")) {
            return request.getLocalAddr();
        } else if (key.equals("SERVER_PORT")) {
            return String.valueOf(request.getLocalPort());
        } else if (key.equals("SERVER_PROTOCOL")) {
            return request.getProtocol();
        } else if (key.equals("SERVER_SOFTWARE")) {
            return "tomcat";
        } else if (key.equals("THE_REQUEST")) {
            return request.getMethod() + " " + request.getRequestURI() + " " + request.getProtocol();
        } else if (key.equals("REQUEST_URI")) {
            return request.getRequestURI();
        } else if (key.equals("REQUEST_FILENAME")) {
            return request.getPathTranslated();
        } else if (key.equals("HTTPS")) {
            return request.isSecure() ? "on" : "off";
        } else if (key.equals("TIME_YEAR")) {
            return String.valueOf(Calendar.getInstance().get(Calendar.YEAR));
        } else if (key.equals("TIME_MON")) {
            return String.valueOf(Calendar.getInstance().get(Calendar.MONTH));
        } else if (key.equals("TIME_DAY")) {
            return String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_MONTH));
        } else if (key.equals("TIME_HOUR")) {
            return String.valueOf(Calendar.getInstance().get(Calendar.HOUR_OF_DAY));
        } else if (key.equals("TIME_MIN")) {
            return String.valueOf(Calendar.getInstance().get(Calendar.MINUTE));
        } else if (key.equals("TIME_SEC")) {
            return String.valueOf(Calendar.getInstance().get(Calendar.SECOND));
        } else if (key.equals("TIME_WDAY")) {
            return String.valueOf(Calendar.getInstance().get(Calendar.DAY_OF_WEEK));
        } else if (key.equals("TIME")) {
            return FastHttpDateFormat.getCurrentDate();
        }
        return null;
    }

    @Override
    public String resolveEnv(String key) {
        Object result = request.getAttribute(key);
        return (result != null) ? result.toString() : System.getProperty(key);
    }

    @Override
    public String resolveSsl(String key) {
        SSLSupport sslSupport = (SSLSupport) request.getAttribute(SSLSupport.SESSION_MGR);
        try {
            // SSL_SRP_USER: no planned support for SRP
            // SSL_SRP_USERINFO: no planned support for SRP
            if (key.equals("HTTPS")) {
                return String.valueOf(sslSupport != null);
            } else if (key.equals("SSL_PROTOCOL")) {
                return sslSupport.getProtocol();
            } else if (key.equals("SSL_SESSION_ID")) {
                return sslSupport.getSessionId();
            } else if (key.equals("SSL_SESSION_RESUMED")) {
                // FIXME session resumption state, not available anywhere
            } else if (key.equals("SSL_SECURE_RENEG")) {
                // FIXME available from SSLHostConfig
            } else if (key.equals("SSL_COMPRESS_METHOD")) {
                // FIXME available from SSLHostConfig
            } else if (key.equals("SSL_TLS_SNI")) {
                // FIXME from handshake SNI processing
            } else if (key.equals("SSL_CIPHER")) {
                return sslSupport.getCipherSuite();
            } else if (key.equals("SSL_CIPHER_EXPORT")) {
                String cipherSuite = sslSupport.getCipherSuite();
                if (cipherSuite != null) {
                    Set<Cipher> cipherList = OpenSSLCipherConfigurationParser.parse(cipherSuite);
                    if (cipherList.size() == 1) {
                        Cipher cipher = cipherList.iterator().next();
                        if (cipher.getLevel().equals(EncryptionLevel.EXP40) ||
                                cipher.getLevel().equals(EncryptionLevel.EXP56)) {
                            return "true";
                        } else {
                            return "false";
                        }
                    }
                }
            } else if (key.equals("SSL_CIPHER_ALGKEYSIZE")) {
                String cipherSuite = sslSupport.getCipherSuite();
                if (cipherSuite != null) {
                    Set<Cipher> cipherList = OpenSSLCipherConfigurationParser.parse(cipherSuite);
                    if (cipherList.size() == 1) {
                        Cipher cipher = cipherList.iterator().next();
                        return String.valueOf(cipher.getAlg_bits());
                    }
                }
            } else if (key.equals("SSL_CIPHER_USEKEYSIZE")) {
                Integer keySize = sslSupport.getKeySize();
                return (keySize == null) ? null : sslSupport.getKeySize().toString();
            } else if (key.startsWith("SSL_CLIENT_")) {
                X509Certificate[] certificates = sslSupport.getPeerCertificateChain();
                if (certificates != null && certificates.length > 0) {
                    key = key.substring("SSL_CLIENT_".length());
                    String result = resolveSslCertificates(key, certificates);
                    if (result != null) {
                        return result;
                    } else if (key.startsWith("SAN_OTHER_msUPN_")) {
                        // Type otherName, which is 0
                        key = key.substring("SAN_OTHER_msUPN_".length());
                        // FIXME OID from resolveAlternateName
                    } else if (key.equals("CERT_RFC4523_CEA")) {
                        // FIXME return certificate[0] format CertificateExactAssertion in RFC4523
                    } else if (key.equals("VERIFY")) {
                        // FIXME return verification state, not available anywhere
                    }
                }
            } else if (key.startsWith("SSL_SERVER_")) {
                X509Certificate[] certificates = sslSupport.getLocalCertificateChain();
                if (certificates != null && certificates.length > 0) {
                    key = key.substring("SSL_SERVER_".length());
                    String result = resolveSslCertificates(key, certificates);
                    if (result != null) {
                        return result;
                    } else if (key.startsWith("SAN_OTHER_dnsSRV_")) {
                        // Type otherName, which is 0
                        key = key.substring("SAN_OTHER_dnsSRV_".length());
                        // FIXME OID from resolveAlternateName
                    }
                }
            }
        } catch (IOException e) {
            // TLS access error
        }
        return null;
    }

    private String resolveSslCertificates(String key, X509Certificate[] certificates) {
        if (key.equals("M_VERSION")) {
            return String.valueOf(certificates[0].getVersion());
        } else if (key.equals("M_SERIAL")) {
            return certificates[0].getSerialNumber().toString();
        } else if (key.equals("S_DN")) {
            return certificates[0].getSubjectX500Principal().toString();
        } else if (key.startsWith("S_DN_")) {
            key = key.substring("S_DN_".length());
            return resolveComponent(certificates[0].getSubjectX500Principal().getName(), key);
        } else if (key.startsWith("SAN_Email_")) {
            // Type rfc822Name, which is 1
            key = key.substring("SAN_Email_".length());
            return resolveAlternateName(certificates[0], 1, Integer.parseInt(key));
        } else if (key.startsWith("SAN_DNS_")) {
            // Type dNSName, which is 2
            key = key.substring("SAN_DNS_".length());
            return resolveAlternateName(certificates[0], 2, Integer.parseInt(key));
        } else if (key.equals("I_DN")) {
            return certificates[0].getIssuerX500Principal().getName();
        } else if (key.startsWith("I_DN_")) {
            key = key.substring("I_DN_".length());
            return resolveComponent(certificates[0].getIssuerX500Principal().toString(), key);
        } else if (key.equals("V_START")) {
            return String.valueOf(certificates[0].getNotBefore().getTime());
        } else if (key.equals("V_END")) {
            return String.valueOf(certificates[0].getNotAfter().getTime());
        } else if (key.equals("V_REMAIN")) {
            long remain = certificates[0].getNotAfter().getTime() - System.currentTimeMillis();
            if (remain < 0) {
                remain = 0L;
            }
            // Return remaining days
            return String.valueOf(TimeUnit.MILLISECONDS.toDays(remain));
        } else if (key.equals("A_SIG")) {
            return certificates[0].getSigAlgName();
        } else if (key.equals("A_KEY")) {
            return certificates[0].getPublicKey().getAlgorithm();
        } else if (key.equals("CERT")) {
            try {
                return PEMFile.toPEM(certificates[0]);
            } catch (CertificateEncodingException e) {
                // Ignore
            }
        } else if (key.startsWith("CERT_CHAIN_")) {
            key = key.substring("CERT_CHAIN_".length());
            try {
                return PEMFile.toPEM(certificates[Integer.parseInt(key)]);
            } catch (NumberFormatException | ArrayIndexOutOfBoundsException | CertificateEncodingException e) {
                // Ignore
            }
        }
        return null;
    }

    private String resolveComponent(String fullDN, String component) {
        HashMap<String,String> components = new HashMap<>();
        StringTokenizer tokenizer = new StringTokenizer(fullDN, ",");
        while (tokenizer.hasMoreElements()) {
            String token = tokenizer.nextToken().trim();
            int pos = token.indexOf('=');
            if (pos > 0 && (pos + 1) < token.length()) {
                components.put(token.substring(0, pos), token.substring(pos + 1));
            }
        }
        return components.get(component);
    }

    private String resolveAlternateName(X509Certificate certificate, int type, int n) {
        try {
            Collection<List<?>> alternateNames = certificate.getSubjectAlternativeNames();
            if (alternateNames != null) {
                List<String> elements = new ArrayList<>();
                for (List<?> alternateName : alternateNames) {
                    Integer alternateNameType = (Integer) alternateName.get(0);
                    if (alternateNameType.intValue() == type) {
                        elements.add(String.valueOf(alternateName.get(1)));
                    }
                }
                if (elements.size() > n) {
                    return elements.get(n);
                }
            }
        } catch (NumberFormatException | ArrayIndexOutOfBoundsException | CertificateParsingException e) {
            // Ignore
        }
        return null;
    }

    @Override
    public String resolveHttp(String key) {
        String header = request.getHeader(key);
        if (header == null) {
            return "";
        } else {
            return header;
        }
    }

    @Override
    public boolean resolveResource(int type, String name) {
        WebResourceRoot resources = request.getContext().getResources();
        WebResource resource = resources.getResource(name);
        if (!resource.exists()) {
            return false;
        } else {
            switch (type) {
                case 0:
                    return resource.isDirectory();
                case 1:
                    return resource.isFile();
                case 2:
                    return resource.isFile() && resource.getContentLength() > 0;
                default:
                    return false;
            }
        }
    }

    private static String emptyStringIfNull(String value) {
        if (value == null) {
            return "";
        } else {
            return value;
        }
    }

    @Override
    public Charset getUriCharset() {
        return request.getConnector().getURICharset();
    }
}
