// 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 com.cloud.consoleproxy;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class ConsoleProxyResourceHandler implements HttpHandler {
    protected Logger logger = LogManager.getLogger(getClass());

    static Map<String, String> s_mimeTypes;
    static {
        s_mimeTypes = new HashMap<String, String>();
        s_mimeTypes.put("jar", "application/java-archive");
        s_mimeTypes.put("js", "text/javascript");
        s_mimeTypes.put("css", "text/css");
        s_mimeTypes.put("jpg", "image/jpeg");
        s_mimeTypes.put("html", "text/html");
        s_mimeTypes.put("htm", "text/html");
        s_mimeTypes.put("log", "text/plain");
    }

    static Map<String, String> s_validResourceFolders;
    static {
        s_validResourceFolders = new HashMap<String, String>();
        s_validResourceFolders.put("applet", "");
        s_validResourceFolders.put("logs", "");
        s_validResourceFolders.put("images", "");
        s_validResourceFolders.put("js", "");
        s_validResourceFolders.put("css", "");
        s_validResourceFolders.put("html", "");
        s_validResourceFolders.put("noVNC", "");
    }

    public ConsoleProxyResourceHandler() {
    }

    @Override
    public void handle(HttpExchange t) throws IOException {
        try {
            if (logger.isDebugEnabled())
                logger.debug("Resource Handler " + t.getRequestURI());

            long startTick = System.currentTimeMillis();

            doHandle(t);

            if (logger.isDebugEnabled())
                logger.debug(t.getRequestURI() + " Process time " + (System.currentTimeMillis() - startTick) + " ms");
        } catch (IOException e) {
            throw e;
        } catch (Throwable e) {
            logger.error("Unexpected exception, ", e);
            t.sendResponseHeaders(500, -1);     // server error
        } finally {
            t.close();
        }
    }

    @SuppressWarnings("deprecation")
    private void doHandle(HttpExchange t) throws Exception {
        String path = t.getRequestURI().getPath();

        if (logger.isInfoEnabled())
            logger.info("Get resource request for " + path);

        int i = path.indexOf("/", 1);
        String filepath = path.substring(i + 1);
        i = path.lastIndexOf(".");
        String extension = (i == -1) ? "" : path.substring(i + 1);
        String contentType = getContentType(extension);

        if (!validatePath(filepath)) {
            if (logger.isInfoEnabled())
                logger.info("Resource access is forbidden, uri: " + path);

            t.sendResponseHeaders(403, -1);     // forbidden
            return;
        }

        File f = new File("./" + filepath);
        if (f.exists()) {
            long lastModified = f.lastModified();
            String ifModifiedSince = t.getRequestHeaders().getFirst("If-Modified-Since");
            if (ifModifiedSince != null) {
                long d = Date.parse(ifModifiedSince);
                if (d + 1000 >= lastModified) {
                    Headers hds = t.getResponseHeaders();
                    hds.set("content-type", contentType);
                    t.sendResponseHeaders(304, -1);

                    if (logger.isInfoEnabled())
                        logger.info("Sent 304 file has not been " + "modified since " + ifModifiedSince);
                    return;
                }
            }

            long length = f.length();
            Headers hds = t.getResponseHeaders();
            hds.set("content-type", contentType);
            hds.set("last-modified", new Date(lastModified).toGMTString());
            t.sendResponseHeaders(200, length);
            responseFileContent(t, f);

            if (logger.isInfoEnabled())
                logger.info("Sent file " + path + " with content type " + contentType);
        } else {
            if (logger.isInfoEnabled())
                logger.info("file does not exist" + path);
            t.sendResponseHeaders(404, -1);
        }
    }

    private static String getContentType(String extension) {
        String key = extension.toLowerCase();
        if (s_mimeTypes.containsKey(key)) {
            return s_mimeTypes.get(key);
        }
        return "application/octet-stream";
    }

    private static void responseFileContent(HttpExchange t, File f) throws Exception {
        try(OutputStream os = t.getResponseBody();
        FileInputStream fis = new FileInputStream(f);) {
            while (true) {
                byte[] b = new byte[8192];
                int n = fis.read(b);
                if (n < 0) {
                    break;
                }
                os.write(b, 0, n);
            }
        }
    }

    private boolean validatePath(String path) {
        int i = path.indexOf("/");
        if (i == -1) {
            if (logger.isInfoEnabled())
                logger.info("Invalid resource path: can not start at resource root");
            return false;
        }

        if (path.contains("..")) {
            if (logger.isInfoEnabled())
                logger.info("Invalid resource path: contains relative up-level navigation");

            return false;
        }

        return isValidResourceFolder(path.substring(0, i));
    }

    private static boolean isValidResourceFolder(String name) {
        return s_validResourceFolders.containsKey(name);
    }
}
