/**
* 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);
}
}