From 83cf815160475b2871b84650a33fdaa6b7fb0f96 Mon Sep 17 00:00:00 2001 From: jrandom Date: Tue, 3 Aug 2004 08:21:29 +0000 Subject: [PATCH] * add new and generally ugly components to allow web based control of tunnels * build an i2ptunnel.war --- apps/i2ptunnel/java/build.xml | 6 + .../net/i2p/i2ptunnel/TunnelController.java | 337 +++++++++++++++++ .../i2p/i2ptunnel/TunnelControllerGroup.java | 249 +++++++++++++ .../i2ptunnel/WebEditPageFormGenerator.java | 307 +++++++++++++++ .../net/i2p/i2ptunnel/WebEditPageHelper.java | 348 ++++++++++++++++++ .../i2p/i2ptunnel/WebStatusPageHelper.java | 157 ++++++++ apps/i2ptunnel/jsp/edit.jsp | 16 + apps/i2ptunnel/jsp/index.jsp | 31 ++ apps/i2ptunnel/jsp/web.xml | 17 + build.xml | 1 + 10 files changed, 1469 insertions(+) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebEditPageFormGenerator.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebEditPageHelper.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebStatusPageHelper.java create mode 100644 apps/i2ptunnel/jsp/edit.jsp create mode 100644 apps/i2ptunnel/jsp/index.jsp create mode 100644 apps/i2ptunnel/jsp/web.xml diff --git a/apps/i2ptunnel/java/build.xml b/apps/i2ptunnel/java/build.xml index e7bceaafc..2357f4ccc 100644 --- a/apps/i2ptunnel/java/build.xml +++ b/apps/i2ptunnel/java/build.xml @@ -22,6 +22,12 @@ + + + + + diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java new file mode 100644 index 000000000..2b301ca5b --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -0,0 +1,337 @@ +package net.i2p.i2ptunnel; + +import java.io.IOException; +import java.io.File; +import java.io.FileOutputStream; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.I2PException; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PClientFactory; +import net.i2p.data.Destination; +import net.i2p.util.Log; + +/** + * Coordinate the runtime operation and configuration of a tunnel. + * These objects are bundled together under a TunnelControllerGroup where the + * entire group is stored / loaded from a single config file. + * + */ +public class TunnelController implements Logging { + private Log _log; + private Properties _config; + private I2PTunnel _tunnel; + private List _messages; + private boolean _running; + + /** + * Create a new controller for a tunnel out of the specific config options. + * The config may contain a large number of options - only ones that begin in + * the prefix should be used (and, in turn, that prefix should be stripped off + * before being interpreted by this controller) + * + * @param config original key=value mapping + * @param prefix beginning of key values that are relevent to this tunnel + */ + public TunnelController(Properties config, String prefix) { + this(config, prefix, false); + } + /** + * + * @param createKey for servers, whether we want to create a brand new destination + * with private keys at the location specified or not (does not + * overwrite existing ones) + */ + public TunnelController(Properties config, String prefix, boolean createKey) { + _tunnel = new I2PTunnel(); + _log = I2PAppContext.getGlobalContext().logManager().getLog(TunnelController.class); + setConfig(config, prefix); + _messages = new ArrayList(4); + _running = false; + if (createKey) + createPrivateKey(); + } + + private void createPrivateKey() { + I2PClient client = I2PClientFactory.createClient(); + File keyFile = new File(getPrivKeyFile()); + if (keyFile.exists()) { + log("Not overwriting existing private keys in " + keyFile.getAbsolutePath()); + return; + } else { + File parent = keyFile.getParentFile(); + if ( (parent != null) && (!parent.exists()) ) + parent.mkdirs(); + } + FileOutputStream fos = null; + try { + fos = new FileOutputStream(keyFile); + Destination dest = client.createDestination(fos); + String destStr = dest.toBase64(); + log("Private key created and saved in " + keyFile.getAbsolutePath()); + log("New destination: " + destStr); + } catch (I2PException ie) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error creating new destination", ie); + log("Error creating new destination: " + ie.getMessage()); + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error creating writing the destination to " + keyFile.getAbsolutePath(), ioe); + log("Error writing the keys to " + keyFile.getAbsolutePath()); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + } + + /** + * Start up the tunnel (if it isn't already running) + * + */ + public void startTunnel() { + if (_running) { + if (_log.shouldLog(Log.INFO)) + _log.info("Already running"); + log("Tunnel " + getName() + " is already running"); + return; + } + String type = getType(); + if ( (type == null) || (type.length() <= 0) ) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Cannot start the tunnel - no type specified"); + return; + } + if ("httpclient".equals(type)) { + startHttpClient(); + } else if ("client".equals(type)) { + startClient(); + } else if ("server".equals(type)) { + startServer(); + } else { + if (_log.shouldLog(Log.WARN)) + _log.error("Cannot start tunnel - unknown type [" + type + "]"); + } + } + + private void startHttpClient() { + setI2CPOptions(); + setSessionOptions(); + setListenOn(); + String listenPort = getListenPort(); + String proxyList = getProxyList(); + if (proxyList == null) + _tunnel.runHttpClient(new String[] { listenPort }, this); + else + _tunnel.runHttpClient(new String[] { listenPort, proxyList }, this); + _running = true; + } + + private void startClient() { + setI2CPOptions(); + setSessionOptions(); + setListenOn(); + String listenPort = getListenPort(); + String dest = getTargetDestination(); + _tunnel.runClient(new String[] { listenPort, dest }, this); + _running = true; + } + + private void startServer() { + setI2CPOptions(); + setSessionOptions(); + String targetHost = getTargetHost(); + String targetPort = getTargetPort(); + String privKeyFile = getPrivKeyFile(); + _tunnel.runServer(new String[] { targetHost, targetPort, privKeyFile }, this); + _running = true; + } + + private void setListenOn() { + String listenOn = getListenOnInterface(); + if ( (listenOn != null) && (listenOn.length() > 0) ) { + _tunnel.runListenOn(new String[] { listenOn }, this); + } + } + + private void setSessionOptions() { + List opts = new ArrayList(); + for (Iterator iter = _config.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = _config.getProperty(key); + if (key.startsWith("option.")) { + key = key.substring("option.".length()); + opts.add(key + "=" + val); + } + } + String args[] = new String[opts.size()]; + for (int i = 0; i < opts.size(); i++) + args[i] = (String)opts.get(i); + _tunnel.runClientOptions(args, this); + } + + private void setI2CPOptions() { + String host = getI2CPHost(); + if ( (host != null) && (host.length() > 0) ) + _tunnel.host = host; + String port = getI2CPPort(); + if ( (port != null) && (port.length() > 0) ) + _tunnel.port = port; + } + + public void stopTunnel() { + _tunnel.runClose(new String[] { "forced", "all" }, this); + _running = false; + } + + public void restartTunnel() { + stopTunnel(); + startTunnel(); + } + + public void setConfig(Properties config, String prefix) { + Properties props = new Properties(); + for (Iterator iter = config.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = config.getProperty(key); + if (key.startsWith(prefix)) { + key = key.substring(prefix.length()); + props.setProperty(key, val); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Set prop [" + key + "] to [" + val + "]"); + } + } + _config = props; + } + public Properties getConfig(String prefix) { + Properties rv = new Properties(); + for (Iterator iter = _config.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = _config.getProperty(key); + rv.setProperty(prefix + key, val); + } + return rv; + } + + public String getType() { return _config.getProperty("type"); } + public String getName() { return _config.getProperty("name"); } + public String getDescription() { return _config.getProperty("description"); } + public String getI2CPHost() { return _config.getProperty("i2cpHost"); } + public String getI2CPPort() { return _config.getProperty("i2cpPort"); } + public String getClientOptions() { + StringBuffer opts = new StringBuffer(64); + for (Iterator iter = _config.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = _config.getProperty(key); + if (key.startsWith("option.")) { + key = key.substring("option.".length()); + if (opts.length() > 0) opts.append(' '); + opts.append(key).append('=').append(val); + } + } + return opts.toString(); + } + public String getListenOnInterface() { return _config.getProperty("interface"); } + public String getTargetHost() { return _config.getProperty("targetHost"); } + public String getTargetPort() { return _config.getProperty("targetPort"); } + public String getPrivKeyFile() { return _config.getProperty("privKeyFile"); } + public String getListenPort() { return _config.getProperty("listenPort"); } + public String getTargetDestination() { return _config.getProperty("targetDestination"); } + public String getProxyList() { return _config.getProperty("proxyList"); } + + public boolean getIsRunning() { return _running; } + + public void getSummary(StringBuffer buf) { + String type = getType(); + if ("httpclient".equals(type)) + getHttpClientSummary(buf); + else if ("client".equals(type)) + getClientSummary(buf); + else if ("server".equals(type)) + getServerSummary(buf); + else + buf.append("Unknown type ").append(type); + } + + private void getHttpClientSummary(StringBuffer buf) { + String description = getDescription(); + if ( (description != null) && (description.trim().length() > 0) ) + buf.append("").append(description).append("
\n"); + buf.append("HTTP proxy listening on port ").append(getListenPort()); + String listenOn = getListenOnInterface(); + if ("0.0.0.0".equals(listenOn)) + buf.append(" (reachable by any machine)"); + else if ("127.0.0.1".equals(listenOn)) + buf.append(" (reachable locally only)"); + else + buf.append(" (reachable at the ").append(listenOn).append(" interface)"); + buf.append("
\n"); + String proxies = getProxyList(); + if ( (proxies == null) || (proxies.trim().length() <= 0) ) + buf.append("Outproxy: default [squid.i2p]
\n"); + else + buf.append("Outproxy: ").append(proxies).append("
\n"); + getOptionSummary(buf); + } + + private void getClientSummary(StringBuffer buf) { + String description = getDescription(); + if ( (description != null) && (description.trim().length() > 0) ) + buf.append("").append(description).append("
\n"); + buf.append("Client tunnel listening on port ").append(getListenPort()); + buf.append(" pointing at ").append(getTargetDestination()); + String listenOn = getListenOnInterface(); + if ("0.0.0.0".equals(listenOn)) + buf.append(" (reachable by any machine)"); + else if ("127.0.0.1".equals(listenOn)) + buf.append(" (reachable locally only)"); + else + buf.append(" (reachable at the ").append(listenOn).append(" interface)"); + buf.append("
\n"); + getOptionSummary(buf); + } + + private void getServerSummary(StringBuffer buf) { + String description = getDescription(); + if ( (description != null) && (description.trim().length() > 0) ) + buf.append("").append(description).append("
\n"); + buf.append("Server tunnel pointing at port ").append(getTargetPort()); + buf.append(" on ").append(getTargetHost()); + buf.append("
\n"); + buf.append("Private destination loaded from ").append(getPrivKeyFile()).append("
\n"); + getOptionSummary(buf); + } + + private void getOptionSummary(StringBuffer buf) { + String opts = getClientOptions(); + if ( (opts != null) && (opts.length() > 0) ) + buf.append("Network options: ").append(opts).append("
\n"); + } + + public void log(String s) { + synchronized (this) { + _messages.add(s); + while (_messages.size() > 10) + _messages.remove(0); + } + if (_log.shouldLog(Log.INFO)) + _log.info(s); + } + + /** + * Pull off any messages that the I2PTunnel has produced + * + * @return list of messages pulled off (each is a String, earliest first) + */ + public List clearMessages() { + List rv = null; + synchronized (this) { + rv = new ArrayList(_messages); + _messages.clear(); + } + return rv; + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java new file mode 100644 index 000000000..c542f5f18 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java @@ -0,0 +1,249 @@ +package net.i2p.i2ptunnel; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; + +import net.i2p.I2PAppContext; +import net.i2p.util.Log; + +/** + * Coordinate a set of tunnels within the JVM, loading and storing their config + * to disk, and building new ones as requested. + * + */ +public class TunnelControllerGroup { + private Log _log; + private List _controllers; + private static TunnelControllerGroup _instance = new TunnelControllerGroup(); + public static TunnelControllerGroup getInstance() { return _instance; } + + private TunnelControllerGroup() { + _log = I2PAppContext.getGlobalContext().logManager().getLog(TunnelControllerGroup.class); + _controllers = new ArrayList(); + } + + /** + * Load up all of the tunnels configured in the given file (but do not start + * them) + * + */ + public void loadControllers(String configFile) { + Properties cfg = loadConfig(configFile); + if (cfg == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unable to load the config from " + configFile); + return; + } + int i = 0; + while (true) { + String type = cfg.getProperty("tunnel." + i + ".type"); + if (type == null) + break; + TunnelController controller = new TunnelController(cfg, "tunnel." + i + "."); + _controllers.add(controller); + i++; + } + if (_log.shouldLog(Log.INFO)) + _log.info(i + " controllers loaded from " + configFile); + } + + /** + * Stop and remove reference to all known tunnels (but dont delete any config + * file or do other silly things) + * + */ + public void unloadControllers() { + stopAllControllers(); + _controllers.clear(); + if (_log.shouldLog(Log.INFO)) + _log.info("All controllers stopped and unloaded"); + } + + /** + * Add the given tunnel to the set of known controllers (but dont add it to + * a config file or start it or anything) + * + */ + public void addController(TunnelController controller) { _controllers.add(controller); } + + /** + * Stop and remove the given tunnel + * + * @return list of messages from the controller as it is stopped + */ + public List removeController(TunnelController controller) { + if (controller == null) return new ArrayList(); + controller.stopTunnel(); + List msgs = controller.clearMessages(); + _controllers.remove(controller); + msgs.add("Tunnel " + controller.getName() + " removed"); + return msgs; + } + + /** + * Stop all tunnels + * + * @return list of messages the tunnels generate when stopped + */ + public List stopAllControllers() { + List msgs = new ArrayList(); + for (int i = 0; i < _controllers.size(); i++) { + TunnelController controller = (TunnelController)_controllers.get(i); + controller.stopTunnel(); + msgs.addAll(controller.clearMessages()); + } + + if (_log.shouldLog(Log.INFO)) + _log.info(_controllers.size() + " controllers stopped"); + return msgs; + } + + /** + * Start all tunnels + * + * @return list of messages the tunnels generate when started + */ + public List startAllControllers() { + List msgs = new ArrayList(); + for (int i = 0; i < _controllers.size(); i++) { + TunnelController controller = (TunnelController)_controllers.get(i); + controller.startTunnel(); + msgs.addAll(controller.clearMessages()); + } + + if (_log.shouldLog(Log.INFO)) + _log.info(_controllers.size() + " controllers started"); + return msgs; + } + + /** + * Restart all tunnels + * + * @return list of messages the tunnels generate when restarted + */ + public List restartAllControllers() { + List msgs = new ArrayList(); + for (int i = 0; i < _controllers.size(); i++) { + TunnelController controller = (TunnelController)_controllers.get(i); + controller.restartTunnel(); + msgs.addAll(controller.clearMessages()); + } + if (_log.shouldLog(Log.INFO)) + _log.info(_controllers.size() + " controllers restarted"); + return msgs; + } + + /** + * Fetch all outstanding messages from any of the known tunnels + * + * @return list of messages the tunnels have generated + */ + public List clearAllMessages() { + List msgs = new ArrayList(); + for (int i = 0; i < _controllers.size(); i++) { + TunnelController controller = (TunnelController)_controllers.get(i); + msgs.addAll(controller.clearMessages()); + } + return msgs; + } + + /** + * Save the configuration of all known tunnels to the given file + * + */ + public void saveConfig(String configFile) { + File cfgFile = new File(configFile); + File parent = cfgFile.getParentFile(); + if ( (parent != null) && (!parent.exists()) ) + parent.mkdirs(); + + + TreeMap map = new TreeMap(); + for (int i = 0; i < _controllers.size(); i++) { + TunnelController controller = (TunnelController)_controllers.get(i); + Properties cur = controller.getConfig("tunnel." + i + "."); + map.putAll(cur); + } + + StringBuffer buf = new StringBuffer(1024); + for (Iterator iter = map.keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = (String)map.get(key); + buf.append(key).append('=').append(val).append('\n'); + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(cfgFile); + fos.write(buf.toString().getBytes()); + if (_log.shouldLog(Log.INFO)) + _log.info("Config written to " + cfgFile.getPath()); + } catch (IOException ioe) { + _log.error("Error writing out the config"); + } finally { + if (fos != null) try { fos.close(); } catch (IOException ioe) {} + } + } + + /** + * Load up the config data from the file + * + * @return properties loaded or null if there was an error + */ + private Properties loadConfig(String configFile) { + File cfgFile = new File(configFile); + if (!cfgFile.exists()) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Unable to load the controllers from " + configFile); + return null; + } + + Properties props = new Properties(); + FileInputStream fis = null; + try { + fis = new FileInputStream(cfgFile); + BufferedReader in = new BufferedReader(new InputStreamReader(fis)); + String line = null; + while ( (line = in.readLine()) != null) { + line = line.trim(); + if (line.length() <= 0) continue; + if (line.startsWith("#") || line.startsWith(";")) + continue; + int eq = line.indexOf('='); + if ( (eq <= 0) || (eq >= line.length() - 1) ) + continue; + String key = line.substring(0, eq); + String val = line.substring(eq+1); + props.setProperty(key, val); + } + + if (_log.shouldLog(Log.INFO)) + _log.info("Props loaded with " + props.size() + " lines"); + return props; + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error reading the controllers from " + configFile, ioe); + return null; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + } + + /** + * Retrieve a list of tunnels known + * + * @return list of TunnelController objects + */ + public List getControllers() { return _controllers; } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebEditPageFormGenerator.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebEditPageFormGenerator.java new file mode 100644 index 000000000..43cf00422 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebEditPageFormGenerator.java @@ -0,0 +1,307 @@ +package net.i2p.i2ptunnel; + +import java.io.File; + +import java.util.Iterator; +import java.util.Properties; +import java.util.Random; +import java.util.StringTokenizer; + +/** + * Uuuugly... generate the edit/add forms for the various + * I2PTunnel types (httpclient/client/server) + * + */ +class WebEditPageFormGenerator { + private static final String SELECT_TYPE_FORM = + "
Type of tunnel: " + + "
\n"; + + /** + * Retrieve the form requested + * + */ + public static String getForm(WebEditPageHelper helper) { + TunnelController controller = helper.getTunnelController(); + + if ( (helper.getType() == null) && (controller == null) ) + return SELECT_TYPE_FORM; + + String id = helper.getNum(); + String type = helper.getType(); + if (controller != null) + type = controller.getType(); + + if ("httpclient".equals(type)) + return getEditHttpClientForm(controller, id); + else if ("client".equals(type)) + return getEditClientForm(controller, id); + else if ("server".equals(type)) + return getEditServerForm(controller, id); + else + return "WTF, unknown type [" + type + "]"; + } + + private static String getEditHttpClientForm(TunnelController controller, String id) { + StringBuffer buf = new StringBuffer(1024); + addGeneral(buf, controller, id); + buf.append("Type: HTTP proxy
\n"); + + addListeningOn(buf, controller, 4444); + + buf.append("Outproxies:
\n"); + + addOptions(buf, controller); + buf.append("\n"); + buf.append("\n"); + buf.append(" confirm \n"); + buf.append("\n"); + return buf.toString(); + } + + private static String getEditClientForm(TunnelController controller, String id) { + StringBuffer buf = new StringBuffer(1024); + addGeneral(buf, controller, id); + buf.append("Type: Client tunnel
\n"); + + addListeningOn(buf, controller, 2025 + new Random().nextInt(1000)); // 2025 since nextInt can be negative + + buf.append("Target: (either the hosts.txt name or the full base64 destination)
\n"); + + addOptions(buf, controller); + buf.append("
\n"); + buf.append("\n"); + buf.append(" confirm \n"); + buf.append("\n"); + return buf.toString(); + } + + private static String getEditServerForm(TunnelController controller, String id) { + StringBuffer buf = new StringBuffer(1024); + addGeneral(buf, controller, id); + buf.append("Type: Server tunnel
\n"); + + buf.append("Target host:
\n"); + + buf.append("Target port:
\n"); + + buf.append("Private key file:
"); + } else { + buf.append("myServer.privKey\" />
"); + buf.append(""); + } + + addOptions(buf, controller); + buf.append("\n"); + buf.append("\n"); + buf.append(" confirm \n"); + buf.append("\n"); + return buf.toString(); + } + + /** + * Start off the form and add some common fields (name, num, description) + * + * @param buf where to shove the form + * @param controller tunnel in question, or null if we're creating a new tunnel + * @param id index into the current list of tunnelControllerGroup.getControllers() list + * (or null if we are generating an 'add' form) + */ + private static void addGeneral(StringBuffer buf, TunnelController controller, String id) { + buf.append("
"); + if (id != null) + buf.append(""); + + buf.append("Name:
\n"); + + buf.append("Description:
\n"); + } + + /** + * Generate the fields asking for what port and interface the tunnel should + * listen on. + * + * @param buf where to shove the form + * @param controller tunnel in question, or null if we're creating a new tunnel + * @param defaultPort if we are creating a new tunnel, default the form to the given port + */ + private static void addListeningOn(StringBuffer buf, TunnelController controller, int defaultPort) { + buf.append("Listening on port:
\n"); + + String selectedOn = null; + if ( (controller != null) && (controller.getListenOnInterface() != null) ) + selectedOn = controller.getListenOnInterface(); + + buf.append("Reachable by: "); + buf.append(" "); + buf.append("Other:
\n"); + } + + /** + * Add fields for customizing the I2PSession options, including helpers for + * tunnel depth and count, as well as I2CP host and port. + * + * @param buf where to shove the form + * @param controller tunnel in question, or null if we're creating a new tunnel + */ + private static void addOptions(StringBuffer buf, TunnelController controller) { + int tunnelDepth = 2; + int numTunnels = 2; + Properties opts = getOptions(controller); + if (opts != null) { + String depth = opts.getProperty("tunnels.depthInbound"); + if (depth != null) { + try { + tunnelDepth = Integer.parseInt(depth); + } catch (NumberFormatException nfe) { + tunnelDepth = 2; + } + } + String num = opts.getProperty("tunnels.numInbound"); + if (num != null) { + try { + numTunnels = Integer.parseInt(num); + } catch (NumberFormatException nfe) { + numTunnels = 2; + } + } + } + + buf.append("Tunnel depth: "); + buf.append("
\n"); + + buf.append("Tunnel count: "); + buf.append("
\n"); + + buf.append("I2CP host: "); + buf.append("
\n"); + buf.append("I2CP port: "); + buf.append("
\n"); + + buf.append("Other custom options: \n"); + buf.append("= pair.length()) ) + continue; + String key = pair.substring(0, eq); + String val = pair.substring(eq+1); + props.setProperty(key, val); + } + return props; + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebEditPageHelper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebEditPageHelper.java new file mode 100644 index 000000000..22f12c342 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebEditPageHelper.java @@ -0,0 +1,348 @@ +package net.i2p.i2ptunnel; + +import java.util.List; +import java.util.Properties; +import java.util.StringTokenizer; + +import net.i2p.I2PAppContext; +import net.i2p.util.Log; + +/** + * UUUUuuuuuugly glue code to handle bean interaction from the web, process + * that data, and spit out the results (or the form requested). The basic + * usage is to set any of the fields with data then query the bean via + * getActionResults() which triggers the request processing (taking all the + * provided data, doing what needs to be done) and returns the results of those + * activites. Then a subsequent call to getEditForm() generates the HTML form + * to either edit the currently selected tunnel (if specified) or add a new one. + * This functionality is delegated to the WebEditPageFormGenerator. + * + */ +public class WebEditPageHelper { + private Log _log; + private String _action; + private String _type; + private String _id; + private String _name; + private String _description; + private String _i2cpHost; + private String _i2cpPort; + private String _tunnelDepth; + private String _tunnelCount; + private String _customOptions; + private String _proxyList; + private String _port; + private String _reachableBy; + private String _reachableByOther; + private String _targetDestination; + private String _targetHost; + private String _targetPort; + private String _privKeyFile; + private boolean _privKeyGenerate; + private boolean _removeConfirmed; + + public WebEditPageHelper() { + _action = null; + _type = null; + _id = null; + _removeConfirmed = false; + _log = I2PAppContext.getGlobalContext().logManager().getLog(WebEditPageHelper.class); + } + + /** + * Used for form submit - either "Save" or Remove" + */ + public void setAction(String action) { + _action = (action != null ? action.trim() : null); + } + /** + * What type of tunnel (httpclient, client, or server). This is + * required when adding a new tunnel. + * + */ + public void setType(String type) { + _type = (type != null ? type.trim() : null); + } + /** + * Which particular tunnel should be edited (index into the current + * TunnelControllerGroup's getControllers() list). This is required + * when editing a tunnel, but not when adding a new one. + * + */ + public void setNum(String id) { + _id = (id != null ? id.trim() : null); + } + String getType() { return _type; } + String getNum() { return _id; } + + /** Short name of the tunnel */ + public void setName(String name) { + _name = (name != null ? name.trim() : null); + } + /** one line description */ + public void setDescription(String description) { + _description = (description != null ? description.trim() : null); + } + /** I2CP host the router is on */ + public void setClientHost(String host) { + _i2cpHost = (host != null ? host.trim() : null); + } + /** I2CP port the router is on */ + public void setClientPort(String port) { + _i2cpPort = (port != null ? port.trim() : null); + } + /** how many hops to use for inbound tunnels */ + public void setTunnelDepth(String tunnelDepth) { + _tunnelDepth = (tunnelDepth != null ? tunnelDepth.trim() : null); + } + /** how many parallel inbound tunnels to use */ + public void setTunnelCount(String tunnelCount) { + _tunnelCount = (tunnelCount != null ? tunnelCount.trim() : null); + } + /** what I2P session overrides should be used */ + public void setCustomOptions(String customOptions) { + _customOptions = (customOptions != null ? customOptions.trim() : null); + } + /** what HTTP outproxies should be used (httpclient specific) */ + public void setProxyList(String proxyList) { + _proxyList = (proxyList != null ? proxyList.trim() : null); + } + /** what port should this client/httpclient listen on */ + public void setPort(String port) { + _port = (port != null ? port.trim() : null); + } + /** + * what interface should this client/httpclient listen on (unless + * overridden by the setReachableByOther() field) + */ + public void setReachableBy(String reachableBy) { + _reachableBy = (reachableBy != null ? reachableBy.trim() : null); + } + /** + * If specified, defines the exact IP interface to listen for requests + * on (in the case of client/httpclient tunnels) + */ + public void setReachableByOther(String reachableByOther) { + _reachableByOther = (reachableByOther != null ? reachableByOther.trim() : null); + } + /** What peer does this client tunnel point at */ + public void setTargetDestination(String dest) { + _targetDestination = (dest != null ? dest.trim() : null); + } + /** What host does this server tunnel point at */ + public void setTargetHost(String host) { + _targetHost = (host != null ? host.trim() : null); + } + /** What port does this server tunnel point at */ + public void setTargetPort(String port) { + _targetPort = (port != null ? port.trim() : null); + } + /** What filename is this server tunnel's private keys stored in */ + public void setPrivKeyFile(String file) { + _privKeyFile = (file != null ? file.trim() : null); + } + /** + * If called with any value, we want to generate a new destination + * for this server tunnel. This won't cause any existing private keys + * to be overwritten, however. + */ + public void setPrivKeyGenerate(String moo) { + _privKeyGenerate = true; + } + /** + * If called with any value (and the form submitted with action=Remove), + * we really do want to stop and remove the tunnel. + */ + public void setRemoveConfirm(String moo) { + _removeConfirmed = true; + } + + /** + * Process the form and display any resulting messages + * + */ + public String getActionResults() { + try { + return processAction(); + } catch (Throwable t) { + _log.log(Log.CRIT, "Internal error processing request", t); + return "Internal error - " + t.getMessage(); + } + } + + /** + * Generate an HTML form to edit / create a tunnel according to the + * specified fields + */ + public String getEditForm() { + try { + return WebEditPageFormGenerator.getForm(this); + } catch (Throwable t) { + _log.log(Log.CRIT, "Internal error retrieving edit form", t); + return "Internal error - " + t.getMessage(); + } + } + + /** + * Retrieve the tunnel pointed to by the current id + * + */ + TunnelController getTunnelController() { + if (_id == null) return null; + int id = -1; + try { + id = Integer.parseInt(_id); + List controllers = TunnelControllerGroup.getInstance().getControllers(); + if ( (id < 0) || (id >= controllers.size()) ) + return null; + else + return (TunnelController)controllers.get(id); + } catch (NumberFormatException nfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid tunnel id [" + _id + "]", nfe); + return null; + } + } + + private String processAction() { + if ( (_action == null) || (_action.trim().length() <= 0) ) + return ""; + if ("Save".equals(_action)) + return save(); + else if ("Remove".equals(_action)) + return remove(); + else + return "Action " + _action + " unknown"; + } + + private String remove() { + if (!_removeConfirmed) + return "Please confirm removal"; + + TunnelController cur = getTunnelController(); + if (cur == null) + return "Invalid tunnel number"; + + List msgs = TunnelControllerGroup.getInstance().removeController(cur); + msgs.addAll(doSave()); + return getMessages(msgs); + } + + private String save() { + if (_type == null) + return "Invalid form submission (no type?)"; + Properties config = getConfig(); + if (config == null) + return "Invalid params"; + + TunnelController cur = getTunnelController(); + if (cur == null) { + // creating new + cur = new TunnelController(config, "", _privKeyGenerate); + TunnelControllerGroup.getInstance().addController(cur); + } else { + cur.setConfig(config, ""); + } + + + return getMessages(doSave()); + } + private List doSave() { + TunnelControllerGroup.getInstance().saveConfig(WebStatusPageHelper.CONFIG_FILE); + return TunnelControllerGroup.getInstance().clearAllMessages(); + } + + /** + * Based on all provided data, create a set of configuration parameters + * suitable for use in a TunnelController. This will replace (not add to) + * any existing parameters, so this should return a comprehensive mapping. + * + */ + private Properties getConfig() { + Properties config = new Properties(); + updateConfigGeneric(config); + + if ("httpclient".equals(_type)) { + if (_port != null) + config.setProperty("listenPort", _port); + if (_reachableByOther != null) + config.setProperty("interface", _reachableByOther); + else + config.setProperty("interface", _reachableBy); + if (_proxyList != null) + config.setProperty("proxyList", _proxyList); + } else if ("client".equals(_type)) { + if (_port != null) + config.setProperty("listenPort", _port); + if (_reachableByOther != null) + config.setProperty("interface", _reachableByOther); + else + config.setProperty("interface", _reachableBy); + if (_targetDestination != null) + config.setProperty("targetDestination", _targetDestination); + } else if ("server".equals(_type)) { + if (_targetHost != null) + config.setProperty("targetHost", _targetHost); + if (_targetPort != null) + config.setProperty("targetPort", _targetPort); + if (_privKeyFile != null) + config.setProperty("privKeyFile", _privKeyFile); + } else { + return null; + } + + return config; + } + + private void updateConfigGeneric(Properties config) { + config.setProperty("type", _type); + if (_name != null) + config.setProperty("name", _name); + if (_description != null) + config.setProperty("description", _description); + if (_i2cpHost != null) + config.setProperty("i2cpHost", _i2cpHost); + if (_i2cpPort != null) + config.setProperty("i2cpPort", _i2cpPort); + + if (_customOptions != null) { + StringTokenizer tok = new StringTokenizer(_customOptions); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int eq = pair.indexOf('='); + if ( (eq <= 0) || (eq >= pair.length()) ) + continue; + String key = pair.substring(0, eq); + String val = pair.substring(eq+1); + if ("tunnels.numInbound".equals(key)) continue; + if ("tunnels.depthInbound".equals(key)) continue; + config.setProperty("option." + key, val); + } + } + + if (_tunnelCount != null) + config.setProperty("option.tunnels.numInbound", _tunnelCount); + if (_tunnelDepth != null) + config.setProperty("option.tunnels.depthInbound", _tunnelDepth); + } + + /** + * Pretty print the messages provided + * + */ + private String getMessages(List msgs) { + if (msgs == null) return ""; + int num = msgs.size(); + switch (num) { + case 0: return ""; + case 1: return (String)msgs.get(0); + default: + StringBuffer buf = new StringBuffer(512); + buf.append("
    "); + for (int i = 0; i < num; i++) + buf.append("
  • ").append((String)msgs.get(i)).append("
  • \n"); + buf.append("
\n"); + return buf.toString(); + } + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebStatusPageHelper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebStatusPageHelper.java new file mode 100644 index 000000000..75848eef6 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/WebStatusPageHelper.java @@ -0,0 +1,157 @@ +package net.i2p.i2ptunnel; + +import java.util.List; +import net.i2p.I2PAppContext; +import net.i2p.util.Log; + +/** + * Ugly hack to let the web interface access the list of known tunnels and + * control their operation. Any data submitted by setting properties are + * acted upon by calling getActionResults() (which returns any messages + * generated). In addition, the getSummaryList() generates the html for + * summarizing all of the tunnels known, including both their status and the + * links to edit, stop, or start them. + * + */ +public class WebStatusPageHelper { + private Log _log; + private String _action; + private int _controllerNum; + private static boolean _configLoaded = false; + + static final String CONFIG_FILE = "i2ptunnel.cfg"; + + public WebStatusPageHelper() { + _action = null; + _controllerNum = -1; + _log = I2PAppContext.getGlobalContext().logManager().getLog(WebStatusPageHelper.class); + synchronized (WebStatusPageHelper.class) { + if (!_configLoaded) { + reloadConfig(); + _configLoaded = true; + } + } + } + + public void setAction(String action) { + _action = action; + } + public void setNum(String num) { + if (num != null) { + try { + _controllerNum = Integer.parseInt(num); + } catch (NumberFormatException nfe) { + _controllerNum = -1; + } + } + } + + public String getActionResults() { + try { + return processAction(); + } catch (Throwable t) { + _log.log(Log.CRIT, "Internal error processing web status", t); + return "Internal error processing request - " + t.getMessage(); + } + } + + public String getSummaryList() { + StringBuffer buf = new StringBuffer(4*1024); + buf.append("
    "); + List tunnels = TunnelControllerGroup.getInstance().getControllers(); + for (int i = 0; i < tunnels.size(); i++) { + buf.append("
  • \n"); + getSummary(buf, i, (TunnelController)tunnels.get(i)); + buf.append("
  • \n"); + } + buf.append("
"); + return buf.toString(); + } + + private void getSummary(StringBuffer buf, int num, TunnelController controller) { + buf.append("").append(controller.getName()).append(": "); + if (controller.getIsRunning()) { + buf.append("running "); + buf.append("stop "); + } else { + buf.append("not running "); + buf.append("start "); + } + buf.append("edit "); + buf.append("
\n"); + controller.getSummary(buf); + } + + private String processAction() { + if ( (_action == null) || (_action.trim().length() <= 0) ) + return getMessages(); + if ("Stop all".equals(_action)) + return stopAll(); + else if ("Start all".equals(_action)) + return startAll(); + else if ("Restart all".equals(_action)) + return restartAll(); + else if ("Reload config".equals(_action)) + return reloadConfig(); + else if ("stop".equals(_action)) + return stop(); + else if ("start".equals(_action)) + return start(); + else + return "Action " + _action + " unknown"; + } + private String stopAll() { + List msgs = TunnelControllerGroup.getInstance().stopAllControllers(); + return getMessages(msgs); + } + private String startAll() { + List msgs = TunnelControllerGroup.getInstance().startAllControllers(); + return getMessages(msgs); + } + private String restartAll() { + List msgs = TunnelControllerGroup.getInstance().restartAllControllers(); + return getMessages(msgs); + } + private String reloadConfig() { + TunnelControllerGroup.getInstance().unloadControllers(); + TunnelControllerGroup.getInstance().loadControllers(CONFIG_FILE); + return "Config reloaded"; + } + private String start() { + if (_controllerNum < 0) return "Invalid tunnel"; + List controllers = TunnelControllerGroup.getInstance().getControllers(); + if (_controllerNum >= controllers.size()) return "Invalid tunnel"; + TunnelController controller = (TunnelController)controllers.get(_controllerNum); + controller.startTunnel(); + return getMessages(controller.clearMessages()); + } + + private String stop() { + if (_controllerNum < 0) return "Invalid tunnel"; + List controllers = TunnelControllerGroup.getInstance().getControllers(); + if (_controllerNum >= controllers.size()) return "Invalid tunnel"; + TunnelController controller = (TunnelController)controllers.get(_controllerNum); + controller.stopTunnel(); + return getMessages(controller.clearMessages()); + } + + private String getMessages() { + return getMessages(TunnelControllerGroup.getInstance().clearAllMessages()); + } + + private String getMessages(List msgs) { + if (msgs == null) return ""; + int num = msgs.size(); + switch (num) { + case 0: return ""; + case 1: return (String)msgs.get(0); + default: + StringBuffer buf = new StringBuffer(512); + buf.append("
    "); + for (int i = 0; i < num; i++) + buf.append("
  • ").append((String)msgs.get(i)).append("
  • \n"); + buf.append("
\n"); + return buf.toString(); + } + } +} diff --git a/apps/i2ptunnel/jsp/edit.jsp b/apps/i2ptunnel/jsp/edit.jsp new file mode 100644 index 000000000..04b339088 --- /dev/null +++ b/apps/i2ptunnel/jsp/edit.jsp @@ -0,0 +1,16 @@ +<%@page contentType="text/html" %> + + + +I2PTunnel edit + + +Back + + + + + + + + diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp new file mode 100644 index 000000000..d0e0bc790 --- /dev/null +++ b/apps/i2ptunnel/jsp/index.jsp @@ -0,0 +1,31 @@ +<%@page contentType="text/html" %> + + + +I2PTunnel status + + + + + + + +
+ + + + + + + +
+Add new: + +
+ + + diff --git a/apps/i2ptunnel/jsp/web.xml b/apps/i2ptunnel/jsp/web.xml new file mode 100644 index 000000000..9a428440d --- /dev/null +++ b/apps/i2ptunnel/jsp/web.xml @@ -0,0 +1,17 @@ + + + + + + + 30 + + + + + index.jsp + + + \ No newline at end of file diff --git a/build.xml b/build.xml index f0bf88fc6..60872ca7c 100644 --- a/build.xml +++ b/build.xml @@ -37,6 +37,7 @@ +