/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Copyright 2015 The ZAP Development Team * * Licensed 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.zaproxy.zap; import java.awt.EventQueue; import java.awt.GraphicsEnvironment; import java.awt.Toolkit; import java.io.FileNotFoundException; import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Locale; import javax.swing.JComponent; import javax.swing.JOptionPane; import javax.swing.UIManager; import javax.swing.UIManager.LookAndFeelInfo; import javax.swing.UnsupportedLookAndFeelException; import org.apache.commons.configuration.ConfigurationException; import org.apache.log4j.BasicConfigurator; import org.apache.log4j.Logger; import org.jdesktop.swingx.JXErrorPane; import org.jdesktop.swingx.error.ErrorInfo; import org.parosproxy.paros.CommandLine; import org.parosproxy.paros.Constant; import org.parosproxy.paros.control.Control; import org.parosproxy.paros.extension.option.OptionsParamView; import org.parosproxy.paros.model.Model; import org.parosproxy.paros.model.OptionsParam; import org.parosproxy.paros.view.View; import org.zaproxy.zap.control.AddOn; import org.zaproxy.zap.control.AddOnLoader; import org.zaproxy.zap.control.AddOnRunIssuesUtils; import org.zaproxy.zap.control.ExtensionFactory; import org.zaproxy.zap.extension.autoupdate.ExtensionAutoUpdate; import org.zaproxy.zap.model.SessionUtils; import org.zaproxy.zap.utils.FontUtils; import org.zaproxy.zap.utils.LocaleUtils; import org.zaproxy.zap.view.LicenseFrame; import org.zaproxy.zap.view.LocaleDialog; import org.zaproxy.zap.view.ProxyDialog; /** * The bootstrap process for GUI mode. * * @since 2.4.2 */ public class GuiBootstrap extends ZapBootstrap { private final Logger logger = Logger.getLogger(GuiBootstrap.class); /** * Flag that indicates whether or not the look and feel was already set. * * @see #setupLookAndFeel() */ private boolean lookAndFeelSet; public GuiBootstrap(CommandLine cmdLineArgs) { super(cmdLineArgs); } @Override public int start() { int rc = super.start(); if (rc != 0) { return rc; } if (!getArgs().isNoStdOutLog()) { BasicConfigurator.configure(); } logger.info(getStartingMessage()); if (GraphicsEnvironment.isHeadless()) { String headlessMessage = Constant.messages.getString("start.gui.headless", CommandLine.HELP); logger.fatal(headlessMessage); System.err.println(headlessMessage); return 1; } EventQueue.invokeLater(new Runnable() { @Override public void run() { startImpl(); } }); return 0; } private void startImpl() { setX11AwtAppClassName(); setDefaultViewLocale(Constant.getLocale()); if (isShowLicense()) { setupLookAndFeel(); showLicense(); } else { boolean firstTime = isFirstTime(); if (firstTime) { createAcceptedLicenseFile(); } init(firstTime); } } private void createAcceptedLicenseFile() { try { Files.createFile(Paths.get(Constant.getInstance().ACCEPTED_LICENSE)); } catch (final IOException ie) { JOptionPane.showMessageDialog(null, Constant.messages.getString("start.unknown.error")); logger.error("Failed to create 'accepted license' file: ", ie); return; } } private void setX11AwtAppClassName() { Toolkit defaultToolkit = Toolkit.getDefaultToolkit(); // See JDK-6528430 : need system property to override default WM_CLASS // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6528430 // Based on NetBeans workaround linked from the issue: Class<?> toolkitClass = defaultToolkit.getClass(); if ("sun.awt.X11.XToolkit".equals(toolkitClass.getName())) { try { Field awtAppClassName = toolkitClass.getDeclaredField("awtAppClassName"); awtAppClassName.setAccessible(true); awtAppClassName.set(null, Constant.PROGRAM_NAME); } catch (Exception e) { logger.warn("Failed to set awt app class name: " + e.getMessage()); } } } /** * Initialises the {@code Model}, {@code View} and {@code Control}. * * @param firstTime {@code true} if it's the first time ZAP is being started, {@code false} otherwise */ private void init(final boolean firstTime) { try { initModel(); setupLookAndFeel(); } catch (Exception e) { setupLookAndFeel(); if (e instanceof FileNotFoundException) { JOptionPane.showMessageDialog( null, Constant.messages.getString("start.db.error"), Constant.messages.getString("start.title.error"), JOptionPane.ERROR_MESSAGE); } logger.fatal(e.getMessage(), e); } OptionsParam options = Model.getSingleton().getOptionsParam(); OptionsParamView viewParam = options.getViewParam(); FontUtils.setDefaultFont(viewParam.getFontName(), viewParam.getFontSize()); setupLocale(options); View.getSingleton().showSplashScreen(); promptForProxyDetailsIfNeeded(options); Thread bootstrap = new Thread(new Runnable() { @Override public void run() { try { initControlAndPostViewInit(); } catch (Throwable e) { if (!Constant.isDevBuild()) { ErrorInfo errorInfo = new ErrorInfo( Constant.messages.getString("start.gui.dialog.fatal.error.title"), Constant.messages.getString("start.gui.dialog.fatal.error.message"), null, null, e, null, null); JXErrorPane errorPane = new JXErrorPane(); errorPane.setErrorInfo(errorInfo); JXErrorPane.showDialog(View.getSingleton().getSplashScreen(), errorPane); } View.getSingleton().hideSplashScreen(); logger.fatal("Failed to initialise GUI: ", e); // We must exit otherwise EDT would keep ZAP running. System.exit(1); } warnAddOnsAndExtensionsNoLongerRunnable(); if (firstTime) { // Disabled for now - we have too many popups occuring when you // first start up // be nice to have a clean start up wizard... // ExtensionHelp.showHelp(); } else { // Dont auto check for updates the first time, no chance of any // proxy having been set final ExtensionAutoUpdate eau = (ExtensionAutoUpdate) Control.getSingleton() .getExtensionLoader() .getExtension("ExtensionAutoUpdate"); if (eau != null) { eau.alertIfNewVersions(); } } } }); bootstrap.setName("ZAP-BootstrapGUI"); bootstrap.setDaemon(false); bootstrap.start(); } /** * Initialises the {@code Control} and does post {@code View} initialisations. * * @throws Exception if an error occurs during initialisation * @see Control * @see View */ private void initControlAndPostViewInit() throws Exception { Control.initSingletonWithView(getControlOverrides()); final Control control = Control.getSingleton(); final View view = View.getSingleton(); EventQueue.invokeAndWait(new Runnable() { @Override public void run() { view.postInit(); view.getMainFrame().setVisible(true); boolean createNewSession = true; if (getArgs().isEnabled(CommandLine.SESSION) && getArgs().isEnabled(CommandLine.NEW_SESSION)) { view.showWarningDialog( Constant.messages.getString( "start.gui.cmdline.invalid.session.options", CommandLine.SESSION, CommandLine.NEW_SESSION, Constant.getZapHome())); } else if (getArgs().isEnabled(CommandLine.SESSION)) { Path sessionPath = SessionUtils.getSessionPath(getArgs().getArgument(CommandLine.SESSION)); if (!Files.exists(sessionPath)) { view.showWarningDialog( Constant.messages.getString("start.gui.cmdline.session.does.not.exist", Constant.getZapHome())); } else { createNewSession = !control.getMenuFileControl().openSession(sessionPath.toAbsolutePath().toString()); } } else if (getArgs().isEnabled(CommandLine.NEW_SESSION)) { Path sessionPath = SessionUtils.getSessionPath(getArgs().getArgument(CommandLine.NEW_SESSION)); if (Files.exists(sessionPath)) { view.showWarningDialog( Constant.messages .getString("start.gui.cmdline.newsession.already.exist", Constant.getZapHome())); } else { createNewSession = !control.getMenuFileControl().newSession(sessionPath.toAbsolutePath().toString()); } } view.hideSplashScreen(); if (createNewSession) { try { control.getMenuFileControl().newSession(false); } catch (Exception e) { logger.error(e.getMessage(), e); View.getSingleton().showWarningDialog(Constant.messages.getString("menu.file.newSession.error")); } } } }); try { // Allow extensions to pick up command line args in GUI mode control.getExtensionLoader().hookCommandLineListener(getArgs()); control.runCommandLine(); } catch (final Exception e) { logger.error(e.getMessage(), e); EventQueue.invokeLater(new Runnable() { @Override public void run() { view.showWarningDialog(e.getMessage()); } }); } } /** * Sets the default {@code Locale} for Swing components. * * @param locale the locale that will be set as default locale for Swing components * @see JComponent#setDefaultLocale(Locale) */ private static void setDefaultViewLocale(Locale locale) { JComponent.setDefaultLocale(locale); } /** * Setups Swing's look and feel. * <p> * <strong>Note:</strong> Should be called only after calling {@link #initModel()}, if not initialising ZAP for the * {@link #isFirstTime() first time}. The look and feel set up might initialise some network classes (e.g. * {@link java.net.InetAddress InetAddress}) preventing some ZAP options from being correctly applied. */ private void setupLookAndFeel() { if (lookAndFeelSet) { return; } lookAndFeelSet = true; String lookAndFeelClassname = System.getProperty("swing.defaultlaf"); if (lookAndFeelClassname != null) { try { UIManager.setLookAndFeel(lookAndFeelClassname); return; } catch (final UnsupportedLookAndFeelException | ClassNotFoundException | ClassCastException | InstantiationException | IllegalAccessException e) { logger.warn("Failed to set the specified look and feel: " + e.getMessage()); } } try { // Set the systems Look and Feel UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); if (Constant.isMacOsX()) { OsXGui.setup(); } else { // Set Nimbus LaF if available for (final LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { if ("Nimbus".equals(info.getName())) { UIManager.setLookAndFeel(info.getClassName()); break; } } } } catch (final UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException e) { logger.warn("Failed to set the \"default\" look and feel: " + e.getMessage()); } } /** * Setups ZAP's and GUI {@code Locale}, if not previously defined. Otherwise it's determined automatically or, if not * possible, by asking the user to choose one of the supported locales. * * @param options ZAP's options, used to check if a locale was already defined and save it if not. * @see #setDefaultViewLocale(Locale) * @see Constant#setLocale(String) */ private void setupLocale(OptionsParam options) { // Prompt for language if not set String locale = options.getViewParam().getConfigLocale(); if (locale == null || locale.length() == 0) { // Dont use a parent of the MainFrame - that will initialise it // with English! final Locale userloc = determineUsersSystemLocale(); if (userloc == null) { // Only show the dialog, when the user's language can't be // guessed. setDefaultViewLocale(Constant.getSystemsLocale()); final LocaleDialog dialog = new LocaleDialog(null, true); dialog.init(options); dialog.setVisible(true); } else { options.getViewParam().setLocale(userloc); } setDefaultViewLocale(createLocale(options.getViewParam().getLocale().split("_"))); Constant.setLocale(Model.getSingleton().getOptionsParam().getViewParam().getLocale()); try { options.getViewParam().getConfig().save(); } catch (ConfigurationException e) { logger.warn("Failed to save locale: ", e); } } } /** * Determines the {@link Locale} of the current user's system. * <p> * It will match the {@link Constant#getSystemsLocale()} with the available locales from ZAP's translation files. * <p> * It may return {@code null}, if the user's system locale is not in the list of available translations of ZAP. * * @return the {@code Locale} that best matches the user's locale, or {@code null} if none found */ private static Locale determineUsersSystemLocale() { Locale userloc = null; final Locale systloc = Constant.getSystemsLocale(); // first, try full match for (String ls : LocaleUtils.getAvailableLocales()) { String[] langArray = ls.split("_"); if (langArray.length == 1) { if (systloc.getLanguage().equals(langArray[0])) { userloc = systloc; break; } } if (langArray.length == 2) { if (systloc.getLanguage().equals(langArray[0]) && systloc.getCountry().equals(langArray[1])) { userloc = systloc; break; } } if (langArray.length == 3) { if (systloc.getLanguage().equals(langArray[0]) && systloc.getCountry().equals(langArray[1]) && systloc.getVariant().equals(langArray[2])) { userloc = systloc; break; } } } if (userloc == null) { // second, try partial language match for (String ls : LocaleUtils.getAvailableLocales()) { String[] langArray = ls.split("_"); if (systloc.getLanguage().equals(langArray[0])) { userloc = createLocale(langArray); break; } } } return userloc; } private static Locale createLocale(String[] localeFields) { if (localeFields == null || localeFields.length == 0) { return null; } Locale.Builder localeBuilder = new Locale.Builder(); localeBuilder.setLanguage(localeFields[0]); if (localeFields.length >= 2) { localeBuilder.setRegion(localeFields[1]); } if (localeFields.length >= 3) { localeBuilder.setVariant(localeFields[2]); } return localeBuilder.build(); } private static void promptForProxyDetailsIfNeeded(OptionsParam options) { if (options.getConnectionParam().isProxyChainPrompt()) { final ProxyDialog dialog = new ProxyDialog(null, true); dialog.init(options); dialog.setVisible(true); } } /** * Shows license dialogue, asynchronously (the method returns immediately after/while showing the dialogue). * <p> * It continues the bootstrap process, by calling {@code init(true)} if the license is accepted. Aborts the bootstrap * process if the license is not accepted. * * @see #init(boolean) */ private void showLicense() { final LicenseFrame license = new LicenseFrame(); license.setPostTask(new Runnable() { @Override public void run() { license.dispose(); if (!license.isAccepted()) { return; } createAcceptedLicenseFile(); init(true); } }); license.setVisible(true); } /** * Warns, through a dialogue, about add-ons and extensions that are no longer runnable because of changes in its * dependencies. */ private static void warnAddOnsAndExtensionsNoLongerRunnable() { final AddOnLoader addOnLoader = ExtensionFactory.getAddOnLoader(); List<String> idsAddOnsNoLongerRunning = addOnLoader.getIdsAddOnsWithRunningIssuesSinceLastRun(); if (idsAddOnsNoLongerRunning.isEmpty()) { return; } List<AddOn> addOnsNoLongerRunning = new ArrayList<>(idsAddOnsNoLongerRunning.size()); for (String id : idsAddOnsNoLongerRunning) { addOnsNoLongerRunning.add(addOnLoader.getAddOnCollection().getAddOn(id)); } AddOnRunIssuesUtils.showWarningMessageAddOnsNotRunnable( Constant.messages.getString("start.gui.warn.addOnsOrExtensionsNoLongerRunning"), addOnLoader.getAddOnCollection(), addOnsNoLongerRunning); } /** * Tells whether or not ZAP license should be shown, if the license was already accepted it does not need to be shown again. * <p> * The license is considered accepted if a file named {@link Constant#ACCEPTED_LICENSE_DEFAULT AcceptedLicense} exists in * the installation and/or home directory. * * @return {@code true} if the license should be shown, {@code false} otherwise. */ private static boolean isShowLicense() { Path acceptedLicenseFile = Paths.get(Constant.getZapInstall(), Constant.getInstance().ACCEPTED_LICENSE_DEFAULT); if (Files.exists(acceptedLicenseFile)) { return false; } return isFirstTime(); } /** * Tells whether or not ZAP is being started for first time. It does so by checking if the license was not yet been * accepted. * * @return {@code true} if it's the first time, {@code false} otherwise. * @see Constant#ACCEPTED_LICENSE */ private static boolean isFirstTime() { Path acceptedLicenseFile = Paths.get(Constant.getInstance().ACCEPTED_LICENSE); return Files.notExists(acceptedLicenseFile); } }