All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.h2.server.web.WebServer Maven / Gradle / Ivy

There is a newer version: 1.0.0-beta2
Show newest version
/*
 * Copyright 2004-2019 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (https://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.server.web;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.h2.engine.Constants;
import org.h2.engine.SysProperties;
import org.h2.message.DbException;
import org.h2.security.SHA256;
import org.h2.server.Service;
import org.h2.server.ShutdownHandler;
import org.h2.store.fs.FileUtils;
import org.h2.util.DateTimeUtils;
import org.h2.util.JdbcUtils;
import org.h2.util.MathUtils;
import org.h2.util.NetUtils;
import org.h2.util.NetworkConnectionInfo;
import org.h2.util.SortedProperties;
import org.h2.util.StringUtils;
import org.h2.util.Tool;
import org.h2.util.Utils;

/**
 * The web server is a simple standalone HTTP server that implements the H2
 * Console application. It is not optimized for performance.
 */
public class WebServer implements Service {

    static final String[][] LANGUAGES = {
        { "cs", "\u010ce\u0161tina" },
        { "de", "Deutsch" },
        { "en", "English" },
        { "es", "Espa\u00f1ol" },
        { "fr", "Fran\u00e7ais" },
        { "hi", "Hindi \u0939\u093f\u0902\u0926\u0940" },
        { "hu", "Magyar"},
        { "ko", "\ud55c\uad6d\uc5b4"},
        { "in", "Indonesia"},
        { "it", "Italiano"},
        { "ja", "\u65e5\u672c\u8a9e"},
        { "nl", "Nederlands"},
        { "pl", "Polski"},
        { "pt_BR", "Portugu\u00eas (Brasil)"},
        { "pt_PT", "Portugu\u00eas (Europeu)"},
        { "ru", "\u0440\u0443\u0441\u0441\u043a\u0438\u0439"},
        { "sk", "Slovensky"},
        { "tr", "T\u00fcrk\u00e7e"},
        { "uk", "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430"},
        { "zh_CN", "\u4e2d\u6587 (\u7b80\u4f53)"},
        { "zh_TW", "\u4e2d\u6587 (\u7e41\u9ad4)"},
    };

    private static final String COMMAND_HISTORY = "commandHistory";

    private static final String DEFAULT_LANGUAGE = "en";

    private static final String[] GENERIC = {
        "Generic JNDI Data Source|javax.naming.InitialContext|" +
                "java:comp/env/jdbc/Test|sa",
        "Generic Teradata|com.teradata.jdbc.TeraDriver|" +
                "jdbc:teradata://whomooz/|",
        "Generic Snowflake|com.snowflake.client.jdbc.SnowflakeDriver|" +
                "jdbc:snowflake://accountName.snowflakecomputing.com|",
        "Generic Redshift|com.amazon.redshift.jdbc42.Driver|" +
                "jdbc:redshift://endpoint:5439/database|",
        "Generic Impala|org.cloudera.impala.jdbc41.Driver|" +
                "jdbc:impala://clustername:21050/default|",
        "Generic Hive 2|org.apache.hive.jdbc.HiveDriver|" +
                "jdbc:hive2://clustername:10000/default|",
        "Generic Hive|org.apache.hadoop.hive.jdbc.HiveDriver|" +
                "jdbc:hive://clustername:10000/default|",
        "Generic Azure SQL|com.microsoft.sqlserver.jdbc.SQLServerDriver|" +
                "jdbc:sqlserver://name.database.windows.net:1433|",
        "Generic Firebird Server|org.firebirdsql.jdbc.FBDriver|" +
                "jdbc:firebirdsql:localhost:c:/temp/firebird/test|sysdba",
        "Generic SQLite|org.sqlite.JDBC|" +
                "jdbc:sqlite:test|sa",
        "Generic DB2|com.ibm.db2.jcc.DB2Driver|" +
                "jdbc:db2://localhost/test|" ,
        "Generic Oracle|oracle.jdbc.driver.OracleDriver|" +
                    "jdbc:oracle:thin:@localhost:1521:XE|sa" ,
        "Generic MS SQL Server 2000|com.microsoft.jdbc.sqlserver.SQLServerDriver|" +
                "jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=sqlexpress|sa",
        "Generic MS SQL Server 2005|com.microsoft.sqlserver.jdbc.SQLServerDriver|" +
                "jdbc:sqlserver://localhost;DatabaseName=test|sa",
        "Generic PostgreSQL|org.postgresql.Driver|" +
                "jdbc:postgresql:test|" ,
        "Generic MySQL|com.mysql.jdbc.Driver|" +
                "jdbc:mysql://localhost:3306/test|" ,
        "Generic HSQLDB|org.hsqldb.jdbcDriver|" +
                "jdbc:hsqldb:test;hsqldb.default_table_type=cached|sa" ,
        "Generic Derby (Server)|org.apache.derby.jdbc.ClientDriver|" +
                "jdbc:derby://localhost:1527/test;create=true|sa",
        "Generic Derby (Embedded)|org.apache.derby.jdbc.EmbeddedDriver|" +
                "jdbc:derby:test;create=true|sa",
        "Generic H2 (Server)|org.h2.Driver|" +
                "jdbc:h2:tcp://localhost/~/test|sa",
        // this will be listed on top for new installations
        "Generic H2 (Embedded)|org.h2.Driver|" +
                "jdbc:h2:~/test|sa",
    };

    private static int ticker;

    /**
     * The session timeout (the default is 30 minutes).
     */
    private static final long SESSION_TIMEOUT = SysProperties.CONSOLE_TIMEOUT;

//    public static void main(String... args) throws IOException {
//        String s = IOUtils.readStringAndClose(new java.io.FileReader(
//                // "src/main/org/h2/server/web/res/_text_cs.prop"), -1);
//                "src/main/org/h2/res/_messages_cs.prop"), -1);
//        System.out.println(StringUtils.javaEncode("..."));
//        String[] list = Locale.getISOLanguages();
//        for (int i = 0; i < list.length; i++) {
//            System.out.print(list[i] + " ");
//        }
//        System.out.println();
//        String l = "de";
//        String lang = new java.util.Locale(l).
//            getDisplayLanguage(new java.util.Locale(l));
//        System.out.println(new java.util.Locale(l).getDisplayLanguage());
//        System.out.println(lang);
//        java.util.Locale.CHINESE.getDisplayLanguage(java.util.Locale.CHINESE);
//        for (int i = 0; i < lang.length(); i++) {
//            System.out.println(Integer.toHexString(lang.charAt(i)) + " ");
//        }
//    }

    // private URLClassLoader urlClassLoader;
    private int port;
    private boolean allowOthers;
    private boolean isDaemon;
    private final Set running =
            Collections.synchronizedSet(new HashSet());
    private boolean ssl;
    private byte[] adminPassword;
    private final HashMap connInfoMap = new HashMap<>();

    private long lastTimeoutCheck;
    private final HashMap sessions = new HashMap<>();
    private final HashSet languages = new HashSet<>();
    private String startDateTime;
    private ServerSocket serverSocket;
    private String url;
    private ShutdownHandler shutdownHandler;
    private Thread listenerThread;
    private boolean ifExists = true;
    private String key;
    private boolean allowSecureCreation;
    private boolean trace;
    private TranslateThread translateThread;
    private boolean allowChunked = true;
    private String serverPropertiesDir = Constants.SERVER_PROPERTIES_DIR;
    // null means the history is not allowed to be stored
    private String commandHistoryString;

    /**
     * Read the given file from the file system or from the resources.
     *
     * @param file the file name
     * @return the data
     */
    byte[] getFile(String file) throws IOException {
        trace("getFile <" + file + ">");
        byte[] data = Utils.getResource("/org/h2/server/web/res/" + file);
        if (data == null) {
            trace(" null");
        } else {
            trace(" size=" + data.length);
        }
        return data;
    }

    /**
     * Remove this web thread from the set of running threads.
     *
     * @param t the thread to remove
     */
    synchronized void remove(WebThread t) {
        running.remove(t);
    }

    private static String generateSessionId() {
        byte[] buff = MathUtils.secureRandomBytes(16);
        return StringUtils.convertBytesToHex(buff);
    }

    /**
     * Get the web session object for the given session id.
     *
     * @param sessionId the session id
     * @return the web session or null
     */
    WebSession getSession(String sessionId) {
        long now = System.currentTimeMillis();
        if (lastTimeoutCheck + SESSION_TIMEOUT < now) {
            for (String id : new ArrayList<>(sessions.keySet())) {
                WebSession session = sessions.get(id);
                if (session.lastAccess + SESSION_TIMEOUT < now) {
                    trace("timeout for " + id);
                    sessions.remove(id);
                }
            }
            lastTimeoutCheck = now;
        }
        WebSession session = sessions.get(sessionId);
        if (session != null) {
            session.lastAccess = System.currentTimeMillis();
        }
        return session;
    }

    /**
     * Create a new web session id and object.
     *
     * @param hostAddr the host address
     * @return the web session object
     */
    WebSession createNewSession(String hostAddr) {
        String newId;
        do {
            newId = generateSessionId();
        } while (sessions.get(newId) != null);
        WebSession session = new WebSession(this);
        session.lastAccess = System.currentTimeMillis();
        session.put("sessionId", newId);
        session.put("ip", hostAddr);
        session.put("language", DEFAULT_LANGUAGE);
        session.put("frame-border", "0");
        session.put("frameset-border", "4");
        sessions.put(newId, session);
        // always read the english translation,
        // so that untranslated text appears at least in english
        readTranslations(session, DEFAULT_LANGUAGE);
        return getSession(newId);
    }

    String getStartDateTime() {
        if (startDateTime == null) {
            SimpleDateFormat format = new SimpleDateFormat(
                    "EEE, d MMM yyyy HH:mm:ss z", new Locale("en", ""));
            format.setTimeZone(DateTimeUtils.UTC);
            startDateTime = format.format(System.currentTimeMillis());
        }
        return startDateTime;
    }

    /**
     * Returns the key for privileged connections.
     *
     * @return key key, or null
     */
    String getKey() {
        return key;
    }

    /**
     * Sets the key for privileged connections.
     *
     * @param key key, or null
     */
    public void setKey(String key) {
        if (!allowOthers) {
            this.key = key;
        }
    }

    /**
     * @param allowSecureCreation
     *            whether creation of databases using the key should be allowed
     */
    public void setAllowSecureCreation(boolean allowSecureCreation) {
        if (!allowOthers) {
            this.allowSecureCreation = allowSecureCreation;
        }
    }

    @Override
    public void init(String... args) {
        // set the serverPropertiesDir, because it's used in loadProperties()
        for (int i = 0; args != null && i < args.length; i++) {
            if ("-properties".equals(args[i])) {
                serverPropertiesDir = args[++i];
            }
        }
        Properties prop = loadProperties();
        port = SortedProperties.getIntProperty(prop,
                "webPort", Constants.DEFAULT_HTTP_PORT);
        ssl = SortedProperties.getBooleanProperty(prop,
                "webSSL", false);
        allowOthers = SortedProperties.getBooleanProperty(prop,
                "webAllowOthers", false);
        setAdminPassword(SortedProperties.getStringProperty(prop, "webAdminPassword", null));
        commandHistoryString = prop.getProperty(COMMAND_HISTORY);
        for (int i = 0; args != null && i < args.length; i++) {
            String a = args[i];
            if (Tool.isOption(a, "-webPort")) {
                port = Integer.decode(args[++i]);
            } else if (Tool.isOption(a, "-webSSL")) {
                ssl = true;
            } else if (Tool.isOption(a, "-webAllowOthers")) {
                allowOthers = true;
            } else if (Tool.isOption(a, "-webDaemon")) {
                isDaemon = true;
            } else if (Tool.isOption(a, "-baseDir")) {
                String baseDir = args[++i];
                SysProperties.setBaseDir(baseDir);
            } else if (Tool.isOption(a, "-ifExists")) {
                ifExists = true;
            } else if (Tool.isOption(a, "-ifNotExists")) {
                ifExists = false;
            } else if (Tool.isOption(a, "-webAdminPassword")) {
                setAdminPassword(args[++i]);
            } else if (Tool.isOption(a, "-properties")) {
                // already set
                i++;
            } else if (Tool.isOption(a, "-trace")) {
                trace = true;
            }
        }
//            if (driverList != null) {
//                try {
//                    String[] drivers =
//                        StringUtils.arraySplit(driverList, ',', false);
//                    URL[] urls = new URL[drivers.length];
//                    for(int i=0; i entry : text.entrySet()) {
                String value = (String) entry.getValue();
                if (value.startsWith("#")) {
                    entry.setValue(value.substring(1));
                }
            }
        } catch (IOException e) {
            DbException.traceThrowable(e);
        }
        session.put("text", new HashMap<>(text));
    }

    ArrayList> getSessions() {
        ArrayList> list = new ArrayList<>(sessions.size());
        for (WebSession s : sessions.values()) {
            list.add(s.getInfo());
        }
        return list;
    }

    @Override
    public String getType() {
        return "Web Console";
    }

    @Override
    public String getName() {
        return "H2 Console Server";
    }

    void setAllowOthers(boolean b) {
        if (b) {
            key = null;
        }
        allowOthers = b;
    }

    @Override
    public boolean getAllowOthers() {
        return allowOthers;
    }

    void setSSL(boolean b) {
        ssl = b;
    }

    void setPort(int port) {
        this.port = port;
    }

    boolean getSSL() {
        return ssl;
    }

    @Override
    public int getPort() {
        return port;
    }

    public boolean isCommandHistoryAllowed() {
        return commandHistoryString != null;
    }

    public void setCommandHistoryAllowed(boolean allowed) {
        if (allowed) {
            if (commandHistoryString == null) {
                commandHistoryString = "";
            }
        } else {
            commandHistoryString = null;
        }
    }

    public ArrayList getCommandHistoryList() {
        ArrayList result = new ArrayList<>();
        if (commandHistoryString == null) {
            return result;
        }

        // Split the commandHistoryString on non-escaped semicolons
        // and unescape it.
        StringBuilder sb = new StringBuilder();
        for (int end = 0;; end++) {
            if (end == commandHistoryString.length() ||
                    commandHistoryString.charAt(end) == ';') {
                if (sb.length() > 0) {
                    result.add(sb.toString());
                    sb.delete(0, sb.length());
                }
                if (end == commandHistoryString.length()) {
                    break;
                }
            } else if (commandHistoryString.charAt(end) == '\\' &&
                    end < commandHistoryString.length() - 1) {
                sb.append(commandHistoryString.charAt(++end));
            } else {
                sb.append(commandHistoryString.charAt(end));
            }
        }
        return result;
    }

    /**
     * Save the command history to the properties file.
     *
     * @param commandHistory the history
     */
    public void saveCommandHistoryList(ArrayList commandHistory) {
        StringBuilder sb = new StringBuilder();
        for (String s : commandHistory) {
            if (sb.length() > 0) {
                sb.append(';');
            }
            sb.append(s.replace("\\", "\\\\").replace(";", "\\;"));
        }
        commandHistoryString = sb.toString();
        saveProperties(null);
    }

    /**
     * Get the connection information for this setting.
     *
     * @param name the setting name
     * @return the connection information
     */
    ConnectionInfo getSetting(String name) {
        return connInfoMap.get(name);
    }

    /**
     * Update a connection information setting.
     *
     * @param info the connection information
     */
    void updateSetting(ConnectionInfo info) {
        connInfoMap.put(info.name, info);
        info.lastAccess = ticker++;
    }

    /**
     * Remove a connection information setting from the list
     *
     * @param name the setting to remove
     */
    void removeSetting(String name) {
        connInfoMap.remove(name);
    }

    private Properties loadProperties() {
        try {
            if ("null".equals(serverPropertiesDir)) {
                return new Properties();
            }
            return SortedProperties.loadProperties(
                    serverPropertiesDir + "/" + Constants.SERVER_PROPERTIES_NAME);
        } catch (Exception e) {
            DbException.traceThrowable(e);
            return new Properties();
        }
    }

    /**
     * Get the list of connection information setting names.
     *
     * @return the connection info names
     */
    String[] getSettingNames() {
        ArrayList list = getSettings();
        String[] names = new String[list.size()];
        for (int i = 0; i < list.size(); i++) {
            names[i] = list.get(i).name;
        }
        return names;
    }

    /**
     * Get the list of connection info objects.
     *
     * @return the list
     */
    synchronized ArrayList getSettings() {
        ArrayList settings = new ArrayList<>();
        if (connInfoMap.size() == 0) {
            Properties prop = loadProperties();
            if (prop.size() == 0) {
                for (String gen : GENERIC) {
                    ConnectionInfo info = new ConnectionInfo(gen);
                    settings.add(info);
                    updateSetting(info);
                }
            } else {
                for (int i = 0;; i++) {
                    String data = prop.getProperty(Integer.toString(i));
                    if (data == null) {
                        break;
                    }
                    ConnectionInfo info = new ConnectionInfo(data);
                    settings.add(info);
                    updateSetting(info);
                }
            }
        } else {
            settings.addAll(connInfoMap.values());
        }
        Collections.sort(settings);
        return settings;
    }

    /**
     * Save the settings to the properties file.
     *
     * @param prop null or the properties webPort, webAllowOthers, and webSSL
     */
    synchronized void saveProperties(Properties prop) {
        try {
            if (prop == null) {
                Properties old = loadProperties();
                prop = new SortedProperties();
                prop.setProperty("webPort",
                        Integer.toString(SortedProperties.getIntProperty(old, "webPort", port)));
                prop.setProperty("webAllowOthers",
                        Boolean.toString(SortedProperties.getBooleanProperty(old, "webAllowOthers", allowOthers)));
                prop.setProperty("webSSL",
                        Boolean.toString(SortedProperties.getBooleanProperty(old, "webSSL", ssl)));
                if (adminPassword != null) {
                    prop.setProperty("webAdminPassword", StringUtils.convertBytesToHex(adminPassword));
                }
                if (commandHistoryString != null) {
                    prop.setProperty(COMMAND_HISTORY, commandHistoryString);
                }
            }
            ArrayList settings = getSettings();
            int len = settings.size();
            for (int i = 0; i < len; i++) {
                ConnectionInfo info = settings.get(i);
                if (info != null) {
                    prop.setProperty(Integer.toString(len - i - 1), info.getString());
                }
            }
            if (!"null".equals(serverPropertiesDir)) {
                OutputStream out = FileUtils.newOutputStream(
                        serverPropertiesDir + "/" + Constants.SERVER_PROPERTIES_NAME, false);
                prop.store(out, "H2 Server Properties");
                out.close();
            }
        } catch (Exception e) {
            DbException.traceThrowable(e);
        }
    }

    /**
     * Open a database connection.
     *
     * @param driver the driver class name
     * @param databaseUrl the database URL
     * @param user the user name
     * @param password the password
     * @param userKey the key of privileged user
     * @param networkConnectionInfo the network connection information
     * @return the database connection
     */
    Connection getConnection(String driver, String databaseUrl, String user,
            String password, String userKey, NetworkConnectionInfo networkConnectionInfo) throws SQLException {
        driver = driver.trim();
        databaseUrl = databaseUrl.trim();
        Properties p = new Properties();
        p.setProperty("user", user.trim());
        // do not trim the password, otherwise an
        // encrypted H2 database with empty user password doesn't work
        p.setProperty("password", password);
        if (databaseUrl.startsWith("jdbc:h2:")) {
            if (!allowSecureCreation || key == null || !key.equals(userKey)) {
                if (ifExists) {
                    databaseUrl += ";FORBID_CREATION=TRUE";
                }
            }
        }
        return JdbcUtils.getConnection(driver, databaseUrl, p, networkConnectionInfo);
    }

    /**
     * Shut down the web server.
     */
    void shutdown() {
        if (shutdownHandler != null) {
            shutdownHandler.shutdown();
        }
    }

    public void setShutdownHandler(ShutdownHandler shutdownHandler) {
        this.shutdownHandler = shutdownHandler;
    }

    /**
     * Create a session with a given connection.
     *
     * @param conn the connection
     * @return the URL of the web site to access this connection
     */
    public String addSession(Connection conn) throws SQLException {
        WebSession session = createNewSession("local");
        session.setShutdownServerOnDisconnect();
        session.setConnection(conn);
        session.put("url", conn.getMetaData().getURL());
        String s = (String) session.get("sessionId");
        return url + "/frame.jsp?jsessionid=" + s;
    }

    /**
     * The translate thread reads and writes the file translation.properties
     * once a second.
     */
    private class TranslateThread extends Thread {

        private final File file = new File("translation.properties");
        private final Map translation;
        private volatile boolean stopNow;

        TranslateThread(Map translation) {
            this.translation = translation;
        }

        public String getFileName() {
            return file.getAbsolutePath();
        }

        public void stopNow() {
            this.stopNow = true;
            try {
                join();
            } catch (InterruptedException e) {
                // ignore
            }
        }

        @Override
        public void run() {
            while (!stopNow) {
                try {
                    SortedProperties sp = new SortedProperties();
                    if (file.exists()) {
                        InputStream in = FileUtils.newInputStream(file.getName());
                        sp.load(in);
                        translation.putAll(sp);
                    } else {
                        OutputStream out = FileUtils.newOutputStream(file.getName(), false);
                        sp.putAll(translation);
                        sp.store(out, "Translation");
                    }
                    Thread.sleep(1000);
                } catch (Exception e) {
                    traceError(e);
                }
            }
        }

    }

    /**
     * Start the translation thread that reads the file once a second.
     *
     * @param translation the translation map
     * @return the name of the file to translate
     */
    String startTranslate(Map translation) {
        if (translateThread != null) {
            translateThread.stopNow();
        }
        translateThread = new TranslateThread(translation);
        translateThread.setDaemon(true);
        translateThread.start();
        return translateThread.getFileName();
    }

    @Override
    public boolean isDaemon() {
        return isDaemon;
    }

    void setAllowChunked(boolean allowChunked) {
        this.allowChunked = allowChunked;
    }

    boolean getAllowChunked() {
        return allowChunked;
    }

    byte[] getAdminPassword() {
        return adminPassword;
    }

    void setAdminPassword(String password) {
        if (password == null || password.isEmpty()) {
            adminPassword = null;
            return;
        }
        if (password.length() == 128) {
            try {
                adminPassword = StringUtils.convertHexToBytes(password);
                return;
            } catch (Exception ex) {}
        }
        byte[] salt = MathUtils.secureRandomBytes(32);
        byte[] hash = SHA256.getHashWithSalt(password.getBytes(StandardCharsets.UTF_8), salt);
        byte[] total = Arrays.copyOf(salt, 64);
        System.arraycopy(hash, 0, total, 32, 32);
        adminPassword = total;
    }

    /**
     * Check the admin password.
     *
     * @param password the password to test
     * @return true if admin password not configure, or admin password correct
     */
    boolean checkAdminPassword(String password) {
        if (adminPassword == null) {
            return false;
        }
        byte[] salt = Arrays.copyOf(adminPassword, 32);
        byte[] hash = new byte[32];
        System.arraycopy(adminPassword, 32, hash, 0, 32);
        return Utils.compareSecure(hash, SHA256.getHashWithSalt(password.getBytes(StandardCharsets.UTF_8), salt));
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy