com.lobobrowser.LoboBrowser.java Source code

Java tutorial

Introduction

Here is the source code for com.lobobrowser.LoboBrowser.java

Source

/*
GNU GENERAL PUBLIC LICENSE
Copyright (C) 2006 The Lobo Project
    
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
verion 2 of the License, or (at your option) any later version.
    
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
    
You should have received a copy of the GNU General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
    
Contact info: lobochief@users.sourceforge.net
 */
/*
 * Created on Mar 5, 2005
 */
package com.lobobrowser;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Authenticator;
import java.net.CookieHandler;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.security.AccessController;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Permission;
import java.security.Policy;
import java.security.PrivilegedAction;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.EventObject;
import java.util.Locale;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.SSLSocketFactory;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.UIManager.LookAndFeelInfo;

import com.lobobrowser.extension.ExtensionManager;
import com.lobobrowser.utils.OS;
import com.lobobrowser.utils.PlatformStreamHandlerFactory;
import com.lobobrowser.reuse.ReuseManager;
import com.lobobrowser.store.StorageManager;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.cobraparser.gui.ConsoleModel;
import org.cobraparser.gui.DefaultWindowFactory;
import org.cobraparser.gui.FramePanel;
import org.cobraparser.request.AuthenticatorImpl;
import org.cobraparser.request.DomainValidation;
import org.cobraparser.request.NOPCookieHandlerImpl;
import org.cobraparser.security.LocalSecurityManager;
import org.cobraparser.security.LocalSecurityPolicy;
import org.cobraparser.ua.NavigatorFrame;
import org.cobraparser.util.GenericEventListener;
import org.cobraparser.util.SimpleThreadPool;
import org.cobraparser.util.SimpleThreadPoolTask;
import org.cobraparser.util.Urls;

import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkUrlFactory;
import com.squareup.okhttp.Protocol;

/**
 * A singleton class that is used to initialize a browser session in the current
 * JVM. It can also be used to open a browser window.
 *
 * @see #getInstance()
 */

public class LoboBrowser {
    private static final String NATIVE_DIR_NAME = "native";
    private static final long DAYS_MILLIS = 24 * 60 * 60 * 1000L;
    private static final long TIMEOUT_DAYS = 120;
    private static final String osName = System.getProperty("os.name").toLowerCase();
    public static final OS OS_NAME = osName.indexOf("win") > -1 ? OS.WINDOWS
            : (osName.indexOf("mac") > -1 ? OS.MAC
                    : (osName.indexOf("sunos") > -1 ? OS.SOLARIS
                            : (osName.indexOf("nix") > -1 || osName.indexOf("aix") > -1
                                    || osName.indexOf("nux") > -1) ? OS.UNIX : OS.UNKNOWN));

    private final SimpleThreadPool threadExecutor;

    // private final GeneralSettings generalSettings;

    private LoboBrowser() {
        // TODO: Research a better way to configure the thread pool
        // TODO: Use thread pools available in JDK?
        this.threadExecutor = new SimpleThreadPool("MainThreadPool", 2, 3, 60 * 1000);

        // One way to avoid a security exception.
        // this.generalSettings = GeneralSettings.getInstance();
    }

    /**
     * Intializes security by installing a security policy and a security manager.
     * Programs that use the browser API should invoke this method (or
     * {@link #init(boolean, boolean) init}) to prevent web content from having
     * full access to the user's computer.
     *
     * @see #addPrivilegedPermission(Permission)
     */
    public static void initSecurity() {
        // Set security policy and manager (essential)
        Policy.setPolicy(LocalSecurityPolicy.getInstance());
        System.setSecurityManager(new LocalSecurityManager());
    }

    /**
     * Initializes the global URLStreamHandlerFactory.
     * <p>
     * This method is invoked by {@link #init(boolean, boolean)}.
     */
    public static void initProtocols(final SSLSocketFactory sslSocketFactory) {
        // Configure URL protocol handlers
        final PlatformStreamHandlerFactory factory = PlatformStreamHandlerFactory.getInstance();
        URL.setURLStreamHandlerFactory(factory);
        final OkHttpClient okHttpClient = new OkHttpClient();

        final ArrayList<Protocol> protocolList = new ArrayList<>(2);
        protocolList.add(Protocol.HTTP_1_1);
        protocolList.add(Protocol.HTTP_2);
        okHttpClient.setProtocols(protocolList);

        okHttpClient.setConnectTimeout(100, TimeUnit.SECONDS);

        // HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory);
        okHttpClient.setSslSocketFactory(sslSocketFactory);
        okHttpClient.setFollowRedirects(false);
        okHttpClient.setFollowSslRedirects(false);
        factory.addFactory(new OkUrlFactory(okHttpClient));
        factory.addFactory(new LocalStreamHandlerFactory());
    }

    /**
     * Initializes the HTTP authenticator and the cookie handler. This is
     * essential for the browser to work properly.
     * <p>
     * This method is invoked by {@link #init(boolean, boolean)}.
     */
    public static void initHTTP() {
        // Configure authenticator
        Authenticator.setDefault(new AuthenticatorImpl());
        // Configure cookie handler
        // CookieHandler.setDefault(new CookieHandlerImpl());
        CookieHandler.setDefault(new NOPCookieHandlerImpl());
    }

    /**
     * Initializes the Swing look & feel.
     */
    public static void initLookAndFeel() throws Exception {
        // Set appropriate Swing L&F
        boolean nimbusApplied = false;
        try {
            for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    UIManager.setLookAndFeel(info.getClassName());
                    nimbusApplied = true;
                    break;
                }
            }
        } catch (final Exception e) {
            e.printStackTrace();
        }

        if (!nimbusApplied) {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }
    }

    public boolean isCodeLocationDirectory() {
        final URL codeLocation = this.getClass().getProtectionDomain().getCodeSource().getLocation();
        return Urls.isLocalFile(codeLocation) && codeLocation.getPath().endsWith("/");
    }

    /**
     * Resets standard output and error streams so they are redirected to the
     * browser console.
     *
     * @see ConsoleModel
     */
    public void initConsole() {
        final java.io.PrintStream oldOut = System.out;
        final ConsoleModel standard = ConsoleModel.getStandard();
        final java.io.PrintStream ps = standard.getPrintStream();
        System.setOut(ps);
        System.setErr(ps);
        if (this.isCodeLocationDirectory()) {
            // Should only be shown when running from Eclipse.
            oldOut.println(
                    "WARNING: initConsole(): Switching standard output and standard error to application console. If running EntryPoint, pass -debug to avoid this.");
        }
    }

    public boolean debugOn = false;

    /**
     * Initializes platform logging. Note that this method is not implicitly
     * called by {@link #init(boolean, boolean)}.
     *
     * @param debugOn
     *          Debugging mode. This determines which one of two different logging
     *          configurations is used.
     */
    public void initLogging(final boolean debugOn) throws Exception {
        this.debugOn = debugOn;

        // Set up debugging & console
        final String loggingToken = debugOn ? "logging-debug" : "logging";
        InputStream in = this.getClass().getResourceAsStream("/properties/" + loggingToken + ".properties");
        if (in == null) {
            in = this.getClass().getResourceAsStream("properties/" + loggingToken + ".properties");
            if (in == null) {
                throw new IOException("Unable to locate logging properties file.");
            }
        }
        try {
            java.util.logging.LogManager.getLogManager().readConfiguration(in);
        } finally {
            in.close();
        }
        // Configure log4j
        final Logger logger = Logger.getLogger(LoboBrowser.class.getName());
        if (logger.isLoggable(Level.INFO)) {
            logger.warning("Entry(): Logger INFO level is enabled.");
            System.getProperties().forEach((k, v) -> logger.info("main(): " + k + "=" + v));
        }
    }

    /**
     * Initializes browser extensions. Invoking this method is essential to enable
     * the primary extension and all basic browser functionality. This method is
     * invoked by {@link #init(boolean, boolean)}.
     */
    public void initExtensions() {
        ExtensionManager.getInstance().initExtensions();
    }

    /**
     * Initializes the default window factory such that the JVM exits when all
     * windows created by the factory are closed by the user.
     */
    public void initWindowFactory(final boolean exitWhenAllWindowsAreClosed) {
        DefaultWindowFactory.getInstance().setExitWhenAllWindowsAreClosed(exitWhenAllWindowsAreClosed);
    }

    /**
     * Initializers the <code>java.library.path</code> property.
     * <p>
     * This method is called by {@link #init(boolean, boolean)}.
     *
     * @param dirName
     *          A directory name relative to the browser application directory.
     */
    public void initNative(final String dirName) {
        // TODO: What is the purpose of this function?
        final Optional<File> appDirOpt = this.getApplicationDirectory();
        if (appDirOpt.isPresent()) {
            final File nativeDir = new File(appDirOpt.get(), dirName);
            System.setProperty("java.library.path", nativeDir.getAbsolutePath());
        }
    }

    /**
     * Initializes some Java properties required by the browser.
     * <p>
     * This method is called by {@link #init(boolean, boolean)}.
     */
    public void initOtherProperties() {
        // Required for array serialization in Java 6.
        System.setProperty("sun.lang.ClassLoader.allowArraySyntax", "true");
        // Don't cache host lookups for ever
        System.setProperty("networkaddress.cache.ttl", "3600");
        System.setProperty("networkaddress.cache.negative.ttl", "1");

    }

    /**
     * Initializes security, protocols, look & feel, console, the default window
     * factory, extensions and <code>java.library.path</code>. This method should
     * be invoked before using other functionality in the browser API. If this
     * method is not called, at the very least {@link #initOtherProperties()},
     * {@link #initProtocols()} and {@link #initExtensions()} should be called.
     * <p>
     * Applications that need to install their own security manager and policy
     * should not call this method.
     *
     * @param exitWhenAllWindowsAreClosed
     *          Whether the JVM should exit when all windows created by the
     *          default window factory are closed.
     * @param initConsole
     *          If this parameter is <code>true</code>, standard output is
     *          redirected to a browser console. See
     *          {@link ConsoleModel}.
     * @see #initSecurity()
     * @see #initProtocols()
     * @see #initExtensions()
     */
    public void init(final boolean exitWhenAllWindowsAreClosed, final boolean initConsole,
            final SSLSocketFactory sslSocketFactory) throws Exception {
        checkReleaseDate();

        initOtherProperties();

        initNative(NATIVE_DIR_NAME);
        initSecurity();
        initProtocols(sslSocketFactory);
        initHTTP();
        initLookAndFeel();
        if (initConsole) {
            initConsole();
        }
        initWindowFactory(exitWhenAllWindowsAreClosed);
        initExtensions();
    }

    public final Properties relProps = new Properties();
    public static final String RELEASE_VERSION_RELEASE_DATE = "version.releaseDate";
    public static final String RELEASE_VERSION_STRING = "version.string";

    private void checkReleaseDate() {
        final InputStream relStream = getClass().getResourceAsStream("/properties/release.properties");
        try {
            relProps.load(relStream);
            final String dateStr = relProps.getProperty(RELEASE_VERSION_RELEASE_DATE);
            final SimpleDateFormat yyyyMMDDFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
            final Date releaseDate = yyyyMMDDFormat.parse(dateStr);
            final Date releaseDatePlusTimeout = new Date(releaseDate.getTime() + (TIMEOUT_DAYS * DAYS_MILLIS));
            final Date currDate = new Date(System.currentTimeMillis());
            if (releaseDatePlusTimeout.before(currDate)) {
                final String version = relProps.getProperty(RELEASE_VERSION_STRING);
                final String checkForUpdatesMessage = "<html><h3><center>This version of LoboBrowser is old</center></h3><p>LoboBrowser "
                        + version + "</p><p>Released on: " + releaseDate + "</p><p>This version is more than "
                        + TIMEOUT_DAYS
                        + " days old and was not intended for long-time use.</p><p>Please check if a newer version is available on https://update.lobobrowser.org</p></html>";
                JOptionPane.showMessageDialog(null, checkForUpdatesMessage);
            }
        } catch (IOException | ParseException e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    /**
     * Opens a window and attempts to render the URL or path given.
     *
     * @param urlOrPath
     *          A URL or file path.
     * @return
     * @throws MalformedURLException
     */
    public NavigatorFrame launch(final String urlOrPath) throws MalformedURLException {
        final URL url = DomainValidation.guessURL(urlOrPath);
        return FramePanel.openWindow(null, url, null, new Properties(), "GET", null);
    }

    /**
     * Opens as many browser windows as there are startup URLs in general
     * settings.
     * @return
     *
     * @see org.cobraparser.settings.GeneralSettings#getStartupURLs()
     * @throws MalformedURLException
     */
    public NavigatorFrame launch() throws MalformedURLException {
        final SecurityManager sm = System.getSecurityManager();
        if (sm == null) {
            final Logger logger = Logger.getLogger(LoboBrowser.class.getName());
            logger.warning("launch(): Security manager not set!");
        }
        /*
         * String[] startupURLs = this.generalSettings.getStartupURLs(); for(String
         * url : startupURLs) { this.launch(url); }
         */
        //return this.launch("about:welcome");
        return this.launch("http://start.lobobrowser.com");
        // this.launch("http://localhost:8000/");
        // this.launch("http://localhost:8000/test_link.html");
        // this.launch("http://localhost:8000/request_permissions.html");
    }

    private boolean windowHasBeenShown = false;
    private @Nullable String grinderKey = null;

    /**
     * Starts the browser by opening the URLs specified in the command-line
     * arguments provided. Non-option arguments are assumed to be URLs and opened
     * in separate windows. If no arguments are found, the method launches URLs
     * from general settings. This method will not return until at least one
     * window has been shown.
     *
     * @see org.cobraparser.settings.GeneralSettings#getStartupURLs()
     */
    public void start(final String[] args) throws MalformedURLException {
        DefaultWindowFactory.getInstance().evtWindowShown.addListener(new GenericEventListener() {
            public void processEvent(final EventObject event) {
                synchronized (LoboBrowser.this) {
                    windowHasBeenShown = true;
                    LoboBrowser.this.notifyAll();
                }
            }
        });
        boolean launched = false;
        for (final String arg : args) {
            if (arg.startsWith("-")) {
                final String grinderKeyPrefix = "-grinder-key=";
                if (arg.startsWith(grinderKeyPrefix)) {
                    grinderKey = arg.substring(grinderKeyPrefix.length());
                }
            } else {
                final String url = arg;
                try {
                    launched = true;
                    this.launch(url);
                } catch (final Exception err) {
                    err.printStackTrace(System.err);
                }
            }
        }
        if (!launched) {
            this.launch();
        }
        synchronized (this) {
            while (!this.windowHasBeenShown) {
                try {
                    this.wait();
                } catch (final InterruptedException ie) {
                    // Ignore
                }
            }
        }
    }

    private static final LoboBrowser instance = new LoboBrowser();

    /**
     * Gets the singleton instance.
     */
    public static LoboBrowser getInstance() {
        return instance;
    }

    /**
     * Performs some cleanup and then exits the JVM.
     */
    public static void shutdown() {
        AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
            try {
                ReuseManager.getInstance().shutdown();
                StorageManager.getInstance().shutdown();
            } catch (final Exception err) {
                err.printStackTrace(System.err);
            }

            System.out.println("Number of active threads: " + Thread.activeCount());

            System.exit(0);
            return null;
        });
    }

    /**
     * Adds one permission to the base set of permissions assigned to privileged
     * code, i.e. code loaded from the local system rather than a remote location.
     * This method must be called before a security manager has been set, that is,
     * before {@link #init(boolean, boolean)} or {@link #initSecurity()} are
     * invoked. The purpose of the method is to add permissions otherwise missing
     * from the security policy installed by this facility.
     *
     * @param permission
     *          A <code>Permission<code> instance.
     */
    public static void addPrivilegedPermission(final Permission permission) {
        LocalSecurityPolicy.addPrivilegedPermission(permission);
    }

    public void scheduleTask(final SimpleThreadPoolTask task) {
        this.threadExecutor.schedule(task);
    }

    private File applicationDirectory;

    public Optional<File> getApplicationDirectory() {
        File appDir = this.applicationDirectory;
        if (appDir == null) {
            final java.security.ProtectionDomain pd = this.getClass().getProtectionDomain();
            final java.security.CodeSource cs = pd.getCodeSource();
            final URL url = cs.getLocation();
            if (url.getProtocol().equals("zipentry")) {
                return Optional.empty();
            }
            final String jarPath = url.getPath();
            File jarFile;
            try {
                jarFile = new File(url.toURI());
            } catch (final java.net.URISyntaxException use) {
                throw new IllegalStateException(use);
            } catch (final IllegalArgumentException iae) {
                throw new IllegalStateException("Application code source apparently not a local JAR file: " + url
                        + ". Only local JAR files are supported at the moment.", iae);
            }
            final File installDir = jarFile.getParentFile();
            if (installDir == null) {
                throw new IllegalStateException(
                        "Installation directory is missing. Startup JAR path is " + jarPath + ".");
            }
            if (!installDir.exists()) {
                throw new IllegalStateException("Installation directory not found. Startup JAR path is " + jarPath
                        + ". Directory path is " + installDir.getAbsolutePath() + ".");
            }
            appDir = installDir;
            this.applicationDirectory = appDir;

            // Static logger should not be created in this class.
            final Logger logger = Logger.getLogger(this.getClass().getName());
            if (logger.isLoggable(Level.INFO)) {
                logger.info("getApplicationDirectory(): url=" + url + ",appDir=" + appDir);
            }
        }
        return Optional.of(appDir);
    }

    private static class LocalStreamHandlerFactory implements java.net.URLStreamHandlerFactory {
        public URLStreamHandler createURLStreamHandler(final String protocol) {
            if (protocol.equals("res")) {
                return new com.lobobrowser.protocol.res.Handler();
            } else if (protocol.equals("vc")) {
                return new com.lobobrowser.protocol.vc.Handler();
            } else if (protocol.equals("cobra")) {
                return new com.lobobrowser.protocol.cobra.Handler();
            } else {
                return null;
            }
        }
    }

    public final boolean verifyAuth(final int port, final @NonNull String passkey) {
        if (grinderKey != null) {
            try {
                final MessageDigest digest = MessageDigest.getInstance("SHA-256");
                final byte[] hash = digest.digest((grinderKey + port).getBytes("UTF-8"));
                final String hashB64 = Base64.getEncoder().encodeToString(hash);
                return hashB64.equals(passkey);
            } catch (final NoSuchAlgorithmException | UnsupportedEncodingException nsa) {
                return false;
            }
        } else {
            return false;
        }
    }
}