/* 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 org.lobobrowser.main; 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 org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.lobobrowser.gui.ConsoleModel; import org.lobobrowser.gui.DefaultWindowFactory; import org.lobobrowser.gui.FramePanel; import org.lobobrowser.request.AuthenticatorImpl; import org.lobobrowser.request.DomainValidation; import org.lobobrowser.request.NOPCookieHandlerImpl; import org.lobobrowser.security.LocalSecurityManager; import org.lobobrowser.security.LocalSecurityPolicy; import org.lobobrowser.store.StorageManager; import org.lobobrowser.ua.NavigatorFrame; import org.lobobrowser.util.GenericEventListener; import org.lobobrowser.util.SimpleThreadPool; import org.lobobrowser.util.SimpleThreadPoolTask; import org.lobobrowser.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 PlatformInit { 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 PlatformInit() { // 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"; java.io.InputStream in = this.getClass().getResourceAsStream("/properties/" + loggingToken + ".properties"); if (in == null) { in = this.getClass().getResourceAsStream("properties/" + loggingToken + ".properties"); if (in == null) { throw new java.io.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(PlatformInit.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 org.lobobrowser.gui.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 gngr is old</center></h3><p>gngr " + 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://gngr.info</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.lobobrowser.settings.GeneralSettings#getStartupURLs() * @throws MalformedURLException */ public NavigatorFrame launch() throws MalformedURLException { final SecurityManager sm = System.getSecurityManager(); if (sm == null) { final Logger logger = Logger.getLogger(PlatformInit.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"); // 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.lobobrowser.settings.GeneralSettings#getStartupURLs() */ public void start(final String[] args) throws MalformedURLException { DefaultWindowFactory.getInstance().evtWindowShown.addListener(new GenericEventListener() { public void processEvent(final EventObject event) { synchronized (PlatformInit.this) { windowHasBeenShown = true; PlatformInit.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 PlatformInit instance = new PlatformInit(); /** * Gets the singleton instance. */ public static PlatformInit 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 java.net.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 java.lang.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 org.lobobrowser.protocol.res.Handler(); } else if (protocol.equals("vc")) { return new org.lobobrowser.protocol.vc.Handler(); } else { return null; } } } 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; } } }