/** * UPnP PortMapper - A tool for managing port forwardings via UPnP * Copyright (C) 2015 Christoph Pirkl <christoph at users.sourceforge.net> * * 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 version 3 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 program. If not, see <http://www.gnu.org/licenses/>. */ package org.chris.portmapper; import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Collection; import java.util.Collections; import java.util.EventObject; import java.util.List; import javax.swing.JOptionPane; import org.chris.portmapper.gui.PortMapperView; import org.chris.portmapper.logging.LogMessageListener; import org.chris.portmapper.logging.LogMessageOutputStream; import org.chris.portmapper.logging.LogbackConfiguration; import org.chris.portmapper.model.PortMappingPreset; import org.chris.portmapper.router.AbstractRouterFactory; import org.chris.portmapper.router.IRouter; import org.chris.portmapper.router.RouterException; import org.jdesktop.application.ResourceMap; import org.jdesktop.application.SingleFrameApplication; import org.jdesktop.application.utils.AppHelper; import org.jdesktop.application.utils.OSXAdapter; import org.jdesktop.application.utils.PlatformType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The main application class */ public class PortMapperApp extends SingleFrameApplication { /** * The name of the system property which will be used as the directory where all configuration files will be stored. */ private static final String CONFIG_DIR_PROPERTY_NAME = "portmapper.config.dir"; /** * The file name for the settings file. */ private static final String SETTINGS_FILENAME = "settings.xml"; private final Logger logger = LoggerFactory.getLogger(this.getClass()); private IRouter router; private Settings settings; private final LogMessageOutputStream logMessageOutputStream = new LogMessageOutputStream(); private final LogbackConfiguration logbackConfig = new LogbackConfiguration(); @Override protected void startup() { logbackConfig.registerOutputStream(logMessageOutputStream); setCustomConfigDir(); loadSettings(); final PortMapperView view = new PortMapperView(this); addExitListener(new ExitListener() { @Override public boolean canExit(final EventObject arg0) { return true; } @Override public void willExit(final EventObject arg0) { disconnectRouter(); } }); show(view); if (AppHelper.getPlatform() == PlatformType.OS_X) { registerMacOSXListeners(); } } private void registerMacOSXListeners() { final PortMapperView view = getView(); OSXAdapter.setPreferencesHandler(view, getMethod(PortMapperView.class, "changeSettings")); OSXAdapter.setAboutHandler(view, getMethod(PortMapperView.class, "showAboutDialog")); } private static Method getMethod(final Class<?> clazz, final String name, final Class<?>... parameterTypes) { try { return clazz.getMethod(name, parameterTypes); } catch (final SecurityException e) { throw new RuntimeException("Error getting method " + name + " of class " + clazz.getName(), e); } catch (final NoSuchMethodException e) { throw new RuntimeException("Error getting method " + name + " of class " + clazz.getName(), e); } } /** * Read the system property with name {@link PortMapperApp#CONFIG_DIR_PROPERTY_NAME} and change the local storage * directory if the property is given and points to a writable directory. If there is a directory named * <code>PortMapperConf</code> in the current directory, use this as the configuration directory. */ private void setCustomConfigDir() { final String customConfigurationDir = System.getProperty(CONFIG_DIR_PROPERTY_NAME); final File portableAppConfigDir = new File("PortMapperConf"); // the property is set: check, if the given directory can be used if (customConfigurationDir != null) { final File dir = new File(customConfigurationDir); if (!dir.isDirectory()) { logger.error("Custom configuration directory '{}' is not a directory.", customConfigurationDir); System.exit(1); } if (!dir.canRead() || !dir.canWrite()) { logger.error("Can not read or write to custom configuration directory '{}'.", customConfigurationDir); System.exit(1); } logger.info("Using custom configuration directory '{}'.", dir.getAbsolutePath()); getContext().getLocalStorage().setDirectory(dir); // check, if the portable app directory exists and use this one } else if (portableAppConfigDir.isDirectory() && portableAppConfigDir.canRead() && portableAppConfigDir.canWrite()) { logger.info("Found portable app configuration directory '{}'.", portableAppConfigDir.getAbsolutePath()); getContext().getLocalStorage().setDirectory(portableAppConfigDir); // use the default configuration directory } else { logger.info("Using default configuration directory '{}'.", getContext().getLocalStorage().getDirectory().getAbsolutePath()); } } /** * Load the application settings from file {@link PortMapperApp#SETTINGS_FILENAME} located in the configuration * directory. */ private void loadSettings() { logger.debug("Loading settings from file {}", SETTINGS_FILENAME); try { settings = (Settings) getContext().getLocalStorage().load(SETTINGS_FILENAME); } catch (final IOException e) { logger.warn("Could not load settings from file " + SETTINGS_FILENAME, e); } if (settings == null) { logger.debug("Settings were not loaded from file {}: create new settings", SETTINGS_FILENAME); settings = new Settings(); } else { logger.debug("Got settings {}", settings); this.setLogLevel(settings.getLogLevel()); } } public void setLogMessageListener(final LogMessageListener listener) { this.logMessageOutputStream.registerListener(listener); } @Override protected void shutdown() { super.shutdown(); logger.debug("Saving settings {} to file {}", settings, SETTINGS_FILENAME); if (logger.isTraceEnabled()) { for (final PortMappingPreset preset : settings.getPresets()) { logger.trace("Saving port mapping {}", preset.getCompleteDescription()); } } try { getContext().getLocalStorage().save(settings, SETTINGS_FILENAME); } catch (final IOException e) { logger.warn("Could not save settings to file " + SETTINGS_FILENAME, e); } } public ResourceMap getResourceMap() { return getContext().getResourceMap(); } public PortMapperView getView() { return (PortMapperView) getMainView(); } public void connectRouter() throws RouterException { if (this.router != null) { logger.warn("Already connected to router. Cannot create a second connection."); return; } final AbstractRouterFactory routerFactory; try { routerFactory = createRouterFactory(); } catch (final RouterException e) { logger.error("Could not create router factory: " + e.getMessage(), e); return; } logger.info("Searching for routers..."); final Collection<IRouter> foundRouters = routerFactory.findRouters(); // No routers found if (foundRouters == null || foundRouters.size() == 0) { throw new RouterException("Did not find a router"); } // One router found: use it. if (foundRouters.size() == 1) { router = foundRouters.iterator().next(); logger.info("Connected to router '{}'", router.getName()); this.getView().fireConnectionStateChange(); return; } // More than one router found: ask user. logger.info("Found more than one router (count: {}): ask user.", foundRouters.size()); final ResourceMap resourceMap = getResourceMap(); final IRouter selectedRouter = (IRouter) JOptionPane.showInputDialog(this.getView().getFrame(), resourceMap.getString("messages.select_router.message"), resourceMap.getString("messages.select_router.title"), JOptionPane.QUESTION_MESSAGE, null, foundRouters.toArray(), null); if (selectedRouter == null) { logger.info("No router selected."); return; } this.router = selectedRouter; this.getView().fireConnectionStateChange(); } private AbstractRouterFactory createRouterFactory() throws RouterException { logger.info("Creating router factory for class {}", settings.getRouterFactoryClassName()); final Class<AbstractRouterFactory> routerFactoryClass = getClassForName(settings.getRouterFactoryClassName()); final Constructor<AbstractRouterFactory> constructor = getConstructor(routerFactoryClass); final AbstractRouterFactory routerFactory = createInstance(constructor); logger.debug("Router factory {} created", routerFactory); return routerFactory; } private AbstractRouterFactory createInstance(final Constructor<AbstractRouterFactory> constructor) throws RouterException { try { return constructor.newInstance(this); } catch (final Exception e) { throw new RouterException("Could not create a router factory using constructor " + constructor, e); } } private static Constructor<AbstractRouterFactory> getConstructor(final Class<AbstractRouterFactory> clazz) throws RouterException { try { return clazz.getConstructor(PortMapperApp.class); } catch (final NoSuchMethodException e) { throw new RouterException("Could not find constructor of " + clazz.getName(), e); } catch (final SecurityException e1) { throw new RouterException("Could not find constructor of " + clazz.getName(), e1); } } @SuppressWarnings("unchecked") private static Class<AbstractRouterFactory> getClassForName(final String className) throws RouterException { try { return (Class<AbstractRouterFactory>) Class.forName(className); } catch (final ClassNotFoundException e) { throw new RouterException("Did not find router factory class for name " + className, e); } } public boolean disconnectRouter() { if (this.router == null) { logger.warn("Not connected to router. Can not disconnect."); return false; } this.router.disconnect(); this.router = null; this.getView().fireConnectionStateChange(); return true; } public IRouter getRouter() { return router; } public Settings getSettings() { return settings; } public boolean isConnected() { return this.getRouter() != null; } /** * Get the IP address of the local host. * * @return IP address of the local host or <code>null</code>, if the address could not be determined. * @throws RouterException */ public String getLocalHostAddress() { try { if (router != null) { logger.debug("Connected to router, get IP of localhost from socket..."); return router.getLocalHostAddress(); } logger.debug("Not connected to router, get IP of localhost from network interface..."); final InetAddress address = getLocalhostAddressFromNetworkInterface(); if (address != null) { return address.getHostAddress(); } else { logger.warn("Did not get IP of localhost from network interface"); } } catch (final RouterException e) { logger.warn("Could not get address of localhost.", e); logger.warn("Could not get address of localhost. Please enter it manually."); } return null; } private InetAddress getLocalhostAddressFromNetworkInterface() throws RouterException { try { final List<NetworkInterface> networkInterfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); logger.trace("Found network interfaces " + networkInterfaces); for (final NetworkInterface nInterface : networkInterfaces) { if (nInterface.isLoopback()) { logger.debug("Found loopback network interface " + nInterface.getDisplayName() + "/" + nInterface.getName() + " with IPs " + nInterface.getInterfaceAddresses() + ": ignore."); } else if (!nInterface.isUp()) { logger.debug("Found inactive network interface " + nInterface.getDisplayName() + "/" + nInterface.getName() + " with IPs " + nInterface.getInterfaceAddresses() + ": ignore."); } else { logger.debug("Found network interface " + nInterface.getDisplayName() + "/" + nInterface.getName() + " with IPs " + nInterface.getInterfaceAddresses() + ": use this one."); final List<InetAddress> addresses = Collections.list(nInterface.getInetAddresses()); if (addresses.size() > 0) { final InetAddress address = findIPv4Adress(nInterface, addresses); logger.debug("Found one address for network interface " + nInterface.getName() + ": using " + address); return address; } logger.debug("Network interface " + nInterface.getName() + " has no addresses."); } } } catch (final SocketException e) { throw new RouterException("Did not get network interfaces.", e); } return null; } private InetAddress findIPv4Adress(final NetworkInterface nInterface, final List<InetAddress> addresses) { if (addresses.size() == 1) { return addresses.get(0); } for (final InetAddress inetAddress : addresses) { if (inetAddress.getHostAddress().contains(".")) { logger.debug("Found IPv4 address " + inetAddress); return inetAddress; } } final InetAddress address = addresses.get(0); logger.info("Found more than one address for network interface " + nInterface.getName() + ": using " + address); return address; } public void setLogLevel(final String logLevel) { this.logbackConfig.setLogLevel(logLevel); } }