diff --git a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java index abde95e80..c02b948fe 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java +++ b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java @@ -325,7 +325,10 @@ abstract class ExtensionHandler { BDecoder dec = new BDecoder(is); BEValue bev = dec.bdecodeMap(); Map map = bev.getMap(); - byte[] ids = map.get("added").getBytes(); + bev = map.get("added"); + if (bev == null) + return; + byte[] ids = bev.getBytes(); if (ids.length < HASH_LENGTH) return; int len = Math.min(ids.length, (I2PSnarkUtil.MAX_CONNECTIONS - 1) * HASH_LENGTH); diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index dfd9bb95f..23d7ed3c7 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -55,8 +55,9 @@ public class I2PSnarkUtil { private String _i2cpHost; private int _i2cpPort; private final Map _opts; - private I2PSocketManager _manager; + private volatile I2PSocketManager _manager; private boolean _configured; + private volatile boolean _connecting; private final Set _shitlist; private int _maxUploaders; private int _maxUpBW; @@ -200,6 +201,7 @@ public class I2PSnarkUtil { */ synchronized public boolean connect() { if (_manager == null) { + _connecting = true; // try to find why reconnecting after stop if (_log.shouldLog(Log.DEBUG)) _log.debug("Connecting to I2P", new Exception("I did it")); @@ -239,6 +241,7 @@ public class I2PSnarkUtil { if (opts.getProperty("i2p.streaming.maxConnsPerHour") == null) opts.setProperty("i2p.streaming.maxConnsPerHour", "20"); _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts); + _connecting = false; } // FIXME this only instantiates krpc once, left stuck with old manager if (_shouldUseDHT && _manager != null && _dht == null) @@ -254,6 +257,9 @@ public class I2PSnarkUtil { public boolean connected() { return _manager != null; } + /** @since 0.9.1 */ + public boolean isConnecting() { return _manager == null && _connecting; } + /** * For FetchAndAdd * @return null if not connected @@ -275,8 +281,11 @@ public class I2PSnarkUtil { // FIXME this can cause race NPEs elsewhere _manager = null; _shitlist.clear(); - if (mgr != null) + if (mgr != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Disconnecting from I2P", new Exception("I did it")); mgr.destroySocketManager(); + } // this will delete a .torrent file d/l in progress so don't do that... FileUtil.rmdir(_tmpDir, false); // in case the user will d/l a .torrent file next... diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java index 71ea93e11..8433242ff 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java @@ -39,13 +39,13 @@ class PeerState implements DataLoader // Interesting and choking describes whether we are interested in or // are choking the other side. - boolean interesting = false; - boolean choking = true; + volatile boolean interesting; + volatile boolean choking = true; // Interested and choked describes whether the other side is // interested in us or choked us. - boolean interested = false; - boolean choked = true; + volatile boolean interested; + volatile boolean choked = true; /** the pieces the peer has */ BitField bitfield; diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index f7a990185..acf5f15ed 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -250,6 +250,7 @@ public class Snark private String rootDataDir = "."; private final CompleteListener completeListener; private boolean stopped; + private boolean starting; private byte[] id; private byte[] infoHash; private String additionalTrackerURL; @@ -509,9 +510,19 @@ public class Snark } /** - * Start up contacting peers and querying the tracker + * Start up contacting peers and querying the tracker. + * Blocks if tunnel is not yet open. */ - public void startTorrent() { + public synchronized void startTorrent() { + starting = true; + try { + x_startTorrent(); + } finally { + starting = false; + } + } + + private void x_startTorrent() { boolean ok = _util.connect(); if (!ok) fatal("Unable to connect to I2P"); if (coordinator == null) { @@ -572,14 +583,24 @@ public class Snark debug("NOT starting TrackerClient???", NOTICE); } } + /** * Stop contacting the tracker and talking with peers */ public void stopTorrent() { + stopTorrent(false); + } + + /** + * Stop contacting the tracker and talking with peers + * @param fast if true, limit the life of the unannounce threads + * @since 0.9.1 + */ + public synchronized void stopTorrent(boolean fast) { stopped = true; TrackerClient tc = trackerclient; if (tc != null) - tc.halt(); + tc.halt(fast); PeerCoordinator pc = coordinator; if (pc != null) pc.halt(); @@ -670,6 +691,22 @@ public class Snark return stopped; } + /** + * Startup in progress. + * @since 0.9.1 + */ + public boolean isStarting() { + return starting && stopped; + } + + /** + * Set startup in progress. + * @since 0.9.1 + */ + public void setStarting() { + starting = true; + } + /** * @since 0.8.4 */ diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 316d761fa..ed14a83a4 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -34,6 +34,8 @@ import net.i2p.util.Log; import net.i2p.util.OrderedProperties; import net.i2p.util.SecureDirectory; import net.i2p.util.SecureFileOutputStream; +import net.i2p.util.SimpleScheduler; +import net.i2p.util.SimpleTimer; /** * Manage multiple snarks @@ -59,6 +61,7 @@ public class SnarkManager implements Snark.CompleteListener { private ConnectionAcceptor _connectionAcceptor; private Thread _monitor; private volatile boolean _running; + private volatile boolean _stopping; private final Map _trackerMap; public static final String PROP_I2CP_HOST = "i2psnark.i2cpHost"; @@ -146,16 +149,26 @@ public class SnarkManager implements Snark.CompleteListener { _connectionAcceptor = new ConnectionAcceptor(_util); _monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true); _monitor.start(); - _context.addShutdownTask(new SnarkManagerShutdown()); + // Not required, Jetty has a shutdown hook + //_context.addShutdownTask(new SnarkManagerShutdown()); } + /* + * Called by the webapp at Jetty shutdown. + * Stops all torrents. Does not close the tunnel, so the announces have a chance. + * Fix this so an individual webaapp stop will close the tunnel. + * Runs inline. + */ public void stop() { _running = false; _monitor.interrupt(); _connectionAcceptor.halt(); - (new SnarkManagerShutdown()).run(); + stopAllTorrents(true); } + /** @since 0.9.1 */ + public boolean isStopping() { return _stopping; } + /** hook to I2PSnarkUtil for the servlet */ public I2PSnarkUtil util() { return _util; } @@ -871,7 +884,7 @@ public class SnarkManager implements Snark.CompleteListener { torrent.startTorrent(); addMessage(_("Fetching {0}", name)); boolean haveSavedPeers = false; - if ((!util().connected()) && !haveSavedPeers) { + if ((_util.connected()) && !haveSavedPeers) { addMessage(_("We have no saved peers and no other torrents are running. " + "Fetch of {0} will not succeed until you start another torrent.", name)); } @@ -1603,21 +1616,137 @@ public class SnarkManager implements Snark.CompleteListener { } } - private class SnarkManagerShutdown extends I2PAppThread { - @Override - public void run() { - Set names = listTorrentFiles(); - int running = 0; - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - Snark snark = getTorrent((String)iter.next()); - if (snark != null && !snark.isStopped()) { - snark.stopTorrent(); - try { Thread.sleep(50); } catch (InterruptedException ie) {} + /** + * If not connected, thread it, otherwise inline + * @since 0.9.1 + */ + public void startTorrent(byte[] infoHash) { + for (Snark snark : _snarks.values()) { + if (DataHelper.eq(infoHash, snark.getInfoHash())) { + if (snark.isStarting() || !snark.isStopped()) { + addMessage("Torrent already started"); + return; } + boolean connected = _util.connected(); + if ((!connected) && !_util.isConnecting()) + addMessage(_("Opening the I2P tunnel")); + addMessage(_("Starting up torrent {0}", snark.getBaseName())); + if (connected) { + snark.startTorrent(); + } else { + // mark it for the UI + snark.setStarting(); + (new I2PAppThread(new ThreadedStarter(snark), "TorrentStarter", true)).start(); + try { Thread.sleep(200); } catch (InterruptedException ie) {} + } + return; + } + } + addMessage("Torrent not found?"); + } + + /** + * If not connected, thread it, otherwise inline + * @since 0.9.1 + */ + public void startAllTorrents() { + if (_util.connected()) { + startAll(); + } else { + addMessage(_("Opening the I2P tunnel and starting all torrents.")); + for (Snark snark : _snarks.values()) { + // mark it for the UI + snark.setStarting(); + } + (new I2PAppThread(new ThreadedStarter(null), "TorrentStarterAll", true)).start(); + try { Thread.sleep(200); } catch (InterruptedException ie) {} + } + } + + /** + * Use null constructor param for all + * @since 0.9.1 + */ + private class ThreadedStarter implements Runnable { + private final Snark snark; + public ThreadedStarter(Snark s) { snark = s; } + public void run() { + if (snark != null) { + if (snark.isStopped()) + snark.startTorrent(); + } else { + startAll(); } } } + /** + * Inline + * @since 0.9.1 + */ + private void startAll() { + for (Snark snark : _snarks.values()) { + if (snark.isStopped()) + snark.startTorrent(); + } + } + + /** + * Stop all running torrents, and close the tunnel after a delay + * to allow for announces. + * If called at router shutdown via Jetty shutdown hook -> webapp destroy() -> stop(), + * the tunnel won't actually be closed as the SimpleScheduler is already shutdown + * or will be soon, so we delay a few seconds inline. + * @param finalShutdown if true, sleep at the end if any torrents were running + * @since 0.9.1 + */ + public void stopAllTorrents(boolean finalShutdown) { + _stopping = true; + if (finalShutdown && _log.shouldLog(Log.WARN)) + _log.warn("SnarkManager final shutdown"); + int count = 0; + for (Snark snark : _snarks.values()) { + if (!snark.isStopped()) { + if (count == 0) + addMessage(_("Stopping all torrents and closing the I2P tunnel.")); + count++; + if (finalShutdown) + snark.stopTorrent(true); + else + stopTorrent(snark, false); + // Throttle since every unannounce is now threaded. + // How to do this without creating a ton of threads? + try { Thread.sleep(20); } catch (InterruptedException ie) {} + } + } + if (_util.connected()) { + if (count > 0) { + // Schedule this even for final shutdown, as there's a chance + // that it's just this webapp that is stopping. + SimpleScheduler.getInstance().addEvent(new Disconnector(), 60*1000); + addMessage(_("Closing I2P tunnel after notifying trackers.")); + if (finalShutdown) { + try { Thread.sleep(5*1000); } catch (InterruptedException ie) {} + } + } else { + _util.disconnect(); + _stopping = false; + addMessage(_("I2P tunnel closed.")); + } + } + } + + /** @since 0.9.1 */ + private class Disconnector implements SimpleTimer.TimedEvent { + public void timeReached() { + if (_util.connected()) { + _util.disconnect(); + _stopping = false; + addMessage(_("I2P tunnel closed.")); + } + } + } + /** * ignore case, current locale * @since 0.9 diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java index f41fc5c3a..7859220ca 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkShutdown.java @@ -26,6 +26,7 @@ import net.i2p.util.I2PAppThread; /** * Makes sure everything ends correctly when shutting down. + * @deprecated unused */ public class SnarkShutdown extends I2PAppThread { @@ -61,7 +62,7 @@ public class SnarkShutdown extends I2PAppThread //Snark.debug("Halting TrackerClient...", Snark.INFO); if (trackerclient != null) - trackerclient.halt(); + trackerclient.halt(true); //Snark.debug("Halting PeerCoordinator...", Snark.INFO); if (coordinator != null) diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index a0ddd32cb..e4d9e958c 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -28,6 +28,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -97,6 +98,7 @@ public class TrackerClient implements Runnable { // these 2 used in loop() private volatile boolean runStarted; private volatile int consecutiveFails; + private volatile boolean _fastUnannounce; private final List trackers; @@ -134,6 +136,7 @@ public class TrackerClient implements Runnable { stop = false; consecutiveFails = 0; runStarted = false; + _fastUnannounce = false; _thread = new I2PAppThread(this, _threadName + " #" + (++_runCount), true); _thread.start(); started = true; @@ -144,8 +147,9 @@ public class TrackerClient implements Runnable { /** * Interrupts this Thread to stop it. + * @param fast if true, limit the life of the unannounce threads */ - public synchronized void halt() { + public synchronized void halt(boolean fast) { boolean wasStopped = stop; if (wasStopped) { if (_log.shouldLog(Log.WARN)) @@ -168,6 +172,7 @@ public class TrackerClient implements Runnable { _log.debug("Interrupting " + t.getName()); t.interrupt(); } + _fastUnannounce = true; if (!wasStopped) unannounce(); } @@ -236,7 +241,7 @@ public class TrackerClient implements Runnable { * Do this one time only (not every time it is started). * @since 0.9.1 */ - public void setup() { + private void setup() { // Construct the list of trackers for this torrent, // starting with the primary one listed in the metainfo, // followed by the secondary open trackers @@ -415,6 +420,9 @@ public class TrackerClient implements Runnable { tr.interval = LONG_SLEEP; // slow down } } + } else { + _util.debug("Not announcing to " + tr.announce + " last announce was " + + new Date(tr.lastRequestTime) + " interval is " + DataHelper.formatDuration(tr.interval), Snark.INFO); } if ((!tr.stop) && maxSeenPeers < tr.seenPeers) maxSeenPeers = tr.seenPeers; @@ -439,6 +447,8 @@ public class TrackerClient implements Runnable { } } } + } else { + _util.debug("Not getting PEX peers", Snark.INFO); } // Get peers from DHT @@ -476,6 +486,8 @@ public class TrackerClient implements Runnable { } } } + } else { + _util.debug("Not getting DHT peers", Snark.INFO); } @@ -534,7 +546,7 @@ public class TrackerClient implements Runnable { if (_util.connected() && tr.started && (!tr.stop) && tr.trackerProblems == null) { try { - (new I2PAppThread(new Unannouncer(tr), _threadName + " Unannounce " + (++i), true)).start(); + (new I2PAppThread(new Unannouncer(tr), _threadName + " U" + (++i), true)).start(); } catch (OutOfMemoryError oom) { // probably ran out of threads, ignore tr.reset(); @@ -611,8 +623,9 @@ public class TrackerClient implements Runnable { _util.debug("Sending TrackerClient request: " + s, Snark.INFO); tr.lastRequestTime = System.currentTimeMillis(); - // Don't wait for a response to stopped. - File fetched = _util.get(s, true, event.equals(STOPPED_EVENT) ? -1 : 0); + // Don't wait for a response to stopped when shutting down + boolean fast = _fastUnannounce && event.equals(STOPPED_EVENT); + File fetched = _util.get(s, true, fast ? -1 : 0); if (fetched == null) { throw new IOException("Error fetching " + s); } @@ -671,7 +684,7 @@ public class TrackerClient implements Runnable { * @return true for i2p hosts only * @since 0.7.12 */ - static boolean isValidAnnounce(String ann) { + public static boolean isValidAnnounce(String ann) { URL url; try { url = new URL(ann); diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index 7633574fc..5fcf95190 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -74,6 +74,7 @@ public class I2PSnarkServlet extends DefaultServlet { _context = I2PAppContext.getGlobalContext(); _log = _context.logManager().getLog(I2PSnarkServlet.class); _nonce = _context.random().nextLong(); + // FIXME instantiate new one every time _manager = SnarkManager.instance(); String configFile = _context.getProperty(PROP_CONFIG_FILE); if ( (configFile == null) || (configFile.trim().length() <= 0) ) @@ -322,7 +323,7 @@ public class I2PSnarkServlet extends DefaultServlet { final long stats[] = {0,0,0,0,0,0}; String peerParam = req.getParameter("p"); - List snarks = getSortedSnarks(req); + List snarks = getSortedSnarks(req); boolean isForm = _manager.util().connected() || !snarks.isEmpty(); if (isForm) { out.write("
\n"); @@ -407,7 +408,9 @@ public class I2PSnarkServlet extends DefaultServlet { ua.startsWith("Dillo")); boolean noThinsp = isDegraded || (ua != null && ua.startsWith("Opera")); - if (_manager.util().connected()) { + if (_manager.isStopping()) { + out.write(" "); + } else if (_manager.util().connected()) { if (isDegraded) out.write(""); if (isDegraded) out.write(""); - } else if (!snarks.isEmpty()) { + } else if ((!_manager.util().isConnecting()) && !snarks.isEmpty()) { if (isDegraded) out.write(" dirs = new TreeSet(Collections.reverseOrder()); @@ -659,16 +656,20 @@ public class I2PSnarkServlet extends DefaultServlet { // step 3 delete dirs bottom-up for (File df : dirs) { if (df.delete()) { - _manager.addMessage(_("Data dir deleted: {0}", df.getAbsolutePath())); - } else if (_log.shouldLog(Log.WARN)) { - _log.warn("Could not delete dir " + df); + //_manager.addMessage(_("Data dir deleted: {0}", df.getAbsolutePath())); + } else { + _manager.addMessage(_("Directory could not be deleted: {0}", df.getAbsolutePath())); + if (_log.shouldLog(Log.WARN)) + _log.warn("Could not delete dir " + df); } } // step 4 delete base if (f.delete()) { - _manager.addMessage(_("Data dir deleted: {0}", f.getAbsolutePath())); - } else if (_log.shouldLog(Log.WARN)) { - _log.warn("Could not delete dir " + f); + _manager.addMessage(_("Directory deleted: {0}", f.getAbsolutePath())); + } else { + _manager.addMessage(_("Directory could not be deleted: {0}", f.getAbsolutePath())); + if (_log.shouldLog(Log.WARN)) + _log.warn("Could not delete dir " + f); } break; } @@ -740,29 +741,9 @@ public class I2PSnarkServlet extends DefaultServlet { _manager.addMessage(_("Error creating torrent - you must enter a file or directory")); } } else if ("StopAll".equals(action)) { - _manager.addMessage(_("Stopping all torrents and closing the I2P tunnel.")); - List snarks = getSortedSnarks(req); - for (int i = 0; i < snarks.size(); i++) { - Snark snark = (Snark)snarks.get(i); - if (!snark.isStopped()) { - _manager.stopTorrent(snark, false); - try { Thread.sleep(50); } catch (InterruptedException ie) {} - } - } - if (_manager.util().connected()) { - // Give the stopped announces time to get out - try { Thread.sleep(2000); } catch (InterruptedException ie) {} - _manager.util().disconnect(); - _manager.addMessage(_("I2P tunnel closed.")); - } + _manager.stopAllTorrents(false); } else if ("StartAll".equals(action)) { - _manager.addMessage(_("Opening the I2P tunnel and starting all torrents.")); - List snarks = getSortedSnarks(req); - for (int i = 0; i < snarks.size(); i++) { - Snark snark = (Snark)snarks.get(i); - if (snark.isStopped()) - snark.startTorrent(); - } + _manager.startAllTorrents(); } else if ("Clear".equals(action)) { _manager.clearMessages(); } else { @@ -826,7 +807,7 @@ public class I2PSnarkServlet extends DefaultServlet { name = name.trim(); hurl = hurl.trim(); aurl = aurl.trim().replace("=", "="); - if (name.length() > 0 && hurl.startsWith("http://") && aurl.startsWith("http://")) { + if (name.length() > 0 && hurl.startsWith("http://") && TrackerClient.isValidAnnounce(aurl)) { Map trackers = _manager.getTrackerMap(); trackers.put(name, new Tracker(name, aurl, hurl)); _manager.saveTrackerMap(); @@ -998,6 +979,8 @@ public class I2PSnarkServlet extends DefaultServlet { statusString = "\"\"" + _("Tracker Error") + "
" + err; } + } else if (snark.isStarting()) { + statusString = "\"\"" + _("Starting"); } else if (remaining == 0 || needed == 0) { // < 0 means no meta size yet // partial complete or seeding if (isRunning) { @@ -1150,6 +1133,7 @@ public class I2PSnarkServlet extends DefaultServlet { if (showPeers) parameters = parameters + "&p=1"; if (isRunning) { + // Stop Button if (isDegraded) out.write("
"); if (isDegraded) out.write(""); - } else { + } else if (!snark.isStarting()) { + if (!_manager.isStopping()) { + // Start Button // This works in Opera but it's displayed a little differently, so use noThinsp here too so all 3 icons are consistent if (noThinsp) out.write(""); if (isDegraded) out.write(""); - + } if (isValid) { + // Remove Button // Doesnt work with Opera so use noThinsp instead of isDegraded if (noThinsp) out.write(""); } + // Delete Button // Doesnt work with Opera so use noThinsp instead of isDegraded if (noThinsp) out.write(" getSessions() { return new ArrayList(_sessions); } @@ -207,6 +208,11 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging { _sessions.remove(session); } + /** + * Generic options used for clients and servers. + * NOT a copy, Do NOT modify for per-connection options, make a copy. + * @return NOT a copy, do NOT modify for per-connection options + */ public Properties getClientOptions() { return _clientOptions; } private void addtask(I2PTunnelTask tsk) { @@ -326,10 +332,14 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging { /** * Configure the extra I2CP options to use in any subsequent I2CP sessions. + * Generic options used for clients and servers * Usage: "clientoptions[ key=value]*" . * * Sets the event "clientoptions_onResult" = "ok" after completion. * + * Deprecated To be made private, use setClientOptions(). + * This does NOT update a running TunnelTask. + * * @param args each args[i] is a key=value pair to add to the options * @param l logger to receive events and output */ @@ -347,6 +357,27 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging { notifyEvent("clientoptions_onResult", "ok"); } + /** + * Generic options used for clients and servers. + * This DOES update a running TunnelTask, but NOT the session. + * A more efficient runClientOptions(). + * + * @param opts non-null + * @since 0.9.1 + */ + public void setClientOptions(Properties opts) { + for (Iterator iter = _clientOptions.keySet().iterator(); iter.hasNext();) { + Object key = iter.next(); + if (!opts.containsKey(key)) + iter.remove(); + } + _clientOptions.putAll(opts); + for (I2PTunnelTask task : tasks) { + task.optionsUpdated(this); + } + notifyEvent("clientoptions_onResult", "ok"); + } + /** * Run the server pointing at the host and port specified using the private i2p * destination loaded from the specified file.

diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java index aca83e490..1e69e0e18 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java @@ -471,8 +471,9 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna } /** - * create the default options (using the default timeout, etc) - * + * Create the default options (using the default timeout, etc). + * Warning, this does not make a copy of I2PTunnel's client options, + * it modifies them directly. */ protected I2PSocketOptions getDefaultOptions() { Properties defaultOpts = getTunnel().getClientOptions(); @@ -483,8 +484,10 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna } /** - * create the default options (using the default timeout, etc) - * + * Create the default options (using the default timeout, etc). + * Warning, this does not make a copy of I2PTunnel's client options, + * it modifies them directly. + * Do not use overrides for per-socket options. */ protected I2PSocketOptions getDefaultOptions(Properties overrides) { Properties defaultOpts = getTunnel().getClientOptions(); @@ -495,6 +498,22 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna return opts; } + /** + * Update the I2PSocketManager. + * + * @since 0.9.1 + */ + @Override + public void optionsUpdated(I2PTunnel tunnel) { + if (getTunnel() != tunnel) + return; + I2PSocketManager sm = _ownDest ? sockMgr : socketManager; + if (sm == null) + return; + Properties props = tunnel.getClientOptions(); + sm.setDefaultOptions(sockMgr.buildOptions(props)); + } + /** * Create a new I2PSocket towards to the specified destination, * adding it to the list of connections actually managed by this diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java index 49261da24..f8113382d 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java @@ -132,8 +132,9 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R } /** - * create the default options (using the default timeout, etc) - * + * Create the default options (using the default timeout, etc). + * Warning, this does not make a copy of I2PTunnel's client options, + * it modifies them directly. */ @Override protected I2PSocketOptions getDefaultOptions() { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index 48a6a60b4..195d4fbad 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -225,7 +225,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } /** - * create the default options (using the default timeout, etc) + * Create the default options (using the default timeout, etc). + * Warning, this does not make a copy of I2PTunnel's client options, + * it modifies them directly. * unused? */ @Override @@ -244,8 +246,10 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } /** - * create the default options (using the default timeout, etc) - * + * Create the default options (using the default timeout, etc). + * Warning, this does not make a copy of I2PTunnel's client options, + * it modifies them directly. + * Do not use overrides for per-socket options. */ @Override protected I2PSocketOptions getDefaultOptions(Properties overrides) { @@ -330,6 +334,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn String ahelperKey = null; String userAgent = null; String authorization = null; + int remotePort = 0; while((line = reader.readLine(method)) != null) { line = line.trim(); if(_log.shouldLog(Log.DEBUG)) { @@ -486,10 +491,12 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn // Host becomes the destination's "{b32}.b32.i2p" string, or "i2p" on lookup failure host = getHostName(destination); - if(requestURI.getPort() >= 0) { - // TODO support I2P ports someday - //if (port >= 0) - // host = host + ':' + port; + int rPort = requestURI.getPort(); + if (rPort > 0) { + // Save it to put in the I2PSocketOptions, + remotePort = rPort; + /******** + // but strip it from the URL if(_log.shouldLog(Log.WARN)) { _log.warn(getPrefix(requestId) + "Removing port from [" + request + "]"); } @@ -500,6 +507,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn method = null; break; } + ******/ + } else { + remotePort = 80; } String query = requestURI.getRawQuery(); @@ -959,12 +969,16 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn return; } - Properties opts = new Properties(); + //Properties opts = new Properties(); //opts.setProperty("i2p.streaming.inactivityTimeout", ""+120*1000); // 1 == disconnect. see ConnectionOptions in the new streaming lib, which i // dont want to hard link to here //opts.setProperty("i2p.streaming.inactivityTimeoutAction", ""+1); - I2PSocket i2ps = createI2PSocket(clientDest, getDefaultOptions(opts)); + //I2PSocketOptions sktOpts = getDefaultOptions(opts); + I2PSocketOptions sktOpts = getDefaultOptions(); + if (remotePort > 0) + sktOpts.setPort(remotePort); + I2PSocket i2ps = createI2PSocket(clientDest, sktOpts); byte[] data = newRequest.toString().getBytes("ISO-8859-1"); Runnable onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId); new I2PTunnelHTTPClientRunner(s, i2ps, sockLock, data, mySockets, onTimeout); @@ -1174,8 +1188,8 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } public static final String DEFAULT_JUMP_SERVERS = "http://i2host.i2p/cgi-bin/i2hostjump?," + - "http://stats.i2p/cgi-bin/jump.cgi?a=," + - "http://i2jump.i2p/"; + "http://stats.i2p/cgi-bin/jump.cgi?a="; + //"http://i2jump.i2p/"; /** * @param jumpServers comma- or space-separated list, or null @@ -1207,15 +1221,21 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn StringTokenizer tok = new StringTokenizer(jumpServers, ", "); while(tok.hasMoreTokens()) { String jurl = tok.nextToken(); - if(!jurl.startsWith("http://")) { + String jumphost; + try { + URI jURI = new URI(jurl); + String proto = jURI.getScheme(); + jumphost = jURI.getHost(); + if (proto == null || jumphost == null || + !proto.toLowerCase(Locale.US).equals("http")) + continue; + jumphost = jumphost.toLowerCase(Locale.US); + if (!jumphost.endsWith(".i2p")) + continue; + } catch(URISyntaxException use) { continue; } // Skip jump servers we don't know - String jumphost = jurl.substring(7); // "http://" - jumphost = jumphost.substring(0, jumphost.indexOf('/')); - if(!jumphost.endsWith(".i2p")) { - continue; - } if(!jumphost.endsWith(".b32.i2p")) { Destination dest = I2PAppContext.getGlobalContext().namingService().lookup(jumphost); if(dest == null) { @@ -1227,8 +1247,8 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn out.write(jurl.getBytes()); out.write(uri.getBytes()); out.write("\">".getBytes()); - out.write(jurl.substring(7).getBytes()); - out.write(uri.getBytes()); + // Translators: parameter is a host name + out.write(_("{0} jump service", jumphost).getBytes()); out.write("\n".getBytes()); } } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java index 62845e3c6..7e666e7dd 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java @@ -77,7 +77,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { } private void setupI2PTunnelHTTPServer(String spoofHost) { - _spoofHost = spoofHost; + _spoofHost = (spoofHost != null && spoofHost.trim().length() > 0) ? spoofHost.trim() : null; getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpserver.blockingHandleTime", "how long the blocking handle takes to complete", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000, 3*60*60*1000 }); getTunnel().getContext().statManager().createRateStat("i2ptunnel.httpNullWorkaround", "How often an http server works around a streaming lib or i2ptunnel bug", "I2PTunnel.HTTPServer", new long[] { 60*1000, 10*60*1000 }); } @@ -96,6 +96,9 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { */ @Override protected void blockingHandle(I2PSocket socket) { + if (_log.shouldLog(Log.INFO)) + _log.info("Incoming connection to '" + toString() + "' port " + socket.getLocalPort() + + " from: " + socket.getPeerDestination().calculateHash() + " port " + socket.getPort()); long afterAccept = getTunnel().getContext().clock().now(); long afterSocket = -1; //local is fast, so synchronously. Does not need that many @@ -115,8 +118,21 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { addEntry(headers, DEST32_HEADER, Base32.encode(socket.getPeerDestination().calculateHash().getData()) + ".b32.i2p"); addEntry(headers, DEST64_HEADER, socket.getPeerDestination().toBase64()); - if ( (_spoofHost != null) && (_spoofHost.trim().length() > 0) ) - setEntry(headers, "Host", _spoofHost); + // Port-specific spoofhost + Properties opts = getTunnel().getClientOptions(); + String spoofHost; + int ourPort = socket.getLocalPort(); + if (ourPort != 80 && ourPort > 0 && ourPort <= 65535 && opts != null) { + String portSpoof = opts.getProperty("spoofedHost." + ourPort); + if (portSpoof != null) + spoofHost = portSpoof.trim(); + else + spoofHost = _spoofHost; + } else { + spoofHost = _spoofHost; + } + if (spoofHost != null) + setEntry(headers, "Host", spoofHost); setEntry(headers, "Connection", "close"); // we keep the enc sent by the browser before clobbering it, since it may have // been x-i2p-gzip @@ -134,7 +150,6 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { // request from the socket, modifies the headers, sends the request to the // server, reads the response headers, rewriting to include Content-encoding: x-i2p-gzip // if it was one of the Accept-encoding: values, and gzip the payload - Properties opts = getTunnel().getClientOptions(); boolean allowGZIP = true; if (opts != null) { String val = opts.getProperty("i2ptunnel.gzip"); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java index da063b0d0..b592ebcf8 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java @@ -111,6 +111,9 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { @Override protected void blockingHandle(I2PSocket socket) { + if (_log.shouldLog(Log.INFO)) + _log.info("Incoming connection to '" + toString() + "' port " + socket.getLocalPort() + + " from: " + socket.getPeerDestination().calculateHash() + " port " + socket.getPort()); try { String modifiedRegistration; if(!this.method.equals("webirc")) { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java index 7c231add1..059823980 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelServer.java @@ -295,6 +295,19 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { } } + /** + * Update the I2PSocketManager. + * + * @since 0.9.1 + */ + @Override + public void optionsUpdated(I2PTunnel tunnel) { + if (getTunnel() != tunnel || sockMgr == null) + return; + Properties props = tunnel.getClientOptions(); + sockMgr.setDefaultOptions(sockMgr.buildOptions(props)); + } + protected int getHandlerCount() { int rv = DEFAULT_HANDLER_COUNT; String cnt = getTunnel().getClientOptions().getProperty(PROP_HANDLER_COUNT); @@ -408,7 +421,8 @@ public class I2PTunnelServer extends I2PTunnelTask implements Runnable { protected void blockingHandle(I2PSocket socket) { if (_log.shouldLog(Log.INFO)) - _log.info("Incoming connection to '" + toString() + "' from: " + socket.getPeerDestination().calculateHash().toBase64()); + _log.info("Incoming connection to '" + toString() + "' port " + socket.getLocalPort() + + " from: " + socket.getPeerDestination().calculateHash() + " port " + socket.getPort()); long afterAccept = I2PAppContext.getGlobalContext().clock().now(); long afterSocket = -1; //local is fast, so synchronously. Does not need that many diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelTask.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelTask.java index 02d384850..4931168b3 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelTask.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelTask.java @@ -58,6 +58,15 @@ public abstract class I2PTunnelTask extends EventDispatcherImpl { public abstract boolean close(boolean forced); + /** + * Notify the task that I2PTunnel's options have been updated. + * Extending classes should override and call I2PTunnel.getClientOptions(), + * then update the I2PSocketManager. + * + * @since 0.9.1 + */ + public void optionsUpdated(I2PTunnel tunnel) {} + /** * For tasks that don't call I2PTunnel.addSession() directly * @since 0.8.13 diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index a2dc146d6..5baef65c2 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -4,8 +4,10 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Random; @@ -326,6 +328,7 @@ public class TunnelController implements Logging { _log.info("Releasing session " + s); TunnelControllerGroup.getInstance().release(this, s); } + // _sessions.clear() ???? } else { if (_log.shouldLog(Log.WARN)) _log.warn("No sessions to release? for " + getName()); @@ -384,20 +387,27 @@ public class TunnelController implements Logging { } } - 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); + /** + * These are the ones stored with a prefix of "option." + * + * @return keys with the "option." prefix stripped + * @since 0.9.1 Much better than getClientOptions() + */ + public Properties getClientOptionProps() { + Properties opts = new Properties(); + for (Map.Entry e : _config.entrySet()) { + String key = (String) e.getKey(); if (key.startsWith("option.")) { key = key.substring("option.".length()); - opts.add(key + "=" + val); + String val = (String) e.getValue(); + opts.setProperty(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); + return opts; + } + + private void setSessionOptions() { + _tunnel.setClientOptions(getClientOptionProps()); } private void setI2CPOptions() { @@ -431,25 +441,37 @@ public class TunnelController implements Logging { startTunnel(); } + /** + * As of 0.9.1, updates the options on an existing session + */ 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); + for (Map.Entry e : config.entrySet()) { + String key = (String) e.getKey(); if (key.startsWith(prefix)) { key = key.substring(prefix.length()); + String val = (String) e.getValue(); props.setProperty(key, val); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Set prop [" + key + "] to [" + val + "]"); } } _config = props; + // tell i2ptunnel, who will tell the TunnelTask, who will tell the SocketManager + setSessionOptions(); + if (_running && _sessions != null) { + for (I2PSession s : _sessions) { + // tell the router via the session + if (!s.isClosed()) { + s.updateOptions(_tunnel.getClientOptions()); + } + } + } } + 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); + for (Map.Entry e : _config.entrySet()) { + String key = (String) e.getKey(); + String val = (String) e.getValue(); rv.setProperty(prefix + key, val); } return rv; @@ -460,19 +482,27 @@ public class TunnelController implements Logging { public String getDescription() { return _config.getProperty("description"); } public String getI2CPHost() { return _config.getProperty("i2cpHost"); } public String getI2CPPort() { return _config.getProperty("i2cpPort"); } + + /** + * These are the ones with a prefix of "option." + * + * @return one big string of "key=val key=val ..." + * @deprecated why would you want this? Use getClientOptionProps() instead + */ public String getClientOptions() { StringBuilder opts = new StringBuilder(64); - for (Iterator iter = _config.keySet().iterator(); iter.hasNext(); ) { - String key = (String)iter.next(); - String val = _config.getProperty(key); + for (Map.Entry e : _config.entrySet()) { + String key = (String) e.getKey(); if (key.startsWith("option.")) { key = key.substring("option.".length()); + String val = (String) e.getValue(); 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"); } @@ -486,6 +516,7 @@ public class TunnelController implements Logging { /** default true */ public boolean getStartOnLoad() { return Boolean.valueOf(_config.getProperty("startOnLoad", "true")).booleanValue(); } public boolean getPersistentClientKey() { return Boolean.valueOf(_config.getProperty("option.persistentClientKey")).booleanValue(); } + public String getMyDestination() { if (_tunnel != null) { List sessions = _tunnel.getSessions(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java index 63c38dc75..36c377857 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java @@ -15,10 +15,12 @@ import java.net.Socket; import java.net.SocketException; import java.util.List; import java.util.Locale; +import java.util.Properties; import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketOptions; import net.i2p.data.DataFormatException; import net.i2p.util.HexDump; import net.i2p.util.Log; @@ -203,7 +205,10 @@ public class SOCKS4aServer extends SOCKSServer { // Let's not due a new Dest for every request, huh? //I2PSocketManager sm = I2PSocketManagerFactory.createManager(); //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null); - destSock = t.createI2PSocket(I2PAppContext.getGlobalContext().namingService().lookup(connHostName)); + Properties overrides = new Properties(); + I2PSocketOptions sktOpts = t.buildOptions(overrides); + sktOpts.setPort(connPort); + destSock = t.createI2PSocket(I2PAppContext.getGlobalContext().namingService().lookup(connHostName), sktOpts); } else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) { String err = "No localhost accesses allowed through the Socks Proxy"; _log.error(err); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java index 45915f6b3..0854c7993 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java @@ -366,7 +366,10 @@ public class SOCKS5Server extends SOCKSServer { } catch (IOException ioe) {} throw new SOCKSException("Host not found"); } - destSock = t.createI2PSocket(I2PAppContext.getGlobalContext().namingService().lookup(connHostName)); + Properties overrides = new Properties(); + I2PSocketOptions sktOpts = t.buildOptions(overrides); + sktOpts.setPort(connPort); + destSock = t.createI2PSocket(I2PAppContext.getGlobalContext().namingService().lookup(connHostName), sktOpts); } else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) { String err = "No localhost accesses allowed through the Socks Proxy"; _log.error(err); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java index 935abdd2f..172e3eb57 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -376,18 +376,6 @@ public class EditBean extends IndexBean { */ private static Properties getOptions(TunnelController controller) { if (controller == null) return null; - String opts = controller.getClientOptions(); - StringTokenizer tok = new StringTokenizer(opts); - Properties props = new Properties(); - 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); - props.setProperty(key, val); - } - return props; + return controller.getClientOptionProps(); } } diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index 147162ca0..7aef0ab31 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -497,8 +497,6 @@