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 =
+ "\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("
+
+
+
+
+
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 @@
+