From 819d8575506144a16e3171ac5e892b39e8ecb68b Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 8 Dec 2008 15:03:45 +0000 Subject: [PATCH 001/191] Do not build tests --- apps/streaming/java/build.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/streaming/java/build.xml b/apps/streaming/java/build.xml index 568367033..a32ca077c 100644 --- a/apps/streaming/java/build.xml +++ b/apps/streaming/java/build.xml @@ -25,7 +25,14 @@ + + + @@ -33,6 +40,9 @@ + + + From 1fdd228a9d1b303eebc0819882d767fcbaca6e75 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 8 Dec 2008 16:38:46 +0000 Subject: [PATCH 002/191] constructor fix --- core/java/src/net/i2p/util/I2PAppThread.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/src/net/i2p/util/I2PAppThread.java b/core/java/src/net/i2p/util/I2PAppThread.java index fd7256578..a4e12c8b1 100644 --- a/core/java/src/net/i2p/util/I2PAppThread.java +++ b/core/java/src/net/i2p/util/I2PAppThread.java @@ -40,7 +40,7 @@ public class I2PAppThread extends I2PThread { super(r, name); } public I2PAppThread(Runnable r, String name, boolean isDaemon) { - super(r, name); + super(r, name, isDaemon); } protected void fireOOM(OutOfMemoryError oom) { From 962a8f6f49763fc51c62faa0dd7fbae9031ae507 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 10 Dec 2008 15:37:28 +0000 Subject: [PATCH 003/191] more splitting classes --- .../net/i2p/router/ClientManagerFacade.java | 26 ---------- .../src/net/i2p/router/CommSystemFacade.java | 2 + .../i2p/router/DummyClientManagerFacade.java | 47 +++++++++++++++++ .../i2p/router/DummyPeerManagerFacade.java | 32 ++++++++++++ .../i2p/router/DummyTunnelManagerFacade.java | 52 +++++++++++++++++++ .../src/net/i2p/router/PeerManagerFacade.java | 12 ----- .../net/i2p/router/TunnelManagerFacade.java | 32 ------------ .../kademlia/RepublishLeaseSetJob.java | 34 ++++++------ .../router/peermanager/PersistProfileJob.java | 29 +++++++++++ .../peermanager/PersistProfilesJob.java | 22 -------- 10 files changed, 179 insertions(+), 109 deletions(-) create mode 100644 router/java/src/net/i2p/router/DummyClientManagerFacade.java create mode 100644 router/java/src/net/i2p/router/DummyPeerManagerFacade.java create mode 100644 router/java/src/net/i2p/router/DummyTunnelManagerFacade.java create mode 100644 router/java/src/net/i2p/router/peermanager/PersistProfileJob.java diff --git a/router/java/src/net/i2p/router/ClientManagerFacade.java b/router/java/src/net/i2p/router/ClientManagerFacade.java index 6dd1c8e21..07f6e561f 100644 --- a/router/java/src/net/i2p/router/ClientManagerFacade.java +++ b/router/java/src/net/i2p/router/ClientManagerFacade.java @@ -92,29 +92,3 @@ public abstract class ClientManagerFacade implements Service { public abstract SessionConfig getClientSessionConfig(Destination dest); public void renderStatusHTML(Writer out) throws IOException { } } - -class DummyClientManagerFacade extends ClientManagerFacade { - private RouterContext _context; - public DummyClientManagerFacade(RouterContext ctx) { - _context = ctx; - } - public boolean isLocal(Hash destHash) { return true; } - public boolean isLocal(Destination dest) { return true; } - public void reportAbuse(Destination dest, String reason, int severity) { } - public void messageReceived(ClientMessage msg) {} - public void requestLeaseSet(Destination dest, LeaseSet set, long timeout, - Job onCreateJob, Job onFailedJob) { - _context.jobQueue().addJob(onFailedJob); - } - public void startup() {} - public void stopAcceptingClients() { } - public void shutdown() {} - public void restart() {} - - public void messageDeliveryStatusUpdate(Destination fromDest, MessageId id, boolean delivered) {} - - public SessionConfig getClientSessionConfig(Destination _dest) { return null; } - - public void requestLeaseSet(Hash dest, LeaseSet set) {} - -} diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java index fc354e384..57ada6bf2 100644 --- a/router/java/src/net/i2p/router/CommSystemFacade.java +++ b/router/java/src/net/i2p/router/CommSystemFacade.java @@ -91,9 +91,11 @@ public abstract class CommSystemFacade implements Service { } +/** unused class DummyCommSystemFacade extends CommSystemFacade { public void shutdown() {} public void startup() {} public void restart() {} public void processMessage(OutNetMessage msg) { } } +**/ diff --git a/router/java/src/net/i2p/router/DummyClientManagerFacade.java b/router/java/src/net/i2p/router/DummyClientManagerFacade.java new file mode 100644 index 000000000..61e312875 --- /dev/null +++ b/router/java/src/net/i2p/router/DummyClientManagerFacade.java @@ -0,0 +1,47 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.SessionConfig; + +/** + * Manage all interactions with clients + * + * @author jrandom + */ +public class DummyClientManagerFacade extends ClientManagerFacade { + private RouterContext _context; + public DummyClientManagerFacade(RouterContext ctx) { + _context = ctx; + } + public boolean isLocal(Hash destHash) { return true; } + public boolean isLocal(Destination dest) { return true; } + public void reportAbuse(Destination dest, String reason, int severity) { } + public void messageReceived(ClientMessage msg) {} + public void requestLeaseSet(Destination dest, LeaseSet set, long timeout, + Job onCreateJob, Job onFailedJob) { + _context.jobQueue().addJob(onFailedJob); + } + public void startup() {} + public void stopAcceptingClients() { } + public void shutdown() {} + public void restart() {} + + public void messageDeliveryStatusUpdate(Destination fromDest, MessageId id, boolean delivered) {} + + public SessionConfig getClientSessionConfig(Destination _dest) { return null; } + + public void requestLeaseSet(Hash dest, LeaseSet set) {} + +} + diff --git a/router/java/src/net/i2p/router/DummyPeerManagerFacade.java b/router/java/src/net/i2p/router/DummyPeerManagerFacade.java new file mode 100644 index 000000000..221d0b2f1 --- /dev/null +++ b/router/java/src/net/i2p/router/DummyPeerManagerFacade.java @@ -0,0 +1,32 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.Writer; +import java.util.List; + +import net.i2p.data.Hash; + +/** + * Manage peer references and keep them up to date so that when asked for peers, + * it can provide appropriate peers according to the criteria provided. This + * includes periodically queueing up outbound messages to the peers to test them. + * + */ +class DummyPeerManagerFacade implements PeerManagerFacade { + public void shutdown() {} + public void startup() {} + public void restart() {} + public void renderStatusHTML(Writer out) { } + public List selectPeers(PeerSelectionCriteria criteria) { return null; } + public List getPeersByCapability(char capability) { return null; } + public void setCapabilities(Hash peer, String caps) {} + public void removeCapabilities(Hash peer) {} + public Hash selectRandomByCapability(char capability) { return null; } +} diff --git a/router/java/src/net/i2p/router/DummyTunnelManagerFacade.java b/router/java/src/net/i2p/router/DummyTunnelManagerFacade.java new file mode 100644 index 000000000..f7f204857 --- /dev/null +++ b/router/java/src/net/i2p/router/DummyTunnelManagerFacade.java @@ -0,0 +1,52 @@ +package net.i2p.router; +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.Writer; + +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.TunnelId; + +/** + * Build and maintain tunnels throughout the network. + * + */ +class DummyTunnelManagerFacade implements TunnelManagerFacade { + + public TunnelInfo getTunnelInfo(TunnelId id) { return null; } + public TunnelInfo selectInboundTunnel() { return null; } + public TunnelInfo selectInboundTunnel(Hash destination) { return null; } + public TunnelInfo selectOutboundTunnel() { return null; } + public TunnelInfo selectOutboundTunnel(Hash destination) { return null; } + public boolean isInUse(Hash peer) { return false; } + public boolean isValidTunnel(Hash client, TunnelInfo tunnel) { return false; } + public int getParticipatingCount() { return 0; } + public int getFreeTunnelCount() { return 0; } + public int getOutboundTunnelCount() { return 0; } + public int getInboundClientTunnelCount() { return 0; } + public int getOutboundClientTunnelCount() { return 0; } + public long getLastParticipatingExpiration() { return -1; } + public void buildTunnels(Destination client, ClientTunnelSettings settings) {} + public TunnelPoolSettings getInboundSettings() { return null; } + public TunnelPoolSettings getOutboundSettings() { return null; } + public TunnelPoolSettings getInboundSettings(Hash client) { return null; } + public TunnelPoolSettings getOutboundSettings(Hash client) { return null; } + public void setInboundSettings(TunnelPoolSettings settings) {} + public void setOutboundSettings(TunnelPoolSettings settings) {} + public void setInboundSettings(Hash client, TunnelPoolSettings settings) {} + public void setOutboundSettings(Hash client, TunnelPoolSettings settings) {} + public int getInboundBuildQueueSize() { return 0; } + + public void renderStatusHTML(Writer out) throws IOException {} + public void restart() {} + public void shutdown() {} + public void startup() {} +} diff --git a/router/java/src/net/i2p/router/PeerManagerFacade.java b/router/java/src/net/i2p/router/PeerManagerFacade.java index bebe1a927..791b77616 100644 --- a/router/java/src/net/i2p/router/PeerManagerFacade.java +++ b/router/java/src/net/i2p/router/PeerManagerFacade.java @@ -32,15 +32,3 @@ public interface PeerManagerFacade extends Service { public void removeCapabilities(Hash peer); public Hash selectRandomByCapability(char capability); } - -class DummyPeerManagerFacade implements PeerManagerFacade { - public void shutdown() {} - public void startup() {} - public void restart() {} - public void renderStatusHTML(Writer out) { } - public List selectPeers(PeerSelectionCriteria criteria) { return null; } - public List getPeersByCapability(char capability) { return null; } - public void setCapabilities(Hash peer, String caps) {} - public void removeCapabilities(Hash peer) {} - public Hash selectRandomByCapability(char capability) { return null; } -} diff --git a/router/java/src/net/i2p/router/TunnelManagerFacade.java b/router/java/src/net/i2p/router/TunnelManagerFacade.java index be0b7e441..a6c1c9614 100644 --- a/router/java/src/net/i2p/router/TunnelManagerFacade.java +++ b/router/java/src/net/i2p/router/TunnelManagerFacade.java @@ -79,35 +79,3 @@ public interface TunnelManagerFacade extends Service { public void setInboundSettings(Hash client, TunnelPoolSettings settings); public void setOutboundSettings(Hash client, TunnelPoolSettings settings); } - -class DummyTunnelManagerFacade implements TunnelManagerFacade { - - public TunnelInfo getTunnelInfo(TunnelId id) { return null; } - public TunnelInfo selectInboundTunnel() { return null; } - public TunnelInfo selectInboundTunnel(Hash destination) { return null; } - public TunnelInfo selectOutboundTunnel() { return null; } - public TunnelInfo selectOutboundTunnel(Hash destination) { return null; } - public boolean isInUse(Hash peer) { return false; } - public boolean isValidTunnel(Hash client, TunnelInfo tunnel) { return false; } - public int getParticipatingCount() { return 0; } - public int getFreeTunnelCount() { return 0; } - public int getOutboundTunnelCount() { return 0; } - public int getInboundClientTunnelCount() { return 0; } - public int getOutboundClientTunnelCount() { return 0; } - public long getLastParticipatingExpiration() { return -1; } - public void buildTunnels(Destination client, ClientTunnelSettings settings) {} - public TunnelPoolSettings getInboundSettings() { return null; } - public TunnelPoolSettings getOutboundSettings() { return null; } - public TunnelPoolSettings getInboundSettings(Hash client) { return null; } - public TunnelPoolSettings getOutboundSettings(Hash client) { return null; } - public void setInboundSettings(TunnelPoolSettings settings) {} - public void setOutboundSettings(TunnelPoolSettings settings) {} - public void setInboundSettings(Hash client, TunnelPoolSettings settings) {} - public void setOutboundSettings(Hash client, TunnelPoolSettings settings) {} - public int getInboundBuildQueueSize() { return 0; } - - public void renderStatusHTML(Writer out) throws IOException {} - public void restart() {} - public void shutdown() {} - public void startup() {} -} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java index a6ca3c0bb..47753998c 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java @@ -81,23 +81,23 @@ public class RepublishLeaseSetJob extends JobImpl { _log.warn("FAILED publishing of the leaseSet for " + _dest.toBase64()); requeue(getContext().random().nextInt(60*1000)); } -} -class OnRepublishSuccess extends JobImpl { - public OnRepublishSuccess(RouterContext ctx) { super(ctx); } - public String getName() { return "Publish leaseSet successful"; } - public void runJob() { - //if (_log.shouldLog(Log.DEBUG)) - // _log.debug("successful publishing of the leaseSet for " + _dest.toBase64()); + class OnRepublishSuccess extends JobImpl { + public OnRepublishSuccess(RouterContext ctx) { super(ctx); } + public String getName() { return "Publish leaseSet successful"; } + public void runJob() { + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("successful publishing of the leaseSet for " + _dest.toBase64()); + } + } + + class OnRepublishFailure extends JobImpl { + private RepublishLeaseSetJob _job; + public OnRepublishFailure(RouterContext ctx, RepublishLeaseSetJob job) { + super(ctx); + _job = job; + } + public String getName() { return "Publish leaseSet failed"; } + public void runJob() { _job.requeueRepublish(); } } } - -class OnRepublishFailure extends JobImpl { - private RepublishLeaseSetJob _job; - public OnRepublishFailure(RouterContext ctx, RepublishLeaseSetJob job) { - super(ctx); - _job = job; - } - public String getName() { return "Publish leaseSet failed"; } - public void runJob() { _job.requeueRepublish(); } -} \ No newline at end of file diff --git a/router/java/src/net/i2p/router/peermanager/PersistProfileJob.java b/router/java/src/net/i2p/router/peermanager/PersistProfileJob.java new file mode 100644 index 000000000..3b230fd86 --- /dev/null +++ b/router/java/src/net/i2p/router/peermanager/PersistProfileJob.java @@ -0,0 +1,29 @@ +package net.i2p.router.peermanager; + +import java.util.Iterator; +import java.util.Set; + +import net.i2p.data.Hash; +import net.i2p.router.JobImpl; +import net.i2p.router.RouterContext; + +class PersistProfileJob extends JobImpl { + private PersistProfilesJob _job; + private Iterator _peers; + public PersistProfileJob(RouterContext enclosingContext, PersistProfilesJob job, Set peers) { + super(enclosingContext); + _peers = peers.iterator(); + _job = job; + } + public void runJob() { + if (_peers.hasNext()) + _job.persist((Hash)_peers.next()); + if (_peers.hasNext()) { + requeue(1000); + } else { + // no more left, requeue up the main persist-em-all job + _job.requeue(); + } + } + public String getName() { return "Persist profile"; } +} diff --git a/router/java/src/net/i2p/router/peermanager/PersistProfilesJob.java b/router/java/src/net/i2p/router/peermanager/PersistProfilesJob.java index 6d43923e3..fc137ccc8 100644 --- a/router/java/src/net/i2p/router/peermanager/PersistProfilesJob.java +++ b/router/java/src/net/i2p/router/peermanager/PersistProfilesJob.java @@ -1,6 +1,5 @@ package net.i2p.router.peermanager; -import java.util.Iterator; import java.util.Set; import net.i2p.data.Hash; @@ -25,24 +24,3 @@ class PersistProfilesJob extends JobImpl { void persist(Hash peer) { _mgr.storeProfile(peer); } void requeue() { requeue(PERSIST_DELAY); } } - -class PersistProfileJob extends JobImpl { - private PersistProfilesJob _job; - private Iterator _peers; - public PersistProfileJob(RouterContext enclosingContext, PersistProfilesJob job, Set peers) { - super(enclosingContext); - _peers = peers.iterator(); - _job = job; - } - public void runJob() { - if (_peers.hasNext()) - _job.persist((Hash)_peers.next()); - if (_peers.hasNext()) { - requeue(1000); - } else { - // no more left, requeue up the main persist-em-all job - _job.requeue(); - } - } - public String getName() { return "Persist profile"; } -} \ No newline at end of file From d16f187394f9e8a3dd0da69399a2cb6055ec0058 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 10 Dec 2008 16:25:09 +0000 Subject: [PATCH 004/191] change restart/shutdown/update links to buttons --- .../i2p/router/web/ConfigClientsHelper.java | 2 +- .../net/i2p/router/web/ConfigRestartBean.java | 26 +++++++++++++------ apps/routerconsole/jsp/summary.jsp | 15 ++++++----- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index 18e746644..d6441736a 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -79,7 +79,7 @@ public class ConfigClientsHelper { } buf.append("/> "); if (!enabled) { - buf.append(""); + buf.append(""); } buf.append(" ").append(desc).append("\n"); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java index 2b8817a31..3c9fe1bbf 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java @@ -1,5 +1,7 @@ package net.i2p.router.web; +import java.util.StringTokenizer; + import net.i2p.data.DataHelper; import net.i2p.router.Router; import net.i2p.router.RouterContext; @@ -47,27 +49,35 @@ public class ConfigRestartBean { return "Shutdown imminent"; } else { return "Shutdown in " + DataHelper.formatDuration(timeRemaining) + "
" - + "Shutdown immediately
" - + "Cancel shutdown "; + + buttons(urlBase, systemNonce, "shutdownImmediate,Shutdown immediately,cancelShutdown,Cancel shutdown"); } } else if (restarting) { if (timeRemaining <= 0) { return "Restart imminent"; } else { return "Restart in " + DataHelper.formatDuration(timeRemaining) + "
" - + "Restart immediately
" - + "Cancel restart "; + + buttons(urlBase, systemNonce, "restartImmediate,Restart immediately,cancelShutdown,Cancel restart"); } } else { - String shutdown = "Shutdown"; if (System.getProperty("wrapper.version") != null) - return "Restart " - + shutdown; + return buttons(urlBase, systemNonce, "restart,Restart,shutdown,Shutdown"); else - return shutdown; + return buttons(urlBase, systemNonce, "shutdown,Shutdown"); } } + /** @param s value,label,... pairs */ + private static String buttons(String url, String nonce, String s) { + StringBuffer buf = new StringBuffer(128); + StringTokenizer tok = new StringTokenizer(s, ","); + buf.append("
\n"); + buf.append("\n"); + while (tok.hasMoreTokens()) + buf.append("\n"); + buf.append("
\n"); + return buf.toString(); + } + private static boolean isShuttingDown(RouterContext ctx) { return Router.EXIT_GRACEFUL == ctx.router().scheduledGracefulExitCode(); } diff --git a/apps/routerconsole/jsp/summary.jsp b/apps/routerconsole/jsp/summary.jsp index 308088914..6668683e6 100644 --- a/apps/routerconsole/jsp/summary.jsp +++ b/apps/routerconsole/jsp/summary.jsp @@ -7,6 +7,9 @@ " /> + + +" />
General
@@ -25,15 +28,15 @@ if (prev != null) System.setProperty("net.i2p.router.web.UpdateHandler.noncePrev", prev); System.setProperty("net.i2p.router.web.UpdateHandler.nonce", nonce+""); String uri = request.getRequestURI(); - if (uri.indexOf('?') > 0) - uri = uri + "&updateNonce=" + nonce; - else - uri = uri + "?updateNonce=" + nonce; - out.print("
Update available"); + out.print("

\n"); + out.print("\n"); + out.print("

\n"); } } %> -
<%=net.i2p.router.web.ConfigRestartBean.renderStatus(request.getRequestURI(), request.getParameter("action"), request.getParameter("consoleNonce"))%> +

+ <%=net.i2p.router.web.ConfigRestartBean.renderStatus(request.getRequestURI(), request.getParameter("action"), request.getParameter("consoleNonce"))%> +


Peers
From 0956393cf366f493a629eebf28c3f9c7f8520077 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 10 Dec 2008 16:32:26 +0000 Subject: [PATCH 005/191] add int getProperty(String prop, int default) --- core/java/src/net/i2p/I2PAppContext.java | 19 +++++++++++++++++++ .../src/net/i2p/router/RouterContext.java | 17 +++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java index cac8cdb29..3e514ea18 100644 --- a/core/java/src/net/i2p/I2PAppContext.java +++ b/core/java/src/net/i2p/I2PAppContext.java @@ -179,6 +179,25 @@ public class I2PAppContext { return System.getProperty(propName, defaultValue); } + /** + * Return an int with an int default + */ + public int getProperty(String propName, int defaultVal) { + String val = null; + if (_overrideProps != null) { + val = _overrideProps.getProperty(propName); + if (val == null) + val = System.getProperty(propName); + } + int ival = defaultVal; + if (val != null) { + try { + ival = Integer.parseInt(val); + } catch (NumberFormatException nfe) {} + } + return ival; + } + /** * Access the configuration attributes of this context, listing the properties * provided during the context construction, as well as the ones included in diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index 8b34f6364..087cde918 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -329,6 +329,23 @@ public class RouterContext extends I2PAppContext { return super.getProperty(propName, defaultVal); } + /** + * Return an int with an int default + */ + public int getProperty(String propName, int defaultVal) { + if (_router != null) { + String val = _router.getConfigSetting(propName); + if (val != null) { + int ival = defaultVal; + try { + ival = Integer.parseInt(val); + } catch (NumberFormatException nfe) {} + return ival; + } + } + return super.getProperty(propName, defaultVal); + } + /** * The context's synchronized clock, which is kept context specific only to * enable simulators to play with clock skew among different instances. From dae6fd47d9b810409ac94a3880a70e611d900877 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 13 Dec 2008 18:07:20 +0000 Subject: [PATCH 006/191] javadoc fixes --- core/java/src/net/i2p/client/package.html | 2 +- router/java/src/net/i2p/router/Router.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/java/src/net/i2p/client/package.html b/core/java/src/net/i2p/client/package.html index 2f67c3688..000a5584d 100644 --- a/core/java/src/net/i2p/client/package.html +++ b/core/java/src/net/i2p/client/package.html @@ -15,7 +15,7 @@ receives asynchronous notification of network activity by providing an implement of {@link net.i2p.client.I2PSessionListener}.

A simple example of how these base client classes can be used is the -{@link net.i2p.client.ATalk} application. It isn't really useful, but it is +ATalk application. It isn't really useful, but it is heavily documented code.

This client package provides the basic necessity for communicating over I2P, diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index 40ecc31be..591414fd0 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -1078,7 +1078,7 @@ public class Router { /** * What fraction of the bandwidth specified in our bandwidth limits should * we allow to be consumed by participating tunnels? - * @returns a number less than one, not a percentage! + * @return a number less than one, not a percentage! * */ public double getSharePercentage() { From 734818f65105996b8d5bcc88a49c731bc427a8fd Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 14 Dec 2008 14:07:37 +0000 Subject: [PATCH 007/191] * Transport: - Cleanup max connections code - Add i2np.udp.maxConnections - Set max connections based on share bandwidth - Add haveCapacity() that can be used for connection throttling in the router - Reject IBGW/OBEP requests when near connection limit - Reduce idle timeout when near connection limit * Tunnel request handler: - Require tunnel.dropLoad* stats - Speed up request loop --- core/java/src/net/i2p/stat/StatManager.java | 2 +- .../src/net/i2p/router/CommSystemFacade.java | 1 + .../transport/CommSystemFacadeImpl.java | 1 + .../net/i2p/router/transport/Transport.java | 1 + .../i2p/router/transport/TransportImpl.java | 29 ++++++++++++++ .../router/transport/TransportManager.java | 13 +++++++ .../router/transport/ntcp/EventPumper.java | 17 +++++++-- .../router/transport/ntcp/NTCPTransport.java | 18 ++++----- .../transport/udp/EstablishmentManager.java | 2 + .../router/transport/udp/UDPTransport.java | 38 ++++++++++++++++++- .../i2p/router/tunnel/pool/BuildExecutor.java | 5 ++- .../i2p/router/tunnel/pool/BuildHandler.java | 9 ++++- 12 files changed, 117 insertions(+), 19 deletions(-) diff --git a/core/java/src/net/i2p/stat/StatManager.java b/core/java/src/net/i2p/stat/StatManager.java index ffb7d6c52..528421048 100644 --- a/core/java/src/net/i2p/stat/StatManager.java +++ b/core/java/src/net/i2p/stat/StatManager.java @@ -50,7 +50,7 @@ public class StatManager { "tunnel.acceptLoad,tunnel.buildRequestTime,tunnel.rejectOverloaded,tunnel.rejectTimeout" + "tunnel.buildClientExpire,tunnel.buildClientReject,tunnel.buildClientSuccess," + "tunnel.buildExploratoryExpire,tunnel.buildExploratoryReject,tunnel.buildExploratorySuccess," + - "tunnel.buildRatio.*,tunnel.corruptMessage," + + "tunnel.buildRatio.*,tunnel.corruptMessage,tunnel.dropLoad*," + "tunnel.decryptRequestTime,tunnel.fragmentedDropped,tunnel.participatingMessageCount,"+ "tunnel.participatingTunnels,tunnel.testFailedTime,tunnel.testSuccessTime," + "tunnel.participatingBandwidth,udp.sendPacketSize,udp.packetsRetransmitted" ; diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java index 57ada6bf2..6d0927c63 100644 --- a/router/java/src/net/i2p/router/CommSystemFacade.java +++ b/router/java/src/net/i2p/router/CommSystemFacade.java @@ -34,6 +34,7 @@ public abstract class CommSystemFacade implements Service { public int countActivePeers() { return 0; } public int countActiveSendPeers() { return 0; } + public boolean haveCapacity() { return true; } public List getMostRecentErrorMessages() { return Collections.EMPTY_LIST; } /** diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java index b80e05694..d20c97846 100644 --- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java +++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java @@ -60,6 +60,7 @@ public class CommSystemFacadeImpl extends CommSystemFacade { public int countActivePeers() { return (_manager == null ? 0 : _manager.countActivePeers()); } public int countActiveSendPeers() { return (_manager == null ? 0 : _manager.countActiveSendPeers()); } + public boolean haveCapacity() { return (_manager == null ? false : _manager.haveCapacity()); } /** * Framed average clock skew of connected peers in seconds, or null if we cannot answer. diff --git a/router/java/src/net/i2p/router/transport/Transport.java b/router/java/src/net/i2p/router/transport/Transport.java index 84a37f68e..f95d2dc8f 100644 --- a/router/java/src/net/i2p/router/transport/Transport.java +++ b/router/java/src/net/i2p/router/transport/Transport.java @@ -40,6 +40,7 @@ public interface Transport { public int countActivePeers(); public int countActiveSendPeers(); + public boolean haveCapacity(); public Vector getClockSkews(); public List getMostRecentErrorMessages(); diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java index ba395b6b5..04a2cde91 100644 --- a/router/java/src/net/i2p/router/transport/TransportImpl.java +++ b/router/java/src/net/i2p/router/transport/TransportImpl.java @@ -31,7 +31,9 @@ import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.MessageSelector; import net.i2p.router.OutNetMessage; +import net.i2p.router.Router; import net.i2p.router.RouterContext; +import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.util.Log; /** @@ -78,6 +80,33 @@ public abstract class TransportImpl implements Transport { * How many peers are we actively sending messages to (this minute) */ public int countActiveSendPeers() { return 0; } + + /** Default is 500 for floodfills... */ + public static final int DEFAULT_MAX_CONNECTIONS = 500; + /** ...and 60/120/180/240/300 for BW Tiers K/L/M/N/O */ + public static final int MAX_CONNECTION_FACTOR = 60; + /** Per-transport connection limit */ + public int getMaxConnections() { + String style = getStyle(); + if (style.equals("SSU")) + style = "udp"; + else + style = style.toLowerCase(); + int def = DEFAULT_MAX_CONNECTIONS; + RouterInfo ri = _context.router().getRouterInfo(); + if (ri != null) { + char bw = ri.getBandwidthTier().charAt(0); + if (bw != 'U' && + ! ((FloodfillNetworkDatabaseFacade)_context.netDb()).floodfillEnabled()) + def = MAX_CONNECTION_FACTOR * (1 + bw - Router.CAPABILITY_BW12); + } + return _context.getProperty("i2np." + style + ".maxConnections", def); + } + + /** + * Can we initiate or accept a connection to another peer, saving some margin + */ + public boolean haveCapacity() { return true; } /** * Return our peer clock skews on a transport. diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java index 3d49780c3..d993a600d 100644 --- a/router/java/src/net/i2p/router/transport/TransportManager.java +++ b/router/java/src/net/i2p/router/transport/TransportManager.java @@ -151,6 +151,19 @@ public class TransportManager implements TransportEventListener { return peers; } + /** + * Is at least one transport below its connection limit + some margin + * Use for throttling in the router. + * Perhaps we should just use SSU? + */ + public boolean haveCapacity() { + for (int i = 0; i < _transports.size(); i++) { + if (((Transport)_transports.get(i)).haveCapacity()) + return true; + } + return false; + } + /** * Return our peer clock skews on all transports. * Vector composed of Long, each element representing a peer skew in seconds. diff --git a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java index 2f81318d9..d27a95172 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java +++ b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java @@ -39,6 +39,7 @@ public class EventPumper implements Runnable { private List _wantsRegister; private List _wantsConRegister; private NTCPTransport _transport; + private long _expireIdleWriteTime; private static final int BUF_SIZE = 8*1024; private static final int MAX_CACHE_SIZE = 64; @@ -50,6 +51,8 @@ public class EventPumper implements Runnable { * the time to iterate across them to check a few flags shouldn't be a problem. */ private static final long FAILSAFE_ITERATION_FREQ = 2*1000l; + private static final long MIN_EXPIRE_IDLE_TIME = 5*60*1000l; + private static final long MAX_EXPIRE_IDLE_TIME = 15*60*1000l; public EventPumper(RouterContext ctx, NTCPTransport transport) { _context = ctx; @@ -57,6 +60,7 @@ public class EventPumper implements Runnable { _transport = transport; _alive = false; _bufCache = new ArrayList(MAX_CACHE_SIZE); + _expireIdleWriteTime = MAX_EXPIRE_IDLE_TIME; } public void startPumping() { @@ -135,8 +139,12 @@ public class EventPumper implements Runnable { int failsafeWrites = 0; int failsafeCloses = 0; int failsafeInvalid = 0; - // pointless if we do this every 2 seconds? - long expireIdleWriteTime = 10*60*1000l; // + _context.random().nextLong(60*60*1000l); + + // Increase allowed idle time if we are well under allowed connections, otherwise decrease + if (_transport.haveCapacity()) + _expireIdleWriteTime = Math.min(_expireIdleWriteTime + 1000, MAX_EXPIRE_IDLE_TIME); + else + _expireIdleWriteTime = Math.max(_expireIdleWriteTime - 3000, MIN_EXPIRE_IDLE_TIME); for (Iterator iter = all.iterator(); iter.hasNext(); ) { try { SelectionKey key = (SelectionKey)iter.next(); @@ -181,8 +189,8 @@ public class EventPumper implements Runnable { failsafeWrites++; } - if ( con.getTimeSinceSend() > expireIdleWriteTime && - con.getTimeSinceReceive() > expireIdleWriteTime) { + if ( con.getTimeSinceSend() > _expireIdleWriteTime && + con.getTimeSinceReceive() > _expireIdleWriteTime) { // we haven't sent or received anything in a really long time, so lets just close 'er up con.close(); failsafeCloses++; @@ -680,4 +688,5 @@ public class EventPumper implements Runnable { private void expireTimedOut() { _transport.expireTimedOut(); } + public long getIdleTimeout() { return _expireIdleWriteTime; } } diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java index 349669066..7d78f618a 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java @@ -300,18 +300,13 @@ public class NTCPTransport extends TransportImpl { return _slowBid; } - private static final int DEFAULT_MAX_CONNECTIONS = 500; public boolean allowConnection() { - int max = DEFAULT_MAX_CONNECTIONS; - String mc = _context.getProperty("i2np.ntcp.maxConnections"); - if (mc != null) { - try { - max = Integer.parseInt(mc); - } catch (NumberFormatException nfe) {} - } - return countActivePeers() < max; + return countActivePeers() < getMaxConnections(); } + public boolean haveCapacity() { + return countActivePeers() < getMaxConnections() * 4 / 5; + } void sendComplete(OutNetMessage msg) { _finisher.add(msg); } /** async afterSend call, which can take some time w/ jobs, etc */ @@ -581,7 +576,10 @@ public class NTCPTransport extends TransportImpl { long totalRecv = 0; StringBuffer buf = new StringBuffer(512); - buf.append("NTCP connections: ").append(peers.size()).append("
\n"); + buf.append("NTCP connections: ").append(peers.size()); + buf.append(" limit: ").append(getMaxConnections()); + buf.append(" timeout: ").append(DataHelper.formatDuration(_pumper.getIdleTimeout())); + buf.append("
\n"); buf.append("\n"); buf.append(" "); buf.append(" "); diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java index f7ca62cc2..6ab159408 100644 --- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java +++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java @@ -271,6 +271,8 @@ public class EstablishmentManager { _log.warn("Receive session request from blocklisted IP: " + from); return; // drop the packet } + if (!_transport.allowConnection()) + return; // drop the packet state = new InboundEstablishState(_context, from.getIP(), from.getPort(), _transport.getLocalPort()); state.receiveSessionRequest(reader.getSessionRequestReader()); isNew = true; diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java index d40c64118..9dd328f7a 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java @@ -90,6 +90,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority /** list of RemoteHostId for peers whose packets we want to drop outright */ private List _dropList; + private int _expireTimeout; + private static final int DROPLIST_PERIOD = 10*60*1000; private static final int MAX_DROPLIST_SIZE = 256; @@ -159,6 +161,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _fragments = new OutboundMessageFragments(_context, this, _activeThrottle); _inboundFragments = new InboundMessageFragments(_context, _fragments, this); _flooder = new UDPFlooder(_context, this); + _expireTimeout = EXPIRE_TIMEOUT; _expireEvent = new ExpirePeerEvent(); _testEvent = new PeerTestEvent(); _reachabilityStatus = CommSystemFacade.STATUS_UNKNOWN; @@ -887,6 +890,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority return null; } } + if (!allowConnection()) + return null; if (_log.shouldLog(Log.DEBUG)) _log.debug("bidding on a message to an unestablished peer: " + to.toBase64()); @@ -922,6 +927,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority // in the IntroductionManager a chance to work. public static final int EXPIRE_TIMEOUT = 30*60*1000; private static final int MAX_IDLE_TIME = EXPIRE_TIMEOUT; + private static final int MIN_EXPIRE_TIMEOUT = 10*60*1000; public String getStyle() { return STYLE; } public void send(OutNetMessage msg) { @@ -1264,6 +1270,18 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority return getPeerState(dest) != null; } + public boolean allowConnection() { + synchronized (_peersByIdent) { + return _peersByIdent.size() < getMaxConnections(); + } + } + + public boolean haveCapacity() { + synchronized (_peersByIdent) { + return _peersByIdent.size() < getMaxConnections() * 4 / 5; + } + } + /** * Return our peer clock skews on this transport. * Vector composed of Long, each element representing a peer skew in seconds. @@ -1622,7 +1640,10 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority int numPeers = 0; StringBuffer buf = new StringBuffer(512); - buf.append("UDP connections: ").append(peers.size()).append("
\n"); + buf.append("UDP connections: ").append(peers.size()); + buf.append(" limit: ").append(getMaxConnections()); + buf.append(" timeout: ").append(DataHelper.formatDuration(_expireTimeout)); + buf.append("
\n"); buf.append("
peerdir
\n"); buf.append(" \n"); - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("\n\n\n"); - _postBodyBuffer.append("
peer"); if (sortFlags == FLAG_ALPHA) @@ -1951,12 +1972,25 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority _expireBuffer = new ArrayList(128); } public void timeReached() { - long inactivityCutoff = _context.clock().now() - EXPIRE_TIMEOUT; + // Increase allowed idle time if we are well under allowed connections, otherwise decrease + if (haveCapacity()) + _expireTimeout = Math.min(_expireTimeout + 15*1000, EXPIRE_TIMEOUT); + else + _expireTimeout = Math.max(_expireTimeout - 45*1000, MIN_EXPIRE_TIMEOUT); + long shortInactivityCutoff = _context.clock().now() - _expireTimeout; + long longInactivityCutoff = _context.clock().now() - EXPIRE_TIMEOUT; + long pingCutoff = _context.clock().now() - (2 * 60*60*1000); _expireBuffer.clear(); synchronized (_expirePeers) { int sz = _expirePeers.size(); for (int i = 0; i < sz; i++) { PeerState peer = (PeerState)_expirePeers.get(i); + long inactivityCutoff; + // if we offered to introduce them, or we used them as introducer in last 2 hours + if (peer.getWeRelayToThemAs() > 0 || peer.getIntroducerTime() > pingCutoff) + inactivityCutoff = longInactivityCutoff; + else + inactivityCutoff = shortInactivityCutoff; if ( (peer.getLastReceiveTime() < inactivityCutoff) && (peer.getLastSendTime() < inactivityCutoff) ) { _expireBuffer.add(peer); _expirePeers.remove(i); diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java index 6ca93d516..e1f040b8b 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java @@ -214,6 +214,9 @@ class BuildExecutor implements Runnable { } */ + /** Set 1.5 * LOOP_TIME < BuildRequestor.REQUEST_TIMEOUT/4 - margin */ + private static final int LOOP_TIME = 1000; + public void run() { _isRunning = true; List wanted = new ArrayList(8); @@ -316,7 +319,7 @@ class BuildExecutor implements Runnable { //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Nothin' doin (allowed=" + allowed + ", wanted=" + wanted.size() + ", pending=" + pendingRemaining + "), wait for a while"); //if (allowed <= 0) - _currentlyBuilding.wait(2000 + _context.random().nextInt(2*1000)); + _currentlyBuilding.wait((LOOP_TIME/2) + _context.random().nextInt(LOOP_TIME)); //else // wanted <= 0 // _currentlyBuilding.wait(_context.random().nextInt(30*1000)); } diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java index c5121e7c8..3c2a9dd20 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java @@ -498,8 +498,15 @@ class BuildHandler { } } + /* + * Being a IBGW or OBEP generally leads to more connections, so if we are + * approaching our connection limit (i.e. !haveCapacity()), + * reject this request. + */ if (response == 0 && (isInGW || isOutEnd) && - Boolean.valueOf(_context.getProperty(PROP_REJECT_NONPARTICIPANT)).booleanValue()) { + (Boolean.valueOf(_context.getProperty(PROP_REJECT_NONPARTICIPANT)).booleanValue() || + ! _context.commSystem().haveCapacity())) { + _context.throttle().setTunnelStatus("Rejecting tunnels: Connection limit"); response = TunnelHistory.TUNNEL_REJECT_BANDWIDTH; } From 847c9dafcec726e3742049460447c19830bd1cad Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 14 Dec 2008 15:03:11 +0000 Subject: [PATCH 008/191] * I2CP, HostsTxtNamingService, I2PTunnel: Implement Base32 Hash hostnames, via the naming service. Names are of the form [52-characters].i2p, where the 52 characters are the Base32 representation of our 256-byte hash. The client requests a lookup of the hash via a brief I2CP session using new I2CP request/reply messages. The router looks up the leaseset for the hash to convert the hash to a dest. Convert the I2PTunnel 'preview' links to use Base32 hostnames as a demonstration. --- .../net/i2p/i2ptunnel/TunnelController.java | 14 + .../src/net/i2p/i2ptunnel/web/IndexBean.java | 13 + apps/i2ptunnel/jsp/index.jsp | 14 +- .../i2p/client/DestReplyMessageHandler.java | 25 ++ .../client/I2PClientMessageHandlerMap.java | 9 +- core/java/src/net/i2p/client/I2PSession.java | 7 + .../src/net/i2p/client/I2PSessionImpl.java | 32 ++- .../src/net/i2p/client/I2PSessionImpl2.java | 6 +- .../src/net/i2p/client/I2PSimpleClient.java | 47 ++++ .../src/net/i2p/client/I2PSimpleSession.java | 123 +++++++++ .../client/naming/HostsTxtNamingService.java | 11 + .../src/net/i2p/client/naming/LookupDest.java | 72 +++++ core/java/src/net/i2p/data/Base32.java | 245 ++++++++++++++++++ .../net/i2p/data/i2cp/DestLookupMessage.java | 76 ++++++ .../net/i2p/data/i2cp/DestReplyMessage.java | 78 ++++++ .../net/i2p/data/i2cp/I2CPMessageHandler.java | 6 +- .../client/ClientMessageEventListener.java | 11 +- .../net/i2p/router/client/LookupDestJob.java | 54 ++++ 18 files changed, 822 insertions(+), 21 deletions(-) create mode 100644 core/java/src/net/i2p/client/DestReplyMessageHandler.java create mode 100644 core/java/src/net/i2p/client/I2PSimpleClient.java create mode 100644 core/java/src/net/i2p/client/I2PSimpleSession.java create mode 100644 core/java/src/net/i2p/client/naming/LookupDest.java create mode 100644 core/java/src/net/i2p/data/Base32.java create mode 100644 core/java/src/net/i2p/data/i2cp/DestLookupMessage.java create mode 100644 core/java/src/net/i2p/data/i2cp/DestReplyMessage.java create mode 100644 router/java/src/net/i2p/router/client/LookupDestJob.java diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 614bc1f74..955d3abd1 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -14,6 +14,7 @@ import net.i2p.I2PException; import net.i2p.client.I2PClient; import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; +import net.i2p.data.Base32; import net.i2p.data.Destination; import net.i2p.util.I2PThread; import net.i2p.util.Log; @@ -361,6 +362,19 @@ public class TunnelController implements Logging { return null; } + public String getMyDestHashBase32() { + if (_tunnel != null) { + List sessions = _tunnel.getSessions(); + for (int i = 0; i < sessions.size(); i++) { + I2PSession session = (I2PSession)sessions.get(i); + Destination dest = session.getMyDestination(); + if (dest != null) + return Base32.encode(dest.calculateHash().getData()); + } + } + return null; + } + public boolean getIsRunning() { return _running; } public boolean getIsStarting() { return _starting; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index c8d321ea2..8c361195a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -437,6 +437,19 @@ public class IndexBean { } } + public String getDestHashBase32(int tunnel) { + TunnelController tun = getController(tunnel); + if (tun != null) { + String rv = tun.getMyDestHashBase32(); + if (rv != null) + return rv; + else + return ""; + } else { + return ""; + } + } + /// /// bean props for form submission /// diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index bcec39c17..cc68096ad 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -180,13 +180,23 @@
- <%=indexBean.getServerTarget(curServer)%> + + <% + if ("httpserver".equals(indexBean.getInternalType(curServer))) { + %> + <%=indexBean.getServerTarget(curServer)%> + <% + } else { + %><%=indexBean.getServerTarget(curServer)%> + <% + } + %>
<% if ("httpserver".equals(indexBean.getInternalType(curServer)) && indexBean.getTunnelStatus(curServer) == IndexBean.RUNNING) { %> - Preview + Preview <% } else { %>No Preview diff --git a/core/java/src/net/i2p/client/DestReplyMessageHandler.java b/core/java/src/net/i2p/client/DestReplyMessageHandler.java new file mode 100644 index 000000000..25699ad02 --- /dev/null +++ b/core/java/src/net/i2p/client/DestReplyMessageHandler.java @@ -0,0 +1,25 @@ +package net.i2p.client; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import net.i2p.I2PAppContext; +import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.DestReplyMessage; + +/** + * Handle I2CP dest replies from the router + */ +class DestReplyMessageHandler extends HandlerImpl { + public DestReplyMessageHandler(I2PAppContext ctx) { + super(ctx, DestReplyMessage.MESSAGE_TYPE); + } + + public void handleMessage(I2CPMessage message, I2PSessionImpl session) { + _log.debug("Handle message " + message); + DestReplyMessage msg = (DestReplyMessage) message; + ((I2PSimpleSession)session).destReceived(msg.getDestination()); + } +} diff --git a/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java index 50b795571..6f0d95051 100644 --- a/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java +++ b/core/java/src/net/i2p/client/I2PClientMessageHandlerMap.java @@ -16,7 +16,6 @@ import net.i2p.data.i2cp.MessageStatusMessage; import net.i2p.data.i2cp.RequestLeaseSetMessage; import net.i2p.data.i2cp.SessionStatusMessage; import net.i2p.data.i2cp.SetDateMessage; -import net.i2p.util.Log; /** * Contains a map of message handlers that a session will want to use @@ -24,9 +23,11 @@ import net.i2p.util.Log; * @author jrandom */ class I2PClientMessageHandlerMap { - private final static Log _log = new Log(I2PClientMessageHandlerMap.class); /** map of message type id --> I2CPMessageHandler */ - private I2CPMessageHandler _handlers[]; + protected I2CPMessageHandler _handlers[]; + + /** for extension */ + public I2PClientMessageHandlerMap() {} public I2PClientMessageHandlerMap(I2PAppContext context) { int highest = DisconnectMessage.MESSAGE_TYPE; @@ -49,4 +50,4 @@ class I2PClientMessageHandlerMap { if ( (messageTypeId < 0) || (messageTypeId >= _handlers.length) ) return null; return _handlers[messageTypeId]; } -} \ No newline at end of file +} diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java index 9d053ef5d..627d1775a 100644 --- a/core/java/src/net/i2p/client/I2PSession.java +++ b/core/java/src/net/i2p/client/I2PSession.java @@ -12,6 +12,7 @@ package net.i2p.client; import java.util.Set; import net.i2p.data.Destination; +import net.i2p.data.Hash; import net.i2p.data.PrivateKey; import net.i2p.data.SessionKey; import net.i2p.data.SigningPrivateKey; @@ -126,4 +127,10 @@ public interface I2PSession { * Retrieve the signing SigningPrivateKey associated with the Destination */ public SigningPrivateKey getPrivateKey(); + + /** + * Lookup up a Hash + * + */ + public Destination lookupDest(Hash h) throws I2PSessionException; } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index 6b62513c5..78f4ba763 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -26,6 +26,7 @@ import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; +import net.i2p.data.Hash; import net.i2p.data.LeaseSet; import net.i2p.data.PrivateKey; import net.i2p.data.SessionKey; @@ -48,7 +49,7 @@ import net.i2p.util.SimpleTimer; * @author jrandom */ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessageEventListener { - private Log _log; + protected Log _log; /** who we are */ private Destination _myDestination; /** private key for decryption */ @@ -63,15 +64,15 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa private LeaseSet _leaseSet; /** hostname of router */ - private String _hostname; + protected String _hostname; /** port num to router */ - private int _portNum; + protected int _portNum; /** socket for comm */ - private Socket _socket; + protected Socket _socket; /** reader that always searches for messages */ - private I2CPMessageReader _reader; + protected I2CPMessageReader _reader; /** where we pipe our messages */ - private OutputStream _out; + protected OutputStream _out; /** who we send events to */ private I2PSessionListener _sessionListener; @@ -90,10 +91,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa private Object _leaseSetWait = new Object(); /** whether the session connection has already been closed (or not yet opened) */ - private boolean _closed; + protected boolean _closed; /** whether the session connection is in the process of being closed */ - private boolean _closing; + protected boolean _closing; /** have we received the current date from the router yet? */ private boolean _dateReceived; @@ -106,7 +107,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa * reading of other messages (in turn, potentially leading to deadlock) * */ - private AvailabilityNotifier _availabilityNotifier; + protected AvailabilityNotifier _availabilityNotifier; void dateUpdated() { _dateReceived = true; @@ -117,6 +118,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa public static final int LISTEN_PORT = 7654; + /** for extension */ + public I2PSessionImpl() {} + /** * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey * from the destKeyStream, and using the specified options to connect to the router @@ -151,7 +155,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa * Parse the config for anything we know about * */ - private void loadConfig(Properties options) { + protected void loadConfig(Properties options) { _options = new Properties(); _options.putAll(filter(options)); _hostname = _options.getProperty(I2PClient.PROP_TCP_HOST, "localhost"); @@ -385,7 +389,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa } } - private class AvailabilityNotifier implements Runnable { + protected class AvailabilityNotifier implements Runnable { private List _pendingIds; private List _pendingSizes; private boolean _alive; @@ -566,7 +570,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Destroy the session", new Exception("DestroySession()")); _closing = true; // we use this to prevent a race - if (sendDisconnect) { + if (sendDisconnect && _producer != null) { // only null if overridden by I2PSimpleSession try { _producer.disconnect(this); } catch (I2PSessionException ipe) { @@ -659,4 +663,8 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa } protected String getPrefix() { return "[" + (_sessionId == null ? -1 : _sessionId.getSessionId()) + "]: "; } + + public Destination lookupDest(Hash h) throws I2PSessionException { + return null; + } } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java index bbaf399f4..81c6ef22f 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl2.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java @@ -31,7 +31,6 @@ import net.i2p.util.Log; * @author jrandom */ class I2PSessionImpl2 extends I2PSessionImpl { - private Log _log; /** set of MessageState objects, representing all of the messages in the process of being sent */ private Set _sendingStates; @@ -41,6 +40,9 @@ class I2PSessionImpl2 extends I2PSessionImpl { private final static boolean SHOULD_COMPRESS = true; private final static boolean SHOULD_DECOMPRESS = true; + /** for extension */ + public I2PSessionImpl2() {} + /** * Create a new session, reading the Destination, PrivateKey, and SigningPrivateKey * from the destKeyStream, and using the specified options to connect to the router @@ -396,6 +398,8 @@ class I2PSessionImpl2 extends I2PSessionImpl { } private void clearStates() { + if (_sendingStates == null) // only null if overridden by I2PSimpleSession + return; synchronized (_sendingStates) { for (Iterator iter = _sendingStates.iterator(); iter.hasNext();) { MessageState state = (MessageState) iter.next(); diff --git a/core/java/src/net/i2p/client/I2PSimpleClient.java b/core/java/src/net/i2p/client/I2PSimpleClient.java new file mode 100644 index 000000000..9ce4b8d6f --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSimpleClient.java @@ -0,0 +1,47 @@ +package net.i2p.client; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.I2PException; +import net.i2p.data.Certificate; +import net.i2p.data.Destination; + +/** + * Simple client implementation with no Destination, + * just used to talk to the router. + */ +public class I2PSimpleClient implements I2PClient { + /** Don't do this */ + public Destination createDestination(OutputStream destKeyStream) throws I2PException, IOException { + return null; + } + + /** or this */ + public Destination createDestination(OutputStream destKeyStream, Certificate cert) throws I2PException, IOException { + return null; + } + + /** + * Create a new session (though do not connect it yet) + * + */ + public I2PSession createSession(InputStream destKeyStream, Properties options) throws I2PSessionException { + return createSession(I2PAppContext.getGlobalContext(), options); + } + /** + * Create a new session (though do not connect it yet) + * + */ + public I2PSession createSession(I2PAppContext context, Properties options) throws I2PSessionException { + return new I2PSimpleSession(context, options); + } +} diff --git a/core/java/src/net/i2p/client/I2PSimpleSession.java b/core/java/src/net/i2p/client/I2PSimpleSession.java new file mode 100644 index 000000000..fcfafe767 --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSimpleSession.java @@ -0,0 +1,123 @@ +package net.i2p.client; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Properties; +import java.util.Set; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.i2cp.DestLookupMessage; +import net.i2p.data.i2cp.DestReplyMessage; +import net.i2p.data.i2cp.I2CPMessageReader; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Create a new session for doing naming queries only. Do not create a Destination. + * Don't create a producer. Do not send/receive messages to other Destinations. + * Cannot handle multiple simultaneous queries atm. + * Could be expanded to ask the router other things. + */ +class I2PSimpleSession extends I2PSessionImpl2 { + private boolean _destReceived; + private Object _destReceivedLock; + private Destination _destination; + + /** + * Create a new session for doing naming queries only. Do not create a destination. + * + * @throws I2PSessionException if there is a problem + */ + public I2PSimpleSession(I2PAppContext context, Properties options) throws I2PSessionException { + _context = context; + _log = context.logManager().getLog(I2PSimpleSession.class); + _handlerMap = new SimpleMessageHandlerMap(context); + _closed = true; + _closing = false; + _availabilityNotifier = new AvailabilityNotifier(); + if (options == null) + options = System.getProperties(); + loadConfig(options); + } + + /** + * Connect to the router and establish a session. This call blocks until + * a session is granted. + * + * @throws I2PSessionException if there is a configuration error or the router is + * not reachable + */ + public void connect() throws I2PSessionException { + _closed = false; + _availabilityNotifier.stopNotifying(); + I2PThread notifier = new I2PThread(_availabilityNotifier); + notifier.setName("Simple Notifier"); + notifier.setDaemon(true); + notifier.start(); + + try { + _socket = new Socket(_hostname, _portNum); + _out = _socket.getOutputStream(); + synchronized (_out) { + _out.write(I2PClient.PROTOCOL_BYTE); + } + InputStream in = _socket.getInputStream(); + _reader = new I2CPMessageReader(in, this); + _reader.startReading(); + + } catch (UnknownHostException uhe) { + _closed = true; + throw new I2PSessionException(getPrefix() + "Bad host ", uhe); + } catch (IOException ioe) { + _closed = true; + throw new I2PSessionException(getPrefix() + "Problem connecting to " + _hostname + " on port " + _portNum, ioe); + } + } + + /** called by the message handler */ + void destReceived(Destination d) { + _destReceived = true; + _destination = d; + synchronized (_destReceivedLock) { + _destReceivedLock.notifyAll(); + } + } + + public Destination lookupDest(Hash h) throws I2PSessionException { + if (_closed) + return null; + _destReceivedLock = new Object(); + sendMessage(new DestLookupMessage(h)); + for (int i = 0; i < 10 && !_destReceived; i++) { + try { + synchronized (_destReceivedLock) { + _destReceivedLock.wait(1000); + } + } catch (InterruptedException ie) {} + } + _destReceived = false; + return _destination; + } + + /** + * Only map message handlers that we will use + */ + class SimpleMessageHandlerMap extends I2PClientMessageHandlerMap { + public SimpleMessageHandlerMap(I2PAppContext context) { + int highest = DestReplyMessage.MESSAGE_TYPE; + _handlers = new I2CPMessageHandler[highest+1]; + _handlers[DestReplyMessage.MESSAGE_TYPE] = new DestReplyMessageHandler(context); + } + } +} diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java index d4ee7e345..4cfb07d40 100644 --- a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java +++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java @@ -55,6 +55,8 @@ public class HostsTxtNamingService extends NamingService { return rv; } + private static final int BASE32_HASH_LENGTH = 52; // 1 + Hash.HASH_LENGTH * 8 / 5 + @Override public Destination lookup(String hostname) { Destination d = getCache(hostname); @@ -69,6 +71,15 @@ public class HostsTxtNamingService extends NamingService { return d; } + // Try Base32 decoding + if (hostname.length() == BASE32_HASH_LENGTH + 4 && hostname.endsWith(".i2p")) { + d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH)); + if (d != null) { + putCache(hostname, d); + return d; + } + } + List filenames = getFilenames(); for (int i = 0; i < filenames.size(); i++) { String hostsfile = (String)filenames.get(i); diff --git a/core/java/src/net/i2p/client/naming/LookupDest.java b/core/java/src/net/i2p/client/naming/LookupDest.java new file mode 100644 index 000000000..775ae6bcc --- /dev/null +++ b/core/java/src/net/i2p/client/naming/LookupDest.java @@ -0,0 +1,72 @@ +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ +package net.i2p.client.naming; + +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.client.I2PSessionException; +import net.i2p.client.I2PClient; +import net.i2p.client.I2PSession; +import net.i2p.client.I2PSimpleClient; +import net.i2p.data.Base32; +import net.i2p.data.Base64; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; + +/** + * Connect via I2CP and ask the router to look up + * the lease of a hash, convert it to a Destination and return it. + * Obviously this can take a while. + * + * All calls are blocking and return null on failure. + * Timeout is set to 10 seconds in I2PSimpleSession. + */ +class LookupDest { + + protected LookupDest(I2PAppContext context) {} + + static Destination lookupBase32Hash(I2PAppContext ctx, String key) { + byte[] h = Base32.decode(key); + if (h == null) + return null; + return lookupHash(ctx, h); + } + + /* Might be useful but not in the context of urls due to upper/lower case */ + /**** + static Destination lookupBase64Hash(I2PAppContext ctx, String key) { + byte[] h = Base64.decode(key); + if (h == null) + return null; + return lookupHash(ctx, h); + } + ****/ + + static Destination lookupHash(I2PAppContext ctx, byte[] h) { + Hash key = new Hash(h); + Destination rv = null; + try { + I2PClient client = new I2PSimpleClient(); + Properties opts = new Properties(); + String s = ctx.getProperty(I2PClient.PROP_TCP_HOST); + if (s != null) + opts.put(I2PClient.PROP_TCP_HOST, s); + s = ctx.getProperty(I2PClient.PROP_TCP_PORT); + if (s != null) + opts.put(I2PClient.PROP_TCP_PORT, s); + I2PSession session = client.createSession(null, opts); + session.connect(); + rv = session.lookupDest(key); + session.destroySession(); + } catch (I2PSessionException ise) {} + return rv; + } + + public static void main(String args[]) { + System.out.println(lookupBase32Hash(I2PAppContext.getGlobalContext(), args[0])); + } +} diff --git a/core/java/src/net/i2p/data/Base32.java b/core/java/src/net/i2p/data/Base32.java new file mode 100644 index 000000000..b2cc2d548 --- /dev/null +++ b/core/java/src/net/i2p/data/Base32.java @@ -0,0 +1,245 @@ +package net.i2p.data; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import net.i2p.util.Log; + +/** + * Encodes and decodes to and from Base32 notation. + * Ref: RFC 3548 + * + * Don't bother with '=' padding characters on encode or + * accept them on decode (i.e. don't require 5-character groups). + * No whitespace allowed. + * + * Decode accepts upper or lower case. + */ +public class Base32 { + + private final static Log _log = new Log(Base32.class); + + /** The 64 valid Base32 values. */ + private final static char[] ALPHABET = {'a', 'b', 'c', 'd', + 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', + '2', '3', '4', '5', '6', '7'}; + + /** + * Translates a Base32 value to either its 5-bit reconstruction value + * or a negative number indicating some other meaning. + * Allow upper or lower case. + **/ + private final static byte[] DECODABET = { + 26, 27, 28, 29, 30, 31, -9, -9, // Numbers two through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // Letters 'A' through 'M' + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'N' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, // Letters 'a' through 'm' + 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + }; + + private final static byte BAD_ENCODING = -9; // Indicates error in encoding + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + /** Defeats instantiation. */ + private Base32() { // nop + } + + public static void main(String[] args) { + if (args.length == 0) { + help(); + return; + } + runApp(args); + } + + private static void runApp(String args[]) { + try { + if ("encodestring".equalsIgnoreCase(args[0])) { + System.out.println(encode(args[1].getBytes())); + return; + } + InputStream in = System.in; + OutputStream out = System.out; + if (args.length >= 3) { + out = new FileOutputStream(args[2]); + } + if (args.length >= 2) { + in = new FileInputStream(args[1]); + } + if ("encode".equalsIgnoreCase(args[0])) { + encode(in, out); + return; + } + if ("decode".equalsIgnoreCase(args[0])) { + decode(in, out); + return; + } + } catch (IOException ioe) { + ioe.printStackTrace(System.err); + } + } + + private static byte[] read(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); + byte buf[] = new byte[4096]; + while (true) { + int read = in.read(buf); + if (read < 0) break; + baos.write(buf, 0, read); + } + return baos.toByteArray(); + } + + private static void encode(InputStream in, OutputStream out) throws IOException { + String encoded = encode(read(in)); + for (int i = 0; i < encoded.length(); i++) + out.write((byte)(encoded.charAt(i) & 0xFF)); + } + + private static void decode(InputStream in, OutputStream out) throws IOException { + byte decoded[] = decode(new String(read(in))); + if (decoded == null) { + System.out.println("FAIL"); + return; + } + out.write(decoded); + } + + private static void help() { + System.out.println("Syntax: Base32 encode "); + System.out.println("or : Base32 encode "); + System.out.println("or : Base32 encodestring "); + System.out.println("or : Base32 encode"); + System.out.println("or : Base32 decode "); + System.out.println("or : Base32 decode "); + System.out.println("or : Base32 decode"); + } + + public static String encode(String source) { + return (source != null ? encode(source.getBytes()) : ""); + } + + public static String encode(byte[] source) { + StringBuffer buf = new StringBuffer((source.length + 7) * 8 / 5); + encodeBytes(source, buf); + return buf.toString(); + } + + private final static byte[] emask = { (byte) 0x1f, + (byte) 0x01, (byte) 0x03, (byte) 0x07, (byte) 0x0f }; + /** + * Encodes a byte array into Base32 notation. + * + * @param source The data to convert + */ + private static void encodeBytes(byte[] source, StringBuffer out) { + int usedbits = 0; + for (int i = 0; i < source.length; ) { + int fivebits; + if (usedbits < 3) { + fivebits = (source[i] >> (3 - usedbits)) & 0x1f; + usedbits += 5; + } else if (usedbits == 3) { + fivebits = source[i++] & 0x1f; + usedbits = 0; + } else { + fivebits = (source[i++] << (usedbits - 3)) & 0x1f; + if (i < source.length) { + usedbits -= 3; + fivebits |= (source[i] >> (8 - usedbits)) & emask[usedbits]; + } + } + out.append(ALPHABET[fivebits]); + } + } + + /** + * Decodes data from Base32 notation and + * returns it as a string. + * + * @param s the string to decode + * @return The data as a string or null on failure + */ + public static String decodeToString(String s) { + byte[] b = decode(s); + if (b == null) + return null; + return new String(b); + } + + public static byte[] decode(String s) { + return decode(s.getBytes()); + } + + private final static byte[] dmask = { (byte) 0xf8, (byte) 0x7c, (byte) 0x3e, (byte) 0x1f, + (byte) 0x0f, (byte) 0x07, (byte) 0x03, (byte) 0x01 }; + /** + * Decodes Base32 content in byte array format and returns + * the decoded byte array. + * + * @param source The Base32 encoded data + * @return decoded data + */ + private static byte[] decode(byte[] source) { + int len58; + if (source.length <= 1) + len58 = source.length; + else + len58 = source.length * 5 / 8; + byte[] outBuff = new byte[len58]; + int outBuffPosn = 0; + + int usedbits = 0; + for (int i = 0; i < source.length; i++) { + int fivebits; + if ((source[i] & 0x80) != 0 || source[i] < '2' || source[i] > 'z') + fivebits = BAD_ENCODING; + else + fivebits = DECODABET[source[i] - '2']; + + if (fivebits >= 0) { + if (usedbits == 0) { + outBuff[outBuffPosn] = (byte) ((fivebits << 3) & 0xf8); + usedbits = 5; + } else if (usedbits < 3) { + outBuff[outBuffPosn] |= (fivebits << (3 - usedbits)) & dmask[usedbits]; + usedbits += 5; + } else if (usedbits == 3) { + outBuff[outBuffPosn++] |= fivebits; + usedbits = 0; + } else { + outBuff[outBuffPosn++] |= (fivebits >> (usedbits - 3)) & dmask[usedbits]; + byte next = (byte) (fivebits << (11 - usedbits)); + if (outBuffPosn < len58) { + outBuff[outBuffPosn] = next; + usedbits -= 3; + } else if (next != 0) { + _log.warn("Extra data at the end: " + next + "(decimal)"); + return null; + } + } + } else { + _log.warn("Bad Base32 input character at " + i + ": " + source[i] + "(decimal)"); + return null; + } + } + return outBuff; + } +} diff --git a/core/java/src/net/i2p/data/i2cp/DestLookupMessage.java b/core/java/src/net/i2p/data/i2cp/DestLookupMessage.java new file mode 100644 index 000000000..13135a2a4 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/DestLookupMessage.java @@ -0,0 +1,76 @@ +package net.i2p.data.i2cp; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; + +/** + * Request the router look up the dest for a hash + */ +public class DestLookupMessage extends I2CPMessageImpl { + public final static int MESSAGE_TYPE = 34; + private Hash _hash; + + public DestLookupMessage() { + super(); + } + + public DestLookupMessage(Hash h) { + _hash = h; + } + + public Hash getHash() { + return _hash; + } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + Hash h = new Hash(); + try { + h.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the hash", dfe); + } + _hash = h; + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_hash == null) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(Hash.HASH_LENGTH); + try { + _hash.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the hash", dfe); + } + return os.toByteArray(); + } + + public int getType() { + return MESSAGE_TYPE; + } + + public boolean equals(Object object) { + if ((object != null) && (object instanceof DestLookupMessage)) { + DestLookupMessage msg = (DestLookupMessage) object; + return DataHelper.eq(getHash(), msg.getHash()); + } + return false; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DestLookupMessage: "); + buf.append("\n\tHash: ").append(_hash); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java b/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java new file mode 100644 index 000000000..1ed601dc2 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/DestReplyMessage.java @@ -0,0 +1,78 @@ +package net.i2p.data.i2cp; + +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; + +/** + * Response to DestLookupMessage + * + */ +public class DestReplyMessage extends I2CPMessageImpl { + public final static int MESSAGE_TYPE = 35; + private Destination _dest; + + public DestReplyMessage() { + super(); + } + + public DestReplyMessage(Destination d) { + _dest = d; + } + + public Destination getDestination() { + return _dest; + } + + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + Destination d = new Destination(); + d.readBytes(in); + _dest = d; + } catch (DataFormatException dfe) { + _dest = null; // null dest allowed + } + } + + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_dest == null) + return new byte[0]; // null response allowed + ByteArrayOutputStream os = new ByteArrayOutputStream(_dest.size()); + try { + _dest.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the dest", dfe); + } + return os.toByteArray(); + } + + public int getType() { + return MESSAGE_TYPE; + } + + public boolean equals(Object object) { + if ((object != null) && (object instanceof DestReplyMessage)) { + DestReplyMessage msg = (DestReplyMessage) object; + return DataHelper.eq(getDestination(), msg.getDestination()); + } + return false; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[DestReplyMessage: "); + buf.append("\n\tDestination: ").append(_dest); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java index 481d26f0e..128c312dc 100644 --- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java @@ -81,6 +81,10 @@ public class I2CPMessageHandler { return new GetDateMessage(); case SetDateMessage.MESSAGE_TYPE: return new SetDateMessage(); + case DestLookupMessage.MESSAGE_TYPE: + return new DestLookupMessage(); + case DestReplyMessage.MESSAGE_TYPE: + return new DestReplyMessage(); default: throw new I2CPMessageException("The type " + type + " is an unknown I2CP message"); } @@ -94,4 +98,4 @@ public class I2CPMessageHandler { e.printStackTrace(); } } -} \ No newline at end of file +} diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java index d75e27fb4..033e28f2f 100644 --- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -11,6 +11,7 @@ package net.i2p.router.client; import net.i2p.data.Payload; import net.i2p.data.i2cp.CreateLeaseSetMessage; import net.i2p.data.i2cp.CreateSessionMessage; +import net.i2p.data.i2cp.DestLookupMessage; import net.i2p.data.i2cp.DestroySessionMessage; import net.i2p.data.i2cp.GetDateMessage; import net.i2p.data.i2cp.I2CPMessage; @@ -78,6 +79,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi case DestroySessionMessage.MESSAGE_TYPE: handleDestroySession(reader, (DestroySessionMessage)message); break; + case DestLookupMessage.MESSAGE_TYPE: + handleDestLookup(reader, (DestLookupMessage)message); + break; default: if (_log.shouldLog(Log.ERROR)) _log.error("Unhandled I2CP type received: " + message.getType()); @@ -85,13 +89,14 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi } /** - * Handle notifiation that there was an error + * Handle notification that there was an error * */ public void readError(I2CPMessageReader reader, Exception error) { if (_runner.isDead()) return; if (_log.shouldLog(Log.ERROR)) _log.error("Error occurred", error); + // Is this is a little drastic for an unknown message type? _runner.stopRunning(); } @@ -228,6 +233,10 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi _runner.leaseSetCreated(message.getLeaseSet()); } + private void handleDestLookup(I2CPMessageReader reader, DestLookupMessage message) { + _context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getHash())); + } + // this *should* be mod 65536, but UnsignedInteger is still b0rked. FIXME private final static int MAX_SESSION_ID = 32767; diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java new file mode 100644 index 000000000..68edbcaa0 --- /dev/null +++ b/router/java/src/net/i2p/router/client/LookupDestJob.java @@ -0,0 +1,54 @@ +/* + * Released into the public domain + * with no warranty of any kind, either expressed or implied. + */ +package net.i2p.router.client; + +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.i2cp.DestReplyMessage; +import net.i2p.data.i2cp.I2CPMessageException; +import net.i2p.router.JobImpl; +import net.i2p.router.RouterContext; + +/** + * Look up the lease of a hash, to convert it to a Destination for the client + */ +class LookupDestJob extends JobImpl { + private ClientConnectionRunner _runner; + private Hash _hash; + + public LookupDestJob(RouterContext context, ClientConnectionRunner runner, Hash h) { + super(context); + _runner = runner; + _hash = h; + } + + public String getName() { return "LeaseSet Lookup for Client"; } + public void runJob() { + DoneJob done = new DoneJob(getContext()); + getContext().netDb().lookupLeaseSet(_hash, done, done, 10*1000); + } + + private class DoneJob extends JobImpl { + public DoneJob(RouterContext enclosingContext) { + super(enclosingContext); + } + public String getName() { return "LeaseSet Lookup Reply to Client"; } + public void runJob() { + LeaseSet ls = getContext().netDb().lookupLeaseSetLocally(_hash); + if (ls != null) + returnDest(ls.getDestination()); + else + returnDest(null); + } + } + + private void returnDest(Destination d) { + DestReplyMessage msg = new DestReplyMessage(d); + try { + _runner.doSend(msg); + } catch (I2CPMessageException ime) {} + } +} From 0c72fe738322426099ee40c5e013a59efc0229a5 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 14 Dec 2008 15:14:05 +0000 Subject: [PATCH 009/191] history for prop -5 --- history.txt | 28 +++++++++++++++++++ .../src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index 8a9ef5791..94c5f231f 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,31 @@ +2008-12-14 zzz + * Contexts: Add int getProperty(String prop, int default) + * I2PAppThread: Constructor fix + * More split classes into their own files for mkvore + * Streaming: Don't build test cases by default + * Summary bar: Replace links with buttons + * Transport: + - Cleanup max connections code + - Add i2np.udp.maxConnections + - Set max connections based on share bandwidth + - Add haveCapacity() that can be used for connection + throttling in the router + - Reject IBGW/OBEP requests when near connection limit + - Reduce idle timeout when near connection limit + * Tunnel request handler: + - Require tunnel.dropLoad* stats + - Speed up request loop + * I2CP, HostsTxtNamingService, I2PTunnel: + Implement Base32 Hash hostnames, via the naming service. + Names are of the form [52-characters].i2p, where + the 52 characters are the Base32 representation of our + 256-byte hash. The client requests a lookup of the hash + via a brief I2CP session using new I2CP request/reply + messages. The router looks up the leaseset for the hash + to convert the hash to a dest. Convert the I2PTunnel + 'preview' links to use Base32 hostnames as a + demonstration. + 2008-12-08 zzz * ATalk: Move from core to apps * Blocklists: enable by default, include blocklist file diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 57680063e..9ec540056 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = "0.6.5"; - public final static long BUILD = 4; + public final static long BUILD = 5; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From 219e96d416bf8379dddfb913ffbb7a36c7669101 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 15 Dec 2008 21:54:52 +0000 Subject: [PATCH 010/191] Remove apps/ bogobot jdom pants q rome stasher syndie --- apps/bogobot/Bogobot.java | 347 - apps/bogobot/Bogoparser.java | 353 - apps/bogobot/LICENSE.log4j.txt | 48 - apps/bogobot/LICENSE.pircbot.txt | 340 - apps/bogobot/bogobot.bat | 1 - apps/bogobot/bogobot.config | 101 - apps/bogobot/bogobot.sh | 2 - apps/bogobot/build-eclipse.xml | 58 - apps/bogobot/build.xml | 64 - apps/bogobot/log4j-1.2.8.jar | Bin 352668 -> 0 bytes apps/bogobot/pircbot.jar | Bin 73748 -> 0 bytes apps/jdom/jdom.jar | Bin 153253 -> 0 bytes apps/jdom/readme.txt | 1 - apps/pants/build.xml | 236 - apps/pants/pants/build.xml | 45 - .../java/src/net/i2p/pants/MatchTask.java | 212 - .../i2p/pants/MergeTypedPropertiesTask.java | 164 - apps/pants/pants/resources/README | 116 - .../resources/pbuild.template.properties | 110 - .../pants/pants/resources/pbuild.template.xml | 69 - apps/pants/pbuilds/fortuna/pbuild.properties | 112 - apps/pants/pbuilds/fortuna/pbuild.xml | 127 - apps/pants/pbuilds/jetty/pbuild.properties | 112 - apps/pants/pbuilds/jetty/pbuild.xml | 89 - apps/pants/world | 2 - apps/q/doc/client.jpg | Bin 32224 -> 0 bytes apps/q/doc/diagrams.html | 26 - apps/q/doc/hub.jpg | Bin 27718 -> 0 bytes apps/q/doc/index.html | 80 - apps/q/doc/manual/index.html | 805 --- apps/q/doc/manual/notes | 23 - apps/q/doc/metadata.html | 372 - apps/q/doc/overall.jpg | Bin 36942 -> 0 bytes apps/q/doc/qnoderefs.txt | 1 - apps/q/doc/screenshot-home.jpg | Bin 60117 -> 0 bytes apps/q/doc/screenshot-iewarn.jpg | Bin 46201 -> 0 bytes apps/q/doc/screenshot-qsite.jpg | Bin 46818 -> 0 bytes apps/q/doc/screenshot-search.jpg | Bin 22617 -> 0 bytes apps/q/doc/screenshot.jpg | Bin 30660 -> 0 bytes apps/q/doc/screenshot.png | Bin 100655 -> 0 bytes apps/q/doc/screenshots.html | 23 - apps/q/doc/spec/index.html | 1460 ---- apps/q/java/build.xml | 90 - apps/q/java/qresources/html/404.html | 10 - apps/q/java/qresources/html/about.html | 27 - apps/q/java/qresources/html/addrefform.html | 40 - apps/q/java/qresources/html/aiealert.html | 29 - apps/q/java/qresources/html/anonalert.html | 52 - apps/q/java/qresources/html/downloads.html | 9 - apps/q/java/qresources/html/genkeysform.html | 28 - .../q/java/qresources/html/genkeysresult.html | 32 - apps/q/java/qresources/html/getform.html | 15 - apps/q/java/qresources/html/help.html | 11 - apps/q/java/qresources/html/jobs.html | 34 - apps/q/java/qresources/html/main.html | 122 - apps/q/java/qresources/html/msiealert.html | 29 - apps/q/java/qresources/html/puterror.html | 10 - apps/q/java/qresources/html/putform.html | 139 - apps/q/java/qresources/html/putok.html | 10 - apps/q/java/qresources/html/putsiteform.html | 101 - apps/q/java/qresources/html/searchform.html | 76 - .../q/java/qresources/html/searchresults.html | 27 - apps/q/java/qresources/html/settings.html | 9 - apps/q/java/qresources/html/status.html | 24 - apps/q/java/qresources/html/tools.html | 6 - .../qresources/html/widgets/itemtype.html | 0 apps/q/java/src/HTML/Template.java | 1131 --- .../src/HTML/Tmpl/Element/Conditional.java | 178 - .../q/java/src/HTML/Tmpl/Element/Element.java | 66 - apps/q/java/src/HTML/Tmpl/Element/If.java | 39 - apps/q/java/src/HTML/Tmpl/Element/Loop.java | 183 - apps/q/java/src/HTML/Tmpl/Element/Unless.java | 39 - apps/q/java/src/HTML/Tmpl/Element/Var.java | 145 - apps/q/java/src/HTML/Tmpl/Filter.java | 145 - apps/q/java/src/HTML/Tmpl/Parsers/Parser.java | 392 - apps/q/java/src/HTML/Tmpl/Util.java | 130 - apps/q/java/src/net/i2p/aum/AumUtil.java | 19 - apps/q/java/src/net/i2p/aum/DupHashtable.java | 67 - apps/q/java/src/net/i2p/aum/EchoClient.java | 161 - apps/q/java/src/net/i2p/aum/EchoServer.java | 167 - apps/q/java/src/net/i2p/aum/EchoTest.java | 51 - .../java/src/net/i2p/aum/EmbargoedQueue.java | 322 - apps/q/java/src/net/i2p/aum/I2PCat.java | 460 -- .../java/src/net/i2p/aum/I2PSocketHelper.java | 15 - .../src/net/i2p/aum/I2PTunnelXMLObject.java | 138 - .../src/net/i2p/aum/I2PTunnelXMLServer.java | 63 - .../java/src/net/i2p/aum/I2PXmlRpcClient.java | 65 - .../net/i2p/aum/I2PXmlRpcClientFactory.java | 226 - .../src/net/i2p/aum/I2PXmlRpcDemoClass.java | 21 - .../java/src/net/i2p/aum/I2PXmlRpcServer.java | 433 -- .../net/i2p/aum/I2PXmlRpcServerFactory.java | 157 - apps/q/java/src/net/i2p/aum/Mimetypes.java | 392 - apps/q/java/src/net/i2p/aum/OOTest.java | 18 - .../java/src/net/i2p/aum/PrivDestination.java | 236 - .../java/src/net/i2p/aum/PropertiesFile.java | 217 - apps/q/java/src/net/i2p/aum/SimpleFile.java | 118 - .../java/src/net/i2p/aum/SimpleFile_old.java | 121 - apps/q/java/src/net/i2p/aum/SimpleQueue.java | 136 - .../java/src/net/i2p/aum/SimpleSemaphore.java | 108 - apps/q/java/src/net/i2p/aum/helloworld.java | 17 - .../q/java/src/net/i2p/aum/http/HtmlPage.java | 72 - .../i2p/aum/http/I2PHttpRequestHandler.java | 47 - .../src/net/i2p/aum/http/I2PHttpServer.java | 120 - .../i2p/aum/http/MiniDemoXmlRpcHandler.java | 22 - .../i2p/aum/http/MiniHttpRequestHandler.java | 574 -- .../net/i2p/aum/http/MiniHttpRequestPage.java | 93 - .../src/net/i2p/aum/http/MiniHttpServer.java | 225 - apps/q/java/src/net/i2p/aum/http/Tag.java | 360 - apps/q/java/src/net/i2p/aum/q/Favicon.java | 61 - apps/q/java/src/net/i2p/aum/q/QClientAPI.java | 187 - .../q/java/src/net/i2p/aum/q/QClientNode.java | 608 -- .../net/i2p/aum/q/QClientWebInterface.java | 755 -- apps/q/java/src/net/i2p/aum/q/QConsole.java | 288 - apps/q/java/src/net/i2p/aum/q/QDataItem.java | 307 - apps/q/java/src/net/i2p/aum/q/QException.java | 48 - apps/q/java/src/net/i2p/aum/q/QIndexFile.java | 230 - .../src/net/i2p/aum/q/QIndexFileIterator.java | 56 - .../net/i2p/aum/q/QKademliaComparator.java | 57 - apps/q/java/src/net/i2p/aum/q/QMgr.java | 927 --- apps/q/java/src/net/i2p/aum/q/QNode.java | 1976 ----- apps/q/java/src/net/i2p/aum/q/QPeer.java | 105 - .../src/net/i2p/aum/q/QServerMethods.java | 388 - .../q/java/src/net/i2p/aum/q/QServerNode.java | 146 - apps/q/java/src/net/i2p/aum/q/QTest.java | 132 - apps/q/java/src/net/i2p/aum/q/QUtil.java | 99 - .../java/src/net/i2p/aum/q/QWorkerThread.java | 347 - .../src/net/i2p/aum/test/HttpServerTest.java | 59 - .../q/java/src/net/i2p/aum/util/Ico2Java.java | 61 - .../i2p/i2ptunnel/I2PTunnelXMLWrapper.java | 406 -- apps/q/java/web.xml | 21 - apps/q/java/xmlrpc.jar | Bin 63771 -> 0 bytes apps/rome/readme.txt | 1 - apps/rome/rome-0.8.jar | Bin 197290 -> 0 bytes apps/stasher/python/README.txt | 77 - apps/stasher/python/noderefs/aum.stasher | 1 - apps/stasher/python/scripts/stasher | 10 - apps/stasher/python/scripts/stasher.py | 9 - apps/stasher/python/setup.py | 46 - apps/stasher/python/src/bencode.py | 254 - apps/stasher/python/src/code.leo | 6339 ----------------- apps/stasher/python/src/stasher.py | 4416 ------------ apps/syndie/doc/intro.sml | 31 - apps/syndie/doc/readme-standalone.txt | 3 - apps/syndie/doc/readme.txt | 27 - apps/syndie/doc/sml.sml | 41 - apps/syndie/java/build.xml | 172 - .../java/src/net/i2p/syndie/Archive.java | 495 -- .../src/net/i2p/syndie/ArchiveIndexer.java | 228 - .../java/src/net/i2p/syndie/BlogManager.java | 1142 --- apps/syndie/java/src/net/i2p/syndie/CLI.java | 270 - .../java/src/net/i2p/syndie/CLIPost.java | 218 - .../java/src/net/i2p/syndie/CachedEntry.java | 268 - .../src/net/i2p/syndie/EntryExtractor.java | 150 - .../src/net/i2p/syndie/HeaderReceiver.java | 45 - .../syndie/NewestEntryFirstComparator.java | 21 - .../i2p/syndie/NewestNodeFirstComparator.java | 31 - .../java/src/net/i2p/syndie/Sucker.java | 995 --- .../src/net/i2p/syndie/ThreadNodeImpl.java | 131 - .../java/src/net/i2p/syndie/Updater.java | 141 - .../src/net/i2p/syndie/UpdaterServlet.java | 40 - .../src/net/i2p/syndie/UpdaterThread.java | 27 - apps/syndie/java/src/net/i2p/syndie/User.java | 350 - .../java/src/net/i2p/syndie/Version.java | 11 - .../net/i2p/syndie/WritableThreadIndex.java | 145 - .../src/net/i2p/syndie/data/ArchiveIndex.java | 510 -- .../src/net/i2p/syndie/data/Attachment.java | 128 - .../src/net/i2p/syndie/data/BlogInfo.java | 299 - .../src/net/i2p/syndie/data/BlogInfoData.java | 135 - .../java/src/net/i2p/syndie/data/BlogURI.java | 119 - .../syndie/data/EncodingTestGenerator.java | 92 - .../java/src/net/i2p/syndie/data/Entry.java | 14 - .../net/i2p/syndie/data/EntryContainer.java | 460 -- .../i2p/syndie/data/FilteredThreadIndex.java | 129 - .../i2p/syndie/data/LocalArchiveIndex.java | 112 - .../java/src/net/i2p/syndie/data/SafeURL.java | 32 - .../src/net/i2p/syndie/data/ThreadIndex.java | 52 - .../src/net/i2p/syndie/data/ThreadNode.java | 36 - .../syndie/data/TransparentArchiveIndex.java | 85 - .../java/src/net/i2p/syndie/sml/Address.java | 16 - .../src/net/i2p/syndie/sml/ArchiveRef.java | 18 - .../java/src/net/i2p/syndie/sml/Blog.java | 20 - .../i2p/syndie/sml/BlogPostInfoRenderer.java | 466 -- .../src/net/i2p/syndie/sml/BlogRenderer.java | 244 - .../net/i2p/syndie/sml/EventReceiverImpl.java | 116 - .../i2p/syndie/sml/HTMLPreviewRenderer.java | 164 - .../src/net/i2p/syndie/sml/HTMLRenderer.java | 1076 --- .../java/src/net/i2p/syndie/sml/Link.java | 14 - .../src/net/i2p/syndie/sml/RSSRenderer.java | 341 - .../src/net/i2p/syndie/sml/SMLParser.java | 472 -- .../i2p/syndie/sml/ThreadedHTMLRenderer.java | 601 -- .../net/i2p/syndie/web/AddressesServlet.java | 504 -- .../src/net/i2p/syndie/web/AdminServlet.java | 79 - .../net/i2p/syndie/web/ArchiveServlet.java | 226 - .../net/i2p/syndie/web/ArchiveViewerBean.java | 822 --- .../src/net/i2p/syndie/web/BaseServlet.java | 1302 ---- .../net/i2p/syndie/web/BlogConfigBean.java | 308 - .../net/i2p/syndie/web/BlogConfigServlet.java | 451 -- .../src/net/i2p/syndie/web/ExportServlet.java | 210 - .../i2p/syndie/web/ExternalLinkServlet.java | 44 - .../net/i2p/syndie/web/ImportFeedServlet.java | 149 - .../net/i2p/syndie/web/MultiPartRequest.java | 422 -- .../java/src/net/i2p/syndie/web/PostBean.java | 226 - .../src/net/i2p/syndie/web/PostServlet.java | 381 - .../net/i2p/syndie/web/ProfileServlet.java | 232 - .../src/net/i2p/syndie/web/RSSServlet.java | 134 - .../net/i2p/syndie/web/RemoteArchiveBean.java | 852 --- .../src/net/i2p/syndie/web/RunStandalone.java | 52 - .../src/net/i2p/syndie/web/SwitchServlet.java | 46 - .../net/i2p/syndie/web/SyndicateServlet.java | 154 - .../net/i2p/syndie/web/ThreadNavServlet.java | 157 - .../net/i2p/syndie/web/ViewBlogServlet.java | 819 --- .../net/i2p/syndie/web/ViewBlogsServlet.java | 168 - .../i2p/syndie/web/ViewThreadedServlet.java | 478 -- apps/syndie/jetty-syndie.xml | 114 - apps/syndie/jsp/_bodyindex.jsp | 40 - apps/syndie/jsp/_leftnav.jsp | 3 - apps/syndie/jsp/_rightnav.jsp | 1 - apps/syndie/jsp/_toplogo.jsp | 5 - apps/syndie/jsp/_topnav.jsp | 47 - apps/syndie/jsp/about.html | 31 - apps/syndie/jsp/images/addToFavorites.png | Bin 275 -> 0 bytes apps/syndie/jsp/images/addToIgnored.png | Bin 266 -> 0 bytes apps/syndie/jsp/images/collapse.png | Bin 917 -> 0 bytes apps/syndie/jsp/images/default_blog_logo.png | Bin 1580 -> 0 bytes apps/syndie/jsp/images/expand.png | Bin 922 -> 0 bytes apps/syndie/jsp/images/favorites.png | Bin 463 -> 0 bytes apps/syndie/jsp/images/noSubthread.png | Bin 129 -> 0 bytes apps/syndie/jsp/images/self.png | Bin 155 -> 0 bytes apps/syndie/jsp/images/syndielogo.png | Bin 3489 -> 0 bytes apps/syndie/jsp/images/threadIndent.png | Bin 129 -> 0 bytes apps/syndie/jsp/import.jsp | 68 - apps/syndie/jsp/index.html | 19 - apps/syndie/jsp/register.jsp | 50 - apps/syndie/jsp/smlref.jsp | 38 - apps/syndie/jsp/style.jsp | 7 - apps/syndie/jsp/syndie.css | 444 -- apps/syndie/jsp/syndie/index.jsp | 1 - apps/syndie/jsp/viewattachment.jsp | 16 - apps/syndie/jsp/viewmetadata.jsp | 35 - apps/syndie/jsp/viewtempattachment.jsp | 23 - apps/syndie/jsp/web.xml | 166 - 241 files changed, 52592 deletions(-) delete mode 100644 apps/bogobot/Bogobot.java delete mode 100644 apps/bogobot/Bogoparser.java delete mode 100644 apps/bogobot/LICENSE.log4j.txt delete mode 100644 apps/bogobot/LICENSE.pircbot.txt delete mode 100644 apps/bogobot/bogobot.bat delete mode 100644 apps/bogobot/bogobot.config delete mode 100644 apps/bogobot/bogobot.sh delete mode 100644 apps/bogobot/build-eclipse.xml delete mode 100644 apps/bogobot/build.xml delete mode 100644 apps/bogobot/log4j-1.2.8.jar delete mode 100644 apps/bogobot/pircbot.jar delete mode 100644 apps/jdom/jdom.jar delete mode 100644 apps/jdom/readme.txt delete mode 100644 apps/pants/build.xml delete mode 100644 apps/pants/pants/build.xml delete mode 100644 apps/pants/pants/java/src/net/i2p/pants/MatchTask.java delete mode 100644 apps/pants/pants/java/src/net/i2p/pants/MergeTypedPropertiesTask.java delete mode 100644 apps/pants/pants/resources/README delete mode 100644 apps/pants/pants/resources/pbuild.template.properties delete mode 100644 apps/pants/pants/resources/pbuild.template.xml delete mode 100644 apps/pants/pbuilds/fortuna/pbuild.properties delete mode 100644 apps/pants/pbuilds/fortuna/pbuild.xml delete mode 100644 apps/pants/pbuilds/jetty/pbuild.properties delete mode 100644 apps/pants/pbuilds/jetty/pbuild.xml delete mode 100644 apps/pants/world delete mode 100644 apps/q/doc/client.jpg delete mode 100644 apps/q/doc/diagrams.html delete mode 100644 apps/q/doc/hub.jpg delete mode 100644 apps/q/doc/index.html delete mode 100644 apps/q/doc/manual/index.html delete mode 100644 apps/q/doc/manual/notes delete mode 100644 apps/q/doc/metadata.html delete mode 100644 apps/q/doc/overall.jpg delete mode 100644 apps/q/doc/qnoderefs.txt delete mode 100644 apps/q/doc/screenshot-home.jpg delete mode 100644 apps/q/doc/screenshot-iewarn.jpg delete mode 100644 apps/q/doc/screenshot-qsite.jpg delete mode 100644 apps/q/doc/screenshot-search.jpg delete mode 100644 apps/q/doc/screenshot.jpg delete mode 100644 apps/q/doc/screenshot.png delete mode 100644 apps/q/doc/screenshots.html delete mode 100644 apps/q/doc/spec/index.html delete mode 100644 apps/q/java/build.xml delete mode 100644 apps/q/java/qresources/html/404.html delete mode 100644 apps/q/java/qresources/html/about.html delete mode 100644 apps/q/java/qresources/html/addrefform.html delete mode 100644 apps/q/java/qresources/html/aiealert.html delete mode 100644 apps/q/java/qresources/html/anonalert.html delete mode 100644 apps/q/java/qresources/html/downloads.html delete mode 100644 apps/q/java/qresources/html/genkeysform.html delete mode 100644 apps/q/java/qresources/html/genkeysresult.html delete mode 100644 apps/q/java/qresources/html/getform.html delete mode 100644 apps/q/java/qresources/html/help.html delete mode 100644 apps/q/java/qresources/html/jobs.html delete mode 100644 apps/q/java/qresources/html/main.html delete mode 100644 apps/q/java/qresources/html/msiealert.html delete mode 100644 apps/q/java/qresources/html/puterror.html delete mode 100644 apps/q/java/qresources/html/putform.html delete mode 100644 apps/q/java/qresources/html/putok.html delete mode 100644 apps/q/java/qresources/html/putsiteform.html delete mode 100644 apps/q/java/qresources/html/searchform.html delete mode 100644 apps/q/java/qresources/html/searchresults.html delete mode 100644 apps/q/java/qresources/html/settings.html delete mode 100644 apps/q/java/qresources/html/status.html delete mode 100644 apps/q/java/qresources/html/tools.html delete mode 100644 apps/q/java/qresources/html/widgets/itemtype.html delete mode 100644 apps/q/java/src/HTML/Template.java delete mode 100644 apps/q/java/src/HTML/Tmpl/Element/Conditional.java delete mode 100644 apps/q/java/src/HTML/Tmpl/Element/Element.java delete mode 100644 apps/q/java/src/HTML/Tmpl/Element/If.java delete mode 100644 apps/q/java/src/HTML/Tmpl/Element/Loop.java delete mode 100644 apps/q/java/src/HTML/Tmpl/Element/Unless.java delete mode 100644 apps/q/java/src/HTML/Tmpl/Element/Var.java delete mode 100644 apps/q/java/src/HTML/Tmpl/Filter.java delete mode 100644 apps/q/java/src/HTML/Tmpl/Parsers/Parser.java delete mode 100644 apps/q/java/src/HTML/Tmpl/Util.java delete mode 100644 apps/q/java/src/net/i2p/aum/AumUtil.java delete mode 100644 apps/q/java/src/net/i2p/aum/DupHashtable.java delete mode 100644 apps/q/java/src/net/i2p/aum/EchoClient.java delete mode 100644 apps/q/java/src/net/i2p/aum/EchoServer.java delete mode 100644 apps/q/java/src/net/i2p/aum/EchoTest.java delete mode 100644 apps/q/java/src/net/i2p/aum/EmbargoedQueue.java delete mode 100644 apps/q/java/src/net/i2p/aum/I2PCat.java delete mode 100644 apps/q/java/src/net/i2p/aum/I2PSocketHelper.java delete mode 100644 apps/q/java/src/net/i2p/aum/I2PTunnelXMLObject.java delete mode 100644 apps/q/java/src/net/i2p/aum/I2PTunnelXMLServer.java delete mode 100644 apps/q/java/src/net/i2p/aum/I2PXmlRpcClient.java delete mode 100644 apps/q/java/src/net/i2p/aum/I2PXmlRpcClientFactory.java delete mode 100644 apps/q/java/src/net/i2p/aum/I2PXmlRpcDemoClass.java delete mode 100644 apps/q/java/src/net/i2p/aum/I2PXmlRpcServer.java delete mode 100644 apps/q/java/src/net/i2p/aum/I2PXmlRpcServerFactory.java delete mode 100644 apps/q/java/src/net/i2p/aum/Mimetypes.java delete mode 100644 apps/q/java/src/net/i2p/aum/OOTest.java delete mode 100644 apps/q/java/src/net/i2p/aum/PrivDestination.java delete mode 100644 apps/q/java/src/net/i2p/aum/PropertiesFile.java delete mode 100644 apps/q/java/src/net/i2p/aum/SimpleFile.java delete mode 100644 apps/q/java/src/net/i2p/aum/SimpleFile_old.java delete mode 100644 apps/q/java/src/net/i2p/aum/SimpleQueue.java delete mode 100644 apps/q/java/src/net/i2p/aum/SimpleSemaphore.java delete mode 100644 apps/q/java/src/net/i2p/aum/helloworld.java delete mode 100644 apps/q/java/src/net/i2p/aum/http/HtmlPage.java delete mode 100644 apps/q/java/src/net/i2p/aum/http/I2PHttpRequestHandler.java delete mode 100644 apps/q/java/src/net/i2p/aum/http/I2PHttpServer.java delete mode 100644 apps/q/java/src/net/i2p/aum/http/MiniDemoXmlRpcHandler.java delete mode 100644 apps/q/java/src/net/i2p/aum/http/MiniHttpRequestHandler.java delete mode 100644 apps/q/java/src/net/i2p/aum/http/MiniHttpRequestPage.java delete mode 100644 apps/q/java/src/net/i2p/aum/http/MiniHttpServer.java delete mode 100644 apps/q/java/src/net/i2p/aum/http/Tag.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/Favicon.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QClientAPI.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QClientNode.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QClientWebInterface.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QConsole.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QDataItem.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QException.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QIndexFile.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QIndexFileIterator.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QKademliaComparator.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QMgr.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QNode.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QPeer.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QServerMethods.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QServerNode.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QTest.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QUtil.java delete mode 100644 apps/q/java/src/net/i2p/aum/q/QWorkerThread.java delete mode 100644 apps/q/java/src/net/i2p/aum/test/HttpServerTest.java delete mode 100644 apps/q/java/src/net/i2p/aum/util/Ico2Java.java delete mode 100644 apps/q/java/src/net/i2p/i2ptunnel/I2PTunnelXMLWrapper.java delete mode 100644 apps/q/java/web.xml delete mode 100644 apps/q/java/xmlrpc.jar delete mode 100644 apps/rome/readme.txt delete mode 100644 apps/rome/rome-0.8.jar delete mode 100644 apps/stasher/python/README.txt delete mode 100644 apps/stasher/python/noderefs/aum.stasher delete mode 100644 apps/stasher/python/scripts/stasher delete mode 100644 apps/stasher/python/scripts/stasher.py delete mode 100644 apps/stasher/python/setup.py delete mode 100644 apps/stasher/python/src/bencode.py delete mode 100644 apps/stasher/python/src/code.leo delete mode 100644 apps/stasher/python/src/stasher.py delete mode 100644 apps/syndie/doc/intro.sml delete mode 100644 apps/syndie/doc/readme-standalone.txt delete mode 100644 apps/syndie/doc/readme.txt delete mode 100644 apps/syndie/doc/sml.sml delete mode 100644 apps/syndie/java/build.xml delete mode 100644 apps/syndie/java/src/net/i2p/syndie/Archive.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/ArchiveIndexer.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/BlogManager.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/CLI.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/CLIPost.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/CachedEntry.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/EntryExtractor.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/HeaderReceiver.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/NewestEntryFirstComparator.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/NewestNodeFirstComparator.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/Sucker.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/ThreadNodeImpl.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/Updater.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/UpdaterServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/UpdaterThread.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/User.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/Version.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/WritableThreadIndex.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/ArchiveIndex.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/Attachment.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/BlogInfo.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/BlogInfoData.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/BlogURI.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/EncodingTestGenerator.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/Entry.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/EntryContainer.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/FilteredThreadIndex.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/LocalArchiveIndex.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/SafeURL.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/ThreadIndex.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/ThreadNode.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/data/TransparentArchiveIndex.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/Address.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/ArchiveRef.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/Blog.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/BlogPostInfoRenderer.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/BlogRenderer.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/EventReceiverImpl.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/HTMLPreviewRenderer.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/Link.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/RSSRenderer.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/SMLParser.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/sml/ThreadedHTMLRenderer.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/AddressesServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/AdminServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ArchiveServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ArchiveViewerBean.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/BaseServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/BlogConfigBean.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/BlogConfigServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ExportServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ExternalLinkServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ImportFeedServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/MultiPartRequest.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/PostBean.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/PostServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/RSSServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/RemoteArchiveBean.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/RunStandalone.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/SwitchServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/SyndicateServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ThreadNavServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ViewBlogServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ViewBlogsServlet.java delete mode 100644 apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java delete mode 100644 apps/syndie/jetty-syndie.xml delete mode 100644 apps/syndie/jsp/_bodyindex.jsp delete mode 100644 apps/syndie/jsp/_leftnav.jsp delete mode 100644 apps/syndie/jsp/_rightnav.jsp delete mode 100644 apps/syndie/jsp/_toplogo.jsp delete mode 100644 apps/syndie/jsp/_topnav.jsp delete mode 100644 apps/syndie/jsp/about.html delete mode 100644 apps/syndie/jsp/images/addToFavorites.png delete mode 100644 apps/syndie/jsp/images/addToIgnored.png delete mode 100644 apps/syndie/jsp/images/collapse.png delete mode 100644 apps/syndie/jsp/images/default_blog_logo.png delete mode 100644 apps/syndie/jsp/images/expand.png delete mode 100644 apps/syndie/jsp/images/favorites.png delete mode 100644 apps/syndie/jsp/images/noSubthread.png delete mode 100644 apps/syndie/jsp/images/self.png delete mode 100644 apps/syndie/jsp/images/syndielogo.png delete mode 100644 apps/syndie/jsp/images/threadIndent.png delete mode 100644 apps/syndie/jsp/import.jsp delete mode 100644 apps/syndie/jsp/index.html delete mode 100644 apps/syndie/jsp/register.jsp delete mode 100644 apps/syndie/jsp/smlref.jsp delete mode 100644 apps/syndie/jsp/style.jsp delete mode 100644 apps/syndie/jsp/syndie.css delete mode 100644 apps/syndie/jsp/syndie/index.jsp delete mode 100644 apps/syndie/jsp/viewattachment.jsp delete mode 100644 apps/syndie/jsp/viewmetadata.jsp delete mode 100644 apps/syndie/jsp/viewtempattachment.jsp delete mode 100644 apps/syndie/jsp/web.xml diff --git a/apps/bogobot/Bogobot.java b/apps/bogobot/Bogobot.java deleted file mode 100644 index 0c0c40c92..000000000 --- a/apps/bogobot/Bogobot.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * bogobot - A simple join/part stats logger bot for I2P IRC. - * - * Bogobot.java - * 2004 The I2P Project - * http://www.i2p.net - * This code is public domain. - */ - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Properties; -import java.util.Timer; -import java.util.TimerTask; - -import org.apache.log4j.DailyRollingFileAppender; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.PatternLayout; -import org.jibble.pircbot.IrcException; -import org.jibble.pircbot.NickAlreadyInUseException; -import org.jibble.pircbot.PircBot; -import org.jibble.pircbot.User; - -/** - * TODO 0.5 Add multi-server capability. - * - * @author hypercubus, oOo - * @version 0.4 - */ -public class Bogobot extends PircBot { - - private static final String INTERVAL_DAILY = "daily"; - private static final String INTERVAL_MONTHLY = "monthly"; - private static final String INTERVAL_WEEKLY = "weekly"; - - private boolean _isIntentionalDisconnect = false; - private long _lastUserlistCommandTimestamp = 0; - private Logger _logger = Logger.getLogger(Bogobot.class); - - private int _currentAutoRoundTripTag = 0; - private long _lastAutoRoundTripSentTime = 0; - private Timer _tickTimer; - - private String _configFile; - - private String _botPrimaryNick; - private String _botSecondaryNick; - private String _botNickservPassword; - private String _botUsername; - private String _ownerPrimaryNick; - private String _ownerSecondaryNick; - private String _botShutdownPassword; - private String _ircChannel; - private String _ircServer; - private int _ircServerPort; - private boolean _isLoggerEnabled; - private String _loggedHostnamePattern; - private boolean _isUserlistCommandEnabled; - private String _logFilePrefix; - private String _logFileRotationInterval; - private long _commandAntiFloodInterval; - private String _userlistCommandTrigger; - private boolean _isRoundTripDelayEnabled; - private int _roundTripDelayPeriod; - - class BogobotTickTask extends TimerTask { - private Bogobot _caller; - - public BogobotTickTask(Bogobot caller) { - _caller = caller; - } - - public void run() { - _caller.onTick(); - } - } - - private void loadConfigFile(String configFileName) { - - _configFile = configFileName; - - Properties config = new Properties(); - FileInputStream fis = null; - - try { - fis = new FileInputStream(configFileName); - config.load(fis); - } catch (IOException ioe) { - System.err.println("Error loading configuration file"); - System.exit(2); - - } finally { - if (fis != null) try { - fis.close(); - } catch (IOException ioe) { // nop - } - } - - _botPrimaryNick = config.getProperty("botPrimaryNick", "somebot"); - _botSecondaryNick = config.getProperty("botSecondaryNick", "somebot_"); - _botNickservPassword = config.getProperty("botNickservPassword", ""); - _botUsername = config.getProperty("botUsername", "somebot"); - - _ownerPrimaryNick = config.getProperty("ownerPrimaryNick", "somenick"); - _ownerSecondaryNick = config.getProperty("ownerSecondaryNick", "somenick_"); - - _botShutdownPassword = config.getProperty("botShutdownPassword", "take off eh"); - - _ircChannel = config.getProperty("ircChannel", "#i2p-chat"); - _ircServer = config.getProperty("ircServer", "irc.postman.i2p"); - _ircServerPort = Integer.parseInt(config.getProperty("ircServerPort", "6668")); - - _isLoggerEnabled = Boolean.valueOf(config.getProperty("isLoggerEnabled", "true")).booleanValue(); - _loggedHostnamePattern = config.getProperty("loggedHostnamePattern", ""); - _logFilePrefix = config.getProperty("logFilePrefix", "irc.postman.i2p.i2p-chat"); - _logFileRotationInterval = config.getProperty("logFileRotationInterval", INTERVAL_DAILY); - - _isRoundTripDelayEnabled = Boolean.valueOf(config.getProperty("isRoundTripDelayEnabled", "false")).booleanValue(); - _roundTripDelayPeriod = Integer.parseInt(config.getProperty("roundTripDelayPeriod", "300")); - - _isUserlistCommandEnabled = Boolean.valueOf(config.getProperty("isUserlistCommandEnabled", "true")).booleanValue(); - _userlistCommandTrigger = config.getProperty("userlistCommandTrigger", "!who"); - _commandAntiFloodInterval = Long.parseLong(config.getProperty("commandAntiFloodInterval", "60")); - } - - public Bogobot(String configFileName) { - - loadConfigFile(configFileName); - - this.setName(_botPrimaryNick); - this.setLogin(_botUsername); - _tickTimer = new Timer(); - _tickTimer.scheduleAtFixedRate(new BogobotTickTask(this), 1000, 10 * 1000); - } - - public static void main(String[] args) { - - Bogobot bogobot; - - if (args.length > 1) { - System.err.println("Too many arguments, the only allowed parameter is configuration file name"); - System.exit(3); - } - if (args.length == 1) { - bogobot = new Bogobot(args[0]); - } else { - bogobot = new Bogobot("bogobot.config"); - } - - bogobot.setVerbose(true); - - if (bogobot._isLoggerEnabled) - bogobot.initLogger(); - - bogobot.connectToServer(); - } - - protected void onTick() { - // Tick about once every ten seconds - - if (this.isConnected() && _isRoundTripDelayEnabled) { - if( ( (System.currentTimeMillis() - _lastAutoRoundTripSentTime) >= (_roundTripDelayPeriod * 1000) ) && (this.getOutgoingQueueSize() == 0) ) { - // Connected, sending queue is empty and last RoundTrip is more then 5 minutes old -> Send a new one - _currentAutoRoundTripTag ++; - _lastAutoRoundTripSentTime = System.currentTimeMillis(); - sendNotice(this.getNick(),"ROUNDTRIP " + _currentAutoRoundTripTag); - } - } - } - - protected void onDisconnect() { - - if (_isIntentionalDisconnect) - System.exit(0); - - if (_isLoggerEnabled) - _logger.info(System.currentTimeMillis() + " quits *** " + this.getName() + " *** (Lost connection)"); - - try { - Thread.sleep(60000); - } catch (InterruptedException e) { - // No worries. - } - connectToServer(); - } - - protected void onJoin(String channel, String sender, String login, String hostname) { - - if (_isLoggerEnabled) { - if (sender.equals(this.getName())) { - - _logger.info(System.currentTimeMillis() + " joins *** " + _botPrimaryNick + " ***"); - - } else { - - String prependedHostname = "@" + hostname; - if (prependedHostname.endsWith(_loggedHostnamePattern)) { - _logger.info(System.currentTimeMillis() + " joins " + sender); - } - - } - } - } - - protected void onMessage(String channel, String sender, String login, String hostname, String message) { - message = message.replaceFirst("<.+?> ", ""); - if (_isUserlistCommandEnabled && message.equals(_userlistCommandTrigger)) { - - if (System.currentTimeMillis() - _lastUserlistCommandTimestamp < _commandAntiFloodInterval * 1000) - return; - - Object[] users = getUsers(_ircChannel); - String output = "Userlist for " + _ircChannel + ": "; - - for (int i = 0; i < users.length; i++) - output += "[" + ((User) users[i]).getNick() + "] "; - - sendMessage(_ircChannel, output); - _lastUserlistCommandTimestamp = System.currentTimeMillis(); - } - } - - protected void onPart(String channel, String sender, String login, String hostname) { - - if (_isLoggerEnabled) { - if (sender.equals(this.getName())) { - _logger.info(System.currentTimeMillis() + " parts *** " + _botPrimaryNick + " ***"); - } else { - String prependedHostname = "@" + hostname; - if (prependedHostname.endsWith(_loggedHostnamePattern)) { - _logger.info(System.currentTimeMillis() + " parts " + sender); - } - } - } - - } - - protected void onPrivateMessage(String sender, String login, String hostname, String message) { - /* - * Nobody else except the bot's owner can shut it down, unless of - * course the owner's nick isn't registered and someone's spoofing it. - */ - if ((sender.equals(_ownerPrimaryNick) || sender.equals(_ownerSecondaryNick)) && message.equals(_botShutdownPassword)) { - - if (_isLoggerEnabled) - _logger.info(System.currentTimeMillis() + " quits *** " + this.getName() + " ***"); - - _isIntentionalDisconnect = true; - disconnect(); - } - } - - protected void onQuit(String sourceNick, String sourceLogin, String sourceHostname, String reason) { - String prependedHostname = "@" + sourceHostname; - - if (sourceNick.equals(_botPrimaryNick)) - changeNick(_botPrimaryNick); - - if (_isLoggerEnabled) { - if (prependedHostname.endsWith(_loggedHostnamePattern)) { - _logger.info(System.currentTimeMillis() + " quits " + sourceNick + " " + reason); - } - } - - } - - private void connectToServer() { - - int loginAttempts = 0; - - while (true) { - try { - connect(_ircServer, _ircServerPort); - break; - } catch (NickAlreadyInUseException e) { - if (loginAttempts == 1) { - System.out.println("Sorry, the primary and secondary bot nicks are already taken. Exiting."); - System.exit(1); - } - loginAttempts++; - try { - Thread.sleep(5000); - } catch (InterruptedException e1) { - // Hmph. - } - - if (getName().equals(_botPrimaryNick)) - setName(_botSecondaryNick); - else - setName(_botPrimaryNick); - - continue; - } catch (IOException e) { - System.out.println("Error during login: "); - e.printStackTrace(); - System.exit(1); - } catch (IrcException e) { - System.out.println("Error during login: "); - e.printStackTrace(); - System.exit(1); - } - } - joinChannel(_ircChannel); - } - - protected void onNotice(String sourceNick, String sourceLogin, String sourceHostname, String target, String notice) { - - if (sourceNick.equals("NickServ") && (notice.indexOf("/msg NickServ IDENTIFY") >= 0) && (_botNickservPassword != "")) { - sendRawLineViaQueue("NICKSERV IDENTIFY " + _botNickservPassword); - } - - if (sourceNick.equals(getNick()) && notice.equals( "ROUNDTRIP " + _currentAutoRoundTripTag)) { - int delay = (int)((System.currentTimeMillis() - _lastAutoRoundTripSentTime) / 100); -// sendMessage(_ircChannel, "Round-trip delay = " + (delay / 10.0f) + " seconds"); - if (_isLoggerEnabled) - _logger.info(System.currentTimeMillis() + " roundtrip " + delay); - } - } - - private void initLogger() { - - String logFilePath = "logs" + File.separator + _logFilePrefix; - DailyRollingFileAppender rollingFileAppender = null; - - if (!(new File("logs").exists())) - (new File("logs")).mkdirs(); - - try { - - if (_logFileRotationInterval.equals("monthly")) - rollingFileAppender = new DailyRollingFileAppender(new PatternLayout("%m%n"), logFilePath, "'.'yyyy-MM'.log'"); - else if (_logFileRotationInterval.equals("weekly")) - rollingFileAppender = new DailyRollingFileAppender(new PatternLayout("%m%n"), logFilePath, "'.'yyyy-ww'.log'"); - else - rollingFileAppender = new DailyRollingFileAppender(new PatternLayout("%m%n"), logFilePath, "'.'yyyy-MM-dd'.log'"); - - rollingFileAppender.setThreshold(Level.INFO); - _logger.addAppender(rollingFileAppender); - } catch (IOException ex) { - System.out.println("Error: Couldn't create or open an existing log file. Exiting."); - System.exit(1); - } - } - -} diff --git a/apps/bogobot/Bogoparser.java b/apps/bogobot/Bogoparser.java deleted file mode 100644 index 9b1944396..000000000 --- a/apps/bogobot/Bogoparser.java +++ /dev/null @@ -1,353 +0,0 @@ -/* - * bogoparser - A simple logfile analyzer for bogobot. - * - * Bogoparser.java - * 2004 The I2P Project - * http://www.i2p.net - * This code is public domain. - */ - -import java.io.BufferedReader; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * @author hypercubus - * @version 0.4 - */ -public class Bogoparser { - - private static void displayUsageAndExit() { - System.out.println("\r\nUsage:\r\n\r\n java Bogoparser [--by-duration] \r\n"); - System.exit(1); - } - - public static void main(String[] args) { - - Bogoparser bogoparser; - - if (args.length < 1 || args.length > 2) - displayUsageAndExit(); - - if (args.length == 2) { - if (!args[0].equals("--by-duration")) - displayUsageAndExit(); - bogoparser = new Bogoparser(args[1], true); - } - - if (args.length == 1) - bogoparser = new Bogoparser(args[0], false); - } - - private Bogoparser(String logfile, boolean sortByDuration) { - - ArrayList sortedSessions; - - if (sortByDuration) { - sortedSessions = sortSessionsByDuration(calculateSessionDurations(sortSessionsByTime(readLogfile(logfile)))); - formatAndOutputByDuration(sortedSessions); - } else { - sortedSessions = calculateSessionDurations(sortSessionsByQuitReason(sortSessionsByNick(sortSessionsByTime(readLogfile(logfile))))); - formatAndOutput(sortedSessions); - } - } - - private ArrayList calculateSessionDurations(ArrayList sortedSessionsByQuitReasonOrDuration) { - - ArrayList calculatedSessionDurations = new ArrayList(); - - for (int i = 0; i+1 < sortedSessionsByQuitReasonOrDuration.size(); i += 2) { - - String joinsEntry = (String) sortedSessionsByQuitReasonOrDuration.get(i); - String[] joinsEntryFields = joinsEntry.split(" "); - - String quitsEntry = (String) sortedSessionsByQuitReasonOrDuration.get(i+1); - Pattern p = Pattern.compile("^([^ ]+) [^ ]+ ([^ ]+) (.*)$"); - Matcher m = p.matcher(quitsEntry); - - if (m.matches()) { - - String currentJoinTime = joinsEntryFields[0]; - String currentNick = m.group(2); - String currentQuitReason = m.group(3); - String currentQuitTime = m.group(1); - long joinsTimeInMilliseconds; - long quitsTimeInMilliseconds; - long sessionLengthInMilliseconds; - - joinsTimeInMilliseconds = Long.parseLong(currentJoinTime); - quitsTimeInMilliseconds = Long.parseLong(currentQuitTime); - sessionLengthInMilliseconds = quitsTimeInMilliseconds - joinsTimeInMilliseconds; - - String hours = "" + sessionLengthInMilliseconds/1000/60/60; - String minutes = "" + (sessionLengthInMilliseconds/1000/60)%60; - - if (hours.length() < 2) - hours = "0" + hours; - - if (hours.length() < 3) - hours = "0" + hours; - - if (minutes.length() < 2) - minutes = "0" + minutes; - - int columnPadding = 19-currentNick.length(); - String columnPaddingString = " "; - - for (int j = 0; j < columnPadding; j++) - columnPaddingString = columnPaddingString + " "; - - calculatedSessionDurations.add(sessionLengthInMilliseconds + " " + currentNick + columnPaddingString + " online " + hours + " hours " + minutes + " minutes " + currentQuitReason); - } else { - System.out.println("\r\nError: Unexpected entry in logfile: " + quitsEntry); - System.exit(1); - } - } - return calculatedSessionDurations; - } - - private void formatAndOutput(ArrayList sortedSessions) { - - String quitReason = null; - - for (int i = 0; i < sortedSessions.size(); i++) { - - String entry = (String) sortedSessions.get(i); - Pattern p = Pattern.compile("^[\\d]+ ([^ ]+ +online [\\d]+ hours [\\d]+ minutes) (.*)$"); - Matcher m = p.matcher(entry); - - if (m.matches()) { - - if (quitReason == null) { - quitReason = m.group(2); - System.out.println("\r\nQUIT: " + ((m.group(2).equals("")) ? "No Reason Given" : quitReason) + "\r\n"); - } - - String tempQuitReason = m.group(2); - String tempSession = m.group(1); - - if (tempQuitReason.equals(quitReason)) { - System.out.println(" " + tempSession); - } else { - quitReason = null; - i -= 1; - continue; - } - } else { - System.out.println("\r\nError: Unexpected entry in logfile: " + entry); - System.exit(1); - } - } - System.out.println("\r\n"); - } - - private void formatAndOutputByDuration(ArrayList sortedSessions) { - System.out.println("\r\n"); - - for (int i = 0; i < sortedSessions.size(); i++) { - String[] columns = ((String) sortedSessions.get(i)).split(" ", 2); - System.out.println(columns[1]); - } - - System.out.println("\r\n"); - } - - private ArrayList readLogfile(String logfile) { - - ArrayList log = new ArrayList(); - - try { - BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(logfile))); - - for (String line; (line = in.readLine()) != null; ) - log.add(line); - - in.close(); - - } catch (FileNotFoundException e) { - System.out.println("\r\nError: Can't find logfile '" + logfile + "'.\r\n"); - System.exit(1); - - } catch (IOException e) { - System.out.println("\r\nError: Can't read logfile '" + logfile + "'.\r\n"); - System.exit(1); - } - return log; - } - - /* - * Performs an odd-even transposition sort. - */ - private ArrayList sortSessionsByDuration(ArrayList calculatedSessionDurations) { - - for (int i = 0; i < calculatedSessionDurations.size()/2; i++) { - for (int j = 0; j+1 < calculatedSessionDurations.size(); j += 2) { - - String[] currentDurationString = ((String) calculatedSessionDurations.get(j)).split(" ", 2); - long currentDuration = Long.parseLong(currentDurationString[0]); - String[] nextDurationString = ((String) calculatedSessionDurations.get(j+1)).split(" ", 2); - long nextDuration = Long.parseLong(nextDurationString[0]); - - if (currentDuration > nextDuration) { - calculatedSessionDurations.add(j, calculatedSessionDurations.get(j+1)); - calculatedSessionDurations.remove(j+2); - } - } - - for (int j = 1; j+1 < calculatedSessionDurations.size(); j += 2) { - - String[] currentDurationString = ((String) calculatedSessionDurations.get(j)).split(" ", 2); - long currentDuration = Long.parseLong(currentDurationString[0]); - String[] nextDurationString = ((String) calculatedSessionDurations.get(j+1)).split(" ", 2); - long nextDuration = Long.parseLong(nextDurationString[0]); - - if (currentDuration > nextDuration) { - calculatedSessionDurations.add(j, calculatedSessionDurations.get(j+1)); - calculatedSessionDurations.remove(j+2); - } - } - } - return calculatedSessionDurations; - } - - private ArrayList sortSessionsByNick(ArrayList sortedSessionsByTime) { - - ArrayList sortedSessionsByNick = new ArrayList(); - - while (sortedSessionsByTime.size() != 0) { - - String entry = (String) sortedSessionsByTime.get(0); - String[] entryFields = entry.split(" "); - String currentNick = entryFields[2]; - - sortedSessionsByNick.add(entry); - sortedSessionsByNick.add(sortedSessionsByTime.get(1)); - sortedSessionsByTime.remove(0); - sortedSessionsByTime.remove(0); - for (int i = 0; i+1 < sortedSessionsByTime.size(); i += 2) { - - String nextEntry = (String) sortedSessionsByTime.get(i); - String[] nextEntryFields = nextEntry.split(" "); - - if (nextEntryFields[2].equals(currentNick)) { - sortedSessionsByNick.add(nextEntry); - sortedSessionsByNick.add(sortedSessionsByTime.get(i+1)); - sortedSessionsByTime.remove(i); - sortedSessionsByTime.remove(i); - i -= 2; - } - } - } - return sortedSessionsByNick; - } - - private ArrayList sortSessionsByQuitReason(ArrayList sortedSessionsByNick) { - - ArrayList sortedSessionsByQuitReason = new ArrayList(); - - while (sortedSessionsByNick.size() != 0) { - - String entry = (String) sortedSessionsByNick.get(1); - Pattern p = Pattern.compile("^[^ ]+ [^ ]+ [^ ]+ (.*)$"); - Matcher m = p.matcher(entry); - - if (m.matches()) { - - String currentQuitReason = m.group(1); - - sortedSessionsByQuitReason.add(sortedSessionsByNick.get(0)); - sortedSessionsByQuitReason.add(entry); - sortedSessionsByNick.remove(0); - sortedSessionsByNick.remove(0); - for (int i = 0; i+1 < sortedSessionsByNick.size(); i += 2) { - - String nextEntry = (String) sortedSessionsByNick.get(i+1); - Pattern p2 = Pattern.compile("^[^ ]+ [^ ]+ [^ ]+ (.*)$"); - Matcher m2 = p2.matcher(nextEntry); - - if (m2.matches()) { - - String nextQuitReason = m2.group(1); - - if (nextQuitReason.equals(currentQuitReason)) { - sortedSessionsByQuitReason.add(sortedSessionsByNick.get(i)); - sortedSessionsByQuitReason.add(nextEntry); - sortedSessionsByNick.remove(i); - sortedSessionsByNick.remove(i); - i -= 2; - } - } else { - System.out.println("\r\nError: Unexpected entry in logfile: " + nextEntry); - System.exit(1); - } - } - } else { - System.out.println("\r\nError: Unexpected entry in logfile: " + entry); - System.exit(1); - } - } - return sortedSessionsByQuitReason; - } - - /** - * Sessions terminated with "parts" messages instead of "quits" are filtered - * out. - */ - private ArrayList sortSessionsByTime(ArrayList log) { - - ArrayList sortedSessionsByTime = new ArrayList(); - - mainLoop: - while (log.size() > 0) { - - String entry = (String) log.get(0); - String[] entryFields = entry.split(" "); - - if (entryFields[1].equals("quits") && !entryFields[1].equals("joins")) { - /* - * Discard entry. The specified log either doesn't contain - * the corresponding "joins" time for this quit entry or the - * entry is a "parts" or unknown message, and in both cases - * the entry's data is useless. - */ - log.remove(0); - continue; - } - - for (int i = 1; i < log.size(); i++) { // Find corresponding "quits" entry. - - String tempEntry = (String) log.get(i); - String[] tempEntryFields = tempEntry.split(" "); - - if (tempEntryFields[2].equals(entryFields[2])) { // Check if the nick fields for the two entries match. - if (!tempEntryFields[1].equals("quits")) { - if (tempEntryFields[1].equals("joins")) { // Don't discard a subsequent "joins" entry. - log.remove(0); - continue mainLoop; - } - log.remove(i); - continue; - } - sortedSessionsByTime.add(entry); - sortedSessionsByTime.add(tempEntry); - log.remove(i); - break; - } - } - /* - * Discard "joins" entry. The specified log doesn't contain the - * corresponding "quits" time for this entry so the entry's - * data is useless. - */ - - log.remove(0); - } - - return sortedSessionsByTime; - } -} diff --git a/apps/bogobot/LICENSE.log4j.txt b/apps/bogobot/LICENSE.log4j.txt deleted file mode 100644 index 030564fc1..000000000 --- a/apps/bogobot/LICENSE.log4j.txt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * ============================================================================ - * The Apache Software License, Version 1.1 - * ============================================================================ - * - * Copyright (C) 1999 The Apache Software Foundation. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modifica- - * tion, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * 3. The end-user documentation included with the redistribution, if any, must - * include the following acknowledgment: "This product includes software - * developed by the Apache Software Foundation (http://www.apache.org/)." - * Alternately, this acknowledgment may appear in the software itself, if - * and wherever such third-party acknowledgments normally appear. - * - * 4. The names "log4j" and "Apache Software Foundation" must not be used to - * endorse or promote products derived from this software without prior - * written permission. For written permission, please contact - * apache@apache.org. - * - * 5. Products derived from this software may not be called "Apache", nor may - * "Apache" appear in their name, without prior written permission of the - * Apache Software Foundation. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU- - * DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS - * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * This software consists of voluntary contributions made by many individuals - * on behalf of the Apache Software Foundation. For more information on the - * Apache Software Foundation, please see . - * - */ diff --git a/apps/bogobot/LICENSE.pircbot.txt b/apps/bogobot/LICENSE.pircbot.txt deleted file mode 100644 index dcfa4c235..000000000 --- a/apps/bogobot/LICENSE.pircbot.txt +++ /dev/null @@ -1,340 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Library General -Public License instead of this License. diff --git a/apps/bogobot/bogobot.bat b/apps/bogobot/bogobot.bat deleted file mode 100644 index 4c17f7f48..000000000 --- a/apps/bogobot/bogobot.bat +++ /dev/null @@ -1 +0,0 @@ -java -cp .;log4j-1.2.8.jar;pircbot.jar Bogobot diff --git a/apps/bogobot/bogobot.config b/apps/bogobot/bogobot.config deleted file mode 100644 index 647ca88ba..000000000 --- a/apps/bogobot/bogobot.config +++ /dev/null @@ -1,101 +0,0 @@ -##### -# Bogobot user configuration -##### - -### -# The bot's nick and backup nick. You will probably want to register these with -# the IRC server's NickServ.(a NickServ interface is forthcoming). -# -botPrimaryNick=somebot -botSecondaryNick=somebot_ - -### -# The bot's password required by Nickserv service's identify command. -# You have to register the nickname yourself first, the bot will not. -# -botNickservPassword= - -### -# The bot's username. Appears in the whois replies -# -botUsername=somebot - -##### -# The bot owner's nick and backup nick. One of these must match the owner's -# currently-used nick or else remote shutdown will not be possible. You will -# probably want to register these with the IRC server's NickServ. -# -ownerPrimaryNick=somenick -ownerSecondaryNick=somenick_ - -### -# The bot will disconnect and shut down when sent this password via private -# message (aka query) from either of the owner nicks specified above. DO NOT USE -# THIS DEFAULT VALUE! -# -botShutdownPassword=take off eh - -### -# The server, channel, and port the bot will connect to. -# -ircChannel=#i2p-chat -ircServer=irc.duck.i2p -ircServerPort=6668 - -### -# Set to "true" to enable logging, else "false" (but don't use quotation marks). -# -isLoggerEnabled=true - -### -# Restrict logging of joins and parts on the user hostname. -# Leave empty to log all of them -# Prepend with a @ for a perfect match -# Otherwise, specify the required end of the user hostname -# -loggedHostnamePattern=@free.duck.i2p - -### -# The prefix to be used for the filenames of logs. -# -logFilePrefix=irc.duck.i2p.i2p-chat - -### -# How often the logs should be rotated. Either "daily", "weekly", or "monthly" -# (but don't use quotation marks). -# -logFileRotationInterval=daily - -### -# Set to "true" to enable the regular round-trip delay computation, -# else "false" (but don't use quotation marks). -# -isRoundTripDelayEnabled=false - -### -# How often should the round-trip delay be recorded. -# (in seconds) -# -roundTripDelayPeriod=300 - -### -# Set to "true" to enable the userlist command, else "false" (but don't use -# quotation marks). -# -isUserlistCommandEnabled=true - -### -# The userlist trigger command to listen for. It is a good idea to prefix -# triggers with some non-alphanumeric character in order to avoid accidental -# trigger use during normal channel conversation. In most cases you will -# probably want to choose a unique trigger here that no other bots in the -# channel will respond to. -# -userlistCommandTrigger=!who - -### -# The number of seconds to rest after replying to a userlist command issued by -# a user in the channel. The bot will ignore subsequent userlist commands during -# this period. This helps prevent flooding. -# -commandAntiFloodInterval=60 diff --git a/apps/bogobot/bogobot.sh b/apps/bogobot/bogobot.sh deleted file mode 100644 index 7da4e2b3d..000000000 --- a/apps/bogobot/bogobot.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -java -cp .:log4j-1.2.8.jar:pircbot.jar Bogobot diff --git a/apps/bogobot/build-eclipse.xml b/apps/bogobot/build-eclipse.xml deleted file mode 100644 index ee101d324..000000000 --- a/apps/bogobot/build-eclipse.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/bogobot/build.xml b/apps/bogobot/build.xml deleted file mode 100644 index 13c0253bf..000000000 --- a/apps/bogobot/build.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/bogobot/log4j-1.2.8.jar b/apps/bogobot/log4j-1.2.8.jar deleted file mode 100644 index 493a3ccc1361b0d84889798a995a351138826511..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 352668 zcmb@u1C%7~wyxV{+wQV$cXhe4Y}>YN+qP}nwrzE(i(R_)ul4VH_E~qGHSQkcWJYG> z7&&ufM#PhI&hP!+2w4CKDD=0#ZkLFPbpLqq_Y>6DT1r?^fL2^ugkJV54D}o2*Zp6^ zQ2%?Flz_Cjh_Hepos@{xbQd%*BO>T7U#tt{3i+*&zwAnW5W>iC-sU9aXS*73aVD2K48gycnt^`JMJzwul&(W zb(6)UgTWmZ6RNavEP+_ZU&-gj!oFJBhhsf4%9IZxx~v4k5Ou>brr9|CDFs6o00GIY zo2C-_^*aZ?9)E4gKR5X7Up5%@>s=drlYeaMzmI_VJ3`M^&%o5^zXks5PC@$L11)V# zSj_+1KK$2jCir*Z0=BkB)`mv*bOx4s4h~+i+zS0aP=jWO5PxV>l|u4iXU)bAPmcqS z_|N%c!E~^$sp;vnSpzeFu)_HG@{ivd3wi=s*X2BUZsHafbFZi((aOJvJrQDkzQy$XLuAIcA=y{%q-R+yBwckgn&M`E&F4(9Lv zp+bfwmC*{$gX9TrVC^F&JtB!6XoJ*8v5|X&^T|I$50ZJwdvpd^R@kwj!828RAQYx@qYVPVZ z+gDoIcVn9)8iy_0@dzGQoob{mbxN-)LWAKIqrv0?_@(*gy=2+HGN4&&8tRS`axU?# z5hEbNP++T5U?pQz>MqoVr|aRt4Ujd%w$vG{{@k#|@CbfAl?5IE+TwKN2l|T$O#6x& z0MNH@g~0zL;-75)>v19fPsc^U!pPFd(Z>3p$0tfj!xlve^Mg&QLAzz7cHE%0At)V+*b$&;$c|9*0{+{o#U`T87j~j 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

QtPh>7vn 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

9N&ZxCO@RdZ0h&V+St6OH{6>db={MNm4>69~ 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

OF5tCb=``Si1g<}%8 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 C%co)h diff --git a/apps/bogobot/pircbot.jar b/apps/bogobot/pircbot.jar deleted file mode 100644 index d936d70ca958e5b5ba51da8b067090aad3c56abb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73748 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

x!QzrVNN;uDVq}UQm$O{=bRe=JrO0 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 IApfBM0o$!y%K!iX diff --git a/apps/jdom/jdom.jar b/apps/jdom/jdom.jar deleted file mode 100644 index 288e64cb5c435f34499a58b234c2106f9d9f0783..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 153253 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

_4s+#N=7*_ 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�s*dMq>JLX-P3 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 z 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 Y`CYb)y=1%oI@n19M~1g#Z8m diff --git a/apps/jdom/readme.txt b/apps/jdom/readme.txt deleted file mode 100644 index d360b3c25..000000000 --- a/apps/jdom/readme.txt +++ /dev/null @@ -1 +0,0 @@ -This is JDOM 1.0 from http://jdom.org/, released under an Apache style license diff --git a/apps/pants/build.xml b/apps/pants/build.xml deleted file mode 100644 index 7770dae10..000000000 --- a/apps/pants/build.xml +++ /dev/null @@ -1,236 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -You currently have the recommended version installed. A newer -version will be installed if you continue and this may break some -applications which depend on this package. Are you sure you want -to update? [y/N] - - - - - - - - Update aborted. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -Pants usage: - - ant [--usejikes] [-Dpbuild={name}] [-Dpbuild.version={version}] - [-D{property}={value}] [-Dno.prompts=true] build | fetch | - help | install | uninstall | update | version - - build Build a pbuild and its dependencies - fetch Get package only - help Display usage synopsis - install Fetch, build and install a pbuild - uninstall Uninstall a pbuild - update Update pbuild(s) to the latest version(s) - version Display pbuild version information - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/pants/pants/build.xml b/apps/pants/pants/build.xml deleted file mode 100644 index 3f8554c07..000000000 --- a/apps/pants/pants/build.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - - - - -
- - - - - diff --git a/apps/pants/pants/java/src/net/i2p/pants/MatchTask.java b/apps/pants/pants/java/src/net/i2p/pants/MatchTask.java deleted file mode 100644 index 6750e6314..000000000 --- a/apps/pants/pants/java/src/net/i2p/pants/MatchTask.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Ports + Ant = Pants, a simple Ant-based package manager - * - * free (adj.): unencumbered; not under the control of others - * - * Written by smeghead in 2005 and released into the public domain with no - * warranty of any kind, either expressed or implied. It probably won't make - * your computer catch on fire, or eat your children, but it might. Use at your - * own risk. - */ - -package net.i2p.pants; - -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.tools.ant.BuildException; -import org.apache.tools.ant.Task; - -/** - *

Custom Ant task for matching the contents of a file against a given - * regular expression and writing any matching groups to a file in - * java.util.Properties format. - *

- *

Each key in the properties file is named after the number corresponding to - * its matching group and its value is the contents of the matching group. - *

- *

Regular expressions passed to this task must conform to the specification - * used by Sun's java.util.regex package and thus are mostly - * compatible with Perl 5 regular expressions. - *

- *

When calling the match task, the attributes - * input, output, and regex are required. - *

- *

Optional boolean attributes may be used to toggle various modes for the - * regular expression engine (all are set to false by default): - *

- * - * - * - * - * - * - * - * - *
canonicaleqEnable canonical equivalence
caseinsensitiveEnable case-insensitive matching
commentsPermit whitespace and comments in pattern
dotallEnable dotall mode
multilineEnable multi-line mode
unicodecaseEnable Unicode-aware case folding
unixlinesEnable Unix lines mode
- *

There is one additional optional boolean attribute, - * failOnNoMatch. If this attribute is true it causes - * the match task to throw a - * org.apache.tools.ant.BuildException and fail if no matches for - * the regular expression are found. The default value is false, - * meaning a failed match will simply result in a warning message to - * STDERR and an empty (0 byte) output file being - * created. - *

- *

- *

Example

- *

- *

Contents of input file letter.txt: - *

- *      Dear Alice,
- * 
- *      How's about you and me gettin' together for some anonymous foo action?
- * 
- *      Kisses,
- *      Bob
- * 
- *

- *

Ant match task and a taskdef defining it: - *

- *      <taskdef name="match" classname="net.i2p.pants.MatchTask" classpath="../../lib/pants.jar" />
- *      <match input="letter.txt"
- *             output="matches.txt"
- *             regex="about (\S*?) and (\S*?) .+anonymous (\S*?)"
- *             />
- * 
- *

- *

Contents of properties file matches.txt written by this task: - *

- *      group.0=about you and me gettin' together for some anonymous foo
- *      group.1=you
- *      group.2=me
- *      group.3=foo
- * 
- *

- *

These values can be loaded from matches.txt into Ant - * properties like so: - *

- *      <loadproperties srcFile="matches.txt" />
- * 
- *

- * - * @author smeghead - */ -public class MatchTask extends Task { - - private boolean _failOnNoMatch; - private String _inputFile; - private String _outputFile; - private String _regex; - private int _regexFlags; - - public void execute() throws BuildException { - int charRead = 0; - FileReader fileReader = null; - FileWriter fileWriter = null; - Matcher matcher = null; - Pattern pattern = null; - PrintWriter printWriter = null; - StringBuffer text = new StringBuffer(); - - if (_inputFile == null) - throw new BuildException("Error: 'match' task requires 'input' attribute"); - - if (_outputFile == null) - throw new BuildException("Error: 'match' task requires 'output' attribute"); - - if (_regex == null) - throw new BuildException("Error: 'match' task requires 'regex' attribute"); - - pattern = Pattern.compile(_regex, _regexFlags); - - try { - fileReader = new FileReader(_inputFile); - - while ((charRead = fileReader.read()) != -1) - text.append((char) charRead); - - fileReader.close(); - matcher = pattern.matcher(text); - - if (matcher.find()) { - printWriter = new PrintWriter(new FileWriter(_outputFile)); - - for (int i = 0; i <= matcher.groupCount(); i++) - printWriter.println("group." + Integer.toString(i) + "=" + matcher.group(i)); - - printWriter.flush(); - printWriter.close(); - } else { - if (_failOnNoMatch) { - throw new BuildException("Error: No matches found in " + _inputFile); - } else { - System.err.println("Warning: No matches found in " + _inputFile); - // Create 0 byte output file. - fileWriter = new FileWriter(_outputFile); - fileWriter.close(); - } - } - } catch (FileNotFoundException fnfe) { - throw new BuildException("File " + _inputFile + " not found", fnfe); - } catch (IOException ioe) { - throw new BuildException(ioe); - } - } - - public void setCanonicalEq(boolean enableCanonicalEq) { - if (enableCanonicalEq) - _regexFlags |= Pattern.CANON_EQ; - } - - public void setCaseInsensitive(boolean enableCaseInsensitive) { - if (enableCaseInsensitive) - _regexFlags |= Pattern.CASE_INSENSITIVE; - } - - public void setComments(boolean enableComments) { - if (enableComments) - _regexFlags |= Pattern.COMMENTS; - } - - public void setDotall(boolean enableDotall) { - if (enableDotall) - _regexFlags |= Pattern.DOTALL; - } - - public void setFailOnNoMatch(boolean failOnNoMatch) { - _failOnNoMatch = failOnNoMatch; - } - - public void setInput(String inputFile) { - _inputFile = inputFile; - } - - public void setMultiLine(boolean enableMultiLine) { - if (enableMultiLine) - _regexFlags |= Pattern.MULTILINE; - } - - public void setOutput(String outputFile) { - _outputFile = outputFile; - } - - public void setRegex(String regex) { - _regex = regex; - } - - public void setUnicodeCase(boolean enableUnicodeCase) { - if (enableUnicodeCase) - _regexFlags |= Pattern.UNICODE_CASE; - } - - public void setUnixLines(boolean enableUnixLines) { - if (enableUnixLines) - _regexFlags |= Pattern.UNIX_LINES; - } -} diff --git a/apps/pants/pants/java/src/net/i2p/pants/MergeTypedPropertiesTask.java b/apps/pants/pants/java/src/net/i2p/pants/MergeTypedPropertiesTask.java deleted file mode 100644 index 90e9dcaad..000000000 --- a/apps/pants/pants/java/src/net/i2p/pants/MergeTypedPropertiesTask.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Ports + Ant = Pants, a simple Ant-based package manager - * - * free (adj.): unencumbered; not under the control of others - * - * Written by smeghead in 2005 and released into the public domain with no - * warranty of any kind, either expressed or implied. It probably won't make - * your computer catch on fire, or eat your children, but it might. Use at your - * own risk. - */ - -package net.i2p.pants; - -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Enumeration; -import java.util.Properties; -import java.util.StringTokenizer; - -import org.apache.tools.ant.BuildException; -import org.apache.tools.ant.Task; - -/** - *

Custom Ant task for loading properties from a - * java.util.Properties file then merging them with lists of - * expected properties. When an expected property is found in the properties - * file it is set to the value given for it in the file. If an expected property - * from a list isn't found in the properties file its value will be set to "" or - * "false", depending on the property's data type. - *

- *

A property's data type is determined by membership in one of two lists - * which can be passed into an instance of this class: a string-typed list and a - * boolean-typed list. Values for string-typed properties may be any valid - * string accepted by java.util.Properties, and values for - * boolean-typed properties must be either "false" or "true". - *

- *

Lists holding more than one property must be comma-delimited. - *

- *

The output of this class is a temporary java.util.Properties - * file which is suitable for reading by the standard Ant - * loadproperties task. - *

- *

Note that if any properties in the given lists have already been defined - * before the mergetypedproperties task is called, their values - * cannot be changed since Ant properties are immutable. - *

- *

Example

- *

- *

Contents of a properties file my.properties: - *

- *      some.property.exists=true
- *      hasValue=false
- *      some.property=this is a value
- *      property0=bork bork
- *      propertyX=this property wasn't passed in a list
- * 
- *

- *

Ant mergetypedproperties task and a taskdef - * defining it: - *

- *      <taskdef name="mergetypedproperties" classname="net.i2p.pants.MergeTypedPropertiesTask" classpath="../../lib/pants.jar" />
- *      <mergetypedproperties input="my.properties"
- *             output="merged-properties.temp"
- *             booleanList="some.property.exists,is.valid,hasValue"
- *             stringList="some.property,another.property,property0"
- *             />
- * 
- *

- *

Contents of properties file merged-properties.temp written by this task: - *

- *      some.property.exists=true
- *      is.valid=false
- *      hasValue=false
- *      some.property=this is a value
- *      another.property=
- *      property0=bork bork
- *      propertyX=this property wasn't passed in a list
- * 
- *

- *

If you don't want this task's output to include properties which weren't - * in the lists of expected properties, you can set the attribute - * onlyExpected to true. In the example, this would - * result in the file merged-properties.temp containing only the - * following properties: - *

- *      some.property.exists=true
- *      is.valid=false
- *      hasValue=false
- *      some.property=this is a value
- *      another.property=
- *      property0=bork bork
- * 
- *

- * - * @author smeghead - */ -public class MergeTypedPropertiesTask extends Task { - - private String _booleanList = ""; - private String _inputFile; - private boolean _onlyExpected; - private String _outputFile; - private Properties _propertiesIn = new Properties(); - private Properties _propertiesOut = new Properties(); - private String _stringList = ""; - - public void execute() throws BuildException { - StringTokenizer strtokBoolean = new StringTokenizer(_booleanList, ","); - StringTokenizer strtokString = new StringTokenizer(_stringList, ","); - String property = ""; - - if (_inputFile == null) - throw new BuildException("Error: 'mergetypedproperties' task requires 'input' attribute"); - - if (_outputFile == null) - throw new BuildException("Error: 'mergetypedproperties' task requires 'output' attribute"); - - // Add some type-checking on the list elements - - try { - _propertiesIn.load(new FileInputStream(_inputFile)); - - while (strtokBoolean.hasMoreTokens()) - _propertiesOut.setProperty(strtokBoolean.nextToken().trim(), "false"); - - while (strtokString.hasMoreTokens()) - _propertiesOut.setProperty(strtokString.nextToken().trim(), ""); - - for (Enumeration enumm = _propertiesIn.elements(); enumm.hasMoreElements(); ) { - property = (String) enumm.nextElement(); - - if (_onlyExpected && !_propertiesOut.containsKey(property)) - continue; - else - _propertiesOut.setProperty(property, _propertiesIn.getProperty(property)); - } - - _propertiesOut.store(new FileOutputStream(_inputFile), "This is a temporary file. It is safe to delete it."); - } catch (IOException ioe) { - throw new BuildException(ioe); - } - } - - public void setBooleanList(String booleanList) { - _booleanList = booleanList; - } - - public void setInput(String inputFile) { - _inputFile = inputFile; - } - - public void setOnlyExpected(boolean onlyExpected) { - _onlyExpected = onlyExpected; - } - - public void setOutput(String outputFile) { - _outputFile = outputFile; - } - - public void setStringList(String stringList) { - _stringList = stringList; - } -} diff --git a/apps/pants/pants/resources/README b/apps/pants/pants/resources/README deleted file mode 100644 index a11829f71..000000000 --- a/apps/pants/pants/resources/README +++ /dev/null @@ -1,116 +0,0 @@ -What is Pants? --------------- - - Pants is an Apache Ant-based package manager for the management of 3rd party - dependencies in Java development projects. It's loosely modeled after - FreeBSD's Ports and Gentoo Linux's Portage, with two major differences: - - * Pants isn't intended for system-wide package management. It's tailored for - per-project 3rd party package management. You will typically have one - Pants repository per project and each repository will be located somewhere - under your project's root directory. If you're familiar with Ports or - Portage, a Pants repository is roughly analogous to /usr/ports or - /usr/portage. - - * Pants is extremely portable. It goes anywhere Apache Ant goes. - - Pants takes a modular approach to the standard Ant buildfile, breaking it - into 3 files for functionality and convenience: - - 1. The Pants public interface, pants/build.xml, provides a single consistent - way to access and manipulate dependency packages and relieves some of the - developer's burden by providing implementations for some frequently-used - and complex Ant operations. - - 2. pbuild.xml is a specially-structured and slimmed-down Ant buildfile in - which you implement custom handling for a package your project depends - on. This is known as the "pbuild" and is roughly analogous to a FreeBSD - port or a Gentoo ebuild. A fairly explanatory template for pbuilds, - pbuild.template.xml, is provided. - - 3. pbuild.properties contains those properties for a specific pbuild which - are most likely to change over time. It uses the java.util.Properties - format which is more human-friendly for hand-editing than Ant/XML. A - fairly explanatory template, pbuild.template.properties, is provided. - - There is one more file that completes the Pants system: the metadata file - pants/world is a database for keeping track of all packages managed by Pants - for your project. - - Pants automatically handles versioning for your project's dependency - packages and keeps track of their recommended versions, currently used - versions, and latest available versions. This makes it extremely simple for - project developers to switch back and forth between different versions of a - dependency, and makes it just as easy to update a dependency. You can even - update all your project's Pants-managed packages with a single command. - - Pbuilds are designed to automatically handle the downloading, building, - repackaging and deployment of source archives, binary archives, and CVS - sources, all in a manner that's completely transparent to the project - developer. Pbuilds currently support tar + gzip, tar + bzip2, and zip - archives. - - Because it is based on Ant, Pants integrates very well with Ant buildfiles - and will fit easily into your project's Ant build framework. However, its - interface is simple enough to be called just as easily by more traditional - build systems such as GNU Make. - - -Why Should I Use Pants? ------------------------ - - There are many applications for Pants, but a few use cases should best serve - to illustrate its usefulness: - - 1. You have a project that you ship with several 3rd party libraries but the - versions you're using are stale. With a single command, Pants can - automatically discover the latest release versions for all of these, then - download, build, and place the fresh libraries where your project's main - build system expects them to be at build time. - - 2. You want to test multiple versions of a 3rd party library against your - project. Pants only requires you to issue a single command to switch - library versions, so can spend more time testing and less time hunting - packages down, unpackaging them, symlinking, etc. - - 3. Pants is public domain. You can ship it with your project if you need to - without having to worry about petty intellectual property or licensing - issues. - - -Minimum Requirements --------------------- - - * Apache Ant 1.6.2 or higher is recommended - - * Any Java runtime and operating system that will run Ant - - -Installation ------------- - - Not finished yet. - - -Why the Silly Name? -------------------- - - Ports + Ant = Pants. Any other explanation is purely a product of your - twisted imagination. - - -Miscellaneous Pocket Fluff --------------------------- - - Author: smeghead - - License: No license necessary. This work is released into the public domain. - - Price: Free! But if you really appreciate Pants, or you're just a sicko, - please send me a picture of your worst or most unusual pair of - pants so I can add it to the Whirling Hall of Pants on pants.i2p, - the official Pants eepsite (that's an anonymous website on I2P--see - http://www.i2p.net for more information). - - -$Id$ diff --git a/apps/pants/pants/resources/pbuild.template.properties b/apps/pants/pants/resources/pbuild.template.properties deleted file mode 100644 index b346816b6..000000000 --- a/apps/pants/pants/resources/pbuild.template.properties +++ /dev/null @@ -1,110 +0,0 @@ -# The properties defined in this file can be overridden on the command line by -# passing them in as parameters like so: -# -# ant -Dpbuild=myapp -Dversion.recommended=2.0.5 install -# -# *** DO NOT DEFINE A PROPERTY BUT LEAVE ITS VALUE BLANK. PANTS WILL BREAK! *** - - -# Recommended Package Version -# -# Set this property's value to the package version you want Pants to use for the -# pbuild by default. The version string specified must match the version -# substring from the package's filename if the filename contains a version -# number. -# -# Comment out this property to force use of the latest available version. -# -# If the pbuild is CVS-based rather than package-based, this property must be -# set to 'CVS'. -# -# Example: -# -# version.recommended=2.0.4 - - -# Latest Package Version -# -# There are currently two ways to inform Pants of the latest version number for -# your package. -# -# Method 1: Manually modify the property 'version.latest' to reflect the latest -# version number. -# -# Method 2: Provide a URL for a page on the package's website and a regular -# expression with which to parse it in order to extract the version -# number of the latest available package. For this you must define the -# properties 'version.latest.find.url', 'version.latest.find.regex', -# and any regular expression engine mode flags needed. The pattern -# defined must have exactly one capturing group to encapsulate the -# version string, otherwise the operation will fail. -# -# You may use both methods, in which case the version number specified by Method -# 1 will be used as the fallback value if Method 2 for some reason is -# unsuccessful. -# -# If neither method is enabled here or they fail to return a valid value to -# Pants, the 'ant update' operation for this pbuild may exit ungracefully unless -# the pbuild is CVS-based (none of the version.latest.* properties are used by -# CVS-based pbuilds). -# -# The following is a list of boolean properties for optional mode flags used by -# the regular expression engine. Set a value of "true" for any you wish to use. -# -# version.latest.find.regex.canonicaleq - Enable canonical equivalence -# version.latest.find.regex.caseinsensitive - Enable case-insensitive matching -# version.latest.find.regex.comments - Permit whitespace and comments -# version.latest.find.regex.dotall - Enable dotall mode -# version.latest.find.regex.multiline - Enable multi-line mode -# version.latest.find.regex.unicodecase - Enable Unicode-aware case folding -# version.latest.find.regex.unixlines - Enable Unix lines mode -# -# Examples: -# -# version.latest=5.1.2 -# version.latest.find.url=http://sourceforge.net/projects/jetty/ -# version.latest.find.regex=Stable.+?Jetty-(.+?) - - -# Package URL -# -# Specify the URL pointing to the pbuild's package from here. The token -# '${pbuild.version}' if used will automatically be expanded to the appropriate -# version string. -# -# The package URL property is not used by CVS-based pbuilds. -# -# Examples: -# -# package.url=ftp://borkbork.se/bork-${pbuild.version}.tar.bz2 -# package.url=http://bork.borkbork.se/bork-${pbuild.version}-src.tar.gz - - -# CVS Repository -# -# The values expected for CVS properties here are the same as those expected by -# their corresponding Apache Ant 'Cvs' task attributes. For details see: -# -# http://ant.apache.org/manual/CoreTasks/cvs.html -# -# Not all of the 'Cvs' task's attributes have corresponding Pants properties. -# The following is a list of all valid CVS properties for Pants (and their -# default values if applicable): -# -# cvs.compression.level -# cvs.date -# cvs.package -# cvs.passfile=~/.cvspass -# cvs.port=2401 -# cvs.root -# cvs.rsh -# cvs.tag -# -# Of these, only the 'cvs.root' property is required for CVS-based pbuilds. -# -# Examples: -# -# cvs.root=:pserver:anoncvs@borkbork.se:/cvsroot/bork -# cvs.rsh=ssh -# cvs.package=borkbork - diff --git a/apps/pants/pants/resources/pbuild.template.xml b/apps/pants/pants/resources/pbuild.template.xml deleted file mode 100644 index 17017f85b..000000000 --- a/apps/pants/pants/resources/pbuild.template.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/pants/pbuilds/fortuna/pbuild.properties b/apps/pants/pbuilds/fortuna/pbuild.properties deleted file mode 100644 index acfc8b4fa..000000000 --- a/apps/pants/pbuilds/fortuna/pbuild.properties +++ /dev/null @@ -1,112 +0,0 @@ -# The properties defined in this file can be overridden on the command line by -# passing them in as parameters like so: -# -# ant -Dpbuild=myapp -Dversion.recommended=2.0.5 install -# -# *** DO NOT DEFINE A PROPERTY BUT LEAVE ITS VALUE BLANK. PANTS WILL BREAK! *** - - -# Recommended Package Version -# -# Set this property's value to the package version you want Pants to use for the -# pbuild by default. The version string specified must match the version -# substring from the package's filename if the filename contains a version -# number. -# -# Comment out this property to force use of the latest available version. -# -# If the pbuild is CVS-based rather than package-based, this property must be -# set to 'CVS'. -# -# Example: -# -# version.recommended=2.0.4 -version.recommended=CVS - -# Latest Package Version -# -# There are currently two ways to inform Pants of the latest version number for -# your package. -# -# Method 1: Manually modify the property 'version.latest' to reflect the latest -# version number. -# -# Method 2: Provide a URL for a page on the package's website and a regular -# expression with which to parse it in order to extract the version -# number of the latest available package. For this you must define the -# properties 'version.latest.find.url', 'version.latest.find.regex', -# and any regular expression engine mode flags needed. The pattern -# defined must have exactly one capturing group to encapsulate the -# version string, otherwise the operation will fail. -# -# You may use both methods, in which case the version number specified by Method -# 1 will be used as the fallback value if Method 2 for some reason is -# unsuccessful. -# -# If neither method is enabled here or they fail to return a valid value to -# Pants, the 'ant update' operation for this pbuild may exit ungracefully unless -# the pbuild is CVS-based (none of the version.latest.* properties are used by -# CVS-based pbuilds). -# -# The following is a list of boolean properties for optional mode flags used by -# the regular expression engine. Set a value of "true" for any you wish to use. -# -# version.latest.find.regex.canonicaleq - Enable canonical equivalence -# version.latest.find.regex.caseinsensitive - Enable case-insensitive matching -# version.latest.find.regex.comments - Permit whitespace and comments -# version.latest.find.regex.dotall - Enable dotall mode -# version.latest.find.regex.multiline - Enable multi-line mode -# version.latest.find.regex.unicodecase - Enable Unicode-aware case folding -# version.latest.find.regex.unixlines - Enable Unix lines mode -# -# Examples: -# -# version.latest=5.1.2 -# version.latest.find.url=http://sourceforge.net/projects/jetty/ -# version.latest.find.regex=Stable.+?Jetty-(.+?) - - -# Package URL -# -# Specify the URL pointing to the pbuild's package from here. The token -# '${pbuild.version}' if used will automatically be expanded to the appropriate -# version string. -# -# The package URL property is not used by CVS-based pbuilds. -# -# Examples: -# -# package.url=ftp://borkbork.se/bork-${pbuild.version}.tar.bz2 -# package.url=http://bork.borkbork.se/bork-${pbuild.version}-src.tar.gz - - -# CVS Repository -# -# The values expected for CVS properties here are the same as those expected by -# their corresponding Apache Ant 'Cvs' task attributes. For details see: -# -# http://ant.apache.org/manual/CoreTasks/cvs.html -# -# Not all of the 'Cvs' task's attributes have corresponding Pants properties. -# The following is a list of all valid CVS properties for Pants (and their -# default values if applicable): -# -# cvs.compression.level -# cvs.date -# cvs.package -# cvs.passfile=~/.cvspass -# cvs.port=2401 -# cvs.root -# cvs.rsh -# cvs.tag -# -# Of these, only the 'cvs.root' property is required for CVS-based pbuilds. -# -# Examples: -# -# cvs.root=:pserver:anoncvs@borkbork.se:/cvsroot/bork -# cvs.rsh=ssh -# cvs.package=borkbork -cvs.root=:ext:anoncvs@savannah.gnu.org:/cvsroot/gnu-crypto -cvs.rsh=ssh -cvs.package=gnu-crypto diff --git a/apps/pants/pbuilds/fortuna/pbuild.xml b/apps/pants/pbuilds/fortuna/pbuild.xml deleted file mode 100644 index 02a3e8ce5..000000000 --- a/apps/pants/pbuilds/fortuna/pbuild.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - -
-
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/apps/pants/pbuilds/jetty/pbuild.properties b/apps/pants/pbuilds/jetty/pbuild.properties deleted file mode 100644 index c377e1299..000000000 --- a/apps/pants/pbuilds/jetty/pbuild.properties +++ /dev/null @@ -1,112 +0,0 @@ -# The properties defined in this file can be overridden on the command line by -# passing them in as parameters like so: -# -# ant -Dpbuild=myapp -Dversion.recommended=2.0.5 install -# -# *** DO NOT DEFINE A PROPERTY BUT LEAVE ITS VALUE BLANK. PANTS WILL BREAK! *** - - -# Recommended Package Version -# -# Set this property's value to the package version you want Pants to use for the -# pbuild by default. The version string specified must match the version -# substring from the package's filename if the filename contains a version -# number. -# -# Comment out this property to force use of the latest available version. -# -# If the pbuild is CVS-based rather than package-based, this property must be -# set to 'CVS'. -# -# Example: -# -# version.recommended=2.0.4 -version.recommended=5.1.2 - -# Latest Package Version -# -# There are currently two ways to inform Pants of the latest version number for -# your package. -# -# Method 1: Manually modify the property 'version.latest' to reflect the latest -# version number. -# -# Method 2: Provide a URL for a page on the package's website and a regular -# expression with which to parse it in order to extract the version -# number of the latest available package. For this you must define the -# properties 'version.latest.find.url', 'version.latest.find.regex', -# and any regular expression engine mode flags needed. The pattern -# defined must have exactly one capturing group to encapsulate the -# version string, otherwise the operation will fail. -# -# You may use both methods, in which case the version number specified by Method -# 1 will be used as the fallback value if Method 2 for some reason is -# unsuccessful. -# -# If neither method is enabled here or they fail to return a valid value to -# Pants, the 'ant update' operation for this pbuild may exit ungracefully unless -# the pbuild is CVS-based (none of the version.latest.* properties are used by -# CVS-based pbuilds). -# -# The following is a list of boolean properties for optional mode flags used by -# the regular expression engine. Set a value of "true" for any you wish to use. -# -# version.latest.find.regex.canonicaleq - Enable canonical equivalence -# version.latest.find.regex.caseinsensitive - Enable case-insensitive matching -# version.latest.find.regex.comments - Permit whitespace and comments -# version.latest.find.regex.dotall - Enable dotall mode -# version.latest.find.regex.multiline - Enable multi-line mode -# version.latest.find.regex.unicodecase - Enable Unicode-aware case folding -# version.latest.find.regex.unixlines - Enable Unix lines mode -# -# Examples: -# -# version.latest=5.1.2 -# version.latest.find.url=http://sourceforge.net/projects/jetty/ -# version.latest.find.regex=Stable.+?Jetty-(.+?) -version.latest=5.1.2 -version.latest.find.url=http://sourceforge.net/projects/jetty/ -version.latest.find.regex=Stable.+?Jetty-(.+?) - -# Package URL -# -# Specify the URL pointing to the pbuild's package from here. The token -# '${pbuild.version}' if used will automatically be expanded to the appropriate -# version string. -# -# The package URL property is not used by CVS-based pbuilds. -# -# Examples: -# -# package.url=ftp://borkbork.se/bork-${pbuild.version}.tar.bz2 -# package.url=http://bork.borkbork.se/bork-${pbuild.version}-src.tar.gz -package.url=http://mesh.dl.sourceforge.net/sourceforge/jetty/jetty-${pbuild.version}.zip - -# CVS Repository -# -# The values expected for CVS properties here are the same as those expected by -# their corresponding Apache Ant 'Cvs' task attributes. For details see: -# -# http://ant.apache.org/manual/CoreTasks/cvs.html -# -# Not all of the 'Cvs' task's attributes have corresponding Pants properties. -# The following is a list of all valid CVS properties for Pants (and their -# default values if applicable): -# -# cvs.compression.level -# cvs.date -# cvs.package -# cvs.passfile=~/.cvspass -# cvs.port=2401 -# cvs.root -# cvs.rsh -# cvs.tag -# -# Of these, only the 'cvs.root' property is required for CVS-based pbuilds. -# -# Examples: -# -# cvs.root=:pserver:anoncvs@borkbork.se:/cvsroot/bork -# cvs.rsh=ssh -# cvs.package=borkbork - diff --git a/apps/pants/pbuilds/jetty/pbuild.xml b/apps/pants/pbuilds/jetty/pbuild.xml deleted file mode 100644 index f10313512..000000000 --- a/apps/pants/pbuilds/jetty/pbuild.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/pants/world b/apps/pants/world deleted file mode 100644 index 9d4335e57..000000000 --- a/apps/pants/world +++ /dev/null @@ -1,2 +0,0 @@ -version.using.fortuna=CVS -version.using.jetty=5.1.2 diff --git a/apps/q/doc/client.jpg b/apps/q/doc/client.jpg deleted file mode 100644 index 1d3702b50be4d937cc294f351a02ea9ec1222ee3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32224 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 diff --git a/apps/q/doc/diagrams.html b/apps/q/doc/diagrams.html deleted file mode 100644 index 633dda014..000000000 --- a/apps/q/doc/diagrams.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - Q System Diagrams - - - -

Q Diagrams

- Informal system diagrams of Q network, hubs and clients. -
-
- -
- -
- -
-
- -
aum
- - -Last modified: Mon Apr 18 14:06:02 NZST 2005 - - - diff --git a/apps/q/doc/hub.jpg b/apps/q/doc/hub.jpg deleted file mode 100644 index 10069e842ce1399b28c36c3d280d931afa040009..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27718 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 - - - Quartermaster - I2P Distributed File Store - - - -
-

Quartermaster
an I2P Distributed File Store

- -

STATUS

- Whole new (incompatible) version currently in development; - ETA for release approx 4-7 days; - view screenshots here - -
-
- - - User Manual | - Protocol Spec | - Metadata Spec | - Q Pr0n (diagrams) | - API Spec | - qnoderefs.txt | - Full Download | - Updated jar - -

- -
- -

Intro

- - Quartermaster, or Q for short, is a distributed file storage framework for I2P. - -

Features

- -
    -
  • Now features 'QSites' - the Q equivalent of Freenet freesites, - static websites which are retrievable even if author is offline
  • -
  • Easy web interface - interact with Q (and view/insert QSites) - from your web browser
  • -
  • Maximum expectations of content retrievability
  • -
  • Content security akin to Freenet CHK and SSK keys
  • -
  • Powerful, flexible search engine
  • -
  • Comfortably accommodates both permanent and transient - nodes without significant network disruption (for instance, - no flooding of the I2P network with futile - calls to offline nodes)
  • -
  • Rapid query resolution, due to distributed catalogue - mirroring which eliminates all in-network query traffic
  • -
  • Modular, extensible architecture
  • -
  • Simple interfaces for 3rd-party app developers
  • -
  • Is custom-designed and built around I2P, so no duplication of - I2P's encryption/anonymity features
  • -
  • Simple XML-RPC interface for all inter-node communication, makes it easy to - implement user-level clients in any language; also allows alternative - implementations of core server and/or client nodes.
  • -
- -
- -

Status

- - Q is presently under development, and a test release is expected soon. - -
- -

Architecture

- - Refer to the Protocol Specification for more information. - -
- - -Last modified: Mon Apr 18 18:55:19 NZST 2005 - - - diff --git a/apps/q/doc/manual/index.html b/apps/q/doc/manual/index.html deleted file mode 100644 index 3b88e8fd7..000000000 --- a/apps/q/doc/manual/index.html +++ /dev/null @@ -1,805 +0,0 @@ - - - - Q User/Programmer Manual - - - - -
-

Q User/Programmer Manual

- - A brief but hopefully easy guide to installing and using the Q distributed file - store within the I2P network - -

- (Return to Q Homepage) -
-
- - Introduction | - Checklist | - Server?orClient? | - Walkthrough | - Server Nodes | - About QMgr | - Contact us - -
- - - -
- -

1. Introduction

- -
- Q is a distributed Peer2Peer file storage/retrieval network that aims to deliver optimal - performance by respecting the properties of the I2P network.
-
- This manual serves as a 'walkthrough' guide, to take you through the steps from initial - download, to everyday usage. It also provides information for the benefit of higher-level - client application authors. -
- -
- -
- -

2. Preliminary Checklist

- -
- OK, we assume here that you've already cracked the tarball, and are looking at - the distribution files.
-
- In order to get Q set up and running, you'll need: -
    -
  1. An I2P router installed, set up and (permanently or transiently) running
  2. -
  3. Your system shell set up with at the environment variables: -
      -
    • CLASSPATH - this should include: -
        -
      • The regular I2P jar files and 3rd party support jar files (eg i2p.jar, - i2ptunnel.jar, streaming.jar, - mstreaming.jar, jbigi.jar)
      • -
      • Apache's XML-RPC support jarfile - included in this Q distro as - xmlrpc.jar
      • -
      • Aum's jarfile aum.jar, which includes Q and all needed support code
      • -
      -
    • -
    • PATH - your execution search path must include the directory - in which your main java VM execution program (java, or on windows systems, - java.exe) resides.
      - NOTE - if java[.exe] is not on your PATH, then Q will - not run.
    • -
    -
-
- -
- -
- -

3. Q Server or Q Client?

- -
- Nearly everyone will want to run a Q Client Node.
-
- It is only client nodes which provide users with full access to the Q network.
-
- However, if you have a (near-) permanently running I2P Router, and you're a kind and - generous soul, you might also be willing to run a Q Server Node in addition - to your Q Client Node.
-
- If you do choose to run a server node, you'll be expected to keep it running as near as - possible to 24/7. While transience of client nodes - frequent entering and leaving the - Q network - causes little or no disruption, transience of server nodes can significantly - impair Q's usability for everyone, particularly if this transience occurs frequently amongst - more than the smallest percentage of the server node pool.
-
- Until you're feeling well "settled in" with Q, your best approach is to just run a - client node for now, and add a server node later when you feel ready.
-
- -
- -
- -

4. Q Walkthrough

- -

4.1. Introduction

- -
- This chapter discusses the deployment and usage of a Q Client Node, and will take you - through the steps of: -
    -
  1. Double-checking that you've met the installation requirements
  2. -
  3. Launching a Q Client Node
  4. -
  5. Verifying that your Q Client Node is running
  6. -
  7. If your node fails to launch, figuring out why
  8. -
  9. Importing one or more noderefs into your node
  10. -
  11. Observing that your node is discovering other nodes on the network
  12. -
  13. Observing that your node is discovering content on the network
  14. -
  15. Searching for items of content that match chosen criteria
  16. -
  17. Retrieving stuff from the network
  18. -
  19. Inserting stuff to the network
  20. -
  21. Shutting down your client node
  22. -
- Setup and running of Q Server Nodes will be discussed in a later chapter. -
- -
- -

4.2. Verify Your Q Installation Is Correct

- -
- Ensure that all the needed I2P jarfiles, as well as xmlrpc.jar and - Q's very own aum.jar are correctly listed in your CLASSPATH environment - varaible, and your main java launcher is correctly listed in your PATH environment - variable.
-
- Typically, you will likely copy the jarfiles aum.jar and xmlrpc.jar - into the lib/ subdirectory of your I2P router installation, along with all - the other I2P jar files. Wherever you choose to put these files, make sure they're - correctly listed in your CLASSPATH. -
- Also, you'll want to add execute permission to your qmgr (or qmgr.bat) - wrapper script, and copy it into one of the directories listed in your PATH - environment variable.
-
- -
- -

4.3. Get Familiar With qmgr

- -
- qmgr (or qmgr.bat) is a convenience wrapper script to save your - sore fingers from needless typing. It's just a wrapper which passes arguments - to the java command java net.i2p.aum.q.QMgr
-
- You can verify you've set up qmgr correctly with the command: -
-qmgr help
- This displays a brief summary of qmgr commands. On the other hand, the command: -
-qmgr help verbose
- floods your terminal window with a detailed explanation of all the qmgr commands - and their arguments.
-
- -
- -

4.4. Running A Q Client Node For The First Time

- -
- Provided you've successfully completed the preliminaries, you can launch your - Q Client Node with the command: -
-qmgr start
- - All going well, you should have a Q Client Node now running in background. -
- -
- -

4.5. Verify that your Q Client Node is actually Running

- -
- After typed the qmgr start command, you will see little or no - evidence that Q is actually running.
-
- You can test if the node is actually up by typing the command: -
-qmgr status
- If your Q Client Node is running, this status command should produce - something like: -
-Pinging node at '/home/myusername/.quartermaster_client'...
-Node Ping:
-  status=ok
-  numPeers=0
-  dest=-3LQaE215uIYwl-DsirnzXGQBI31EZQj9u~xx45E823WqjN5i2Umi37GPFTWc8KyislDjF37J7jy5newLUp-qrDpY7BZum3bRyTXo3Udl8a3sUjuu4qR5oBEWFfoghQiqDGYDQyJV9Rtz7DEGaKHGlhtoGsAYRXGXEa8a43T2llqZx2fqaXs~836g8t6sLZjryA5A9fpq98nE5lT0hcTalPieFpluJVairZREXpUiAUmGHG7wAIjF6iszXLEHSZ8Qc622Xgwy0d1yrPojL2yhZ64o05aueYcr~xNCiFxYoHyEJO3XYmkx~q-W-mzS3nn6pRevRda74MnX1~3fFDZ0u~OG6cLZoFkWgnxrwrWGFUUVMR87Yz251xMCKJAX6zErcoGjGFpqGZsWxl4~yq7yfkjPnq3GuTxp2cB75bRAOZRIAieqBOVJDEodFYW5amCinu4AxYE7G1ezz4ghqHFe~0yaAdO74Q1XoUny138YT6P33oNOOlISO1cAAAA
-  uptime=4952
-  load=0.0
-  id=6LVZ9-~GgJJ52WUF1fLHt3UnH50TnXSoPQXy7WZ4GA=
-  numLocalItems=47
-  numRemoteItems=2173
- - If you see something like this, then smile, because Q is now up on your system.
-
- If the node launch failed, you might see something like: -
-Pinging node at '/home/myusername/.quartermaster_client'...
-java.io.IOException: Connection refused
-        at org.apache.xmlrpc.XmlRpcClient$Worker.execute(Unknown Source)
-        at org.apache.xmlrpc.XmlRpcClient.execute(Unknown Source)
-        at net.i2p.aum.q.QMgr.doStatus(QMgr.java:310)
-        at net.i2p.aum.q.QMgr.execute(QMgr.java:813)
-        at net.i2p.aum.q.QMgr.main(QMgr.java:869)
-Failed to ping node
- This indicates that your Q client node has either crashed, or failed to launch in the - first place.
-
- If you're having trouble like this, you might like to try running your Q client node - in foreground, instead of spawning it off into background.
-
- The command to run a Q client node in foreground is: -
-qmgr foreground
- You should see some meaningless startup messages, and no return to your shell prompt.
- -
- -
- -

4.6. Diversion - Q Storage Directories

- -
- By default, when you run a Q Client Node, it creates a datastore directory tree - at ~/.quartermaster_client. (Windows users note - you'll find this directory - wherever your user home directory is - this depends on what version of Windows - you have installed).
-
- Within this directory tree, you should see a file called node.log, which - will contain various debug log messages, and can help you to rectify any problems - with your Q installation. If you hit a wall and can't rectify the problems - yourself, you should send this file to the Q author (aum).
-
- It's possible to run your Q node from another directory, by passing that directory - as a -dir <path> argument to the - qmgr start, foreground and stop - commands. See qmgr help verbose for more information. -
- -
- -

4.7. Importing a Noderef

- -
- Note from the prior qmgr status command the line: -
-numPeers=0
- This means that your Q client node is running standalone, and doesn't have any contact - with any Q network. As such, your node is effectively useless. We need to hook up - your node with other nodes in the Q network.
-
- Q doesn't ship with any means for new client nodes to automatically connect to any Q - server nodes. This is deliberate.
-
- In all likelihood, there will be one 'main' Q network running within I2P, largely - based around the author's own Q server node, and most people will likely want to - use this Q network. But the author doesn't want to stop other people running their - own private Q networks, for whatever purpose has meaning for them. - -
-
- This is especially relevant for Q as opposed to Freenet. With Freenet, there's - no way for a user to know of the existence of any item of content without - first being given its 'key'. However, since Q works with published catalogs, - any user can know everything that's available on a Q network, which might - not be desirable to those wishing to share content in a private situation.
-
- The Q author anticipates, and warmly supports, people running their own - private Q networks within I2P, in addition to accessing the mainstream - 'official' Q network.
-
- The way Q is designed and implemented, there is no way for anyone, including - Q's author, to know of the existence of anyone else's private Q network. - It is beyond the author's control, (and thus arguably the author's - legal responsibility), what private Q networks people set up, and what - kind of content is trafficked on these networks. This claim of plausible - deniability on the part of Q's author parallels that of a hardware retailer - denying responsibility for what people do with tools that they purchase. -
-
-
- - Ok, getting back on topic - your brand new virgin Q client node is useless and lonely, - and desperately needs some Q server nodes to talk to. So let's hook up your node to - the mainstream Q network.
-
- You'll need to get one or more 'noderefs' for Q server nodes.
-
- There's nothing fancy about a Q noderef. It's just a regular I2P 'destination', with - which your Q Client Node can connect with a Q Server Node.
-
- A 'semi-official' list of noderefs for the mainstream Q network can be downloaded - from the url:
http://aum.i2p/q/qnoderefs.txt.
-
- Download this file, save it as (say) qnoderefs.txt. (Alternatively, if you're - wanting to subscribe into a private Q network, then get a noderef for at least one - of that network's server nodes from someone on that network who trusts you).
-
- Import these noderefs into your Q client node via the command: -
-qmgr addref qnoderefs.txt
- If all goes well, you should see no output from this command, or (possibly) a brief - line or two suggesting success.
-
- Your client node is now subscribed into the Q network of your choice. Verify this - with the command: -
-qmgr status
- In the output from that command, you should see the numPeers= line showing at least - 1 peer.
-
- If there is more than one Q Server Node on the Q network you've just subscribed to, - then your local node should sooner or later discover all these server nodes, and - the numPeers value should increase over time.
-
-
-
- While Q is in its early development and testing stages, the author may abdicate - the mainstream Q network, and publish nodrefs for a whole new mainstream Q network. - This will especially happen if the author makes any substantial changes to the - inter-node protocol, and/or releases incompatible new versions of Q client/server - nodes. Remember that - http://aum.i2p/q/qnoderefs.txt will - serve as the authoritative source for noderefs for the mainstream Q network within - the mainstream I2P network. -
-
- - When your client node gets its noderefs to a Q network, it will periodically, - from then on, retrieve differential peer list and catalog updates from servers - it knows about.
-
- Even if you only feed your client just one ref for a single server node, it will - in time discover all other operating server nodes on that Q network, and will - build up a full local catalog of everything that's available on that Q network.
-
- Provided that your client is running ok, and has been fed with at least one - ref for a live Q network that contains content, then over time, successive: -
-qmgr status
- commands should report increasing values in the fields: -
    -
  • numPeers - number of peers this client node knows about
  • -
  • numLocalItems - number of locally stored content items, ie items - which you have either inserted to, or retrieved from, your client node
  • -
  • numRemoteItems - number of unique data items which are available - on remote server nodes in the Q network, and which can be retrieved through - your local client node.
  • -
- -
-
- -

4.7.1. One Big Warning

- - If you are participating in more than one distinct Q network, then do not - insert noderefs for different networks into the same running instance of a - local Q client, unless you don't plan on inserting content via that client.
-
- For instance, let's say you are participating in two different Q networks: -
    -
  • The 'mainstream' Q netowrk
  • -
  • A secret Q network - "My friends' teen angst diaries"
  • -
- If you get a noderef for both these networks, and insert both of these into the - same running Q client node, then this local client node will be transparently - connected to both networks.
-
- If you only ever plan on retrieving content, and never inserting content, this - won't be a problem, except that you won't be able to tell which content - resides on the mainstream Q network, and which resides in the secret Q network.
-
- The big problem arises from inserting content. Whenever you insert data through this - 'contaminated' - Q client node, this node picks 3 different servers to which upload a copy of this - data. You won't have any control over whether the data gets inserted to the mainstream - Q network, or your secret Q network. You might insert something sensitive, intending it - to go only into the secret Q network, where in fact it also ends up in the mainstream - network, with consequences you might not want. -
-
- -
- -

4.8. Content Data and Metadata

- -
- Whenever content gets stored on Q, it is actually stored as two separate items: -
    -
  • The raw data - whether a text file, or the raw bytes of image files, - audio files etc
  • -
  • The metadata, which contains human-readable and machine-readable - descriptions of the data
  • -
- Metadata consists of a set of category=value pairs.
-
- Confused yet? Don't worry, I'm confused as well. Let's illustrate this with an - example of metadata for an MP3 audio recording: -
    -
  • title=Fight_Last_Thursday.mp3
  • -
  • type=audio
  • -
  • mimetype=audio/mpeg
  • -
  • abstract=upcoming single recorded in our garage last April
  • -
  • keywords=grunge,country,indie
  • -
  • artist=Ring of Fire
  • -
  • size=4379443
  • -
  • contact=ring-of-fire@mail.i2p
  • -
  • key=blah37blah24-yada23hfhyada
  • -
- All metadata categories are optional. In fact, you can insert content with no metadata - at all.
-
- If you fail to provide metadata when inserting an item, a blank set of metadata will - be created with at least the following categories: -
    -
  • key - the derived key, under which the item will later be retrievable - by yourself and others
  • -
  • title - if not provided at insert time, this will be set to the key
  • -
  • size - size of the item's raw data, in bytes
  • -
- Within Q, there is a convention to supply a minimal amount of metadata. While this - is not expected or enforced, including all these categories is most strongly - recommended. These core categories are: -
    -
  • title - a meaningful title for the data item, consisting only of characters - which are legal in filenames on all platforms, and which ends with a file extension.
  • -
  • type - one of a superset of eMule classifiers, such as: -
      -
    • text - plain text
    • -
    • html - HTML content
    • -
    • image - content is in an image format, such as .png, .jpg, .gif etc
    • -
    • audio - content is an audio sample, such as .ogg, .mp3, .wav etc
    • -
    • video - due to the sheer size of video files, and Q's present design, - it's unlikely people will be inserting video content anytime soon (unless it's - very short)
    • -
    • archive - packed file collections, such as .tar.gz, .zip, .rar etc
    • -
    • misc - content does not fit into any of the above categories
    • -
    -
  • -
  • mimetype - not as important as the type category, but providing - this category in your metadata is still strongly encouraged. Value for this category - should be one of the standard mimetypes, eg text/html, audio/ogg etc.
  • -
  • abstract - a short description (<255 characters), intended for human reading
  • -
  • keywords - a comma-separated list of keywords, intended for - machine-readability, should be all lowercase, no spaces
  • -
- Note that you can supply extra metadata categories in addition to the above, and that - people searching for content can search on these extra categories if they know about - them. -
- -
- -

4.9. Searching For Content

- -
- As mentioned earlier - in constrast with Freenet, local Q nodes build up a complete - catalog of all available content on whatever Q network they are connected to.
-
- This is a design decision, based on the choice to eliminate query traffic.
-
- The author hopes that this will result in a distributed storage network with a - high retrievability guarantee, in contrast with freenet which offers no such - guarantee.
-
- With Freenet, you only ever know of the existence of something if someone tells - you about it.
-
- But with Q, your local client node builds up a global catalog of everything that's - available within the whole network.
-
- The QMgr client has a command for searching your Q client node: -
-qmgr search -m category1=pattern1 category2=pattern2 ...
- For example: -
-qmgr search -m type=audio artist=Mozart keywords=symphony
- or: -
-qmgr search -m type=text title="bible|biblical|(Nag Hammadi)" keywords="apocrypha|Magdalene"
- As implied in the latter example, search patterns are regular expressions. This example will - locate all text items, whose title metadata category contains one of bible, biblical or Nag Hammadi, and whose keywords category contains either - or both the words apocrypha or Magdalene.
-
- Please use the search function carefully, otherwise (if and when Q usage grows) you - could be inundated with thousands or even millions of entries.
-
- If a search turns up nothing, qmgr will simply exit. But if it turns up one or more items, - it will the items out one at a time, with the key first, then each metadata entry - on an indented line following. -
- -
- -

4.10. Retrieving Content

- -
- Now, we're actually going to retrieve something.
-
- Presumably, after following the previous section, you will have seen one or more search - results come up, with the 'keys' under which the items can be accessed.
-
- Now, choose one of the keys, preferably for a short text item. Try either of the following - commands: -
-qmgr get <keystring> something.txt
-or: -
-qmgr get <keystring> > something.txt
- (both have the same effect - the first one explicitly writes to the named file, the second - one dumps the raw data to stdout, which we shell-redirect into the file.
-
- Note - redirection of fetched data to a file via shell is not working at present. Use only - the first form till we fix the bug. - -
- -
- -

4.11. Inserting Content

- -
- Our last example in this walkthrough relates to inserting content.
-
- Firstly, create a small text file with 2-3 lines of text, and save it as (say) - myqinsert.txt.
-
- Now, think of some metadata to insert along with the file. Or, you can just use - the set: -
-type=text
-keywords=test
-abstract=My simple test of inserting into Q
-title=myqinsert.txt
- - Now, let's insert the file. Ensure your Q client node is running, then type: -
-qmgr put myqinsert.txt -m type=text keywords=test title="myqinsert.txt" \
- abstract="My simple test of inserting into Q"
- If all went well, this command should produce half a line of gibberish, followed - immediately by your shell prompt, eg: -
-aRoFC~9MU~pM2C-uCTDBp5B7j79spFD8gUeu~BNkUf0=$
-
- The '$' at the end is your shell prompt, and all the characters before it are the 'key' - which was derived from the content you just inserted.
-
- To avoid the hassle of copying/pasting the key, you could just add output redirection - to the above command, eg: -
-qmgr put myqinsert.txt -m type=text keywords=test title="myqinsert.txt" \
- abstract="My simple test of inserting into Q" \
- > myqinsert.key
- This will cause the generated key to be written safe and sound into the file - myqinsert.key.
-
- You can verify that this insert worked by a 'get' command, as in: -
-qmgr get `cat myqinsert.key` somefilename.ext
- (Note that this won't work on windows because the DOS shell is irredeemably brain-damaged. If - you're using Windows, you will have to cut/paste the key. -
- -
- -

4.12. Shutting Down your Node

- -
- If you've worked through to here, then congratulations! You've got your Q Client Node set up - and working, and ready to meet all your distributed file storage and retrieval needs.
-
- You can leave your client node running 24/7 if you want. In fact, we recommend you keep your - client node running as much of the time as possible, so that you get prompt catalog updates, - and can more quickly stay in touch with new content.
-
- However, if you need to shut down your node, the command for doing this is: -
-qmgr stop
- This command will take a while to complete (since the node has to wait for the I2P - java shutdown hooks to complete before it can rest in peace). But once your node is - shut down, you can start it up again at any time and pick up where you left off. -
- - - -
- -

5. Running a Q Server Node

- -

5.1. Introduction

-
- This section describes the requirements for, and procedures involved with, running - a Q Server Node.
-
- We'll use a similar 'walkthrough' style to that which we used in the previous section - on client nodes. -
- -
- -

5.2. Requirements and Choices

-
- Running a Q server is a generous thing to do, and helps substantially with making - Q work at its best for everyone. However, please do make sure you can meet some - basic requirements: -
    -
  • You are running a permanent (24/7) I2P Router, on a box with at least (say) - 98% uptime.
  • -
  • You have a little bandwidth to spare, and don't mind the extra memory, disk and - CPU-usage footprint of running a fulltime Q server node
  • -
  • You have already been able to successfully run a Q client node.
  • -
- Also, please decide whether you want your server node to contribute to the mainstream - Q network, or whether you want to create your own private Q network, or join someone - else's private network. Your contribution will be most appreciated, though, if you - can run a server within the mainstream Q network. -
- -
- -

5.3. Starting Your Server Node

- -
- Starting up a Q Server node is very similar to starting up a Q client node, except - that with the qmgr command line, you must put the keyword arg server before the - command word. So the command is: -
-qmgr server start
- Similar to Q client nodes, you can check the status of a running Q server node with - the command: -
-qmgr server status
- (Note that this command will take longer to complete than with client nodes, because - the communication passes through a multi-hop I2P tunnel, rather than just through - localhost TCP).
-
- If the status command succeeds, then you'll know your new Q Server Node is happily - running in background. -
- -
- -

5.4. Joining A Q Network

- -
- When a Q Server node starts up for the first time, it is in a private network - all by itself.
-
- If you want to link your server into an existing Q network, you'll have to add a - noderef for at least one other server on that network. The command to do this - is similar to that for subscribing a client node to a network: -
-qmgr server addref <noderef-file>
- where <noderef-file> is a file into which you've saved the noderef for - the network you want to join. -
-
- Recall from the section on client nodes that the authoritative noderefs - for the mainstream Q network can be downloaded from -
http://aum.i2p/q/qnoderefs.txt. -
-
- After you've added the noderef, subsequent qmgr server status commands - should show numPeers having a value of at least 1 (and growing, as more - server nodes come online in the mainstream Q network.) - -
- -
- -

5.5. Private Networks - Exporting Your Server's Noderef

- -
- If you're planning to start your own private Q network, and want to include other - server operators in this network, then you'll have to export your server's noderef - and make it available to the others you want to invite into your network.
-
- The command to export your Q Server noderef is: -
-qmgr server getref <noderef-file>
- This will extract the I2P Destination of your running server node, and - write it into <noderef-file>. You can then privately share this file with - others who you want to invite into your private network. Each recipient of - this file will do a qmgr server addref <noderef-file> command - to import your ref into their servers.
-
- Don't forget that if you're running, or participating in, a private Q network, then - you'll need to run a separate client for accessing this network, separate from any - mainstream Q network client you may already be running.
-
- To start this extra client, you'll have to choose a directory where you want this - client to reside, a port number you want your client to listen on locally for - user commands, and run the command: -
-qmgr -dir /path/to/my/new/client -port <portnum> start
- You need the -port <portnum> command, because otherwise it'll fail - to launch (if you already have a client node running off the mainstream Q network).
-
- This will create, and launch, a new instance of a Q client, accessing your private - Q network. Don't forget to import your server's noderef into this client. Also, - note that you'll have to use this same -port <portnum> argument when - doing any operation on this client instance, such as get, put, status, search. - -
- - - -
- -

6. About the qmgr Utility

- - qmgr (or, to people fluent in Java, net.i2p.aum.q.QMgr), is just one simple - Q client application, that happens to be bundled in with the Q distro.
-
- It is by no means the only, or even main facility for accessing the Q network. We - anticipate that folks will write all manner of client apps, including fancy GUI - apps.
-
- Anyway, qmgr does give you a rudimentary yet workable client for basic access - to the Q network. Until fancy apps get written, qmgr will have to do.
-
- Don't forget that qmgr has very detailed inbuilt help. Run: -
-qmgr help
- for a quick help summary, or: -
-qmgr help verbose
- for the 'War and Peace' treatise.
-
-

- One crucial concept to remember with qmgr is that client and server node instances - are uniquely identified by the directories at which they reside. If you are running - multiple server and/or client instances, you can specify an instance with the - -dir <dirpath> option - see the help for details. -
- -
- - One last note - we strongly discourage any writing of client apps that spawn a qmgr - process, pass it arguments and parse its results. This is most definitely a path to - pain, since qmgr's shell interface is subject to radical change at any time without - notice.
-
- qmgr is for human usage, or at most, inclusion in init/at/cron scripts. Please respect - this.
-
- If you want to write higher-level clients, your best course of action is to use the - official client api library, which we anticipate will have versions available in - Java, Python, Perl and C++. If you want to write in another language, such as - OCaml, Scheme etc, then the existing api lib implementations should serve as an excellent - reference to support you in writing a native port for your own language. - -
- -
- -

8. Contacting the Author

- - I am aum, and can be reached as aum on in-I2P IRC networks, and also - at the in-I2P email address of aum@mail.i2p.
-
- -
- -
- Return to Q Homepage
-
- - Introduction | - Checklist | - Server?orClient? | - Walkthrough | - Server Nodes | - About QMgr | - Contact us - -
- -
- - - -Last modified: Sun Apr 3 20:06:53 NZST 2005 - - - diff --git a/apps/q/doc/manual/notes b/apps/q/doc/manual/notes deleted file mode 100644 index b75d421a9..000000000 --- a/apps/q/doc/manual/notes +++ /dev/null @@ -1,23 +0,0 @@ - - rise on each hit: - - dy = (1 - y) / kRise - - fall after each time unit: - - dy = y / kFall - - fall after time dt: - - dy = - y ** - (dt / kFall) - - after the next hit: - - y = y - y ** (- dt / kFall) + (1 - y) / kRise - -first attempt at a load measurement algorithm: - - kFall is an arbitrary constant which dictates decay rate of load - in the absence of hits - - kRise is another constant which dictates rise of load with each hit - - dt is the time between each hit - diff --git a/apps/q/doc/metadata.html b/apps/q/doc/metadata.html deleted file mode 100644 index ece77e536..000000000 --- a/apps/q/doc/metadata.html +++ /dev/null @@ -1,372 +0,0 @@ - - - - Q Metadata Specification - - - - - - -

Q Metadata Specification

- -

1. Introduction

- - This document lists the standard metadata keys for Q data items, - discussing the rules of metadata insertion, processing and validation.
- -
- -

1.1. Definitions

- - To avoid confusions in terminology, this document will strictly abide the following definitions: -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TermDefinition
keyA metadata category name, technically a key as the word is used with - Java Hashtable and Python dict objects.
uriA Uniform Resource Indicator for an item of content stored within the Q network.
- Q URIs have the form: Q:<basename>[,<cryptoKey>][<path>] -
-
- Some examples: -
    -
  • Q:fhvnr3HFSK234khsf90sdh42fsh (a plain hash uri, no cryptoKey)
  • -
  • Q:e54fhjeo39schr2kcy4osEH478D/files/johnny.mp3 (a secure space URI, - no cryptoKey)
  • -
  • Q:vhfh4se987WwfkhwWFEwkh3234S,47fhh2dkhseiyu (a plain hash URI, with - a cryptoKey)
  • -
basenameThe basic element of a Q uri. This will be a base64-encoded hash - refer below to - URI calculation procedures
cryptoKeyAn optional session encryption key for the stored data, encoded as base64. - This affords some protection to server node operators, and gives them a level - of plausible deniability for whatever gets stored in their server's - datastore without their direct human awareness.
pathWhever an item of content is inserted in secure space mode, this path - serves as a pseudo-pathname, and is conceptually similar to the path - component in (for example) standard HTTP URLs - http://<domainname>[:<port>][<path>], such as - http://slashdot.org/faq/editorial.shtml (whose path - is /faq/editorial.shtml).
-
- Paths, if not empty, should contain a leading slash ("/"). - If an application specifies a non-empty path that doesn't begin with a - leading '/', a '/' will be automatically prepended by the receiving node. -
plain hashA mode of inserting items, whereby the security of the resulting URI comes from - computing the URI from a hash of the item's data and metadata (and imposing a - mathematical barrier against spoofing content under a given URI). Corresponds to - Freenet's CHK@ keys.
secure spaceA mode of inserting items where the security of the URI is based not on a hash of the - item's data and metadata (as with plain hash mode), - but on the privateKey provided by the - application, and a content signature created from that private key. - Corresponds to Freenet's SSK@ keys. Within a secure space, you - can insert any number of items under different pseudo-pathnames (as is the case - with Freenet SSK keys). - -
- -

- -
- -

2.1. Keys Inserted By Application Before sending putItem RPCs

- - As the heading suggests, this is a list of metadata keys which should be inserted by a - Q application prior to invoking a putItem RPC on the local Q client node.
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyData TypeDescription
titleStringOptional but strongly recommended. A free-text short description of the item, - should be less than 80 characters. The idea is that applications should - support a 'view' of catalogue data that shows item titles. (Prior Q convention of - titles expressed as valid filename syntax has been abandoned). -
pathStringOptional but strongly recommended. - A virtual 'pathname' for the item, which should be in valid *nix - absolute pathname syntax (beginning with '/', containing no '//', consisting - only of alphanumerics, '-', '_', '.' and '/'.
-
- In Q web interfaces, the filename component of this path will - serve as the recommended filename when downloading/saving the item.
-
- If the application also provides a - privateKey key, the path - is used in conjunction with the private key to generate publicKey - and signature keys (see below), and ultimately the final uri - under which the item can be retrieved by others.
-
- Refer also to mimetype below. -
encryptStringOptional. If this key is present, and has a value "1", "yes" or "true", - this indicates that the application wishes the data to be stored on servers in - encrypted form.
-
- If this key is present and set to a positive value, the Q node, on receiving the - putItem RPC, will: -
    -
  1. Generate a random symmetric encryption key
  2. -
  3. Encrypt the item's data using this encryption key
  4. -
  5. Delete the encrypt key from the metadata
  6. -
  7. Enclose a base64 representation of this encryption key in the RPC response - it sends back to the application (embedded in the uri
  8. -
-
typeStringOptional but strongly recommended. A standard ed2k specifier, one of text html image - audio video archive other
mimetypeStringOptional but moderately recommended. Mimetype designation of data, eg text/html, - image/jpeg etc. If not specified, an attempt will be made to guess - a mometype from the value of the path key. If this attempt fails, then - this key will be set to application/x-octet-stream by the node receiving - the putItem RPC.
keywordsStringOptional but moderately recommended. - A set of keywords, under which the inserting app would like this item to be - discoverable. Keywords should be entirely lower case and comma-separated. Content - inserts should consider keywords carefully, and only use space characters inside - keywords when necessary (eg, for flagging a distinctive phrase containing very - common words).
privateKeyStringOptional. A Base64-encoded signing private key, in cases where the application wishes - to insert an item in signed space mode. This can be accompanied by another key, - path, indicating a 'path' within the signed space. If 'path' - is not given, it will default to '/'.
-
- Either way, when a node receives a - putItem RPC containing a privateKey in its metadata, - it removes this key and replaces it with publicKey and - signature. -
pathStringOptional. The virtual pathname, within signed space, under which to store the item. - This gets ignored and deleted unless the application also provides a - privateKey as well. But if the private key is given, the path - is used in conjunction with the private key to generate publicKey - and signature keys (see below).
- path should be a 'unix-style pathname', ie, containing only slashes - as (pseudo) directory delimiters, and alphanumeric, '-', '_' and '.' characters, - and preferably ending in a meaningful file extension such as .html -
expiryintUnixtime at which the inserted item should expire. When this expiry time - is reached, the item won't necessarily be deleted straight away, but may - be deleted whenever a node's data store is full.
-
- If this is not provided, it will default to a given duration according to - the client node's configuration.
-
- If it is provided, by an application, then the client node will transparently - generate the required 'rent payment' before caching the data item and uploading - it to servers. -
- -

- -
- -

2.2. Keys Inserted By Node Upon Receipt Of putItem RPC

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyData TypeDescription
sizeIntegerSize of the data to be inserted, in bytes.
dataHashStringbase64-encoded SHA256 hash of data.
uriStringThis depends on whether the item is being inserted in plain or - signed space mode.
-
- If inserting in plain mode, then the uri is in the form - Q:somebase64hash, where the hash is computed according to - the plain hash calculation procedure.
-
- If inserting in signed space mode, then the uri will be in the form - Q:somebase64hash/path.ext, where the hash is computed as per - the signed space hash calculation procedure, and - the /path.ext is the verbatim value of the app-supplied - path key. -
publicKeyStringBase64-encoded signing public key. In cases where app provides - privateKey, - a node will derive the signing public key from the private key, - delete the private key from the metadata, and replace it with its corresponding - public key - key.
signatureStringBase64-encoded signature of path+dataHash, created using - the app-provided privateKey.
rentStringA rent payment for the data's accommodation on the server.
- Intention is to support a variety of payment tokens. Initially, the - only acceptable form of payment will be a hashcash-like token, - in the form hashcash:base64string. The hashcash: - prefix indicates that this payment is in hashcash currency, in which case - the base64String should decode to a 16-byte string whose - SHA256 hash partially collides with dataHash. - The greater the number of bits in the collision, - the longer the data's accommodation will be 'paid up for'.
-
- If this key is already present, a Q node will verify the hashcash, - and adjust the expiry key value to the time the item's accommodation - is paid up till.
-
- If the key is not present: -
    -
  • A client node will generate a value for this key with enough collision bits - to pay the accommodation up till the given app-specified expiry date.
  • -
  • A server node will grant temporary free accommodation, and adjust the expiry - key to the end of the free accommodation period.
  • -
-
- -

- - - -
- -

3. URI Determination Procedures

- -

3.1. Plain Hash URI Calculation Procedure

- - When items are inserted in plain mode, the final URI is determined from - a hash of the data and metadata. Security of the item is based on the mathematical difficulty - of creating an arbitrary data+metadata set whose hash collides with the target URI.
-
- Specifically, the recipe for calculating plain hash URIs is: -
    -
  1. If the key size is missing, set this to the size of the data, - in bytes
  2. -
  3. If the key dataHash is missing, set this to the base64-encoded - SHA256(data)
  4. -
  5. If the key title is missing, set this to the value of dataHash
  6. -
  7. From the metadata, create a set of strings, each in the form key=value, - where each line contains a metadata key and its value, and - is terminated by an ASCII linefeed (\n, 0x10).
  8. -
  9. Ensure that key uri is omitted
  10. -
  11. Sort the strings into ascending ASCII sort order
  12. -
  13. Concatenate the strings together into one big string
  14. -
  15. Calculate the SHA256 hash of this string
  16. -
  17. Encode the hash into Base64
  18. -
  19. Prepend the string Q: to this
  20. -
- -
- -
- -

3.2. Signed Space URI Calculation Procedure

- - This is much simpler than determining plain hash URI, since the security of the URI - is based not on hashes of data and metadata, but on the cryptographic privateKey - given by the application.
-
- Calculation recipe for Signed Space URIs is: -
    -
  1. Calculate the SHA256 hash of the private key's binary data (not its base64 representation)
  2. -
  3. Encode this hash into base64, dropping any trailing '=' characters
  4. -
  5. Append to this the value of metadata item path (recall that path, - if not empty, must begin with a '/')
  6. -
  7. Prepend the string Q: to this
  8. -
- The resulting URI then is in the form Q:pubkeyHash/path.ext - -
- - -Last modified: Wed Apr 6 00:36:37 NZST 2005 - - - diff --git a/apps/q/doc/overall.jpg b/apps/q/doc/overall.jpg deleted file mode 100644 index 16441f9104e6f25137e9097173255f2c6f753d84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36942 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 diff --git a/apps/q/doc/qnoderefs.txt b/apps/q/doc/qnoderefs.txt deleted file mode 100644 index d194dd65b..000000000 --- a/apps/q/doc/qnoderefs.txt +++ /dev/null @@ -1 +0,0 @@ -rxvXpHKfWGWsql4PJaHglAERSUYyrdKKAzK6jPHT4QXRf9jgcVd4mInq0j6H4inVOzT9dG4L6c9GrlQwe4ysUm5jSTyZemxiZpQDCAazsoRzNDv6gevA40J6uGl10JtVtOjqXW8Ej0JUKubz88g~ogPb1h4Xibc-RrtqrvsJebg5xYFkLlnr7DxDtiWzIMRSZ9Ri2P~eq0SwZzd81tvASPj5fb3nySHeABAuY8HrNu0gqRLjeayDpd3OK1ogrxf1lMvfutn5pnLrlVcvKHa~6rNWWGSulsuEYWtpUd4Itj9aKqIgF9ES7RF77Z73W1f6NRTHO48ZLyLLaKVLjDIsHQP-0mOevszcPjFWtheqRKvT2D28WEMpVC-mPtfw91BkdgBa3pwWhwG~7KIhvWhGs8bj2NOKkqrwYU7xhNVaHdDDkzv4gsweCutHNiiCF~4yL54WzCIfSKDjcHjQxxVkh2NKeaItzgw9E~mPAKNZD22X~2oAuuL9i~0lldEV1ddUAAAA \ No newline at end of file diff --git a/apps/q/doc/screenshot-home.jpg b/apps/q/doc/screenshot-home.jpg deleted file mode 100644 index 99eb41cd4f2a0e5b56cea4f2d4b5b34bd8dba177..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60117 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 b?Q^tMtY0W+<*huh9 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 diff --git a/apps/q/doc/screenshot-qsite.jpg b/apps/q/doc/screenshot-qsite.jpg deleted file mode 100644 index ba1e9c5bc65d117f84c63ce95b1b105ec5387db0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46818 zcmeFZ2Ut|gvoAVikRT&D2@F9pl7@_=Axe^*CC4FyAQ=S7IU^up$eCdXO3pb-R)Rzc zDoH_sH@bE2ea`vcd(OG%p7-v3-+%G3W__!BRaaM4_v)%&^}1QOSpyI%$tlVKP*6|+ z3dlde%{qV-;%#RQ0H~+{*Z=?kHu4{N+$;lR0Jm@9;^X4n#>c}465a-qGLe%K6O(e% zfvA`SxP=7yx%v4-rL`4AAE-az<5#j&R@XH!eQYW$Z|iJhPs!U=kkmjI;45Xrs}&?U6@x${AerwZunTnG4$#(;Nwf%6=(UQ<5Qp7(b0Gt0Q=$H>Pmp?d+%#r)X z`gaaX&i;YG)+^+HD@$W{v2vLmnw)QbCGid9SQ7uB50+AMxnBAT5hw9m_Rc@si2UoR zf2?A8G;%XLPCHQ_d6;>t|7?4jy=H&lYJVdu5f>%WW{}dX|7kP7RGfnp!f)%~|7keK z*SDTtQ^J2M9!4WWPVb6uD=Lfyvw+WhtRH?`rbZALt;}lB6JBt%^uzv`f9OQn_wein z(EZEao=owTu3Gv0C56vFwO5o_E4M*DReQqdTH8NTSCk-}Cit!KOaN(Bb1;xq_tf9) z3fA_M6}}3!eWZr}Xb36VE5&xxE;Z90owz&5{=7%px>0%$G?!$aAY`@k&3x^&!;)#4 zp~Veg^;Bn}sCEkIYaW9gktiuE8-)sDR0je0A4+LD(~lBhVWo;9VQayIOnimKcIlHPR?q&+UtjpCE1dQ5`nTO670 z&$q;3G6`8(#to65?g-!wNC(&40E%a9JzBn3F4nJdw(vP_U6Qm$P-&1`3<1RBN%tH= zpP$|8h_;y7nI0*ew(}6A;CuIFvx95dWZuLX^EII;Nhz?^OcoF1j?vq$A$*LA(ZcQI zd6`VYKQ&9)Y)G}{Kw~}Rm2fQMmB0yyc7W6-oW-!#Bq0nF_>tL4D50RFfi@f_QHsFLL(P z!$8OocQbf0g#7DZGggX^WT-EJ_FmS#5VRP9+Ezt+cPP~hD&M5-R}5Ja*|$~Z>o=-r zcv2>DLN^_uUdb#iW`Xke(e^L5K3fS9_l(>CW|~`G1pq}=opKNlgm5q^FoYuHOl@Jj zh@BU3#Oi&_o^6~pZ}wK$8HpA&iJrQ4j#NBVXdA$i+h*k8z!L~!!8 z#O47P8R?=6&ej9zY>jCB8S4NjIBTACtZ?c|=epuECKdIax^8`=!J2~DUaPooV8;%o z{%OrFKf%bVOuIN+MrUzn-N{za0R;_x+EJ3!JQTc*>6UM zR$U#eI;@{E){0Iq7j25!lyNiW!Y9A)X7kn6pr$6{AR zj}0Ok6n4^Dy`KO6;P`c`tdGSd{05*^hf`vuPN@`OcWk7H4w5!@ckvSGp4+|w)PEm~ zmRD9$^I6M0u^wTp$Ig%xb@Dn~!U(!d9gfm{_~~3*4W}_K1|E7UL|7=~XwI@sT;Kb3 zBqFtljJp4*tqXI<8FV^L)P>TwWJ_NjBS}T?!j<-tCn(gM586HnH5*ObnAQSkYn50Q zdnxQ^I?ipn*e#(nw`|>wOBKuTPLTG@T;`DKRKit`c*1#vy-mH$S?8<#9P)5?Wnl0< zc>?MelZzE<=hJ7O#IV`6N^=(JC|{ZEo`!vMnwC0-4!J-+21>(1wMU2Xz^r9Gpfu)s z!ALGA3jcy-6nwEQlc)$8b;~$AgYJzJ{QidnDaWQWBMwC#Jo5O|Q1?Sc$7hM|)}+wO zTi$nZ;;*-9ORk5D8Fa;j&Pi0L4oT#)m6mU_vrFAgY|1x$a=pzB^U;tZf%r37tbAr- z_6q`9Waz#1{>C<)13S6;T4=gO;n5Bp65zzwN&yM6Gf$#2jT-DHfDMz)>(t`xKC_ll z;X$XA3+PY62R~kha>D)D&q&dfNib;0lb|%z8{R`MEiQ};2b)VpdDu|})vViDHW8N7 z^}LX*kGbP7p9J`r;5h`V#xm%=%}|`8V2i6TI4%<2a-g zkgz1j8^CQ0Zj1^C_D|j=l^pIlz6u2M`n^zF0Ir^!$%jujfR&Ncz{P<3ufbwmFS_vR zTM3=|A8=;ElN|s)7e^h)3Ey3C>IPu7b6C(;m3Z~F{05-^#*=73mnH3Kl((S;Rlzgy zYSJYmM=0WoI@DhKJzkNAs~zhqeT0H3V>fg2UFqd_n+uoXY%8ZuY4XC2sX#10=KF{N ztClW?-TLpZ2x73$e5^`%`WY}mV7MDv1}*Y!r(1735z;2^sjcFB3hceK(H!6~JGPS0 z@udy%Q2-n+VDw#(rH~|#K*A-;f`-%~FNW~rFCmK&wN#FNxl*+!?6HM-<&KAG>aym7 z;I-$$z9t*rxKz8zHt>WK2Gs7R=^`>O3J98#cuB(#s3g%Z{aM#k2&XpORhI-ult59H zfyJ^d5yHxdX#6xR=ylO^qBMOwZY6XQCy+3I6L{DN2RXfnXh-at$59Gp?JOlN;xIrn z^qXv?9w-%JppW(1^%#oU?d9&=myk=-g9lM!z6<=~W-$EqE-twfqeJd`VQdg?TBOtY z(|K4*i|$g!RyLt?7cW zkqGqc=0nCLwKFDPwQu2955AZvD=;mfUN9s%;f3e|?s0`VDrObC1GF)FsS1g&vK;1od zyY)#%4;1MVw8n?{%>`S+UJhcZjy<>AY+}>7)m;-831ubhe``*v#NS|vCHT!i;W+-y zFlnsX5zT4fTkQZ6sxpgWqVWZN!pKo{{X^7cufV0@H}XGAU8V~yJObGe{f&ehsRXdX zJGF^+=9punZkd$WO`{5X>JRSR(b4F`&S_Ot2uR8~z$g&RHtuu}9%?dwrGaM8fagPp zRrgKAU~Tsa+7Z!xe3k{{G0L^L-j{~ncZ#@O7o^R$ti6qb&W2#Q^(=Tzf8M>9(qAha0 z^Q}141rFSu4q#ufY&av98`8~JL%#A;@Uhuui#-|QFgxs!BEvnO7l5yi)A55hbHgsC z5R}H$P<#uD)R*o6vTfuN5AxE-#bODqK38L0A{VlST$)ba04SPOIoS4jMT%KH4tNKs zsHe>V--)12b#}`Wo<}(P?6&uJi%an0^b8i>F}5)BeuE&4P<@o9vvQKjBbkctJ%=}C z90x8Wo|eygYk6c5-pkU_L`?BYV_T1yKBj`sOtm7zm!96rO|cW=+#7Kf;ksR^iCCw^|6cc{raq*Cp& z*rg)l{dA@VCjn1;HVt{z1uBMj{f^eYK6JPM4$Q8-meXp@^``#ekK4{`_J;*G zfQji2ytkqFK(aUPKp+Yb86EgvIQg@ef4JX)W^4MqZL+mmUm((AVIRI&)d3!>*iK6$ zi_}|us7+oH+<0t$+oL~DZ%nKn>bc^$y_#pe z3iBH>{CX}Pm{0@1xH9Y$Emw@On zt}j?e6yNjxjy~o1g6iUXyv%errq8Gj10vH*3jJ%Q9!7UTs&8JnP(e$(ks(B2D%4l3D+0?0E--_g;3yFDd~ZZhP)q&P-L=r#e9EeReMa{a+=# zBY!;XFxDI4_Vb$QC!fTFxlZ)|hUFl+6ZX4n0O$q&o3M>DW$2qKhbwk{AkYq77UKPq z=k>!ca8w#6O)C)&V<=bD4d9mcA0&u4+T^~O7G#pXUXiA*|0x=~f6e0$PT;;j+a>s; z0~b>xu!+>yX$a>jJxF$C)zp86YqD*jt=Qle1L(w$KHKOtzB2IB3G4;`3z7ZkHT0*1 z(|+F91o<>S6Sn%HD6(yZt;Dbv1MVAu?rtS$Y;NvSXKB70#(tOOU&^WGPbvS<)zpC| zsNIR73oVm9!!Y#~q@8Rx*0SoclTJ3SANQ1Mv`FLJTpmfvA8q_iEDC>EL*(DY^2g;d z`D#$o#sd6o>BM}7(9O2Npp(EbF6_LtD&5p-Y3}WYxA4xvxT%O>@U`MMzVz|fwZNoZ z+gHvs9kfl;5bBe28}1yc&Km&ZRp!W!+@}Xa9{eiro8F&ag5rFB*AE!oIw|y~-E5^~ z{=EG8n?F!q?04-4)@~P5uWuh4;}dxwe!XgsD9rs`JFh|5Z}%P61y^|FYp?If7plg@ z@7ijMFWNnv(Y~k4$i5Q|c;H;Xvh%z4y%7Tp8m6IzxD8cu-24Ue;b1I4?_^S0h}OpTFij-t#>BP-UvAL8hl1~O6Cfm zuWhTz^fN!qCGs=qZPUL2Jga7ITOxDtznpEaADeyoYK6Ah<~Xd2%(vvK=RpdW@p|rf z2d)zi5a><$KX`L^x5-NVc&!l2%A(84{`y5mhR|jIyG|GzXWkKGpRzaRm_AH=XL1!- z7GwU}82Pl}27qIv;!lG{)yn6E`qASC@b|uOd zRMlhR9U$knbD_wcd)FC#d|K*e{_T*g^}z}0VZ9x%vI?p7LsS2PFY?#smVIBAxIF^5 zIUO&OW+0i#WUIDqNdG7!lgIx&`Yr86WW)A9s?I!-$;q$(XQe+M%a*W&Pg(ya%KW>P ze-i(17mL*oMY;!W|B`ut`5&tIc^j$UKUMMSe?>z^zpUj?YJaHT6=@?Ys~3@fs=xRz zv-s=&YR2EK;>mAT0?hxgiuC`W$+7>_694}-K%#r)G9qydjT;FNV`WF*^dt7krTk=;ZPg(<4 z_%5W^qT6P!*Yk!+eS5@n0!UZxfIy%4LEJ!!uOC01KOKAUC8H`1qDgaocS679`h=uz z@YQn4|KMHlKXETe@vQ|lvBFb?L@7vXG*A-9>2}buj;WU3DQxc zJq?rF(tYNw!en&XC=eTE@AZ6eUZA^kX%1ZXRp#+hQPIHt;b9zhyL|>vF;o2Q!;zCv zIQNT}kx7APx!MoyHkn5(}u5cYSB;-7fDn4M*f}O=)G(i<(S3Q3W{Q z;^!$Td@Hf=c5o>%f4WQ~H;o3f<*H-E)x&+a>8Kqa=1pfAH=iZz3bxsb9bi|q#Y#>) zavC?T*m#S6|0{ibdse+c>6Rb&xSa>ysO55ssk0y*z|quwZ5HY6yylndhf~_FIC8of z;5;zEi${OW6HkGf+xA)Bvz2dHwtU^c!?dOu3ma|1Zt zAuKKkF}e#oifTLKb~kq?m?YiwvH^$x58H$^I{1 zsg1Vp;8w@q>0#YxKfn2q`jOnNb4a_T&Y(0=c3=1n3X7GF%eulDGDVoAZ+@(tkY zQ{oJX2inYh%(tUw*X%F;el71`uvRyU8c8O?RdZn!(vjGdLJmew?o|= zq2C8Sn5-uUexXGI8>I_Za#2KtsbjUim4|t@SAeI4D$l)T;w-95v#~>OL&dl`@XCBo z<4zAYw{{E`4y~*NL#DMdH}Wg+V%m-{4rBYq2&m9%hUMwEOle|8yD}FAjL|;q@~Bi> zK8!tNxsoLfAi2@6Cd_3B=?AIae=av5 zB)4#PpFpYrgbcajuHt^6TF0c3i7?~#J56aMaNMd*C#B+FWe6HGJB3P}2{}L0Q)TA~ z4{uBw#5mT2=Vh%K+f{q2Fbc(qhfln}aQHx%8C)5SHW1G3hxJ>eF@I=ZUERi+Rj+l+-C18$kuNer`Uo(8wH=AzLv^f7woH4ev|iAX~O~G z{nr$9DS|2JVSa$|ZVwsfo7ln);rfCM>jkptf5bcY{A%c;5E0!uiJZ6ooq0#=KQr}6 zUdA*|X33dov@TNr@UYkylh_D@27kR|QHmau&qGe3cy$ar>bFZ z2CJxAadHXb<5Rf3QJ**7OXCQZZ^)~o?1e$8-Q$FJHC!^IaBzZ z-K8&R>fkNz;^tg-5!bPb| zM0>mzsGvJ_SbL^r_K!n2IcDUkZLTI<(DqrFAWDt~;V5W%nxu*=2J?GV>5mz+SCg&U z-9)sGbu=Nk%t-~za^I|cbJ2_gLdO%NElnetqgZ0<75e2cMpo7Uv?8Ayx_3RsQp4Fz zgH6P8q67QULFuF{q%YsQ&4liM?w|1dJN}6Yw{Aqt_a|R{#jW$cjp_`5%Hoh%j!osv z4ZtUAVbl0<7(1f;KR$S0vS|iV!Ona-h?PE62yjd*nG9b>1aDeU(LA4<4lIWo@KNBo zekI1$Lm`$nT)tZ5x;^2^sWn6(S;=B!lt9JZ^BS5cr$s5TImdHM;0AvE>6-t8n| z0g4ft5ab+SF%9P-lU!oO*QDO{vc^YGghLmbY&b6}3unv4_H+;WR#cP<$5R7Fjr$-5 z2H_61^-(QvG$c0U(y_vE?+`etJDZvN%<2(8^hg-o4%s%F?Ytw*oqGc~*FB?*7t!_w zJ|WU5J%Q=gk-0KiNy{}R&00HOXR)2P>IB!K5vd%0KSIB<$J2RgA8Bt{>~4AkcyEtZ zWY+uuwfhR`r?YE+_4^2oW7WfYDf{4{?<=`cNTJ$$wgzUSCJ?3vRvBxmST5a5tw4&z#A(>1ZkFNs9{6wzXIGt3X- zcPozBEQRm6TApN&h3JSEJP@88s1j9Z$R3@E0xuT%)HAeY7uHce^QT1Qen_O(xHn0Q z1(&g`(7}OKF^G^p>P6j}6$5+q`xG~~ICYZmhBpA)3BAy7mA@s}`72=2&oj)1~OOXXARMr7@Wk;m9YsX@=-a2C`GhPRTD06pDmmUP8XJN@(h1z zRV*v4AxNCCZE=b%5()w!%x8x7POFUn&eIP|(NWY1x3`bS1|?-6)UNo0r}-SzQtyr& z-+6FMT3>Jq5W8t?&mOVPEby;2r@ldzddR0jcK4*q0w|4b7o8wGL{VL1)qiV!G z2MSY$E@=q*eCs69)airC*s47?`iAm?AT$p(rx-(6q_Ad*Z;{sh9oVnQ;6iGcL7}WRHtc=M^tQ17Z?A)n> zv)RMKVuM9-HgzATKkPoFPFy^)DuPB1Z*ZSb89l{@C`3fPjEF*=_`FT?+W)y1Cx&L% z#2PYMeEc1vG)F=ffB*3PfoPqW&vR#5v$xU4#EFUTM@7BA5ge%9HCrB7cw#;;`+xN< z6ULZ9*QXR)jv5b*zEA{zf9OU%h?s1O2sm&9^we z4($SgxY49RpMiOQy|^Mt_?Dw)weP&8u7!}3Eqk>}2SkhcLwHu#56-dzKk^t-M>n>} zF)naSlxN0dRu=j5f972!5LD7r39Cx6VubEA@m>u;A{2F>w_8t3;Zuk83mLu7`rOh) zhPsOFc$!mTMnd7(W9@_@BW-*)En84AlKfelA27auM@7{)}?&6%Q#v`^dpI6%?BHWlQQ3IX+Ay zVUG8Q*st-RTo3M(nPw>(YslkG#LMk5I7D;%6-6)YMrx4wNmo3|Pb*}0>#9pfSPWuZ zF%MQ?N43%Dz&`|0q}Gm_V}2QX%;is|NGpx|+m(wx#yx#T+QbwV;Wh2GYSkhPUd_wS zI}M5`bo|^3J6)eo0}C8M<2pCi@UOk{%FrVUJw78~DO;-_1_#8e>Xzmba4uIQ8bPQa z5O&4f8~dtf;Wi!(ymZ`7n(!;so!v(w-ph}M-9DaKTI9|x9OBavoi+R0GD2u%KTppn zYw*?g5#heuCwh!Av`_>Al6c$08{ZJGrx6M~YyS?LUv!pinhWU$(Sd64b<$c3Uhr9> zr2AheS1UBVG~73l(S}u(mf+O}@qX-J^&G3(CB2-~hnNzU&z4%1;*INxrI{>7f>FOO zJCZz;BB60BFj!oJmBbzf>3>XmG{Mny-xj3=)+9E+$#%>VMK+6FnqXt)DZ?Z&(&1jI`dioYjtNxHYOJYp;Gq8aI+a;zbyOY}s|su$*241s^{3;v-Mf zFqILvSG+>q;;#5Dc6QwS(w@)ei+F0`N;ebv|>7Y^BRg z^3FLa0rsNE@5P7S3q6S5+~7KYTt>^-F{-6cICh+ZOZX8NUFfu#)aN{-4!LvbFDlU& znisMOa)R>mfhV3qGE6UftvuORH8?JXaJ$?K(Kw*)&Tu#Du@Pufy|$(UbG5MTP5|?T z34P9#Cr=se^kH7M_yqsnvDPPIwh-KRjkyPOVXb@ua9 zd*xdmOU%Q|Noa2TfTvxrU$0g0yUeJ6eZWw=Brmt!?U$&!yULLD&MN_j@yg+mA_Wnj z!g<~6bl2MDTJFhBsgpunCvumaDGjm-HMEs4aGyt3@hlcOdyIN_tH&XbM6LR|My*-1 zB6C23AeNL^@HQ}1)tezLxE(`L7X0$?V9<-wJL7MW$ zYPe;AQ{gdSbEy%&NC1|*$f9b`P{vadG{r}tBFj&Sav=9bJUFiwzwm_X@HRZ!%FdQ- z0er0QA4OO4Oj9BmH=KgnWZk_n(-DUe+?%aO<(3NxReRp0tSG%#y93U(Zdb}!FJT(@ z5wtLwi%vkxsf|XnI#USU&hI{pin6tW`Drox*i)dOgTdhf!A1BXn=Om}j2>0gUa9e& zMa5S4lekY83oLyqM?0zb3C!F8y9N=VJ5=H7g9p+@WJwI%Am~ zLH2+%G=AKx96wj&vr721rV6fI4sipH1;>;?_#u^#lOZ&l4TftdjM8oJ6Qn*@Vei?C$uv2} zCLjfWl|5YaIy8M4At8^~^TDdDL;xT7re2D%-n;G4-O)*piF?@4nGt|pw`r=OwUpkE z!BHpAbl$2r8uFHu!1?M@YcN!l+#>H;2FwFCf%N*ll`G|@-B1G}DLsVp7sV-wWlF zO74-eQAmf@=hmp|ESpDaG^APjpeUgiC-@6>}w#Ag(ZSZo4gwoyC>}cU6(qz3M@&qjw*V_ zEKk4sQW3>aoD41};a1A1C2^?fE&8+3eLaV$5BJZ`XoR=ajWNbWg)8LlSc2``bDq5I zy{oET4SCkhQ0cD?EG3?u>LFG$b8)eJa%?#6v~L9MxF>QcE0wB&|L`t81P!kyOK8I~ z?D+^MM*e2tn{*!PFZ!v(99eAfr0Qd8Y8;?kNsx!NuJ|EEePSR}OZCQsm8WF0pxfEK zwf%kK$}Dz9{UeI`zJ})bFo!j2Cp(8al>LBVKj$jPe$P%X1TlX~r zTEL;Qeh3mp8i+V~)ZmzDZLqOb zy;genc4Hj+pjV>B69Ns_v#^Vzu0_@!Q%;W9Bu$R9+jNa{p5w6t8OCc{r!+4r_7bs% zw5HSur}|Ky-}cn1_7JJqa_XFe+b&5BEBLl@$&HmiHKH576k=oGg7J=M6gm=wgZ1cs!Ef1?mj_Xx8CM#ZrC#qRdXEts+M$>#odR@G|LM z^X5mQ3= zmPW|V5AmN!Z^Qm0nyev+bcT{zqxHYxWZO^fsfP(elA-@5bI6+OOQ0tWg%*u~)# z+t*ctPF>CUK#EH0^yT+iDH?5l_=zC@quRQ|7BY0b*u1amjgu;oV&Ot#DtA@xeUtP- zZ%R)ew37(o9M;{=!z(Gttglxp*nZco)M6r$utTWbODi1TZPUw#NIm1_39byW*Ht9C zIGT@*5p`5VW+!iyU|Yh;W2#}kADHswQM|c49$Up$L(k(LF1A1s=Rc-d>*{pXpWBfL+#xinytE1=KN1#n&oWeFXorXtOj>5v8)>-2C?WL zO!g4 z9v{n&9b^j>Vf=TnqN!R6%)i5mj9+0?jkdQC`n`AQrwkRf4P`Fvjn1Qs8pksm#R0cP z#@r22*fY)YV{J{Nl_*`;=hU?Ji?f>`xCRZZa&6L{%pjw_lH?v)`YHbUmAs3gj+Af=F*Gw3_nd+UZA?e?XYY$yDQ;jL^I1X83WFG<1(Z0aBgXXVR)|P zUAhqAZCx8sT1Xm}?zAFAUW}RUy{6byOI+F^11s0ki5vcTJd<+)o--yZYuTsmRuQ0z zri0p3f8f|KWRMK?8>dT!dZ)1r$7n%{#lx^8kLg}R9|Z;$*M;qs1JsR9?gCj_Ub@&! z`}@O`8CxY$@?#tcElCgE2+5tej*X;t?yAA`U|s7472SqvYN?<45BbkqT<}Ix$1mQx z{NU@IEG<0-KjFI8rv_|C=d|sziJo_#=0mXU93;8PnENl}v@eT7>RH_FYaU0Q5%p2K zkP0ATc=D@WRk;~Sw!cM~8bsVLbMY=Mug%Ah(Cwpw&jdGi@^R^k?zqqe&eYlKP(FQp zAnl6Cy%)3W#lMi~tg3Ow&W_w-J3N|HR|D;82 zYRBd3@jN*@{nEYcUa{rz%;52w-6k(Tft%j;;i-O~g5LC}kI zVlPt+ek}G%KrCEt;rK`sQ&D=S_Idm8? z1w3xRTTl_|Wec^ls&>d`u-~MQ!5>3NQ>vxRm_IeNs3md6^Av|z6#Dy1mWA7ZMO@Y2 zr-8Fk*VIy|!P}nKB*BKeQR6-1dBimp&PU&)yzU|%_U74b2KH8wT*QGP(1lWw{2&jAsRip5g(mg}N-}TOx zJ<$__9w5^wew7jZs)~#){VG%WRh6jVpJe_J<3DBdPrLb#42HkG3{$3uOM5yR*7>J~{+@hmrai9MN@XmasmpPyOU}{4s zv@ByuFNer;UJ{2ZnFkUfJm8&NFU!4!zZ2wLn?{rwbQkN?JSA==Or z+Tvja2RrN7BpFS-n8Zc$Z3S%iR+Jsl2L*+K*j+mXl^&^lV7+|1@Yq7TI9?~`fWQ#2 zJHJP%ewaCDesO*qJ3TDxUMA&^bq3F&YcrzTQH`}_xAm<@j77%0c$LRFNl&q~I6aP|vBB4h5hgHReBtT5;JzG7zdfWsWhb;hFG zHV?_shgm!vRrGU_W$vbo6Vshjx0hExGJy(9b46_7$)fCh0!?+Iih#gJz(;?K zuPTzC$O>;aW^MXvO6&IAb}ibzjs`l!;0JuA^8>yD;s3_FNUX@V!nR5j<{Ib0_@3vc zW}DP=7j0tYk;3e87;eHZ_u$SX&r3O#v}#F4FCOp_oSL8>sgjow%fpc$6uVaUXQYnPPY*Piay_nIZ+m-rrs zKZ?;tC;CB0{U5KIkwX1%U+7{-M2!E1jO6IbT~nHRcFa)0u}T9!&@}`8?i}$Ba#2!D{>=MX!Ul08 zpP1aXL>8!@SMH9%{CJ&>n*>kbu#L=98>^i#94)zr%(SX^9A?SLB!V@v;5 zZ+4PDBl6Qd*b&GKR^%MbLB8Ra;#7j?>erkN@5SA+O?YiOsW%s2#JQw9Hs|0yK!=(d zMM&7^-Ex9GAGM=qXD)Y~Nc|CH?3YJXor}|1PIgvQmw(9Ch{8;(QPY|qUs39(YHJ5w zk+Y~p9M+<~PxLpZyP&60(P2_~v_#sY{V#X)mGx&ohMt39|)vdz`r3I0O4zQ4w;Hl|0MLHZA8=1~~ZC12&4cwk+=v z)Rb6L;aUjXZ^dkkZO~hD#{y2GBwPlhB3@cCa#Z-M+wqCFD8JZn1Umw<`5nm`tAp|H z!wQH`dj%O3EOPmvdt%QHm%O4&tppOxJh?Lua=q$~tV8aMT!>;m+PM>u8|F!9HoboT z>v*!7!ax)o%fb*XIoR_elDd$4Dto%pf>$KXKxD9xS3I7_(wRMU`LX!RiPBuF(wF{> z+&=;^JIdq@I6m~RU&%N-9D>f<5Olq1aUxHP8}1Zm)ssQ;2E(QWPROXYI?={bz~2Ci z9zDGa{+1$d1Qbzox`EWzlB3hY{k-e7`UO00l8eLbkw6N5&Wh-+klg}z2U@(OdckNQ zL-iW|#gUF?nQU_BclO(*ao+i9GW?%~!xi1EY+avpd%pF}dwE<_A*(wY!Qf;N!y8^K z{1_R5;je9p&)B59zhb=E9O_?a1Jg%FVDihmvwufmmewdz1ja8`uxXL|CJakMZv%UF z`*cXqXDRJPyu@QM(e9fy7)*NI!Kik6`bztSYHXuthQDY5bC#flE&$u9e+&O!;mGDr z{fsOvH>c>!;;hUR6nOF*g|!}~3w?4Yb<{2)Zf?TAv9L#uG)ymBT%v4BD#7; zkKQ(}ev`0)NvE?f-n^+^goMMLmt`T83M(FogF7R7SNbQBOyF7$J9NZ+6AOi*CoJeu zYg*)bFXj-vFM}g9X+H->sB$Frvn#P+yMo(^z*+4LtwvCA?X2RXfrp1mQjCwO60ErZ z(Ri;=sB{LNaX8LS$$bVJU5qPPh~hu*@}+`@`hH(`0NqZ3a%#Dd4ahJ_D7jD%QpzV? zNCrray76AfO?h4~30 z^LolaLbJ(cr(8u*B9G=I^r-vWj-GI7nkx)*52{?7cL?>8+_5#(w@+{2vCx0suE5w< zNxjlPPP)Z-o7Mjg-Nh|iR#;YzVYhXbWWNkCN?a1SS&%P_W^h^fhzwl2Y5H24GM0$7 zoMlm)lSWi;U`U=xE@Kk|8ThfaYac8ACSi3(C`BVmy^x(EeB`t2+tR;zWrc(rclXe? zEXmtA&Ug_*nYeGMUd#pcZ+kh(y#hNVTMRjrH}VT;r=F?gj+Oe26As!kCUlQ1##y%X z7@Ap(I{SvSw*V=c541`6YiFhpl9a-I^!WrbRb3RQ=WS_K(r6$YeJMvtI5G`FIuZs? zX6^2Brs%_9qe~l{wT{iW`-vncF~y#kxK*M_l*P04U`caegN4ytZF8cF5p?=UvY;|7 zuGjW$gytA)viQS;$hZKN;7U9EB}oF81z{&298(z$Mf`^e?bhiT#+gCQTOEqd^q^&K zUw7_>5S~XLf>zIrI=lDtLU8E(Eni-=(E0Jpp*-I#EYFz+3(iPbcTZBK7K3TO5E?s4 zO*BH}F)Z(IzcHC2`d||!w8r58CAW~#cX#X?r!$3)rOy@Mz2;3rrl2P!v8ZNQW44Wv zxemXD4WxA*T&$4oP~v;4FxcB|#`wgw&gJgn0_vshdrJ+3&=Pq+V6L&C6J?NW4^xL7*CM|-iu~>p z4+{}Qwa1ktu5${pSigcY zuxBeo39^22oPFT#U3*>JH`iKv}x72X#ScvawGXplMgkJ?Z8hT0?%$;>3au z4uux)&Tjy1+0_5Jg9%!0lzjyIkn(_S#ff))rh`x%Dh)rdd| z(~$;?i)e~2F=(|YhUpgXYmVwO)FR;u1u-g*7#tx8mzc&vZXV%ktiq%(hPx3L z!}jD589Sa^tkzS!EPJXEEisQDw-9VPG*~YwfKf-r9u*iV**SpHlx{_(V@dZXxbtHQ zOi)*w;a?zDLWCs}fu@4tX3(;N3P0pCIo= zC^m~M3DU^nOk=TqA6*GZ8^v-CmCQ*%H*BzoOra_1`QU)u?Ka(yYq9`< z$2rJNWh_5lwmL6Okl-p$^UjQ(UDrZ>lIfDuM8vDxV9)B_dbq(ilr4Js7&Gnr5J}bz zkB>!#j?!5M@+!n}J9N=op65o3{@sjt_Bs+zD@BON;~zyR^T|YqGP?7Io91>+T<5S` zA-{%0^7Hcr|K_VW#JW*Ue`VNO{pGX57ByZ*L`O^imp8jbyq2|j|7SP_PGay!%U1!K@o;) z$kSZ7@#9`8(f*m3|G#VdYeWQJ-EYLj;8yi-=aVf-6WnmOG)~{%U~tdO-H=2!G014o z$#I&?FL7k@m0=>~V6DSeI(|4JXJ&$*D?iJ7JZYMb4y99K^^8BnNeU~~B%ZJ|kFc~j zBoUS}-I%0sp)7?F>)DBqT$nSDOEuXSN|9PKF3x^I;qzfb{ z(!12q5v2DHq4yF(XaN*KrARMA=pE_3Cx8VJkREz3f^-m60u~sYIZ zci!`U*EiRdKVYxD*WT;5*4oLs*ZsTi2yki}+q7ENCe)MvAw4}NPf{X~5~-yF%-CM( z(qcQ~$i|*nT24|ig}n7 zDQa9i))#zwHOi0v-$J+D2?Ntav@&Wjl%bG1lxfPe4yZ|k znRaY%M91c2bif9*S(*y@9d-K7$7T8mv9*N@A!;}iEUU`z$?L(f>LBouoF1kDK1lrP zZgS2C(|Jwo6h99wc%@st|LEN6K}lPz2v;}DQrw8hig0ZVu-3CQQlN9lKyfM@=4?kbQc`xX0ax%^-Fbv(OJc>N6ucnWHO|m2Iu&{o+jnj@ z<$allQeWI1Ia1(8mu&RY4JaAw95Xcr7da7;r8l?+h5#{e&ZNgMc8Uu*N(o8M=I)*) z2;b)N_Ja!hwy#vzW({90EIqT5wbEK}DR{!Nu=KXGIaDA;25gH{AmnYIB-GI6=b)I3 z_Z@KC#^kwKBdiu1Px*|f|6Ps)|G&#|V9{*lc*V!fpw1WhQD@(aYQ(;%CZG1aIn*YZugCA*iCX7*3+om6s~TR>vW zv(Iuw>H9bons>ACTSc<(qh2;L%8>yc?P<(?tLIzdglf1fFT?dX4UWCGYX@Qt?_lp? z65zAC;Gl(jd;~`Hq$xaVO0y5nxTlEizjAOCJ&0@r2<73_HscFWR;n6zcp-ef6LCqDLunDwj|_4+O~XBq5jST*aC}a( zN%O>KZTl>9c?C&0z}j78rya9zIAI2BORAz6ylR_n_ZhE`;57U}Ih(!Ot-GZ}u7X@y zaAkzbiNCaz2;*tk``W2GPO|w8gNRbqFjciO8k+xd?}PtH8h%wtmwD^u`jY4tMiKp` zT*mHI^MX;z)|QjbW%zZej;&L^&GLteC7w7v2@gpN6^* zB~gc_(>ElGsnJCVrzYiU-*xXfvvjAs^FDj=Wfpje?6g*}L>M^};8RgjSOQ?5%)~3@ zdKc{TlX9yy4oWNkM!9>Up(9Dmst)D1=5AT!*Lj1m8o+mv(`?4Z@OGGadPLROQqn{} z{@56(T9P!zO{-U@mhiIGn$Q7NxUN} zli2o%D?TS(B9Y#|l)m6~GHXD-w6HD4X!7*c`}+|hUcI33H%YBt!RR$dd%HLS+GJ9< zzH-Sngd(c-`Do5DsLnxA-2#gTvmppc^R1FiCLWPb?k)35ZV0IOdY*mvsN&FXX zLexZrTgrqM`F02Uct{}v&UQ$WmVTpBmFdf@q=Gt6%-eLUQA%2ygB?SVe;8$WlN9%^ zJF?p3jUashUAzXk!KzujOi<=A)GzqjX+kh~YkA*Z8^`#kdR$XjQ}9ty59Ug%$5)&p z4_Rr`woh-nU=2&Ss`fk)wcXa@u_kCZL&oLIVxyxJSVuhV&X`9PRU9jl|K->nV_7?T z&OJS(3rvq0$>~L4YT`!!NBA*z*ND@vFq zYCsT0H0lg33!fMQ@|IOh?VzV~`j7w|Wb)BJmyP!l#nNI&OshbtLT&=vr6AzqWh{hSck zYFc8cK3KnJkP2B;ZPoVRybMoTndG}~JpJK2-?brXqvdX_M|q)+7GFSHqst_ab`Gtx zxbexuqpNqUt63%`wC?n%GUASB(|LR+bbnzO+`^+!Oxi#3rnv!g-#3Wd-YMEf_Rbbu zQuh|*akTXQG8aV#WfKmNgN4Cb?V-&J;Kn9ZX6jy!w^e+$92Lg_mOD8;i)Dg?ib(RI zQGnEcr^j}}pZ%LUMAaSTXhR(WPp*=R(DCX@oCk}Zu&-Ei866Dk zb(R(4pHyZqm8^1=m14A)Eh6>j8#U>Q+>F~CY9Yia!fI%21c_SGLFNp)_7Vd^P2l4I zgC1ISm#A?EQp26MW zYmDlsb*Q6{y+%!FYd=mJ8;G@-ES>mJr{t%AE=MlPU-~H93KL#^vWu|EcOfBvuyhKuRM5&NDFP>}BzdVUjU#)D_&i@4qSx^yg*YjnzQm zPW{2wtr0=v*YjU4!;Vh3+oxVI{WgET26x@EzYb&7krWRD!nyU@PhEem zotvB)Jg}IZ8Q|Ej5Q_wHg?IPwKq5BVhq-?NsAX)ax|yKnZwam(tW{YAyU- z@1C`KoB{x*a6qC6Uuuw`;Djn5JZQ6ASk{bL^b)U2fd6dw0QxL)O3WZF?L;K|1YTT*(e5-!M-y^we9meS;?tcl1zIeRU`Lmhhhw3mYK3Q`rq&k{5Q z8t@jpF5i$vF~B)zFGuiK*-Jd20uPesog^G;Uh$SS-YhpteuA`IBMdZDbPMHOW)t=q zlgUcmkTC8)NmqHPNAGR}gYO6-AgfD;AnN=^h|TF;W8y-`shk*1%eF5`Le`rSAKRux zHx^kw>|VL5Lw%R!Yo3qKt)~4qutHV=yrjqDL_J3&`KOdI2bzox-GUe!ZNT{acc*ndcWcJNn%w~It`6@Es!1@Zc;87_-d?Y4!+k!G8rhH94x}q zYe2WpL(Q~7E7R`WvRHA%86ZTce1e>qo6U7Id9x8_Vluo?__--_&@ux(tdA*Mn;Zn+ zm;*-7sGi@Jq{ zyev?Oqs$U1uwXZoJuYi^@x$sMd}!aC-=n66?`@5muoi!S;B0SRcK?9wycF6A^kxJe z>A5W0G`L94VmG1W2UG%goF&D~;K)0adO}`}RSm8jeVAO5SLB7=I0>9lVajmqYZ;4- z1wE4wgRfT3{R$TFvlW)95Cn{HZStLXy zAc)de&d2X|p_;KSdy8lMf}3Qn>`=Zvr+%b5M1TC@)CUzWMZcz~)4hny8-y7UueJ~E zqUorIHkUa&+!QghG8?t=U8uui2JSM+X5HO3(WGHw3!B%At*l|M-?~z}ykRmgHxt-) zK$OPw-zg7_LomHW>^+S;?{bwy@EjLVOip)sl#YRzb|U71fVa{K32s}(Mm;g2@b-NbXJPM8YV{U-Q2Kw zRx5etOKd&#DDLNvaA<7Oj>B0w16$8=$kRcpC7YZ3K)rYAGpGPe&&s*QP^njPPF=_i z&?63LCm{Ux>ptvB{fzPnlQ#`ynvlV*WpbE1b$i0&^#_Cv^~z5MdT5}nmt*tuj|^rl z&7+ zw0=~56YpzW{lH97*w3$I+5S23Pdxq^4*%P;fxR@@d?Zi6PH5ga52xk+2MB`ycdE=& z-fvRaYPiW7DfNd5?)3-Tp3?vd@4Z(GaaWgX;IGn)A4cf4-SpRJP z#kXWcZ!wRpkcAn5l(LA2{u;mgfWPFQTIJ--tGfUw1>k5Cx>*Mat;BRL&CS_0f1isuuj1C z{!(P{eceP_(vhXB>o&l9W#l%?lQ{L20@H&UP3Zoj`5AXjkvexOP6|lZRTvDw8|;Ox zL6-yd-7JhZ1u639G?y;LD3?*f5>C{%@{7ssR#vu&&jr22szxk;_Hi?^8u+$)Cb|o8 zi$AY%vKGm`z@i+cJ;s{T6mMH|9c77*jErZ_pRN)nh~?2V)P%PxN4q7h#6Uc7#C|h* z(9a|WzW{FZnbxb`9pl+)d?qQGZ-BAC<2k3sUCLwGcQN}px}r7itoA*n)nJ-YTHVHd z&!2iteiYQZXjKi(`Q4Pf_on(sfq4t(jLTc;qTH?zGd~JyEonW)0)NyA`(7|x7bnRq z`{$df<|Y=n)16qnX370g0Atrv*Z*-}y&nar)c;>T(EW`f>VSH$4&S(kN5KUZYf4tI zW1g2A7A zUz|Cd!sqi&wafI~e5(ZZ0?uQcarM3|SEwp5k&|ukcJ5c_USgBVg3;P_w=^Iwh*HAj zF!{`6+nNJ5Ot+Fb9Z#vSrBP6su6+0lX9n72$S>H(t z1IQODk?`$Xk;X=}AY}(t{jW&Gkcb|F5g-{-1^&7_s_<$_u~}1$Ow@<{tLs!2)IqNJ zeXa`{THA|odS!Kx6qhQ(z{1GbiL;MOsFjS+e#oP%M|~MvYpC|v(Hp5H{8{G>}qP@5jT&HC13It&1+fGUa5*= z)>8yw9a~$*#L+>g6+x#pW`Pq%ThpCqMWJ=cQkt9I){?~q@g$q36wV+wMC5YeWZ&nM z*E%V9Y&JCLl_&^H&hixQ)3SrZPB;hmNAfwm%G7{cSjxlqoUC~=Ar|jK&5;GoGUKU7 z8J$Z`aafAyike_x@Jx7V!E?TX=K@mwGwKzxL6|~Irg>DIb3xG4)Oq~5#SzTd^B9J* z!b%J0ttna+gz6{MmjAXN^Yp^Fc z>DBZ}=ONcGWqpKc`tCKNs5I?Q{kM=E_Hf9j} zgg*B+9aB&f&&%4&@P*%;*eb7o)kV$Vw?HfXvPnF)V$>JjqcK=z-zD(M>t$NvvF}|| z7u>vb-9UUZV@$OFjAauTc`cpRzav2|1xuTKU&w$>*UjKd+N6DX<@~B;L&xg1iEqfJ<(5?saY3zaooM+P8sdfK2+g`!uef~im;(F2( z@$4b5F-EjgP!jHI5cp$gw5kiJLj#ipTHGhMC!Z>MH|V&%D4DlUOPq+!{mL=q$EJQg z7`cfvixEK$NJ(q{I-pwhZ`|Uy)aMq?IP^+_L3V?MpXYmlDf27S3T}x$AgsvhT1)vUI@?p)>^tv(+_jg-9W*_LJpBk-q)X^R{-7V3y#qPyoWU#zVDHo8Xhu9Sq=-9YMeiL${QL-JXIR{&2n z#_%(18l-&}YgyL!7Rk%0h}Xkz1cZnDnBlsRGWri<{7WgG>~R&hUBOT`{{;wbFv(33 zV~zO9A$la))k$FPd`#abneu+jQ%&N&lcga7o$nW$@x86y-<}-dwm6d$)N7TNY;WgU zfqy(@I7_1~O`5$v9R_7#6#?XjB_!(`!v}G|>5pD5Wh{|o#}jDH+B@-BO}l9!*|Ihn z(l{bor?BC)IOIi$)^G_jRB73MUZ;1~X&|OFGxj=t>~%);s2iUs8EMXFMMapb;g=Dc ztGkx%gl;55V9@8?g%@TWrK}IlO^q}^>1boF!!{7Ct?-r(0(76<5CQD_^O|bbks;d4 z@7t|gH#PQfeuuyRf@jLNyzkim^wq4F|LGO6A4>a;7O3%zQI_ld`J3!}(*S z0o`hw+*e!Pj=iql6CoEhmzim&z|$!W>GIfMqD1EcwvgQQ_q-`!pw-;2z_D5MoD&|_ z%=7qJlj_1|FhYfV$>nJ*Arj~yD5D_YO6}g2><8wM_fFK4hbQCf_eZxa{R9ryzs>cD zzl$@q%`5=-rf_#L1b<$nvtn5f^1qM^-K=z*VjJn><=?3eooQ*20_STmPLV!+d54oS zi_)WO*?-w7@^TM9Mhpm4W!{_Zp9*d~=RzG6IB?d9JlcsFJ1qj32g^->@ z51Tu%DMw{VkwYCodaCZXK;tutC%3f+dv_$$l$2pO(MU}464}l+4oD*zdviLQ+STTW zsYoV~H|hV}xme-_r&tg~z)8}h&Z}S6_Szcg@KS5gIahKwKoZS8fL0@J%jm5Wte3q~ zteA&y^(oQT@W z>>LXY$XXoq2IHK-3fhz(u5h;-Rwu~Ar)0%hWvW8!JYWrP3VPP1y-;j$tNO2%>ZQTe`xXYN|j z^zHHHQ4l?z>}TqE(?l(ZlU8Os+gZ=>j`oO-&Lx9zNE{Ym)zuxe@Op=^$^bn0Q?pSM z@?7-espiSB!R&?Hw@2(spGodgsXwasETOP^25zOfjBK=U9w8J;mdcV4d0wH@5$>;| zgx{Ms)nGn7^OU-*y3)(Q>-rIbGZ^0LiVX)G0cha)&uXx2kL?Xi50Sx`tmDOd(AO^N2= zT|%c4d!^H*#VaHy>X?qE(&~3JJLb8K^FH0?Z|~1jNj_ zHlIDWKKJ@BfGXr|TD-xztRwEN6ae`>YUiOCGfbo|$cgFUJC{ab9uAu+Quu&K)3y16 z`yAb6ZrABX>+dO=cZFQYxWe++HMdY}_w5`Qm)KsF_X?;<3Q_Fj8&7p6j|1aE%O5hX zSPw}73M$VVCwloydiFRHlyF41fN@wHMQWcMc!F#@@MSwMxg)ic9A!f)s>M`8Ln7mb zYsn3S6^F-yxYc?Lt&dM_-rO5hbX=RsO$igdT%BS2l32(V)$R1RxQrG9C35KZCC4`z zo^KLr|I_uyzbp8|ngnN!@pr6A{>+Lf4|snWVT}_N4sv5wlDpBe_4|6n3TM=HeR`x5 zb0q#OH~6;9^NQQ)IQ-E-cvq@z3=7>}+4+?7+N6Y30{adTqWhk^(00rRTCj~nB>y?( zpSb)p82;iU;g^2>ImRvOc)8BX=OgX!xa6Nyrv8I)Nt)k{aw@8;;*q)9@iDO*ah-*m zqx90h?LSmIee{FPv?0;%>eH3!Y6RO0ih%3IOYd+Vr}$5;Q=k|VdQq_DH8+`2XB#}W zENRJS1)Nd*XHb5X56n=={&fjdz)=#mZ-<6CCn|Y(T}B3+)Tuo!l7Ew?#7mNaR;>D6 zvbvsl*_LxYNSqE{kH!>Q@UWL!-giz#N=f*@`kj{BB80yy?F(Kq;*xKYqdQJC<2r&> z(q$=g#5Lg+r%dQYC1~JG9eJU78teT6CI&(}ZXWfNe$}Zj(MTO?^7?y`J~xC{s}yq> zy^fCIgwxEPT!l7unDWjWe#0w|=V3t|{^S9xuJO5tVl|H&eSQIyN-8DJb`@udpa+IM zMnVT1z-RSsiaFn&sc(@TVK>kw(*W)8m4C=^E04lWUGre6)9w@X=eJap_Xc8Wq8F^eVRY**a&`(beKsi&VVpr1~DgD?Wg~|?*P#S7RS1^aI8G&0Q1qRqj)igQo zTJZ;ww%Ctak5&+F=ynDn#q zXuqs`XOeuMM1INZu;k;8Ov55)(77xlHZjMYdtF?Uv%TFdbhvC zXRv6-w*2K@!c`af>Qc_FdORA&iNBVziE}{u&*sjfWf*jS+eyONt-kmE6YIv1!GPpJ zIL>@^NYVq$^PyDt+ z&vEl#lAq0kf6)9y%UlWZk6`Ej{kg+x9ci>QqE!fs-94lLuje_J&uaz1xwY~rRO_&% z9zg*e9@vB8i??F_{I%oL4E}msp3?J*w8^jL>I|`FX6B}97UWK?%o2KloxzQD@w=e~ z=&dKYjdXAYNO^LhXIu*zXwZMPo--_{!#)LTbEwH8YjEqc)}FoGW&x%5b;B?GTa{tz z^R6Bv)Fb&wGGOTQVw=O|(T15#QIjXm#Z^ca1hmpwUGv<>pR8RWT$3gD`{kB)QjS$M z@%&afM11U8EG$6ZiMRMR!a>Ws+f7R^AZ7ux5NCcE->UWzQ%Ec0Hv4=>Yf{WO4xKGK zePB9umyyz3&>fk~t!82XnkFGDB(VAuH&Pgy(mJ12Pl|2M~&&k*?=J9_b;j^#y%Y3Q3 zf52O_fp_Q(*^%hn?(do7*$0(AX7k)vxHc@BMuB$vg}fD1m7vZ*0203!)Cf zPmwq+e25Dk!cW>(i&e=hhb#NmOGkel>{?b@(aw2>;~PSvQIwFSON%1YYbMf=CA}r^ znzykJ?Cve4rK!7gT6>V6<>N8lwI-DT{glM!Suw=!Z>{Fg%ga7v&l0XioA)1UnJ%mf zhfm2Q;me4-4hOBASqlp*@DaFc?dkD6b{mO{^$hSgHbrxAI57}PT}Q+MLIqdK$d`tB zj|W}jE%WqDisQDUal(_xYw64I!QJPbB>H{vlyIKMxS(621OuJ-b3r!!Bnop_YedsE zgPXqqX45Pop&@Klt|lIP8_uJtLm4@7-0$!f3kCsw$14Lu zEh#=BoRqfTA6-AX8I>4JGeoy>UyCD!ViZi=-BDqzGD4!^W&+$?jDFtoXin= zB`j^vVt*&OqEO~dkjJ)1$Yo9TH*ApaJ#87t)aqE9nai)8n{(aS?kl;q1LaX$YCA54 zeG=B#D>@up>Ef#&`B z7*~^7%3%;AS_E1XFrNLgn1blY*Bg!oqw3M0mgqAe)n(qn9-gPV4oFKSM=$5XpUp6x z*obc<{n5d<(A~dLvvDUKsb(UipqTV1z=2Vf0C5GFI=>7n$TK}Q-+JYln4CJvTw3lx zG6ZXJ=YE~v?X`^Sw}S8eX3mmnATikw3J|71bSBjMlVJQF6z!T?CaZC4fHP~A_S;ze z=b}m62W8$n1$T4!jHO7P7Dj!s>}7s9xU6glU1N(w$*3oG7@jBIkEwowMoaTO^2w9BiBHHXV38fcO_Cn@yv9 zg$JJ5*Q>ofwlnG}6}9|G;QihDlkohSQX@vMJ2M8kjEcDx-1?xs?bKpP3;_AgI@~*j z>b2S-WK>LxDNsSxNS7=g%0l4MB)o|JRxc=4!pNjo6fe#MTp|nIh&!TkuJG`7&=BdA zQ@E)%r++xC9vmP;a+ZD>JMQC&NzEh+TgEs%Mnf%0) zLJzKol4CBNL8$p=tk$)oo5qcavvY>s2iXM%r4EeA>K$F~SJm4h1P$00451~j`kH0q zLzO4N>Lw zY{IMLF(WTmWzJz~wOFrk+{`C8-?s#FBZKQcfppCakU0|LS?HQ*q!JVP(qSihVd>kO z(BrgtMy<#!{vb(AN5!nfnP5AGn6Y0*@1iyju}yUU!e!npo4vBot$9Og1gPu`3Qod| z3ojG6TrqNP1%`O?_njiZq!kl=pSEOH9a~<`bEdVr){nq|THJCJqMDw3HBOmQ>eFN% zzXw$1UKDYO;O@qo7BG#9>3IZ(yy8O+M?q7fX{gNGjk-zMk$T>t)S%qNLsT&e68M()XNa(fQqvCN_ zh#5uE;1;r^xnGg# zeE&gldmPv1+IH>=K6-c&kQJ^M&R4HjD z81kk_dt^)~j_aK0)DrOnLYo|A|c#6043OoA?>xBIZkA^rli zj$(Q`(wSAVHMi9_g_AMNAaX0@ z^B`C?S6Q2|#kvlw#r;W6=tRI|8l3R*NNqH;tc!!PBysf}+sdhg-OkoRK)tE51yy;V z5U>IQZHR6>oh;31xgIE-Vz+PJ~`8SG3{CsD^}jaT|;%x z)!HRz(TbdXxW7qWrkgzNwkrT>0De^y8#_|m<5n&a;aQ~a(Kz5jzEe4`X@|_1w{?`cQr1Fhy+?s`7@N~t>*FiMH9PO>^Sya9LGob*FDp^X@dOw`QRoyu$DS^T>( z!2L*Ug0htwHpFT4nl#i8ey;d5K+)96q38 z%o7kGvq0)xT;ip_Jf=4;rPgVR_#!_9<;D5;W@@X;v{zNoHlNkm_@_0k1f15lsk%+B zA5ZD%e>v5ih$GLVUny-ES?ano6V}mQ}b2c2aKhWb_?zo@;fUn`wTiOE;D1_v*=2A3Q-W z-NHLP#Ky{BB&bol8o!}aV^xr!jnLLMo~CyaY>>H6WwVeDtm z6q`%xf>3tI&t2KswMeB&<7O~30Y=#(>^}8>y5$_n=A1vZvgIZP=EEYYWEvmX}_8xEVOTh(n{xIT4K`U!Uc;Bw$Y zPXxVamNFK^g>A(Va8ES?p+_r%nh~+HKuq*^|grp=ttRtJC0&#!)vAk zO8iNhZm}^oS->l9OL*+dGMOc8!woMB%s8=^qGNP8STl}y9AVa8$yb=8hU81(Pv|A= zs9KQs1YF|UbC8`W)3HnbsBQq>e&9Qw$$e{&*hV7Z_?C*|7vhydO{kV=bobq$m9PAd z;@|2E!d&VrGaj$)4{(Xfa-T$}d=S?7ss2GJ&Q*+gR)Qh$F2udwu{1qpuMZpy-S&np zt*jA$+TFE}uVdHRsiCZ?f>u(06f*fV;6*^_fzP5e4v4?{1=3Xe_oiHjymn`-XzVFCCWedZQ+ZiF_jQ z(UytRO2?czy=%JH1k_>Mmw6n}FC7HiJoenns_t7=Jjs|+G@?luChF^mczbk(CvV>( zl~_m}|2(z`mubg&t9(`zY~{5sz0+|=xHTpn-bCYw($WhbAaH7`NW$dna5w6*!@8jW z$W+9qYW-@gW_GH-6(yC}?Cs5L!pga@kGj6p*)lfoEpz*ED&Hg_^N+H)Ur()6rT(?-pKAVz z%|GMje`7L$+>BYvoq?07+wl(Moag=8EcpPzgFt~^M*f;mu2z? zc=YLVIt>l={|O9CsH%2rAKQ`mtT27<=HAaW%s216&##kLw|v~^nIxYp!^#SBoFjfY z{k+ogl(v~;5B2E$K@IF04w5)AV+iy$o@ttwtSGF~s7&J^&vu3#g?1`G?bJk%Ysc5p z)z0wRQ|dUmEa`rv>uf%PwyI$}p9YA~C04mQbntuTpe1uTTqFb6&VYcMf zS`-Zv>Z^@@cR1ali(5$#?`hZ5KQ1KDwlwCy#y~>_>t!wVeY8$ZBBj!B*Vt3B5?4#< zdCkW!;pL@Bw|)?g=3J1c_X6AE+s)B`EWHN#thuk!M>Qy)2D)$lGK63YyX?05)U3|^ zi_O~cg)%3coqg&5WLp8ZUDU|n^Fw^~+M7$me~@?7X(?5vxx)A@N9=!{cJlJ4=m)&G z9J7v_zT?~Ee>M;z4*E@eRX`;*#6pL`g_vw|3ZJ~s=TKGX_{un_?8m~Nr0&VA>5m#WOqF0B0> iAM^R$KeS=~jnu0;o!O!<+8YUbdrYg7c})*~S^RIs(*Fnm diff --git a/apps/q/doc/screenshot-search.jpg b/apps/q/doc/screenshot-search.jpg deleted file mode 100644 index cbba0a7c487e5721b27037b93c786d70a23bff00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22617 zcmeHv1zgnI*61KDf^^M*G}6)`DkBVCLpRbx#{hzWNJvTzozmS63eufJhm^FSNGNg; z9-r006+c_ki1DryUdJUS#3v*pAttBhp``|asPC|IGVsWV z$;(NJNkWu$;Ks`8wi*yg(;ze3haNt@K8g>*;-7iN!5{m0eno;pLPkbSNqw7^_O_?0 zq^jq?{5fv{5MZG^zA}J{LI=1)fPzYZa^3*|0#E=~P%mcj^Mi_pas?d|i!ylk(oD zclK>HcG~;Ez$o5!fs5jTRbM|p#{r;RK|@8qXksC_Kzs!q?JFJM0Z^_GprR3SLuq+< zjfj1Sz$%Wd;uYt!fNK{tTr?2??g2gv)C@aVqrg*a0AvhBqPqGGYA%YOndCpDGAj-T zaVhIhPftMA3j&PkhPhT|cl)vmANC5>zHrLlPTe<15A9!Fam0bSdy`JtsZ*&o%XHSv z#Y(BYM;wv(e-=ss=TaN~c;aa5x&fWIz1jq}B>Q|0*wJ7X3?_jHgcs#fn_a-9e(iMu zv-SDu~Hq8CupAZu(MF)3~2FCw@P5DkTmr}>b5^_1nSF9O^%IUA@@3`@4#@^LOTreF z21{VVpN3RBWb}Q-MHHlPYrpwB`sh&WI@a)lMaJzo><4-M9V@th-IitS`BIT)>^j>o zOod-EwZIJR-t3LjHPv-v>kwO|U4cSpPs*pF7fMJmnt$C+4E*UP$e}&fiGwheYlwGUMw|>;EFp zZ!Hj`uPv5=@gKfR?{_Mdbs%TnzmWtFu0Wuqe@D=yXBVC0{;IQ?;-H7fYE-8S?64#V}^9oqk^_%-^RiH%{M70QHngEF+|< ztHzEP34h)ohcn*o=YR)wTF(|3Cb(gnjmCemztT4NGvCo>E?Lex18TpE=I;sM7oin4 znJewS*fWrCD(AN9NDO%;!)-)BCPzm{Lq~VzufAx=1Spkwwj43k1}%g@3AP@3h7%qI z{1SG;`TqDe&iEmr+Pq!PskiUaG~Kv)XLJmcmz0+M5v7e%z-#K*_0K=r?n=+Gi5qaD z`ZdwaKYqXo>W*r3RYhcKsv)ae`8b>d);eqNqE^EkPra~}G;a^yUL{yBe4HgFl$>6# zx2WhdPA{vY(*7J*{yA+y=yt0Vf2TL zMu+7uMTMWwJHV?W?EqvzK9mA3OvxD4UQtD!^6HvtJFRtNpqPl{t6sTFHKjtXjU%Up z7wse$8hkHU`ql6`!1yKGzDmV!D!!mqo&k%*2*O?q3w`sMcc@^2aRK(q!c5mLIs)#` z0ez9#uBcD36YdA#P# zcgnCveN7!0a3|gR8r}Li;0fMoTw7tT_8dq!w`Qgdv>u(2lLc!=cylRKDPTvHaX(YA ze6WrgWy}i87+jwT6hL-ailEe-1H|I}GJ^|U9b*ak$fVFEXC_A8T&%G+11O(KmH$NU_|bPmvZy--P7F$yMku;v8KGZTeY+l=)N#QSbP z>*F%X9(ra}-l#007_mM3rfsTBOG>1mrr6n6n3^RUPT2w(oXL_op7X{}4@73bR^C{UU39PwE=4yANv0646Ik-2G z(v)9Vtx^il2(GEMOGIu|B&2-7RIYM9QSYtv5`FKHZQbiVc|{8FPJsqUQeCaKzr<E*^ z^0zkN=9)cP4znKtIByRNkru;9ru6~acppnnG@s=YGQ~&IR-mb#v9RargO1jL*}&pj z1gRSKY`^sMf!O>P-Gx4gvOb<%k&CNRNX{-UytSUP6#K?ucHnOcx~gNHGK$j=?6BrM zaH6e~_e+eYrq63_O_uh{TpQ-{ zP^4qGS>b+6Lv=T{!Iy^-y^ipUgS#`z2_W8gZvedfM0UO5G^eEPj^@3>j(fAzu?bl= z&;UV_fMP|?yHz>H7l+oFQ`yy*6=zDPhDd+#UNf(Hn=Y*b2AK2ayv&64Cgz^Dt!HU| zm6p7*-U0zmZ@Bs>LgY`=y2jnMGzX2|@ifsS>)HX*$0+fy`u2CNP9t(u; z9eHhtvQ5u^JndC4$iKCb$}2V$5Go)g zwAmC+pL?R3F?t6yltgE+-q1QJU}RBDMJw||F`>$tpy|ycWEgfCwpcD7YCe4|t9iug zsLE%C4GedhlmaGQdr!Ce;&}r5*u=nr;%7AX!iaoIJf=-fvbl7dt4m?NQqq_K>jBXN zT=wJnZ?b$@c%K>JjoD*G&b=)kzk{=|S*E1P`%P8b`}2VmM)WxC^TgKGmkW}vvfe_~ zCc`x1f`gV6Y6$O^F-6xn)BS>i>#NS@&<887b3YBlp(F>m1AH!4k{64c2xIbZQX@4* zHl=wEPi7TJ$85=vT(wiW3KyrjHbS7E!lZLn^XXmfa{&F>^E}RnjjTFR6#2eJs3~Zb zWPY}$z*Nzs&n|50M1!dGmT{tQ%A9E7Wwy^S&H+iW)yjOLM(~O{oX~|0LIJ&0>1`ps zF1|N=Tu*1Zjmf5RKN)=T-F6rsSDR41Q)W$R5)H(`Lrhguk&&*~kLD<~Ew;kr>^KRM z)@vVPmiB3kinWrP;J{7emK{kO8rG0py#+?&#P6ahjGAx#W+Kpm``&tXYh*KowIIwW zM33Fl5!W(oipGj|Hhhjv>1^k5Y?dXh&CJMH_-bfo+RKA+jo&^^K8641q*lT56z< z4(?~%vJ>>8rQo%-9kia2DZ?0?xg)k&!tLw%aWK$~jweM1?dLp6+*L~U-MG`Xs92YK zuOD0UKj$_AtkjCqESv)Z>B?t?bdd9$G!3PU!|}uCA@$?YZZ@Sx`j+ZZ+$NW^d|(1)&kLoGZeUvy?03UfmHO=%MW>u75mGs zipJi^cS_|=g)k^^!B^5mF&->QbD?K1D)E1gjaMJUNj9ioX@C&}^sprx`v?@c>B)48o>KPKS9!fFOrtwq9cpN>4t56Sl`guv4d2Y%=AqW0Q+6fXGT46SfnICa$5i&=yt8eGn7B$ECv4UPC5D=gc z;Q!T^b94Cm@qY9j8=*r(+pt%+IlR9_fV{uB*}>C3z1sS$4R58H2a?QYohay|&nSOV zyRW0`{{kma$bqbJzcjIFS=oz@;8}xaQZ2|tV-Dx+pFYi}%9t5=TAFJ$V(o_Pe#E?zyuV?LCG}je6i6v#7bBaAQqy;1=@q`>=$q~^(klmGm%u>l<)s{zYK0~hojHT7qHALrI3*(#Z}X{LW~ zrmElA#me~moXV%u=4RSunsazqat}P>|F$E<_Ah738T`%?y?wmu8#`l($0k2<{(&JO zv+hx(m#kp<4n;UN@B@2m@LJ0X8K}=x#qyO5;-8uK)6qMVF9Q6$TVm9ngu3=Lek^lwwc{*D za`1QQhkEQ9j*HsghTWhnvr%iB6)k*6(G)F7e2GA%eVAdo;Iis2Kd2`(DBS zubo)GF{0`V$@l5Xf19r8*cP6AD5GX=8d=jWcOc?7gVogMzH4O}C$?{LojJb9eqKAQ zJm5F#=;ybqi59hx8RK2Snb3?`9i~7!4;=d*nRO@ad+`IYf*O4OTABQng88pK!)*B? z@L^1Z^eo47|KvvQf{4Za#qqKE!0!_6_#w=sJE5ET4%(-0)c<+Ve*x=%bI_uh zi29^E@rDpN=0=s1{I$d}^Bi336o5d>SPx)Qwy}cS5=}z+a>IIFAG4?8&9L9r9On8} zKkP5uG%uNSvUs*7V(tlkRJos$_(nuA@j1RhaRXfc93b~qJH&iFF2PD{$C-8M-fe2z z*UDBx7JSYD*s5hZ9+FT3<;&yrFTTL-CvvgOy#qPnse?h|_i%FIcJ!2db7P-b_LYly z$ogZJ%ktPqw1GO}r5r`b?0zB)DUj3Z7F&kbhX6jI+R54o!vaU1JE+}A(g`SZd=!cS zvQ04J6c7Q-|L^LHZBi z`P}})v59U41`@T(-XnAgEyu@{vYk@Mt2&+;{MaEG2VE%DHl5*1Kpfyg_deT3s+1-Z zv==VGUO*4L#l>$yV3snMe7*1Zb3rBl>Kz z5v6IcJp%lWbr`DH;bSv}IL7BJUw}pInfYPw;NtWio`Qh&^OLC&u9*I67s4+jy(67UY-{HBM3ilMi-3hCD`ayCW-lbB$jT`W z8X#Xsa?L?rmmDE9L>m;YYgMkF#^c9qTE9_TYOCLSMQirnxS$AtlF#ap(+<*W8_l{F zI5iT;J+{ECfZIx*>NFS8uE~?Ug&`k}i#BsNvD6k+KS1z=6B|?PRBGLKjmaRVpwN~# zD?c}JWyj{jQipJi;ka||qagac!Z;YUmNv*Ov{;%!YkRcc)(DzdqixS_`Z*B-mBXd4 zXga9F76F&f6&v zLHlgNA+cR=^(G^alTHB3-`}V9ZIdciL2eu-P(Xw9MoTDHTCWR9177lziPi1IIbgV* ztg8tck+) z;xa$xHLUyvhTNE@@u9l}Q zU}BPX65jeMoX=;P_1mhw&6>Sc~)T9MtNcJD`Q^=OG2C=RgB?!<=OPv4^qPMM9=km+) z47m}TcLc(u8_On5S&!O{{Tv{a7}TrB_o)fgd6o82}+l(`JBlc3g1@#Ca57 zawl`2wc@T;j*Sbv<5REabU59h&Qbk2U^27z!U4*4rO3V}fC(MJ@q>J{q(%2vPcQY& zl%KbAy={yO2O%X#9XN&C+IgPxZVc4Q)z?>pn6c@#@M?nB8*!v-tY=r2rmpDNEa7EY zPoH2NmBd~+Ki{;qjto6r7*cv^vRV(}=dAZpeY}_J#OBHC`Z?@KY`sD7;?s#eyAf%c zH1VGFJ`M_YC*j?2$%X*|2aL5mqv`UQWoL?yeeJlRRX**w0aaeo3=oPmnr_j)mM%ij z&iz4pyH2|FknC8(e(r$aVvocx-J-`FQ(2N#849Sm4h|Qff{^x7sv!XuN7kDbg?YIg z%S4@|99j!NZ=G4o<-7=C(c<7V)p^Q0W@tf~W)dp=;lV4CqSGSxu&kK1M)*0VKlXGr zh2vR2on5lS0$0+7WV&r-^2pkZzGXJXhN@b>ZM}|2<7uP2Qe-eihEY#AW&v;kqMB^d zPZssZz%pDZtXbZOIE!e6N%-|w6KsMliMiFshQtCgS+MPB$oK5#W>18S!BTUm-(ZRJ+j~ZG{abRwwFKAV8>^M`+*1mkmiaG{EwV`4C5BgX`@-T6+>w{i(+H zy#uVCjA>S;oVprF$4=8n$BGs7Hu9 z{W1-6-RBqo;hw@v=b@*s3JgV?({wwpO*9UD&B4lF8>5i6Zw>-pof zVZia5Rs7X{yfjS*?w$;{kd9qd7$}9!h3D2}3?VedZcbHo3 z(;|eEhj@ioSIFiH>~aDL!&!@;tD{-juM_t_n9n3N!uc)C$SPdlfa9Z=(33nZO6v8R@(H24gh*Di_rk@n2M%`NW+IN({>*u4DSJq94vCmt|L6* z${&eD$}x&tQnxc%i}qLFYFq+FnFQiiha0#|zf|WIy3%Xnz~K@^bHI^hVjNa%wpXpC zhwy?46AW#K<1M0>APNSYBMG|(col^LIfqnp8YkRLxlONf%dyaq3S=kF^FGFi)X%O` zc=}}YG2{@-H(bhNQGwREI5zV>xi(8A#x$%$tU0;BdIh1CunFLiqBodK(`943DbT@R z0<~lq?X+2aW_D!kKqiOBjewm+n2r&&je+iKlErD)i>6~!+U6ZjR$M#S6MK+YIhaO= zQK>0CCD67!M5Bi*;?8F+a^2Ak^D6sT(WZGp8kjASUOzUK3NkEFz3cUx=S3CmYd;Sa7D)lcNw@` z%91C&6Q(a$l?+aK&|TN74f5*6A-Y|yL={rE^V+6_0wXtYHenl;)8qI&fn;jEw<$o3 z**zNpuZKFstM{F@PK#d*+X)EQ(c{;&R$QgDf+}uNn<6Y3YMFbjr>!Ufq)0Cq%c>>l zlOoz^FMhi}PsW&$i2X9YaJ6OS_CHWSY|37_ukvoxcDksf|EA!rc+WOuyVE0*j;`hM ztUmrFDjNYe0*GMtNK)IVO^d?%babW;FGZHnX+$3q8{eh)6J@g*XD?}vgh^8Er z+or_YBsN#p3FSosZuP+z)pG!&s7pmJ3FwKCK8-1hW&&5;s6Z|x=zF!Q=5_d%l&`aKQbjP@AJhag^@C!bOkjd#S4<)VO>7v` zi5`o&GPn!C=$}?kfFn$=`Ml#^=>X>Ycwx%Sm7pK5+Emm^4>8@z7`zQuXXSv#(C5Uq zi2G64?mk?G#H;o>rPejGW;PB#2Gm)3(q~DeBG+~K~j!97* zHxZN@0Xe`tPnUtGOXjOaE${TiweE(|@3FQDPNo;<@pEHTd~Tpz`gnJ$ZF#W23K>a5 z^7P0k4L@iw(FjHj^aJU8?89oq1cAhyeIUBhClJs;h)k*_uAeC^>^Hok=`B?Ci8$q& zXIa(;-lrDv^P%yGDb&Xvjoai-HWf0mxXQOw>J#1!PS0V(&=w=9T(KAfZ+43o!@@Nx zb=R{U$W6B!yQ^B%Epxi~RIY+8L`i$Gmi$pJZhAd>9?-?wa)^Si~k? zlCdVpIR|u($IJ-tH3LqbIjDM@C9q5Sj>5eJ(89pzN8-%lB&L#u8O2GOgzS)ZjQ+=u zW4$biZ33xnjJj4fEGK29A!yD6gB@6A@2#Wlb#gd7pOv;umXC1{hHLHz*250Hpcl(z zL~ns9$Md?)2MD7?LTDcMlT*nP@FYiXWe<}u*w_6Hw=7WU%ZxNRa4FEXVZ{B4Z)L-J zs0eL-;I;Bp8vR`!fsYj;ot-rWGvaPmZ_?zK3QtzA-CLTx5pWjkCnFk(wau!_L_rUi zj<`PI;F>BTQ&S748DGO_D7Ai*hE0ClYJ}Tq@e-vE5?n9Z_*t!_G_HIKVS>YbZD^b>HIF%D`rOk-+|!mvwbK5Hb2v7fGibhK8s)aXs6h#k9r5Ssx5& zxFz{0sKm5xPP=>9L|FAPeKR3TOSF3Rt7CNM5#bHy+2kAY3jp5X|nEP~L z?@YAz`7cVorrBb1%YR0qfWWMY!b&E}VsEH&G^%KhC@C*wT}??h&B`zx9in1a zz>SmBk;0hYYI>iZ9F^NQAS@}I^M|z~mP*5*CR~4$ZphGASMf#}Y<^hs#eU(THnj-L z^zob`kZLbV5iJQ!Y1qGwj$TOFs*jZz*$=XV!Kr9a@o%#$odfvwb(Ep4t+1uM&Io>> zcF1JY(=s!WEB`>FUg*64V?>U z5nA0B!Q@aslMHx4rl*hh2 zNk6xRSi_Bw#v;=ub;Eat=KxvhseAhMu7mPlT$ktgoeAW^A3uKKu$z>4vfEy3wMnN3 zuR59ww3$fx-p(of`Lf00Z5`fBQF^fB@bHu!Rh8-`5`8qeaMwq5gI}sN;FrBdB*?MMOzw{ziZ*Y|W&HtV$1Cg+qe-zM_?Yl$V1zFYdx1hqg9MgO<^H*6orLD!&Hhe9;u3` zB}>aTIZw9GEt+jwOb8jCXa-*elKVi3!i60!!@##LB|HQgq`^ho)_AHeogPrIJjm`y zSTPzGUwdqag2c3f#7|tKEFc@G z1bW9VG&`cR!#K;15r%2VgPnhO%y2AX&bYDQcMHh4sfU2T^xqw!)i}Zz$W79smNqSB zcck2bRCDmW3SoPMdCF*hVQ%V)GG*R$G{kdUt~>e~zeTppm_B{SKyDaTyCx*(d;(H; z+lSLX0Y6V10PnZo> zh`GZ2?6t&h&~1EwAtOrr%8% zC4|$17^7#k=K!5McL`9m`_68bY~I+5ZSW~A2zOEx;2uCl3~?|o<4mTFf^!Z}c`Bvr zZ$7$KI{@@N5lmvr+jW8=kh3psrSP0h z*Y(-h`;2tlLYVyF(O_QOL&aV?c&mpF2#FkV0dOo|SBZ*o9Y|zlOdRPk=)Q-0R$yC= zlG$-kppUAr3i>28-F*vBPN@31Kg}#?-CXqS+B<>>^u3kZWJAPTmu&8Jqi|9C#~1-C z?k`e}ic^bRz~A%HBsTGdMPK^Q0bAPYl@`9UDPp1HqM`f6JugGazi#Dxo^SuA_puv) z)e?R;D=ziGwal)soBzK@^TeWB@d{7_1jgc1b5iFcG~G;PAZY0N#QA_>i*-LFjuS_| zM`vYeLAAfHIBf2ka&%Hy)S>~YkBby@QgHuzT*fqzuh_=cTFl|P1KH?f#ryo0jhlqB zA!!(6U0Q9@T=2tPcxc4BIw(XhOLU&vwK?PSM(y67CI)}0NPGD{s>ecCr|z=}bqkB| zJU$Bg>7G5l>Se}elFq|O7yKMEK=2zu=xt-X4q7~N$>g1;hB7ma!Zah%HY}@aPQbWI z4Xg#}F^^+~b3nw3FZsSdvRk@)P}FR^Vvr$nED^LJ1m}CgM^vCgqE6>anHR`c(IML{ zsbes=T#qR&5=8ywA|af zjdinEU1cZjB!Bf9j{?xx`bm|J!Q`>t`ucD8@bfS-;|fzN)kNGj9F4jreW#2#M<4LP zL_Wu{!O&v%rSKgIhk^408drub9jTwlCb80j+O(OH9#-QIY*FJL&9*!milO*D+>rBo z=s)BWjRfSf90^5|C>(H<3 z6FGa>KUiZ8{3HrFQPt>>!J zA43+ftr*kDjbkLH8uMS)guU{+2?!%?ey-3fc=Ptn28WQD*Q#VV?0oNArUC@^2}hPg zrOw1)b*xWkWR5@}3nrhkT+?^A#LO5PKi*J#f*yj0{K>FW=WXy5WH37|S5jSy0wKkn-p(k` z8UBuDQv`iSvRqhG<|3nvE>jarco&RL?O5z?&b~=0wNhF*!=qYPc$7u&I!VsvyPe@JWH6w(*?KvqlK z7blzRE1yb~?JFp>ur`2}$|w_MCA=InE3CDTzB6@2Efo6C=@&d-PAX1ln%(u=hRUZ0 zb8c>Iew0&GM$_&nP47*NOS-HItE#$HldeVWn7UEwI3O0UuAE%#cJL^qJ>sSUyYn;8 zu`Yc%5K;zLIBEzU$SaKs7KzPJSGh@B=mwVqNfbMqCpE&#F`)q=dkX}S#&di-7 zXb-trl0e54ig|TY$=-#F{_!!bA})kO4HuCy)_pq^PoSFg1-5G_N)~u(z}Mjvu2=;7us$vn}z5Q-v~pLGr|^M0>7;?$e+Ms~H)ahhG4 zta5#0+-mKZ-#OsUhuYc zl>HGk$eTMuA;l=Ni+X;EjH5Z(w(>O^RBSpsahK`f9e(714M2k;SFzHjm-H&&>Xg=qyQ%1sVV+73e8R*+wGl(@F+cHbsS@J=|HK9Z1w zX2ikA=~q@wFw z*U4hNUd1!zSQ-sm%ZaC;I97p02Fr{#(a~LzLgIY(h4~&icA#YkJi8{2asZXcPRV6l zqR4a@y++7~EZ?1MO+%e&VqE$&8nJs<#acpDy5)@!gow}|4e!GLvBkf}1McnfBTom zI^~~_^;k&3@4(PAt>^MZOZJi?Yo|8h0Ov7dvZQ6ywL31_$f@M5Kh=b{-E`)?luHCr8 zr2Qq$@|osb*`!7wQ^kj&ncO4`1&ZKdN85JAegI|x2D2B0k!{U+rq>isKG~4Y9S-+% z)-srttaQ0)9^zJ1>4wM|D<63K28ITjJJQ0v5wbaq!%FL{f>Y7ftq^Ozx*oioq!YaE z$;p_|Z^Mb?NXLB@?oVP_$mkpulGN}ZwF48(K*Mt;r%-(Wk~)irCJs1R+%oDsO9L+A z1~hvtj+$wieh6;pOA0nI@k<8w(BRwg&8~M_YG_kqO7Gaa3L001wL7C2mBU`G$l{I? zD63Z*6qbGBItS#KY4oPySUFzdQr_q4tD@Wg=6U?L_CUTnM{Js2Dpf(Y7Y;kkD705? zL?$}ZIZ|>1IN$Ij)yx2@(%EX6h@+~nWlk9uJgLI{Ue-hSIQ!kNE%02TH`j^9Q# zj|~eef#IRNyK=(S+ap;bsDDKUs@gK^)=w^j{_b&VsiWg*UsjY{j775F(A4-n(T@w% zSC_hKy0u)X#q2$%r;8ku3X69>jIaAKcu-Jd#HBFQwbeHx@G^;u(+Nb)x0!9g)Bd#W zSYuxCb^JSp6RHEu31D1sxVY1#Y%8#r(%u{U)rV7Macboxtu6A;dCwZ-gtHNpaQc)D zJo88lUl+Ffh1tsDYJ2N(Lcv;_{c>{`)o@#wP}GsSP^R^*f`pmX5G`XH?VjC-0E}|* zRYz$g?RA7yP-tDI>;wLCd)>_fz(=Vb`d9Ke$irucxhZ^ zLbdiU!h4;z07RPf3Lc&I8;``07VTY5`VnkZhy;Ct9K)F6M_-Cm`qt(bTGzFaetbWy z6}8_(f)xgxu{zf?=R+Y|F@y#1N=bXWYn6hp6}^kQL#`Ag{aT^fdY zKVtGO7{BL2r%LW*-TYb7a2XiS>%#5&Sece7kH^9ItiJxRD5O+wLi23x9B`N&=H@=X zeUiPr0WaE)hY;6qI2<%g9Lcl~ZD>w#UPQ5P2c`~{6)I9c6lpGShH?tei@tO5Qn>_@ z%M5lLy6hNz38YUf7yj`2?NmdxluMw^izs&euhz65H#84r4QiC5?=P|oN@ z2>t)J!OPaP0#y0j2J9meS30$!RAzKw0@5E<_@tyz^uK4@{@af+~xp(wD z;3$^*$AZa!OB*w;IxY@m z6A|PSHUKl~5(kfPdDLGN%IZ!kso(Dl*o&7{V5s6AG69<;+iFn0q%AYXYveSY7ojK! z#O|;%&|1>splH)~oi)nYHQO)!bYc@jT~n%j=#-CFQV_)<&+Plx-|7_QHUN za)uz~m*z9nPWSm1PAKoGZUWVV;E4$b$Gr8DqGau%aZ{^}e>xQ&6~3B(brxr8AOS)7 z&)laVmeN+q53ek~;`emSp1{LMzQD8)#?U(|B25nud#BeTgH%r>%}S$C$cl}iF&K1U zl&-<=#1_tvj<&%?#&9PU?mF<`BPerUJ^e@MI z{HYl95`Wj9+JB`##b#KkMB(-2Z>+)Am&xLD&KA;s@92oOh&4XB zm;fNDHyLOLxhoS7 z>IZ*$mMPK~E8!CRz(`7t3_aJB!_mQ>Wo=D`y$}tNndejeA?_9eXa37=y_GkgV_9i( z`%s9w>@rP!irasXJP;pY*D(cyn)a0{M(N$)8Y`MfYKgZ$;E|{7E+doQpI6nu~kz@6N^ja$7=0 zj!#)C+>!!_`T0)Eb?u9iq+kL|;);Vzks z0mqlaO32}U2zTxho(%dgqj{P!8C2p!K=OOVvjoZSHrF50u>XG1yF~U`v24?ZArkz2 z!iH*JSYpEJ*3}Zf$AT+VrCF(f9msZuhgDe)l(?l>G=BNkUz0nsvXf&f6C$i8O3I+N zkEAON?znh+_Mw_gsW?_TQ#_VvmE$s~#yle01p{sz))9p$Xu#bO^|;mJDyLda8QxsL zo%eSMYD+X~YBnWDj(K<<1#gpoBqi;E6uWGrPAaS96&2Z<@tTuhhy;?Ryrs8(T;R}# z!bw>JEZN-1HixW{W&&c!aSdTHbFYh z+ezn5O0_*zDO?8Uws&DsL8lyo09{$Ef~7+1M1~2oPt+hcVBnePt3+i*%&i*?E^D?PnQUvXU)QnJxA@$pCX61R(~-fFlwRrPbmjbW{19{5jREtM z_wIXPHC8nx1HQ$hN+hmIL*tFE6sexJFq-w}g)OjCHR74?bqQ5}s7t6O@k$iF;Az|i zPjQ0HdHD2mYDX{@yT!D_|01x0!#=bUEgXo)b-3%XTs}4Ial)QamUwvRBuC}jyap5Na57^>lN$?K;qen}T2~F>8*JJ83hp=lC8)oHFo| zqYA^A&Gxc*m0bWOfUNwox-qQSJfK<&*9W4Gou7vweOX2Rt-3Lu)$ci4gsnJ_UkY5E zYjE}i_1>8U(th`V163d0%f;paqPN`)b`D@New-f%L-I*bG9&J`<A_w5-wxUOr6js#dqJGX@BJ6mkJ4MR^1`9) zj{Ff<7Vw@(Ef=Vl*gY;yitPwWo9fxatuSgswXbS>m0`dTYfzZ%k0BhvEl2T!8hy8# zpbxLZMj!p{}-;lL=DA=41_(c zo!ppH9E3a?n`MBIvPQfJExt3}3H{{g64>ag1~tzAt-i7SwY>U`hh$nF`HcSq@;`j} z&n$+CxWv!^0ZCxBX!Hq9$dB7yg|R>Gy=Yj^`w);`I%$9JLb}pFxR4(IHy6^0_rDic zDW?C(7l|C@Rh#{4bo?|w{`fyFrk2zY(NAahlA`IC=w zJuYnFK~6>pjuqv~(%i+(BCTza9WS&;lkmmkGfPKS*0MsnTr59Gb|>6R<0>-!XVK%| z)dB46nODdEP7JF2AIG4)|LrlTzk2f<-OXF?3*WbDsP^g6Ihx?Sw#7xmDHMA9kaL{9 zXDy{e`~D~2OiiUC>D2g#>!X{bph6w1#sZ$KEYAvlU#aRg{bh{MnHHn^<}jg>9K&wi z2Got;=$>nk9yDTBi~vTVFwi)NC_sA1xFjWG%!&M?)ckEAVxW z2Nk4Mkj^KQ&hwj-C2vaRUnnQM733VJN#9T)QP&$C5O>p)=UygtW$GG(m&NL*Tbf=+ z^E<1)c>huZNMrhf)QN)dI;Y+HsHY}U=YV^o&YA*3+omPHGL{E65hO#mT%IB|P=1Dd zF5Qm5{30d(8)qf++tms4rq=&x0dxJ~jS8%nB@8ncZ&WCX=U@2wLIp|%BIc(bWVyL_ zIUJhHok?^?(vM$`rqudMO>zsG1pE^{ zd7Y8&E_aW(+=VKybLK7Nyk`qa+Fn&%=@D&w{wV%s%6C+;9SSZ^EFZr2RAl$-FZfj8 z(YM0X*g$;zy(cp%cy0W*@uoFLB7MD(KNAsLS_--Qy=U)_Vf7@YH0hpy@45P8*it&{ z|6T;&{!TEn`>zD2ekVBk<*x)Q?tkO3%-?XB+1=!u02KEde{iDkf`|XFapJ!{!A)QH y!3BpU_{J+#YF238<6gY|X@5oG$)t?P6ID7EULLHV^h-ti_wOw^4C=l*pZQ-1>K=ap diff --git a/apps/q/doc/screenshot.jpg b/apps/q/doc/screenshot.jpg deleted file mode 100644 index fac0c886a880c1f828cf73fe6d84fde4f2ab1bea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30660 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 diff --git a/apps/q/doc/screenshot.png b/apps/q/doc/screenshot.png deleted file mode 100644 index 4115a84e88968a23bf99a1179379845334daefcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100655 zcmV*-Kr+9HP) z06_wTnV<7sV%{48)Bp_tb4Mf~K~Nwd1Og}s=m(uI00AfhAW*y^6#qgH0P)bP&71>B z0P~IzimRwCpW5;VK@T4P9(@tw7~>o70w7!jAP+i9XwD;o6g¨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 g&uJPqEv|Ysi0h~dQFaQ7m diff --git a/apps/q/doc/screenshots.html b/apps/q/doc/screenshots.html deleted file mode 100644 index 6f003a12b..000000000 --- a/apps/q/doc/screenshots.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - Q Screenshots - - - -

Q Screenshots

- -
- -
-
aum
- - -Last modified: Mon Apr 18 14:06:02 NZST 2005 - - - diff --git a/apps/q/doc/spec/index.html b/apps/q/doc/spec/index.html deleted file mode 100644 index 854ee8388..000000000 --- a/apps/q/doc/spec/index.html +++ /dev/null @@ -1,1460 +0,0 @@ - - - - Q Protocol Specification - - - - - - - -
-

Q Protocol Specification

- - (first draft by aum)
-
- Return to Q Homepage
-
- - Introduction | - XML-RPC | - Architecture | - Commands | - Example Code | - Metadata | - Security | - Contact us - - -
- - - -
- -

1. Introduction

- - This document describes details of the interfaces between the various entities - in the Q network - server nodes, client nodes and client applications.
-
- Purpose is to: -
    -
  • Assist with people writing user client applications, such as GUI apps, command-line - apps, or integrate Q in to existing apps. -
  • - -
  • Permit alternative implementations of any of these entities, in any - programming language. -
  • - -
  • Help interested parties to gain a quick understanding of Q's architecture, - perhaps with a view to contributing ideas for improvement.
  • -
- - - -
- -

2. XML-RPC Interface

- -

2.1. WTF? All those ugly complicated angle-brackets?!?

- - If you haven't come across XML-RPC before, the whole concept might seem frightening, like - you've gotta write thousands of lines of code for parsing and encoding XML, and - negotiate some mind-numbingly complex multi-layered protocol.
-
- This is most certainly not the case. XML-RPC libraries are way simple to use.
-
- XML-RPC client and server libraries are available for all major (and most minor) - programming languages, and are structured in a way that hides all the intricate - details and presents an extremely simple and quickly learnable API over the top. - -
- -

2.2. Why XML-RPC??

- - I've chosen XML-RPC as the node interface framework because: -
    -
  • It's easy and quick to learn, regardless of programming language
  • -
  • It's supported by free libraries in all major programming languages
  • -
  • It avoids the maintenance problems of home-brew interfaces (people writing - implementations in several languages, some falling into disuse then breaking)
  • -
  • It reduces the opportunity for writing vulnerable client code (compare to writing - raw socket handlers in C, and inadvertently opening oneself up to buffer - overruns etc)
  • -
  • It allows for rapid client development
  • -
- - - -
- -

3. Architectural Overview

- - The Q network is structured as a two-level hierarchy of server nodes and - client nodes. Additionally, client applications are run by users, and - form the human interface to Q.
-
- Let's quickly overview the difference between these three entities: -
    -
  • Server nodes: -
      -
    • Are exptected to stay up all or most of the time
    • -
    • Are suited for running on permanently-up I2P routers
    • -
    • Run an XML-RPC server, listening exclusively within the I2P network for - commands from other peer server nodes as well as from client - nodes
    • -
    • Run XML-RPC clients, for sending commands via I2P to other server nodes
    • -
    • When joining the network, announce themselves as peers to - other server nodes
    • -
    • Usually have no direct contact with client applications
    • -
    • Receive and execute commands from client nodes, as well as - from other peer server nodes.
    • -
    • Will never send commands to client nodes.
    • -
    • Store content, which is served up by request to client nodes
    • -
    • Send catalogues of their stored content on request to client nodes
    • -
    • Store lists of their known peer server nodes, and send these lists - on request to client nodes -
    • Manage load by advising client nodes, and peer server nodes, - in command replies, of the next advisable time for contact
    • -
    • Should preferably be implemented in platform-independent code
    • -
    -
  • -
    - -
  • Client nodes: -
      -
    • May run as continuously or as intermittently as desired without causing - disruption to the network
    • -
    • Run an XML-RPC server, listening exclusively within the user's local - TCP/IP network (usually a localhost port), as opposed to server nodes - which run their XML-RPC server listening within I2P
    • -
    • Run XML-RPC clients, for sending commands via I2P to server nodes
    • -
    • Never announce themselves as peers to server nodes
    • -
    • Never have contact with other client nodes -
    • Are suited for use over permanent or transient I2P routers
    • -
    • Periodically contact servers requesting differential updates to - content catalogues, as well as peer lists. From this info, they maintain - a local mirror of what's available globally
    • -
    • When receiving any command reply from a given server, are expected to - honour the next advised contact time specified by that server
    • -
    • Form the official point of access to the Q network for client - applications
    • -
    • Should preferably be implemented in platform-independent code
    • -
    -
  • -
    - -
  • Client applications: -
      -
    • Form the point of human (or third-party program) access to the Q network
    • -
    • Offer the user a means of searching for content, inserting content and - retrieving content
    • -
    • Include GUI apps, CLI apps, web apps, and apps with other user or program - interfaces.
    • -
    • Usually never run an XML-RPC server at all
    • -
    • Run a single XML-RPC client, for sending commands via TCP to a - local client node
    • -
    • Are implemented and maintained separately to the core Q framework, though - at any time might be included in official Q distributions
    • -
    • Can be freely implemented in platform-independent or platform-dependent - code. For instance, Macintosh-only, or Windows-only implementations are - perfectly acceptable (but not quite as welcome as platform-independent - implementations)
    • -
    -
  • - -
- - - -
- -

4. Q Command Interface Description

- -

4.1. Overview

- - As mentioned earlier, communication between all Q entities takes place via an - XML-RPC mechanism.
-
- This chapter describes the actual primitives which are supported by both server - nodes and client nodes.
-
- Although the primitives are the same for both server and client, the way they are actioned - internally may vary.
-
- - For example, with the getItem primitive, server nodes will only look in - their local content store for the item, returning either that item's data and - metadata, or a failure reply. On the other hand, client nodes will try their - local content store first, and if the item is not found, will look in their - peer catalogues. If the item is found in a peer catalogue, the client node will - then on-send getItem calls to all server nodes believed to hold that item, - until or unless it retrieves a verifiable copy of that item - -
- -
- -

4.2. XML-RPC Data Types

- -
-
- - It's possibly a good idea here to get a hold of the XML-RPC library for - your favourite programming language, as well as the manual, and look up - the description of data types. Also, if you're especially keen, - you might like to read up on XML-RPC in general: - - -
-
- - XML-RPC supports a canonical set of data types, which are seamlessly integrated into - all its high level language implementations. A quick overview of the XML-RPC data types - used in Q appears below.
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
XML-RPC Data TypeDescription
intPlain 32-bit integer
stringSequence of ASCII bytes, viewed as java.lang.String objects in java, and str - objects (strings) in Python. - Note that ASCII control chars, and high-bit-set chars, are highly illegal and will - cause failure.
binary dataRaw binary data, viewed as byte [] in java, and xmlrpclib.Binary objects - in Python. This is the format used for raw content data.
listSequence of objects, viewed as java.util.Vector in java, and list objects in Python. -
structAn unordered set of (key, value) pairs. - Represented as java.util.Hashtable objects in java, and - dict objects in Python, (associative array in perl, ...)
- -
- -

4.3. General Command/Response Format

- - With Q's XML-RPC usage, all commands are a sequence of zero or more arguments. All - responses are a struct with at least the key status, whose value, a - string, is one of: -
    -
  • "ok" - the command was successful; any additional data is included - under other keys, depending on the command
  • -
  • "error" - the command failed, and an additional key error - contains a terse description of the error
  • -
- Note that all commands are also implemented with an alternative entry point, one which - takes a single Hashtable (struct/dict/assoc-array) argument. Refer to the javadocs for - further info: - -
- -

4.4. Exceptions - XML-RPC and Otherwise

-
- In certain cases, XML-RPC calls to Q nodes may return an exception.
-
- For instance, any attempt to invoke any primitive other than those listed below - will most definitely cause an exception, because in the Q XML-RPC implementation, - no provision is made for default handlers.
-
- Apart from this, it's possible that calls to known legal methods may trigger an - exception. This is not supposed to happen, and the author will be working over - time to intercept all such exceptions and wrap them in appropriate response - structures. But in the meantime, client app developers should catch any exceptions - resulting from their XML-RPC calls and recover appropriately. -
- -
- -

4.5. Overview of Q XML-RPC Primitives

- - The XML-RPC primitives supported by Q server and client nodes include: -
    -
  • i2p.q.ping - test if a server node is alive
  • -
  • i2p.q.hello - one new server node introduces itself to another server node
  • -
  • i2p.q.getItem - retrieve an item of content
  • -
  • i2p.q.putItem - insert an item of content
  • -
  • i2p.q.getUpdate - retrieve a differential update of peers list (and optionally, catalog update)
  • -
  • i2.q.search - search a client node for data items matching certain patterns
  • -
- -
- -

4.6. i2p.q.ping

- -
- -

Overview

- -
- - The i2p.q.ping primitive is used to test if a given server or client node - is presently online. It can be sent by server nodes, client nodes and client apps. - -
- -

Arguments

- -
- This primitive accepts no arguments, and will fail if any arguments are given. -
- -

Server Behaviour

- -
- No action on the part of the receiving server is required, apart from sending back: -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
statusstring"ok"
idstringThe node's nodeId, as a base64 string
deststringNode's destination, represented as base64 string
uptimeintThe number of seconds that this node has been running for
loadfloatCurrent load this node is experiencing, as a float between - 0.0 (no load) to 1.0 (impossibly flatlined)
- -

Client Behaviour

- -
- Same as server. -
- -
- -
- -

4.7. i2p.q.hello

- -
- -

Overview

- -
- - The i2p.q.hello primitive is sent by new server nodes to advise other existing - server nodes of their existence. It is only sent by server nodes to other server - nodes. It is considered an abuse for a client node to send this command. - -
- -

Arguments

- -
    -
  • destination (string) - the base64 representation of the calling node's - I2P destination (on which the calling node's in-I2P XML-RPC server may be - subsequently reached). Same format as the I2P hosts.txt listing. -
- -

Server Behaviour

- -
- If the destination is valid, the receiving server will reply with: -
- - - - - - - - -
KeyTypeDescription
statusstring"ok"
- -
- If the destination is invalid, the receiving server will send back: -
- - - - - - - - - - - - - -
KeyTypeDescription
statusstring"error"
errorstring"baddest"
- -

Client Behaviour

- -
- i2p.q.hello calls to clients are illegal. Client nodes receiving such - calls will respond with: -
- - - - - - - - - - - - - -
KeyTypeDescription
statusstring"error"
errorstring"unimplemented"
- -
- -
- -

4.8. i2p.q.getItem

- -
- -

Overview

- -
- - The i2p.q.getItem primitive is used to attempt retrieval of an item of content - from a client or server node. - -
- -

Arguments

- -
    -
  • key (string) - the base64 key under which the item in question is - stored
  • -
- -

Server Behaviour

- -
- Servers receiving this command will only search their own datastore for the item. - They will never attempt to on-request this item from other servers.
-
- If the server possesses the requested item in its datastore, it will respond with: -
- - - - - - - - - - - - - - - - - - -
KeyTypeDescription
statusstring"ok"
metadatastructA nested struct, containing the metadata for the key. (Refer section on - metadata).
databinary dataThe raw data.
- -
- If the server doesn't possess the data, it will respond with: -
- - - - - - - - - - - - - -
KeyTypeDescription
statusstring"error"
errorstring"notfound"
- - -

Client Behaviour

- -
- If the client possesses the key in its own local datastore, it will send back - the full data immediately: -
- - - - - - - - - - - - - - - - - - -
KeyTypeDescription
statusstring"ok"
metadatastructA nested struct, containing the metadata for the key. (Refer section on - metadata).
databinary dataThe raw data.
- -
- If the client doesn't possess the key, it will search its internal catalogues - for a server which does have the key.
-
- If one or more servers possessing the key are found, the client will on-send - an i2p.q.getItem command to each of those servers in turn, until it - either successfully retrieves the data, or fails.
-
- If the client successfully retrieves the data from one or more of its servers, - it will add the data to its internal cache, and reply with the above success - response.
-
- If the client was unable to source the complete data from any of its servers, - it will reply with: -
- - - - - - - - - - - - - -
KeyTypeDescription
statusstring"error"
errorstring"notfound"
- -
- -
- -

4.9. i2p.q.putItem

- -
- -

Overview

- -
- - The i2p.q.putItem primitive is used by client nodes to insert a new item - of content onto a server node.
-
- It is also used by client apps to insert a new item onto their - client node.
-
- Also, if a server node is receiving a high traffic of requests for a given item, - it may at its discretion send i2p.q.putItem commands to peer servers - to mirror the item on those servers, and spread the load. -
- -

Arguments

- -
    -
  • data - (binary) - the raw data to insert. Refer earlier - the compatible - Java datatype is byte[], and Python datatype is xmlrpclib.Binary.
  • -
  • metadata - (struct) - optional - a struct of metadata to - insert alongside the data. If this is not given, a minimal metadata set will - be automatically created by the recipient. See the section on - metadata. -
- -

Server Behaviour

- -
- If the server successfully received and stored the data (and optionally provided - metadata), it will reply with: -
- - - - - - - - - - - - - -
KeyTypeDescription
statusstring"ok"
keystringThe base64 key under which this item has been stored, and which should - be used for any subsequent i2p.q.getItem requests for that item - within the Q network.
- -
- However, if the server's datastore is full, the server will not be able to store - this item, in which case it will respond with: -
- - - - - - - - - - - - - -
KeyTypeDescription
statusstring"error"
errorstring"storefull"
- -

Client Behaviour

- -
- Client nodes receiving this command will attempt to store the item in their own - datastore, and respond immediately with one of the above server responses.
-
- In addition, client nodes will enqueue a background job to upload this item to - one or more selected server nodes. -
- -
- -
- -

4.10. i2p.q.getUpdate

- -
- -

Overview

- -
- - The i2p.q.getUpdate primitive is used to request a differential peers list - update (which optionally can include a catalog update as well).
-
- Client apps invoke this primitive on client nodes to get up-to-date - listings of items available in the network. Note that client apps will not - hand over any peers list.
-
- Client nodes periodically schedule a background job to invoke this primitive - on their known servers, such that they keep the most recent possible view of - available data and other servers.
-
- -

Arguments

- -
    -
  • since - (int) - unix time in seconds to update from. The recipient - will send back a list of all content it has become aware of since this - time.
  • -
  • includePeers - (int) - set to 1 to include peer list update in the return - data, 0 to omit.
  • -
  • includeCatalog - (int) - set to 1 to include catalog update in the return - data, 0 to omit.
  • -
- -

Server Behaviour

- -
- On receiving this command, a server node will send back lists of metadata records - for all new content (and/or all new peers) it has become aware of since the given - date. The full response is formatted as follows: -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
statusstring"ok"
itemslistA list of metadata records for new items. Refer to the section on - metadata for more information. If the server - has not become aware of any new data since the given date (or if the - includeCatalog argument was 0), this list will be empty.
peerslistA list of destinations of new peers. If the server has not discovered - any new peers since the given date (or if the includePeers argument - was 0), this list will be empty. -
timeUpdateEndsintunixtime in secs that this update ends. The peer receiving this - response should note this time, and quote it as the since argument - in the next getUpdate request
timeNextContactintAdvised time (unixtime in sec) for sending the next getUpdate command. The sending - peer should not issue any getCatalog commands before this time, but is - welcome to issue them after this time. The actual time value is guesstimated - by the server node, depending on its current load.
- -

4.11. i2p.q.search

- -
- -

Overview

- -
- - The i2p.q.search primitive is invoked by client apps to search a client node - for data items matching a set of criteria. -
- Only client nodes support this primitive. Server nodes will return an empty - result set and an error response. -
- -

Arguments

- -
    -
  • criteria - (hashtable) - a set of metadata criteria to match. Each key in - this hashtable is a metadata key (eg title, type etc), and the - corresponding value is a regular expression string to match. Regular expression - syntax is documented in the java API in the - section - on class 'Pattern'.
    -
    - The search criteria work 'AND-style', in that if more than one metadata key - match pattern is given, then only items matching all of the given criteria - will be returned.
    -
    - Python example (using XML-RPC proxy - see code samples below): -
    -result = mynode.i2p.q.search({"type":"text", "summary":"^War.*"})
    -metaRecs = result['items']
    -
    - Java Example (using XML-RPC proxy - see code examples below): -
    -Hashtable criteria = new Hashtable();
    -criteria.put("type", "text");
    -criteria.put("summary", "^War.*");
    -Vector args = new Vector();
    -args.addElement(criteria);
    -Hashtable result = (Hashtable)mynode.execute("i2p.q.search", args);
    -Vector metaRecs = (Vector)result.get("items");
    -
    - Note that if the criteria argument is empty (no keys/values), then the - client node will send back metadata for every item of content it knows of, which - (depending on the size of the Q network), could be quite a resource-hungry operation. -
  • -
- -

Server Behaviour

- -
- Servers receiving this command will send back an error response: -
- - - - - - - - - - - - - -
KeyTypeDescription
statusstring"error"
errorstring"unimplemented"
- -

Client Behaviour

- -
- Client nodes receiving this command will send back the following response: -
- - - - - - - - - - - - -
KeyTypeDescription
statusstring"ok"
itemsvectorA list of metadata records (Hashtables) for items which match the given - search criteria, and are retrievable through this client - node (ie, the client node either possesses the item, or knows one or more - servers which possess the item).
-
- -
- - -
- -

5. Client Program Examples

- -

5.1. Overview

- - This section provides a couple of simple examples of client app programming.
-
- At present, only Python and Java examples are given.
-
- (If you don't know either of these languages, you should be - able to get the general drift by studying the examples, sufficient to map the concepts to the - XML-RPC API available to your preferred language.)
-
- The examples below communicate with a client node XML-RPC server (running on the - local machine and listening on its default port of 7651), and perform simple - operations of data insertion, catalog fetching and data retrieval. - -
- -

5.2. Java Example

- - To run this example, you'll need: -
    -
  • A running I2P installation, with an instance of a Q client node. -
  • The I2P standard jarfiles declared in your java CLASSPATH
  • -
  • The standard Apache XML-RPC library jarfile in your CLASSPATH (which you will - already have on your CLASSPATH, because this is part of installing Q). Recall that you - can get a copy of Apache java XML-RPC lib jarfile from - http://ws.apache.org/xmlrpc).
  • -
- Now for the code (heavily annotated, so you don't necessarily need to know or understand Java), which - should be written to a source file called QDemo.java. Note that this client would be a - significantly shorter if it instantiated a QClientNode class directly and invoked its methods, - but that is not what we're showing here - we're demonstrating the use of the client node's XML-RPC - interface. -
-
-// QDemo.java
-//
-// A simple demo example of a Q client application, which 
-// communicates with a running Q client node on the local
-// machine via its TCP XML-RPC interface
-//
-// If your client node is not running on localhost, or
-// if it's listening on a port other than the default
-// 7651, you'll need to change the code below.
-//
-// Note that this demo is bloated by the fact we're using
-// raw XML-RPC.
-// 
-// The following exercises are left to the reader:
-//  1. Modify this app so that instead of using the XML-RPC
-//     interface, it instantiates a QClientNode, and
-//     invokes its methods directly.
-//  2. Write a thin wrapper class which instantiates an XML-RPC
-//     client, and offers simpler access methods (thus avoiding
-//     the need to create and populate Vectors of args before
-//     calling, and pick through a reply Hashtable after the call),
-//     and create a version of this demo which uses the wrapper.
-
-// pull in some standard java stuff
-import java.*;
-import java.lang.*;
-import java.util.*;
-import java.net.*;
-import java.io.*;
-
-// pull in some xml-rpc stuff
-import org.apache.xmlrpc.*;
-
-// since we're talking to the node via xmlrpc, and talking to
-// it in a separate VM, we don't need to import any Q packages
-
-// Define a minimal demo class, which kust defines a
-// main method enabling us to run the demo from a shell.
-//
-// For the purposes of this demo, we're assuming that your Q client node is
-// running on your local machine, and that you haven't altered the
-// listening port (default 7651) for the client's XML-RPC interface.
-
-public class QDemo {
-
-    // just define a main so we can run this from a shell
-    static public void main(String [] args)
-        throws MalformedURLException, XmlRpcException, IOException
-    {
-        // for getting and analysing replies from node
-        Hashtable result;
-        String status;
-
-        // Create a new client app object
-        XmlRpcClient myClient = new XmlRpcClient("http://127.0.0.1:7651");
-
-        // -------------------------------------
-        // First action - execute a 'ping' on this peer
-        // -------------------------------------
-
-        Vector noArgs = new Vector();
-        result = (Hashtable)myClient.execute("i2p.q.ping", noArgs);
-        print("ping: result=" + result);
-
-        // -------------------------------------
-        // Second action - insert an item of data
-        // -------------------------------------
-
-        // mark the current time, we'll use this later
-        Integer then = new Integer((int)(new Date().getTime() / 1000));
-
-        // create metadata
-        // (note from previous chapter that metadata is optional)
-        Hashtable meta = new Hashtable();
-        meta.put("type", "text");
-        meta.put("abstract", "a simple piece of demo data");
-        meta.put("mimetype", "text/plain");
-
-        // create some data
-        String data = "Hello, world";
-
-        // set up the arguments list
-        Vector insertArgs = new Vector();
-        insertArgs.addElement(meta);
-        insertArgs.addElement(data.getBytes()); // must insert data as byte[]
-            
-        // and do the insert
-        result = (Hashtable)myClient.execute("i2p.q.putItem", insertArgs);
-        print("putItem: result=" + result);
-
-        // check what happened
-        status = (String)result.get("status");
-        String key;
-        if (status.equals("ok")) {
-            // insert succeeded
-            key = (String)result.get("key");
-            print("Insert successful");
-        } else {
-            // insert failed, bail
-            print("Insert failed: error=" + (String)result.get("error"));
-            return;
-        }
-
-        // -------------------------------------
-        // Third action - check for catalog updates
-        // (which should include what we've just inserted)
-        // -------------------------------------
-
-        // create an args list, with just the date we noted before the insert
-        Vector updateArgs = new Vector();
-        updateArgs.addElement(then);
-        // add the flags
-        updateArgs.addElement(new Integer(0));   // 'includePeers'
-        updateArgs.addElement(new Integer(1));   // 'includeCatalog'
-
-        // execute the 'getCatalog'
-        result = (Hashtable)myClient.execute("i2p.q.getUpdate", updateArgs);
-        print("getUpdate: result="+result);
-
-        // pick out the results, and search for what we just inserted
-        int i;
-        Vector items = (Vector)result.get("items");
-        int nitems = items.size();
-        boolean foundit = false;
-        for (i = 0; i < nitems; i++) {
-            // get the nth item
-            Hashtable metaRec = (Hashtable)items.get(i);
-            String thisKey = (String)metaRec.get("key");
-            if (thisKey.equals(key)) {
-                // yay, got it!
-                foundit = true;
-                break;
-            }
-        }
-
-        // did we get it?
-        if (!foundit) {
-            print("wtf? we inserted it but it's not in the catalog!");
-            return;
-        }
-
-        // yep, we got it, so try to retrieve it back
-        Vector getArgs = new Vector();
-        getArgs.addElement(key);
-        result = (Hashtable)myClient.execute("i2p.q.getItem", getArgs);
-        print("getItem: result=" + result);
-
-        // did we get it?
-        status = (String)result.get("status");
-        if (!status.equals("ok")) {
-            print("getItem failed: " + (String)result.get("error"));
-            return;
-        }
-
-        // yep, got it
-        byte [] binData = (byte [])result.get("data");
-        String strData = new String(binData);
-        print("getItem: success, data='"+strData+"'");
-
-        print("--- END OF Q CLIENT DEMO ---");
-    }
-
-    // a convenient shorthand method for printing stuff to stdout
-    static void print(String msg) {
-        System.out.println(msg);
-    }
-}
-        
-
- -
- -

5.3. Python Example

- - To run this example, you will need a running I2P installation, including a running instance - of a Q client node.
-
- Note that, in contrast to Java, Python 2.3 and later have all the necessary XML-RPC libraries built in. -
- Now for some code (again, heavily annotated). This, together with the previous example, present an - interesting comparison between some of Java and Python's ways of doing things. -
-
-#!/usr/bin/env python
-"""
-QDemo.py
-
-A simple demo example of a Q client application, which 
-communicates with a running Q client node on the local
-machine via its TCP XML-RPC interface
-
-If your client node is not running on localhost, or
-if it's listening on a port other than the default
-7651, you'll need to change the code below.
-
-Note that this demo is bloated by the fact we're using
-raw XML-RPC.
-
-The following exercise is left to the reader:
- * Write a thin wrapper class which instantiates an XML-RPC
-   client, and offers simpler access methods (thus avoiding
-   the need to pick through a reply dict after the call),
-   and create a version of this demo which uses the wrapper.
-"""
-
-# a coupla needed imports
-from time import time
-from xmlrpclib import ServerProxy, Binary
-
-# For the purposes of this demo, we're assuming that your Q client node is
-# running on your local machine, and that you haven't altered the
-# listening port (default 7651) for the client's XML-RPC interface.
-
-def qdemo():
-    # Create a new client app object
-    myClient = ServerProxy("http://127.0.0.1:7651")
-
-    # -------------------------------------
-    # First action - execute a 'ping' on this peer
-    # -------------------------------------
-
-    result = myClient.i2p.q.ping()
-    print "ping: result=%s" % result
-
-    # -------------------------------------
-    # Second action - insert an item of data
-    # -------------------------------------
-
-    # mark the current time, we'll use this later
-    then = int(time())
-
-    # create metadata
-    # (note from previous chapter that metadata is optional)
-    meta = {
-        "type" : "text",
-        "abstract" : "a simple piece of demo data",
-        "mimetype" : "text/plain",
-        }
-
-    # create some data, and binary-wrap it
-    data = "Hello, world"
-    binData = Binary(data)
-
-    # and do the insert
-    result = myClient.i2p.q.putItem(meta, binData)
-    print "putItem: result=%s" % result
-
-    # check what happened
-    if result["status"] == "ok":
-        # insert succeeded
-        key = result["key"]
-        print "Insert successful"
-    else:
-        # insert failed, bail
-        print "Insert failed: error=%s" % result['error']
-        return;
-
-    # -------------------------------------
-    # Third action - check for catalog updates
-    # (which should include what we've just inserted)
-    # -------------------------------------
-
-    # execute the 'getUpdate'
-    result = myClient.i2p.q.getUpdate(then, 0, 1)
-    print "getUpdate: result=%s" % result
-
-    # pick out the results, and search for what we just inserted
-    foundit = False
-    for metaRec in result['items']:
-        if metaRec['key'] == key:
-            # yay, got it!
-            foundit = True
-            break
-
-    # did we get it?
-    if not foundit:
-        print "wtf? we inserted it but it's not in the catalog!"
-        return;
-
-    # yep, we got it, so try to retrieve it back
-    print "getCatalog: found the item we just inserted"
-    result = myClient.i2p.q.getItem(key)
-    print "getItem: result=%s" % result
-
-    # did we get it?
-    if result["status"] != "ok":
-        print "getItem failed: %s" + result["error"]
-        return;
-
-    # yep, got it (note that data is an xmlrpclib.Binary object,
-    # and the raw data we want is in its .data attribute)
-    print "getItem: success, data='%s'" % result['data'].data
-
-    print "--- END OF Q CLIENT DEMO ---"
-
-# run the demo func if this script is executed directly
-if __name__ == '__main__':
-    qdemo()
-        
-
- - - -
- -

6. Keys and Metadata

- -

6.1. Overview

- Like Freenet, content is stored in Q as (data, metadata) pairs.
-
- However, there's a difference. On Freenet, metadata is stored as a string of up to - 32k length, and must be parsed (and sometimes executed) by client code. On the other - hand, metadata is exposed in Q as an XML-RPC struct (Java Hashtable or - Properties object, or Python dict, or Perl associative array etc).
-
- If a content item gets inserted to the Q network without metadata, a minimal metadata set - will be transparently generated, and is guaranteed to contain at least the following - elements:
-
- - - - - - - - - - - - -
KeyTypeDescription
sizeintSize of the stored data item, in bytes
dataHashstringa base64 representation of the SHA256 hash of the full raw data, using the I2P - base64 alphabet
-
- -
- -

6.2. Node IDs

- - When Q nodes are first created, they generate themselves a random - I2P privKey/dest keypair using the in-I2P services.
-
- The I2P destination gets converted to what we call a Q Node ID, as follows: -
    -
  • Start with binary destination (not base64)
  • -
  • Determine the SHA256 binary digest of this dest
  • -
  • Encode the resulting binary string via I2P's base64 alphabet
  • -
- -
- -

6.3. Keys

- - Here, 'key' means the unique short string, by which items of content can be - retrieved, and which is returned from an i2p.q.putItem command.
-
- Like Freenet's CHK@ keytype, Q keys are hashes of the key's content and - metadata.
-
- The recipe for calculating the 'key' of a particular item of metadata+data is: -
    -
  1. If no metadata is submitted with the data, create a minimal metadata as per above
  2. -
  3. Serialise out the metadata into a string representation, with the fieldnames in - alphanumeric order. The format of such string is one line per metadata field/value - pair per line, in the format: -
    - metadatakeyname=metadatakeyvalue\n -
    -
  4. -
  5. Calculate the binary SHA1 digest of this serialised metadata string
  6. -
  7. Base64-encode this binary digest via the I2P Base64 alphabet
  8. -
- -
- -

6.4. Q Metadata Conventions

- - Additional to the core metadata defined above, there is a convention in Q that the - following optional extra metadata - keys be provided on insert, and recognised and honoured on retrieve.
-
- It is highly recommended that these keys be included - in metadata when content is inserted:
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
KeyTypeDescription
titlestringA short and descriptive title for the item, preferably formatted as - a filename which is legal and convenient on all main operating systems, ie, - containing only alphanumerics, '-', '_' and '.'.
-
- It is highly advisable that an appropriate file extension appear at the - end of the title. Refer to the Security Considerations - section below. -
- It is expected that client applications will use this title field when - displaying available content lists to users. -
typestringGeneric type of material, using the following superset of the eMule/Donkey - classifications: -
    -
  • text
  • -
  • html
  • -
  • image
  • -
  • audio
  • -
  • video
  • -
  • software
  • -
  • archive
  • -
  • misc
  • -
-
mimetypestringA recognised mime-type, as per RFC1341, RFC1521, RFC1522, such as - audio/mpeg, text/plain etc.
-
- This will help client app developers devise ways of disposing with data items - they request from client nodes.
-
- For instance, client apps with http front ends - may send back this mimetype as the value of the Content-type: header, - (and possibly take preventative action with potentially hazardous mimetypes, such - as those which some browsers such as IE might trust and execute blindly as - binary code).
-
- Alternatively, gui-based or cli-based client apps may convert this mimetype to - an appropriate benign file extension (such as .txt, - .ogg, .jpg etc). See Security - Considerations below. -
keywordsstringA set of space-separated keywords describing this item, intended for - human reading, as well as automatic parsing by client apps.
abstractstringA short descriptive summary of the nature of the data, intended for - human reading, as well as automatic pattern matching searches by client - apps.
-
-
- -
- -

6.5. One Data Item, Many Metadata Sets?

- - It is perfectly possible, and legal, for one item of data to be referenced by two - completely different items of metadata.
-
- Since content keys are a hash of metadata, which in turn contains a hash of the data, - then two pieces of metadata referencing the same data item, but containing different - metadata values, will end up with different keys.
-
- So as far as key addresses go, there will be a many-to-1 relationship between raw - content keys, and the data returned under these keys.
-
- - - -
- -

7. Security Considerations

- - All Peer2Peer software (as with all networked software in general) carries with it a set of - devastating security risks which should be respected to the utmost.
-
- This applies in no small part to Q.
-
- So this brief sermon is addressed to anyone writing any client applications or - APIs talking to the Q network.
-
- Any material which involves the execution of code on a client machine is risky. - However, much of the risk can be managed if the code is open source and peer-reviewed.
-
- Perhaps the biggest issue as far as Q is concerned is this: - -
- Client app developers should never, NEVER implicitly - trust incoming content, and should always assume that malicious remote users - will insert content which attempts to compromise other users' systems. -
- - If a Q client app wants to offer filetype-specific support, then perhaps a good - strategy is for the client app to use a whitelist of - known low-risk file extensions, such as .txt, or (possibly) - .ogg, .png etc. Recall that in some Windows configurations, even - .jpg can carry an arbitrary code execution attack!
-
- Note that .html (text/html) is especially dangerous, and - should be respected accordingly.
-
- Support for .html could be a real boon. For instance, it could allow - I2P users to publish an I2P equivalent of freenet's freesites - static - HTML websites which are accessible even when the author goes offline.
-
- However, if a client app chooses to recognise .html, it should either - use a code-screening mechanism like freenet's fproxy and keep it - up to date with all the latest advisories, or use a mandatory-proxy - mechanism like I2P's eepProxy.
-
- One possiblility is to serve up such content via a totally in-I2P http interface, - such that Joe can view the content via his regular eeproxy-configured browser.
-
- This is a typical case where security and ease/convenience can end up in - direct conflict. Automatic handling of content according to data type - is great from a Joe Sixpack Windows User point of view, but it is a snake-pit - of risks that can potentially result in any of the following (or worse): -
    -
  • Set up Joe's computer as a spambot
  • -
  • Get Joe's personal credit card and other info, and use this criminally
  • -
  • Download child pornography or terrorist information onto Joe's PC, use an - exploit to get Joe's IP address and/or identity details, and report this to - authorities, thus framing Joe and sending him off undeservedly to Club Fed or - Her Majesty's
  • -
  • Mount a DDoS, anonymity or other attack on the I2P network
  • -
  • Further spread additional content for achieving more of the above on - other unsuspecting users.
  • -
- - The crux of this lecture is that client app writers have a huge responsibility to - ensure their apps are safe against malicious content.
-
- Perhaps the best and most - practical solution is to just store downloaded material into a directory - known to and owned by the user, and make it the user's task and responsibility to - manually copy materials out of this directory and take responsibility for how s/he uses - this content thereafter.
-
- -
- -
- -

8. Contacting the Author

- - I am aum, and can be reached as aum on in-I2P IRC networks, and also - at the in-I2P email address of aum@mail.i2p.
-
- -
-
- - Introduction | - XML-RPC | - Architecture | - Commands | - Example Code | - Metadata | - Security | - Contact us - -
-
-
- - - -Last modified: Sat Apr 2 13:31:08 NZST 2005 - - - diff --git a/apps/q/java/build.xml b/apps/q/java/build.xml deleted file mode 100644 index 8fed7c7a2..000000000 --- a/apps/q/java/build.xml +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/q/java/qresources/html/404.html b/apps/q/java/qresources/html/404.html deleted file mode 100644 index c25e9661c..000000000 --- a/apps/q/java/qresources/html/404.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
Item Not Found
Failed to retrieve item:
- -
diff --git a/apps/q/java/qresources/html/about.html b/apps/q/java/qresources/html/about.html deleted file mode 100644 index 39e0b5523..000000000 --- a/apps/q/java/qresources/html/about.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - -
About Q
-

Version - -

- - 0.0.1 - -

Designed and engineered by aum

- -

Copyright © 2005 by aum. -
Released under the -
GNU Lesser General Public License

- -

- Many thanks to jrandom, smeghead and frosk
- for their patient and knowledgeable support
- in helping this python programmer
- get partly proficient in java.

-
- diff --git a/apps/q/java/qresources/html/addrefform.html b/apps/q/java/qresources/html/addrefform.html deleted file mode 100644 index 02a96a4ad..000000000 --- a/apps/q/java/qresources/html/addrefform.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - -
Add Hub Noderef
- - - - - - - - - - - - - -
- In order for your node to join a Q network, it must have a ref to at - least one of the running Q Hubs. You need to get a Q Hub noderef and - paste it here. -
- -
- -
-
- diff --git a/apps/q/java/qresources/html/aiealert.html b/apps/q/java/qresources/html/aiealert.html deleted file mode 100644 index 01ddecbd5..000000000 --- a/apps/q/java/qresources/html/aiealert.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - -
Critical Anonymity Alert!
- - - - - - -
-

You are attempting to view a QSite via the Internet Explorer web browser. -

-

We have blocked the connection to protect your anonymity. -

-

As a matter of policy, Q does not support QSite browsing via Microsoft - Internet Explorer (MSIE), because of that browser's abysmal track record with - regard to security. If we did allow you to browse Q via MSIE, it would - be easy for a malicious QSite author to embed hostile content which - undermines your computer's security and compromises your anonymity. -

-

If you want to surf I2P QSites, you'll need to use a more secure web - browser such as Mozilla or Firefox. -

-
diff --git a/apps/q/java/qresources/html/anonalert.html b/apps/q/java/qresources/html/anonalert.html deleted file mode 100644 index 48a980d7f..000000000 --- a/apps/q/java/qresources/html/anonalert.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - -
Critical Anonymity Alert!
- - - - - - -
-

You are attempting to view a QSite via an unprotected link. -

-

We have blocked the connection to protect your anonymity. If we don't - do this, then a malicious QSite author can insert content into the QSite - which triggers oubound hits to arbitrary servers on the mainstream web, - which in turn can easily reveal a lot of personal information about you - and the QSite you are accessing. -

-

If you want to browse QSites with your web browser with greater safety, - you'll have to follow these simple steps: -

-
    -
  1. Edit your I2P hosts.txt file and add the following entry - (all on one line): -
    - (and make sure you do NOT reveal this entry to anyone else) -
  2. -
  3. Configure one of your web browsers to use the proxy server: -
    localhost:4444
    - (or whatever address you have configured your I2P EEProxy to listen on, - if you've changed it)

    -
  4. -
  5. Start up the browser you have just configured, and enter the web address: -
    http://q.i2p
    -
  6. -
-

Even if you do this, you still won't have a 100% guarantee of anonymity, since - a malicious QSite author can send code to your browser which exploits a vulnerability - in the browser to compromise your anonymity (eg, accessing third party cookies, executing - arbitrary code, accessing your local filesystem, uploading compromising information - about you to hostile I2P EEPsites etc). But you can relax (somewhat) in the knowledge - that such attacks are much more difficult, particularly if you use a decent web browser. -

-

Thank you for your co-operation. We wish you happy and safe browsing. -

-
-
diff --git a/apps/q/java/qresources/html/downloads.html b/apps/q/java/qresources/html/downloads.html deleted file mode 100644 index f30ed5d1e..000000000 --- a/apps/q/java/qresources/html/downloads.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - -
Q Downloads
Not implemented yet
- diff --git a/apps/q/java/qresources/html/genkeysform.html b/apps/q/java/qresources/html/genkeysform.html deleted file mode 100644 index 3ffaa1f1f..000000000 --- a/apps/q/java/qresources/html/genkeysform.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - -
Generate New Keypair
- - - - -
- Click the button to generate a new keypair for - future inserts of signed space keys. -
-
-
- - -
-
- diff --git a/apps/q/java/qresources/html/genkeysresult.html b/apps/q/java/qresources/html/genkeysresult.html deleted file mode 100644 index c693d306f..000000000 --- a/apps/q/java/qresources/html/genkeysresult.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - -
Your New Keys
- - - - -
- Please save these keys in a safe place, - preferably on an encrypted partition: -
-
Public Key: - -
Private Key:
- diff --git a/apps/q/java/qresources/html/getform.html b/apps/q/java/qresources/html/getform.html deleted file mode 100644 index 313d7cdc6..000000000 --- a/apps/q/java/qresources/html/getform.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - -
Retrieve Content
URI:
diff --git a/apps/q/java/qresources/html/help.html b/apps/q/java/qresources/html/help.html deleted file mode 100644 index b50fbcc81..000000000 --- a/apps/q/java/qresources/html/help.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - -
Q Help
- Not written yet -
- diff --git a/apps/q/java/qresources/html/jobs.html b/apps/q/java/qresources/html/jobs.html deleted file mode 100644 index 08191f931..000000000 --- a/apps/q/java/qresources/html/jobs.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - -
Current Background Jobs
- - - - - - - - - - - - - - - - - -
Secs From NowDescription
- - - -
(No current jobs)
diff --git a/apps/q/java/qresources/html/main.html b/apps/q/java/qresources/html/main.html deleted file mode 100644 index b0941de6a..000000000 --- a/apps/q/java/qresources/html/main.html +++ /dev/null @@ -1,122 +0,0 @@ - - - -Q Web Interface - - - - - - - - - -
- - - - - -
- - - - - - - - - - - - -
"tabcell""tabcell_inactive"> - - - Status - - Settings -
-
- Q Node -
-
-
-
- -
- -
-
-
-
- - diff --git a/apps/q/java/qresources/html/msiealert.html b/apps/q/java/qresources/html/msiealert.html deleted file mode 100644 index 01ddecbd5..000000000 --- a/apps/q/java/qresources/html/msiealert.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - -
Critical Anonymity Alert!
- - - - - - -
-

You are attempting to view a QSite via the Internet Explorer web browser. -

-

We have blocked the connection to protect your anonymity. -

-

As a matter of policy, Q does not support QSite browsing via Microsoft - Internet Explorer (MSIE), because of that browser's abysmal track record with - regard to security. If we did allow you to browse Q via MSIE, it would - be easy for a malicious QSite author to embed hostile content which - undermines your computer's security and compromises your anonymity. -

-

If you want to surf I2P QSites, you'll need to use a more secure web - browser such as Mozilla or Firefox. -

-
diff --git a/apps/q/java/qresources/html/puterror.html b/apps/q/java/qresources/html/puterror.html deleted file mode 100644 index 9b184d9d6..000000000 --- a/apps/q/java/qresources/html/puterror.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
Error Inserting QSiteItem
Your insert failed because:
- -
diff --git a/apps/q/java/qresources/html/putform.html b/apps/q/java/qresources/html/putform.html deleted file mode 100644 index 7ceaa3c66..000000000 --- a/apps/q/java/qresources/html/putform.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Insert Content
Insert A QSite Instead

Type: - Text - HTML - Image - Audio - Video - Archive - Other -

Title: - -
- (A short descriptive title for this item) -
-

Path: - -
- (If you're inserting in plain-hash mode, this should be a suggested - filename for people to save the item as when downloading it; If you're - inserting in signed-space mode, this can be a longer pathname) -
-

Mimetype: - -
- (Leave blank unless you know exactly what this means) -
-

Keywords: - -
- (Comma-separated list of short descriptive keywords) -
-

Abstract: - -
- (A short descriptive summary for the item, preferably no - longer than 256 characters) -
-

File: - -
- (Leave this blank if you are entering the raw data in the text box below) -
-

Private Key: - -
- (Only if inserting in signed space mode) -
-

- -

Raw Data: -
- (You may enter the raw data here as text instead of selecting a - file above) -
- -
\ No newline at end of file diff --git a/apps/q/java/qresources/html/putok.html b/apps/q/java/qresources/html/putok.html deleted file mode 100644 index 0a4e8b97e..000000000 --- a/apps/q/java/qresources/html/putok.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - -
QSiteItem Inserted Successfully
Item URI:
- -
\ No newline at end of file diff --git a/apps/q/java/qresources/html/putsiteform.html b/apps/q/java/qresources/html/putsiteform.html deleted file mode 100644 index 20a0966f9..000000000 --- a/apps/q/java/qresources/html/putsiteform.html +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Insert A QSite
Insert A Single File Instead

Name: - -
- (A short name for the QSite - should contain only alphanumerics, '-', and '_'; - should definitely not contain '/', ':', '\', ' ' etc) -
-

Private Key: - -
- (Mandatory - if you don't have a signed-space keypair, get one by - clicking on Tools) -
-

Directory: - -
- (Absolute directory path where the QSite's files reside) -
-

Title: - -
- (A short descriptive title for this QSite) -
-

Keywords: - -
- (Comma-separated list of short descriptive keywords) -
-

Abstract: - -
- (A short descriptive summary for the QSite, preferably no - longer than 256 characters) -
-

- -
diff --git a/apps/q/java/qresources/html/searchform.html b/apps/q/java/qresources/html/searchform.html deleted file mode 100644 index a40d6a55a..000000000 --- a/apps/q/java/qresources/html/searchform.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Search For Content
Type: - ANY - QSite - Text - HTML - Image -
- Audio - Video - Archive - Other -
Title: checked>regexp
Path: checked>regexp
Mimetype: checked>regexp
Keywords: checked>regexp
Summary: checked>regexp
- Search Mode: - - AND - OR -

- -
\ No newline at end of file diff --git a/apps/q/java/qresources/html/searchresults.html b/apps/q/java/qresources/html/searchresults.html deleted file mode 100644 index 21ac24552..000000000 --- a/apps/q/java/qresources/html/searchresults.html +++ /dev/null @@ -1,27 +0,0 @@ -width="95%"> - - - - - - - - - -
Search Results
items found
- - - - - - -
-
- -
-
-
-
() bytes
-
-
-
diff --git a/apps/q/java/qresources/html/settings.html b/apps/q/java/qresources/html/settings.html deleted file mode 100644 index c6686e90f..000000000 --- a/apps/q/java/qresources/html/settings.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - -
Q Settings
Not implemented yet
- diff --git a/apps/q/java/qresources/html/status.html b/apps/q/java/qresources/html/status.html deleted file mode 100644 index f0fac2d6e..000000000 --- a/apps/q/java/qresources/html/status.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - -
Q Node Status
- - - - - - - - - - - - - - -
ItemValue
-
diff --git a/apps/q/java/qresources/html/tools.html b/apps/q/java/qresources/html/tools.html deleted file mode 100644 index 84aecb3fb..000000000 --- a/apps/q/java/qresources/html/tools.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - -
Q Tools
- diff --git a/apps/q/java/qresources/html/widgets/itemtype.html b/apps/q/java/qresources/html/widgets/itemtype.html deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/q/java/src/HTML/Template.java b/apps/q/java/src/HTML/Template.java deleted file mode 100644 index 6b6ce914a..000000000 --- a/apps/q/java/src/HTML/Template.java +++ /dev/null @@ -1,1131 +0,0 @@ -/* -* HTML.Template: A module for using HTML Templates with java -* -* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) -* -* This module is free software; you can redistribute it -* and/or modify it under the terms of either: -* -* a) the GNU General Public License as published by the Free -* Software Foundation; either version 1, or (at your option) -* any later version, or -* -* b) the "Artistic License" which comes with this module. -* -* This program is distributed in the hope that it will be -* useful, but WITHOUT ANY WARRANTY; without even the implied -* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -* PURPOSE. See either the GNU General Public License or the -* Artistic License for more details. -* -* You should have received a copy of the Artistic License -* with this module, in the file ARTISTIC. If not, I'll be -* glad to provide one. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the Free -* Software Foundation, Inc., 59 Temple Place, Suite 330, -* Boston, MA 02111-1307 USA -* -* Modified by David McNab (david@rebirthing.co.nz) to allow nesting of -* templates (ie, passing a child Template object as a value argument -* to a .setParam() invocation on a parent Template object). -* -*/ - -package HTML; -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.Reader; -import java.util.EmptyStackException; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.NoSuchElementException; -import java.util.Properties; -import java.util.Stack; -import java.util.StringTokenizer; -import java.util.Vector; - -import HTML.Tmpl.Filter; -import HTML.Tmpl.Util; -import HTML.Tmpl.Element.Conditional; -import HTML.Tmpl.Element.Element; -import HTML.Tmpl.Element.If; -import HTML.Tmpl.Element.Var; -import HTML.Tmpl.Parsers.Parser; - -/** - * Use HTML Templates with java. - *

- * The HTML.Template class allows you to use HTML Templates from within - * your java programs. It makes it possible to change the look of your - * servlets without having to recompile them. Use HTML.Template to - * separate code from presentation in your servlets. - *

- *

- *	Hashtable args = new Hashtable();
- *	args.put("filename", "my_template.tmpl");
- *
- *	Template t = new Template(args);
- *
- *	t.setParam("title", "The HTML Template package");
- *	t.printTo(response.getWriter());
- * 
- *

- * HTML.Template is based on the perl module HTML::Template by Sam Tregar - *

- * Modified by David McNab (david@rebirthing.co.nz) to allow nesting of - * templates (ie, passing a child Template object as a value argument - * to a .setParam() invocation on a parent Template object). - *

- * @author Philip S Tellis - * @version 0.1.2 - */ -public class Template -{ - private If __template__ = new If("__template__"); - private Hashtable params = new Hashtable(); - - private boolean dirty = true; - - private boolean strict = true; - private boolean die_on_bad_params = false; - private boolean global_vars = false; - private boolean case_sensitive = false; - private boolean loop_context_vars = false; - private boolean debug = false; - private boolean no_includes = false; - private boolean search_path_on_include = false; - private int max_includes = 11; - private String filename = null; - private String scalarref = null; - private String [] arrayref = null; - private String [] path = null; - private Reader filehandle = null; - private Filter [] filters = null; - - private Stack elements = new Stack(); - private Parser parser; - - /** - * Initialises a new HTML.Template object with the contents of - * the given file. - * - * @param filename a string containing the name of - * the file to be used as a - * template. This may be an - * absolute or relative path to a - * template file. - * - * @throws FileNotFoundException If the file specified does not - * exist. - * @throws IllegalStateException If <tmpl_include> is - * used when no_includes is in - * effect. - * @throws IOException If an input or output Exception - * occurred while reading the - * template. - * - * @deprecated No replacement. You should use either - * {@link #Template(Object [])} or - * {@link #Template(Hashtable)} - */ - public Template(String filename) - throws FileNotFoundException, - IllegalStateException, - IOException - { - this.filename = filename; - init(); - } - - - /** - * Initialises a new Template object, using the name/value - * pairs passed as default values. - *

- * The parameters passed may be any combination of filename, - * scalarref, arrayref, path, case_sensitive, loop_context_vars, - * strict, die_on_bad_params, global_vars, max_includes, - * no_includes, search_path_on_include and debug. - * Each with its own value. Any one of filename, scalarref or - * arrayref must be passed. - *

- * Eg: - *

-	 *	String [] template_init = {
-	 *		"filename",  "my_template.tmpl",
-	 *		"case_sensitive", "true",
-	 *		"max_includes",   "5"
-	 *	};
-	 *
-	 *      Template t = new Template(template_init);
-	 * 
- *

- * The above code creates a new Template object, initialising - * its input file to my_template.tmpl, turning on case_sensitive - * parameter matching, and restricting maximum depth of includes - * to five. - *

- * Parameter values that take boolean values may either be a String - * containing the words true/false, or the Boolean values Boolean.TRUE - * and Boolean.FALSE. Numeric values may be Strings, or Integers. - * - * @since 0.0.8 - * - * @param args an array of name/value pairs to initialise - * this template with. Valid values for - * each element may be: - * @param filename [Required] a String containing the path to a - * template file - * @param scalarref [Required] a String containing the entire - * template as its contents - * @param arrayref [Required] an array of lines that make up - * the template - * @param path [Optional] an array of Strings specifying - * the directories in which to look for the - * template file. If not specified, the current - * working directory is used. If specified, - * only the directories in this array are used. - * If you want the current directory searched, - * include "." in the path. - *

- * If you have only a single path, it can be a - * plain String instead of a String array. - *

- * This is effective only for the template file, - * and not for included files, but see - * search_path_on_include for how to change that. - * @param case_sensitive [Optional] specifies whether parameter - * matching is case sensitive or not. A value - * of "false", "0" or "" is considered false. - * All other values are true. - *

- * Default: false - * @param loop_context_vars [Optional] when set to true four loop - * context variables are made available inside a - * loop: __FIRST__, __LAST__, __INNER__, __ODD__, __COUNTER__. - * They can be used with <TMPL_IF>, - * <TMPL_UNLESS> and <TMPL_ELSE> to - * control how a loop is output. Example: - *

-	 *	    <TMPL_LOOP NAME="FOO">
-	 *	       <TMPL_IF NAME="__FIRST__">
-	 *	         This only outputs on the first pass.
-	 *	       </TMPL_IF>
-	 *
-	 *	       <TMPL_IF NAME="__ODD__">
-	 *	         This outputs on the odd passes.
-	 *	       </TMPL_IF>
-	 *
-	 *	       <TMPL_UNLESS NAME="__ODD__">
-	 *	         This outputs on the even passes.
-	 *	       </TMPL_IF>
-	 *
-	 *	       <TMPL_IF NAME="__INNER__">
-	 *	         This outputs on passes that are 
-	 *		neither first nor last.
-	 *	       </TMPL_IF>
-	 *
-	 *	       <TMPL_IF NAME="__LAST__">
-	 *	         This only outputs on the last pass.
-	 *	       <TMPL_IF>
-	 *	    </TMPL_LOOP>
-	 *			
- *

- * NOTE: A loop with only a single pass will get - * both __FIRST__ and __LAST__ - * set to true, but not __INNER__. - *

- * Default: false - * @param strict [Optional] if set to false the module will - * allow things that look like they might be - * TMPL_* tags to get by without throwing - * an exception. Example: - *

-	 *          <TMPL_HUH NAME=ZUH>
-	 *			
- *

- * Would normally cause an error, but if you - * create the Template with strict == 0, - * HTML.Template will ignore it. - *

- * Default: true - * @param die_on_bad_params [Optional] if set to true - * the module will complain if you try to set - * tmpl.setParam("param_name", "value") and - * param_name doesn't exist in the template. - *

- * This effect doesn't descend into loops. - *

- * Default: false (may change in later versions) - * @param global_vars [Optional] normally variables declared outside - * a loop are not available inside a loop. This - * option makes TMPL_VARs global throughout - * the template. It also affects TMPL_IF and TMPL_UNLESS. - *

-	 *	    <p>This is a normal variable: <TMPL_VAR NORMAL>.</p>
-	 *
-	 *	    <TMPL_LOOP NAME="FROOT_LOOP>
-	 *	       Here it is inside the loop: <TMPL_VAR NORMAL>
-	 *	    </TMPL_LOOP>
-	 *			
- *

- * Normally this wouldn't work as expected, since - * <TMPL_VAR NORMAL>'s value outside the loop - * isn't available inside the loop. - *

- * Default: false (may change in later versions) - * @param max_includes [Optional] specifies the maximum depth that - * includes can reach. Including files to a - * depth greater than this value causes an error - * message to be displayed. Set to 0 to disable - * this protection. - *

- * Default: 10 - * @param no_includes [Optional] If set to true, disallows the - * <TMPL_INCLUDE> tag in the template - * file. This can be used to make opening - * untrusted templates slightly less dangerous. - *

- * Default: false - * @param search_path_on_include [Optional] if set, then the - * path is searched for included files as well - * as the template file. See the path parameter - * for more information. - *

- * Default: false - * @param debug [Optional] setting this option to true causes - * HTML.Template to print random error messages - * to STDERR. - * - * @throws ArrayIndexOutOfBoundsException If an odd number of - * parameters is passed. - * @throws FileNotFoundException If the file specified does not - * exist or no filename is passed. - * @throws IllegalArgumentException If an unknown parameter is - * passed. - * @throws IllegalStateException If <tmpl_include> is - * used when no_includes is in - * effect. - * @throws IOException If an input or output Exception - * occurred while reading the - * template. - */ - public Template(Object [] args) - throws ArrayIndexOutOfBoundsException, - FileNotFoundException, - IllegalArgumentException, - IllegalStateException, - IOException - - { - if(args.length%2 != 0) - throw new ArrayIndexOutOfBoundsException("odd number " + - "of arguments passed"); - - for(int i=0; i - * The parameters passed are the same as in the Template(Object []) - * constructor. Each with its own value. Any one of filename, - * scalarref or arrayref must be passed. - *

- * Eg: - *

-	 *	Hashtable args = new Hashtable();
-	 *	args.put("filename", "my_template.tmpl");
-	 *	args.put("case_sensitive", "true");
-	 *	args.put("loop_context_vars", Boolean.TRUE);
-	 *	// args.put("max_includes", "5");
-	 *	args.put("max_includes", new Integer(5));
-	 *
-	 *	Template t = new Template(args);
-	 * 
- *

- * The above code creates a new Template object, initialising - * its input file to my_template.tmpl, turning on case_sensitive - * parameter matching, and the loop context variables __FIRST__, - * __LAST__, __ODD__ and __INNER__, and restricting maximum depth of - * includes to five. - *

- * Parameter values that take boolean values may either be a String - * containing the words true/false, or the Boolean values Boolean.TRUE - * and Boolean.FALSE. Numeric values may be Strings, or Integers. - * - * @since 0.0.10 - * - * @param args a Hashtable of name/value pairs to initialise - * this template with. Valid values are the same - * as in the Template(Object []) constructor. - * - * @throws FileNotFoundException If the file specified does not - * exist or no filename is passed. - * @throws IllegalArgumentException If an unknown parameter is - * passed. - * @throws IllegalStateException If <tmpl_include> is - * used when no_includes is in - * effect. - * @throws IOException If an input or output Exception - * occurred while reading the - * template. - * - * @see #Template(Object []) - */ - public Template(Hashtable args) - throws FileNotFoundException, - IllegalArgumentException, - IllegalStateException, - IOException - - { - Enumeration e = args.keys(); - while(e.hasMoreElements()) { - String key = (String)e.nextElement(); - Object value = args.get(key); - - parseParam(key, value); - } - - init(); - } - - /** - * Prints the parsed template to the provided PrintWriter. - * - * @param out the PrintWriter that this template will be printed - * to - */ - public void printTo(PrintWriter out) - { - out.print(output()); - } - - /** - * Returns the parsed template as a String. - * - * @return a string containing the parsed template - */ - public String output() - { - return __template__.parse(params); - } - - /** - * Sets the values of parameters in this template from a Hashtable. - * - * @param params a Hashtable containing name/value pairs for - * this template. Keys in this hashtable must - * be Strings and values may be either Strings - * or Vectors. - *

- * Parameter names are currently not case - * sensitive. - *

- * Parameter names can contain only letters, - * digits, ., /, +, - and _ characters. - *

- * Parameter names starting and ending with - * a double underscore are not permitted. - * eg: __myparam__ is illegal. - * - * @return the number of parameters actually set. - * Illegal parameters will not be set, but - * no error/exception will be thrown. - */ - public int setParams(Hashtable params) - { - if(params == null || params.isEmpty()) - return 0; - int count=0; - for(Enumeration e = params.keys(); e.hasMoreElements();) { - Object key = e.nextElement(); - if(key.getClass().getName().endsWith(".String")) { - Object value = params.get(key); - try { - setParam((String)key, value); - count++; - } catch (Exception pe) { - // key was not a String or Vector - // or key was null - // don't increment count - } - } - } - if(count>0) { - dirty=true; - Util.debug_print("Now dirty: set params"); - } - - return count; - } - - /** - * Sets a single scalar parameter in this template. - * - * @param name a String containing the name of this parameter. - * Parameter names are currently not case sensitive. - * @param value a String containing the value of this parameter - * - * @return the value of the parameter set - * @throws IllegalArgumentException if the parameter name contains - * illegal characters - * @throws NullPointerException if the parameter name is null - * - * @see #setParams(Hashtable) - */ - public String setParam(String name, String value) - throws IllegalArgumentException, NullPointerException - { - try { - return (String)setParam(name, (Object)value); - } catch(ClassCastException iae) { - return null; - } - } - - /** - * Sets a single Integer parameter in this template. - * - * @param name a String containing the name of this parameter. - * Parameter names are currently not case sensitive. - * @param value an Integer containing the value of this parameter - * - * @return the value of the parameter set - * @throws IllegalArgumentException if the parameter name contains - * illegal characters - * @throws NullPointerException if the parameter name is null - * - * @see #setParams(Hashtable) - */ - public Integer setParam(String name, Integer value) - throws IllegalArgumentException, NullPointerException - { - try { - return (Integer)setParam(name, (Object)value); - } catch(ClassCastException iae) { - return null; - } - } - - /** - * Sets a single int parameter in this template. - * - * @param name a String containing the name of this parameter. - * Parameter names are currently not case sensitive. - * @param value an int containing the value of this parameter - * - * @return the value of the parameter set - * @throws IllegalArgumentException if the parameter name contains - * illegal characters - * @throws NullPointerException if the parameter name is null - * - * @see #setParams(Hashtable) - */ - public int setParam(String name, int value) - throws IllegalArgumentException, NullPointerException - { - return setParam(name, new Integer(value)).intValue(); - } - - /** - * Sets a single boolean parameter in this template. - * - * @param name a String containing the name of this parameter. - * Parameter names are currently not case sensitive. - * @param value a boolean containing the value of this parameter - * - * @return the value of the parameter set - * @throws IllegalArgumentException if the parameter name contains - * illegal characters - * @throws NullPointerException if the parameter name is null - * - * @see #setParams(Hashtable) - */ - public boolean setParam(String name, boolean value) - throws IllegalArgumentException, NullPointerException - { - return setParam(name, new Boolean(value)).booleanValue(); - } - - /** - * Sets a single Boolean parameter in this template. - * - * @param name a String containing the name of this parameter. - * Parameter names are currently not case sensitive. - * @param value a Boolean containing the value of this parameter - * - * @return the value of the parameter set - * @throws IllegalArgumentException if the parameter name contains - * illegal characters - * @throws NullPointerException if the parameter name is null - * - * @see #setParams(Hashtable) - */ - public Boolean setParam(String name, Boolean value) - throws IllegalArgumentException, NullPointerException - { - try { - return (Boolean)setParam(name, (Object)value); - } catch(ClassCastException iae) { - return null; - } - } - - - /** - * Sets a single parameter in this template to a nested Template - * - * @param name a String containing the name of this parameter. - * Parameter names are currently not case sensitive. - * @param value a Template object to be nested in - * - * @return the value of the parameter set - * @throws IllegalArgumentException if the parameter name contains - * illegal characters - * @throws NullPointerException if the parameter name is null - */ - public Template setParam(String name, Template value) - throws IllegalArgumentException, NullPointerException - { - try { - return (Template)setParam(name, (Object)value); - } catch(ClassCastException iae) { - return null; - } - } - - - /** - * Sets a single list parameter in this template. - * - * @param name a String containing the name of this parameter. - * Parameter names are not currently case sensitive. - * @param value a Vector containing a list of Hashtables of parameters - * - * @return the value of the parameter set - * @throws IllegalArgumentException if the parameter name contains - * illegal characters - * @throws NullPointerException if the parameter name is null - * - * @see #setParams(Hashtable) - */ - public Vector setParam(String name, Vector value) - throws IllegalArgumentException, NullPointerException - { - try { - return (Vector)setParam(name, (Object)value); - } catch(ClassCastException iae) { - return null; - } - } - - /** - * Returns a parameter from this template identified by the given name. - * - * @param name a String containing the name of the parameter to be - * returned. Parameter names are not currently case - * sensitive. - * - * @return the value of the requested parameter. If the parameter - * is a scalar, the return value is a String, if the - * parameter is a list, the return value is a Vector. - * - * @throws NoSuchElementException if the parameter does not exist - * in the template - * @throws NullPointerException if the parameter name is null - */ - public Object getParam(String name) - throws NoSuchElementException, NullPointerException - { - if(name == null) - throw new NullPointerException("name cannot be null"); - if(!params.containsKey(name)) - throw new NoSuchElementException(name + - " is not a parameter in this template"); - - if(case_sensitive) - return params.get(name); - else - return params.get(name.toLowerCase()); - } - - - private void parseParam(String key, Object value) - throws IllegalStateException - { - if(key.equals("case_sensitive")) - { - this.case_sensitive=boolify(value); - Util.debug_print("case_sensitive: "+value); - } - else if(key.equals("strict")) - { - this.strict=boolify(value); - Util.debug_print("strict: "+value); - } - else if(key.equals("global_vars")) - { - this.global_vars=boolify(value); - Util.debug_print("global_vars: "+value); - } - else if(key.equals("die_on_bad_params")) - { - this.die_on_bad_params=boolify(value); - Util.debug_print("die_obp: "+value); - } - else if(key.equals("max_includes")) - { - this.max_includes=intify(value)+1; - Util.debug_print("max_includes: "+value); - } - else if(key.equals("no_includes")) - { - this.no_includes=boolify(value); - Util.debug_print("no_includes: "+value); - } - else if(key.equals("search_path_on_include")) - { - this.search_path_on_include=boolify(value); - Util.debug_print("path_includes: "+value); - } - else if(key.equals("loop_context_vars")) - { - this.loop_context_vars=boolify(value); - Util.debug_print("loop_c_v: "+value); - } - else if(key.equals("debug")) - { - this.debug=boolify(value); - Util.debug=this.debug; - Util.debug_print("debug: "+value); - } - else if(key.equals("filename")) - { - this.filename = (String)value; - Util.debug_print("filename: "+value); - } - else if(key.equals("scalarref")) - { - this.scalarref = (String)value; - Util.debug_print("scalarref"); - } - else if(key.equals("arrayref")) - { - this.arrayref = (String [])value; - Util.debug_print("arrayref"); - } - else if(key.equals("path")) - { - if(value.getClass().getName().startsWith("[")) - this.path = (String [])value; - else { - this.path = new String[1]; - this.path[0] = (String)value; - } - Util.debug_print("path"); - for(int j=0; j not " + - "allowed when " + - "no_includes in effect" - ); - if(max_includes == 0) { - throw new IndexOutOfBoundsException( - "include too deep"); - } else { - // come here if positive - // or negative - elements.push(e); - read_file(p.getProperty("name")); - } - } - else if(type.equals("var")) - { - String name = p.getProperty("name"); - String escape = p.getProperty("escape"); - String def = p.getProperty("default"); - Util.debug_print("name: " + name); - Util.debug_print("escape: " + escape); - Util.debug_print("default: " + def); - e.add(new Var(name, escape, def)); - } - else if(type.equals("else")) - { - Util.debug_print("adding branch"); - ((Conditional)e).addBranch(); - } - else if(p.getProperty("close").equals("true")) - { - Util.debug_print("closing tag"); - if(!type.equals(e.Type())) - throw new EmptyStackException(); - - e = (Element)elements.pop(); - } - else - { - Element t = parser.getElement(p); - e.add(t); - elements.push(e); - e=t; - } - } - return e; - } - - private void read_file(String filename) - throws FileNotFoundException, - IllegalStateException, - IOException, - EmptyStackException - { - BufferedReader br=openFile(filename); - - String line; - - Element e = null; - if(elements.empty()) - e = __template__; - else - e = (Element)elements.pop(); - - max_includes--; - while((line=br.readLine()) != null) { - Util.debug_print("Line: " + line); - e = parseLine(line+"\n", e); - } - max_includes++; - - br.close(); - br=null; - - } - - private void read_line_array(String [] lines) - throws FileNotFoundException, - IllegalStateException, - IOException, - EmptyStackException - { - - Element e = __template__; - - max_includes--; - for(int i=0; i 0) - type = type.substring(type.lastIndexOf(".")+1); - - String valid_types = ",String,Vector,Boolean,Integer,Template"; - - if(valid_types.indexOf(type) < 0) - throw new ClassCastException( - "value is neither scalar nor list nor Template"); - - name=case_sensitive?name:name.toLowerCase(); - - if(!case_sensitive && type.equals("Vector")) { - value = lowerCaseAll((Vector)value); - } - - Util.debug_print("setting: " + name); - params.put(name, value); - - dirty=true; - return value; - } - - private static Vector lowerCaseAll(Vector v) - { - Vector v2 = new Vector(); - for(Enumeration e = v.elements(); e.hasMoreElements(); ) { - Hashtable h = (Hashtable)e.nextElement(); - if(h == null) { - v2.addElement(h); - continue; - } - Hashtable h2 = new Hashtable(); - for(Enumeration e2 = h.keys(); e2.hasMoreElements(); ) { - String key = (String)e2.nextElement(); - Object value = h.get(key); - String value_type = value.getClass().getName(); - Util.debug_print("to lower case: " + key + "(" + value_type + ")"); - if(value_type.endsWith(".Vector")) - value = lowerCaseAll((Vector)value); - h2.put(key.toLowerCase(), value); - } - v2.addElement(h2); - } - return v2; - } - - private static boolean boolify(Object o) - { - String s; - if(o.getClass().getName().endsWith(".Boolean")) - return ((Boolean)o).booleanValue(); - else if(o.getClass().getName().endsWith(".String")) - s = (String)o; - else - s = o.toString(); - - if(s.equals("0") || s.equals("") || s.equals("false")) - return false; - return true; - } - - private static int intify(Object o) - { - String s; - if(o.getClass().getName().endsWith(".Integer")) - return ((Integer)o).intValue(); - else if(o.getClass().getName().endsWith(".String")) - s = (String)o; - else - s = o.toString(); - - try { - return Integer.parseInt(s); - } catch(NumberFormatException nfe) { - return 0; - } - } - - private static String stringify(boolean b) - { - if(b) - return "1"; - else - return ""; - } - - private BufferedReader openFile(String filename) - throws FileNotFoundException - { - boolean add_path=true; - - if(!elements.empty() && !search_path_on_include) - add_path=false; - - if(filename.startsWith("/")) - add_path=false; - - if(this.path == null) - add_path=false; - - Util.debug_print("open " + filename); - if(!add_path) - return new BufferedReader(new FileReader(filename)); - - BufferedReader br=null; - - for(int i=0; i 0) - control_class = control_class.substring( - control_class.lastIndexOf(".")+1); - - if(control_class.equals("String")) { - return !(((String)control_val).equals("") || - ((String)control_val).equals("0")); - } else if(control_class.equals("Vector")) { - return !((Vector)control_val).isEmpty(); - } else if(control_class.equals("Boolean")) { - return ((Boolean)control_val).booleanValue(); - } else if(control_class.equals("Integer")) { - return (((Integer)control_val).intValue() != 0); - } else { - throw new IllegalArgumentException("Unrecognised type"); - } - } -} - diff --git a/apps/q/java/src/HTML/Tmpl/Element/Element.java b/apps/q/java/src/HTML/Tmpl/Element/Element.java deleted file mode 100644 index 2dea977fa..000000000 --- a/apps/q/java/src/HTML/Tmpl/Element/Element.java +++ /dev/null @@ -1,66 +0,0 @@ -/* -* HTML.Template: A module for using HTML Templates with java -* -* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) -* -* This module is free software; you can redistribute it -* and/or modify it under the terms of either: -* -* a) the GNU General Public License as published by the Free -* Software Foundation; either version 1, or (at your option) -* any later version, or -* -* b) the "Artistic License" which comes with this module. -* -* This program is distributed in the hope that it will be -* useful, but WITHOUT ANY WARRANTY; without even the implied -* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -* PURPOSE. See either the GNU General Public License or the -* Artistic License for more details. -* -* You should have received a copy of the Artistic License -* with this module, in the file ARTISTIC. If not, I'll be -* glad to provide one. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the Free -* Software Foundation, Inc., 59 Temple Place, Suite 330, -* Boston, MA 02111-1307 USA -*/ - - -package HTML.Tmpl.Element; -import java.util.Hashtable; -import java.util.NoSuchElementException; - -public abstract class Element -{ - protected String type; - protected String name=""; - - public abstract String parse(Hashtable params); - public abstract String typeOfParam(String param) - throws NoSuchElementException; - - public void add(String data){} - public void add(Element node){} - - public boolean contains(String param) - { - try { - return (typeOfParam(param) != null?true:false); - } catch(NoSuchElementException nse) { - return false; - } - } - - public final String Type() - { - return type; - } - - public final String Name() - { - return name; - } -} diff --git a/apps/q/java/src/HTML/Tmpl/Element/If.java b/apps/q/java/src/HTML/Tmpl/Element/If.java deleted file mode 100644 index 4384e8fbd..000000000 --- a/apps/q/java/src/HTML/Tmpl/Element/If.java +++ /dev/null @@ -1,39 +0,0 @@ -/* -* HTML.Template: A module for using HTML Templates with java -* -* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) -* -* This module is free software; you can redistribute it -* and/or modify it under the terms of either: -* -* a) the GNU General Public License as published by the Free -* Software Foundation; either version 1, or (at your option) -* any later version, or -* -* b) the "Artistic License" which comes with this module. -* -* This program is distributed in the hope that it will be -* useful, but WITHOUT ANY WARRANTY; without even the implied -* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -* PURPOSE. See either the GNU General Public License or the -* Artistic License for more details. -* -* You should have received a copy of the Artistic License -* with this module, in the file ARTISTIC. If not, I'll be -* glad to provide one. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the Free -* Software Foundation, Inc., 59 Temple Place, Suite 330, -* Boston, MA 02111-1307 USA -*/ - -package HTML.Tmpl.Element; - -public class If extends Conditional -{ - public If(String control_var) throws IllegalArgumentException - { - super("if", control_var); - } -} diff --git a/apps/q/java/src/HTML/Tmpl/Element/Loop.java b/apps/q/java/src/HTML/Tmpl/Element/Loop.java deleted file mode 100644 index cb1911a87..000000000 --- a/apps/q/java/src/HTML/Tmpl/Element/Loop.java +++ /dev/null @@ -1,183 +0,0 @@ -/* -* HTML.Template: A module for using HTML Templates with java -* -* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) -* -* This module is free software; you can redistribute it -* and/or modify it under the terms of either: -* -* a) the GNU General Public License as published by the Free -* Software Foundation; either version 1, or (at your option) -* any later version, or -* -* b) the "Artistic License" which comes with this module. -* -* This program is distributed in the hope that it will be -* useful, but WITHOUT ANY WARRANTY; without even the implied -* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -* PURPOSE. See either the GNU General Public License or the -* Artistic License for more details. -* -* You should have received a copy of the Artistic License -* with this module, in the file ARTISTIC. If not, I'll be -* glad to provide one. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the Free -* Software Foundation, Inc., 59 Temple Place, Suite 330, -* Boston, MA 02111-1307 USA -*/ - -package HTML.Tmpl.Element; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.NoSuchElementException; -import java.util.Vector; - -public class Loop extends Element -{ - private boolean loop_context_vars=false; - private boolean global_vars=false; - - private Vector control_val = null; - private Vector data; - - public Loop(String name) - { - this.type = "loop"; - this.name = name; - this.data = new Vector(); - } - - public Loop(String name, boolean loop_context_vars) - { - this(name); - this.loop_context_vars=loop_context_vars; - } - - public Loop(String name, boolean loop_context_vars, boolean global_vars) - { - this(name); - this.loop_context_vars=loop_context_vars; - this.global_vars=global_vars; - } - - public void add(String text) - { - data.addElement(text); - } - - public void add(Element node) - { - data.addElement(node); - } - - public void setControlValue(Vector control_val) - throws IllegalArgumentException - { - this.control_val = process_var(control_val); - } - - public String parse(Hashtable p) - { - if(!p.containsKey(this.name)) - this.control_val = null; - else { - Object o = p.get(this.name); - if(!o.getClass().getName().endsWith(".Vector") && - !o.getClass().getName().endsWith(".List")) - throw new ClassCastException( - "Attempt to set with a non-list. tmpl_loop=" + this.name); - setControlValue((Vector)p.get(this.name)); - } - - if(control_val == null) - return ""; - - StringBuffer output = new StringBuffer(); - Enumeration iterator = control_val.elements(); - - boolean first=true; - boolean last=false; - boolean inner=false; - boolean odd=true; - int counter=1; - - while(iterator.hasMoreElements()) { - Hashtable params = (Hashtable)iterator.nextElement(); - - if(params==null) - params = new Hashtable(); - - if(global_vars) { - for(Enumeration e = p.keys(); e.hasMoreElements();) { - Object key = e.nextElement(); - if(!params.containsKey(key)) - params.put(key, p.get(key)); - } - } - - if(loop_context_vars) { - if(!iterator.hasMoreElements()) - last=true; - inner = !first && !last; - - params.put("__FIRST__", first?"1":""); - params.put("__LAST__", last?"1":""); - params.put("__ODD__", odd?"1":""); - params.put("__INNER__", inner?"1":""); - params.put("__COUNTER__", "" + (counter++)); - } - - Enumeration de = data.elements(); - while(de.hasMoreElements()) { - - Object e = de.nextElement(); - if(e.getClass().getName().indexOf("String")>-1) - output.append((String)e); - else - output.append(((Element)e).parse(params)); - } - first = false; - odd = !odd; - } - - return output.toString(); - } - - public String typeOfParam(String param) - throws NoSuchElementException - { - for(Enumeration e = data.elements(); e.hasMoreElements();) - { - Object o = e.nextElement(); - if(o.getClass().getName().endsWith(".String")) - continue; - if(((Element)o).Name().equals(param)) - return ((Element)o).Type(); - } - throw new NoSuchElementException(param); - } - - private Vector process_var(Vector control_val) - throws IllegalArgumentException - { - String control_class = ""; - - if(control_val == null) - return null; - - control_class=control_val.getClass().getName(); - - if(control_class.indexOf("Vector") > -1) { - if(control_val.isEmpty()) - return null; - } else { - throw new IllegalArgumentException("Unrecognised type"); - } - - return control_val; - } - -} - diff --git a/apps/q/java/src/HTML/Tmpl/Element/Unless.java b/apps/q/java/src/HTML/Tmpl/Element/Unless.java deleted file mode 100644 index 8caca00c6..000000000 --- a/apps/q/java/src/HTML/Tmpl/Element/Unless.java +++ /dev/null @@ -1,39 +0,0 @@ -/* -* HTML.Template: A module for using HTML Templates with java -* -* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) -* -* This module is free software; you can redistribute it -* and/or modify it under the terms of either: -* -* a) the GNU General Public License as published by the Free -* Software Foundation; either version 1, or (at your option) -* any later version, or -* -* b) the "Artistic License" which comes with this module. -* -* This program is distributed in the hope that it will be -* useful, but WITHOUT ANY WARRANTY; without even the implied -* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -* PURPOSE. See either the GNU General Public License or the -* Artistic License for more details. -* -* You should have received a copy of the Artistic License -* with this module, in the file ARTISTIC. If not, I'll be -* glad to provide one. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the Free -* Software Foundation, Inc., 59 Temple Place, Suite 330, -* Boston, MA 02111-1307 USA -*/ - -package HTML.Tmpl.Element; - -public class Unless extends Conditional -{ - public Unless(String control_var) throws IllegalArgumentException - { - super("unless", control_var); - } -} diff --git a/apps/q/java/src/HTML/Tmpl/Element/Var.java b/apps/q/java/src/HTML/Tmpl/Element/Var.java deleted file mode 100644 index bf761b9c0..000000000 --- a/apps/q/java/src/HTML/Tmpl/Element/Var.java +++ /dev/null @@ -1,145 +0,0 @@ -/* -* HTML.Template: A module for using HTML Templates with java -* -* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) -* -* This module is free software; you can redistribute it -* and/or modify it under the terms of either: -* -* a) the GNU General Public License as published by the Free -* Software Foundation; either version 1, or (at your option) -* any later version, or -* -* b) the "Artistic License" which comes with this module. -* -* This program is distributed in the hope that it will be -* useful, but WITHOUT ANY WARRANTY; without even the implied -* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -* PURPOSE. See either the GNU General Public License or the -* Artistic License for more details. -* -* You should have received a copy of the Artistic License -* with this module, in the file ARTISTIC. If not, I'll be -* glad to provide one. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the Free -* Software Foundation, Inc., 59 Temple Place, Suite 330, -* Boston, MA 02111-1307 USA -* -* Modified by David McNab (david@rebirthing.co.nz) to allow nesting of -* templates (ie, passing a child Template object as a value argument -* to a .setParam() invocation on a parent Template object). -*/ - -package HTML.Tmpl.Element; -import java.util.Hashtable; -import java.util.NoSuchElementException; - -import HTML.Template; -import HTML.Tmpl.Util; - -public class Var extends Element -{ - public static final int ESCAPE_NONE = 0; - public static final int ESCAPE_URL = 1; - public static final int ESCAPE_HTML = 2; - public static final int ESCAPE_QUOTE = 4; - - public Var(String name, int escape, Object default_value) - throws IllegalArgumentException - { - this(name, escape); - this.default_value = stringify(default_value); - } - - public Var(String name, int escape) - throws IllegalArgumentException - { - if(name == null) - throw new IllegalArgumentException("tmpl_var must have a name"); - this.type = "var"; - this.name = name; - this.escape = escape; - } - - public Var(String name, String escape) - throws IllegalArgumentException - { - this(name, escape, null); - } - - public Var(String name, String escape, Object default_value) - throws IllegalArgumentException - { - this(name, ESCAPE_NONE, default_value); - - if(escape.equalsIgnoreCase("html")) - this.escape = ESCAPE_HTML; - else if(escape.equalsIgnoreCase("url")) - this.escape = ESCAPE_URL; - else if(escape.equalsIgnoreCase("quote")) - this.escape = ESCAPE_QUOTE; - } - - public Var(String name, boolean escape) - throws IllegalArgumentException - { - this(name, escape?ESCAPE_HTML:ESCAPE_NONE); - } - - public String parse(Hashtable params) - { - String value = null; - - if(params.containsKey(this.name)) - value = stringify(params.get(this.name)); - else - value = this.default_value; - - if(value == null) - return ""; - - if(this.escape == ESCAPE_HTML) - return Util.escapeHTML(value); - else if(this.escape == ESCAPE_URL) - return Util.escapeURL(value); - else if(this.escape == ESCAPE_QUOTE) - return Util.escapeQuote(value); - else - return value; - } - - public String typeOfParam(String param) - throws NoSuchElementException - { - throw new NoSuchElementException(param); - } - - private String stringify(Object o) - { - if(o == null) - return null; - - String cname = o.getClass().getName(); - if(cname.endsWith(".String")) - return (String)o; - else if(cname.endsWith(".Integer")) - return ((Integer)o).toString(); - else if(cname.endsWith(".Boolean")) - return ((Boolean)o).toString(); - else if(cname.endsWith(".Date")) - return ((java.util.Date)o).toString(); - else if(cname.endsWith(".Vector")) - throw new ClassCastException("Attempt to set with a non-scalar. Var name=" + this.name); - else if(cname.endsWith(".Template")) - return ((Template)o).output(); - else - throw new ClassCastException("Unknown object type: " + cname); - } - - // Private data starts here - private int escape=ESCAPE_NONE; - private String default_value=null; - -} diff --git a/apps/q/java/src/HTML/Tmpl/Filter.java b/apps/q/java/src/HTML/Tmpl/Filter.java deleted file mode 100644 index 5d5f82112..000000000 --- a/apps/q/java/src/HTML/Tmpl/Filter.java +++ /dev/null @@ -1,145 +0,0 @@ -/* -* HTML.Template: A module for using HTML Templates with java -* -* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) -* -* This module is free software; you can redistribute it -* and/or modify it under the terms of either: -* -* a) the GNU General Public License as published by the Free -* Software Foundation; either version 1, or (at your option) -* any later version, or -* -* b) the "Artistic License" which comes with this module. -* -* This program is distributed in the hope that it will be -* useful, but WITHOUT ANY WARRANTY; without even the implied -* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -* PURPOSE. See either the GNU General Public License or the -* Artistic License for more details. -* -* You should have received a copy of the Artistic License -* with this module, in the file ARTISTIC. If not, I'll be -* glad to provide one. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the Free -* Software Foundation, Inc., 59 Temple Place, Suite 330, -* Boston, MA 02111-1307 USA -*/ - - -package HTML.Tmpl; - -/** - * Pre-parse filters for HTML.Template templates. - *

- * The HTML.Tmpl.Filter interface allows you to write Filters - * for your templates. The filter is called after the template - * is read and before it is parsed. - *

- * You can use a filter to make changes in the template file before - * it is parsed by HTML.Template, so for example, use it to replace - * constants, or to translate your own tags to HTML.Template tags. - *

- * A common usage would be to do what you think you're doing when you - * do <TMPL_INCLUDE file="<TMPL_VAR name="the_file">">: - *

- * myTemplate.tmpl: - *

- *	<TMPL_INCLUDE file="<%the_file%>">
- * 
- *

- * myFilter.java: - *

- *	class myFilter implements HTML.Tmpl.Filter
- *	{
- *		private String myFile;
- *		private int type=SCALAR
- *
- *		public myFilter(String myFile) {
- *			this.myFile = myFile;
- *		}
- *
- *		public int format() {
- *			return this.type;
- *		}
- *
- *		public String parse(String t) {
- *			// replace all <%the_file%> with myFile
- *			return t;
- *		}
- *
- *		public String [] parse(String [] t) {
- *			throw new UnsupportedOperationException();
- *		}
- *	}
- * 
- *

- * myClass.java: - *

- *	Hashtable params = new Hashtable();
- *	params.put("filename", "myTemplate.tmpl");
- *	params.put("filter", new myFilter("myFile.tmpl"));
- *	Template t = new Template(params);
- * 
- * - * @author Philip S Tellis - * @version 0.0.1 - */ -public interface Filter -{ - /** - * Tells HTML.Template to call the parse(String) method of this filter. - */ - public final static int SCALAR=1; - - /** - * Tells HTML.Template to call the parse(String []) method of this - * filter. - */ - public final static int ARRAY=2; - - /** - * Tells HTML.Template what kind of filter this is. - * Should return either SCALAR or ARRAY to indicate which parse method - * must be called. - * - * @return the values SCALAR or ARRAY indicating which parse method - * is to be called - */ - public int format(); - - /** - * parses the template as a single string, and returns the parsed - * template as a single string. - *

- * Should throw an UnsupportedOperationException if it isn't implemented - * - * @param t a string containing the entire template - * - * @return a string containing the template after you've parsed it - * - * @throws UnsupportedOperationException if this method isn't - * implemented - */ - public String parse(String t); - - /** - * parses the template as an array of strings, and returns the parsed - * template as an array of strings. - *

- * Should throw an UnsupportedOperationException if it isn't implemented - * - * @param t an array of strings containing the template - one line - * at a time - * - * @return an array of strings containing the parsed template - - * one line at a time - * - * @throws UnsupportedOperationException if this method isn't - * implemented - */ - public String [] parse(String [] t); -} - diff --git a/apps/q/java/src/HTML/Tmpl/Parsers/Parser.java b/apps/q/java/src/HTML/Tmpl/Parsers/Parser.java deleted file mode 100644 index 78b9ceff9..000000000 --- a/apps/q/java/src/HTML/Tmpl/Parsers/Parser.java +++ /dev/null @@ -1,392 +0,0 @@ -/* -* HTML.Template: A module for using HTML Templates with java -* -* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) -* -* This module is free software; you can redistribute it -* and/or modify it under the terms of either: -* -* a) the GNU General Public License as published by the Free -* Software Foundation; either version 1, or (at your option) -* any later version, or -* -* b) the "Artistic License" which comes with this module. -* -* This program is distributed in the hope that it will be -* useful, but WITHOUT ANY WARRANTY; without even the implied -* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -* PURPOSE. See either the GNU General Public License or the -* Artistic License for more details. -* -* You should have received a copy of the Artistic License -* with this module, in the file ARTISTIC. If not, I'll be -* glad to provide one. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the Free -* Software Foundation, Inc., 59 Temple Place, Suite 330, -* Boston, MA 02111-1307 USA -*/ - - -package HTML.Tmpl.Parsers; -import java.util.NoSuchElementException; -import java.util.Properties; -import java.util.StringTokenizer; -import java.util.Vector; - -import HTML.Tmpl.Util; -import HTML.Tmpl.Element.Element; -import HTML.Tmpl.Element.If; -import HTML.Tmpl.Element.Loop; -import HTML.Tmpl.Element.Unless; - -public class Parser -{ - private boolean case_sensitive=false; - private boolean strict=true; - private boolean loop_context_vars=false; - private boolean global_vars=false; - - public Parser() - { - } - - public Parser(String [] args) - throws ArrayIndexOutOfBoundsException, - IllegalArgumentException - { - if(args.length%2 != 0) - throw new ArrayIndexOutOfBoundsException("odd number of arguments passed"); - - for(int i=0; i is not allowed inside a template tag - // so we can be sure that if this is a - // template tag, it ends with a > - - // add the closing > as well - if(i -1) - { - do { - temp.append(tag.charAt(0)); - tag=new StringBuffer( - tag.toString().substring(1)); - } while(tag.charAt(0) != '<'); - } - - Util.debug_print("tag: " + tag); - - String test_tag = tag.toString().toLowerCase(); - // if it doesn't contain tmpl_ it is not - // a template tag - if(test_tag.indexOf("tmpl_") < 0) { - temp.append(tag); - continue; - } - - // may be a template tag - // check if it starts with tmpl_ - - test_tag = cleanTag(test_tag); - - Util.debug_print("clean: " + test_tag); - - // check if it is a closing tag - if(test_tag.startsWith("/")) - test_tag = test_tag.substring(1); - - // if it still doesn't start with tmpl_ - // then it is not a template tag - if(!test_tag.startsWith("tmpl_")) { - temp.append(tag); - continue; - } - - // now it must be a template tag - String tag_type=getTagType(test_tag); - - if(tag_type == null) { - if(strict) - throw new - IllegalArgumentException( - tag.toString()); - else - temp.append(tag); - } - - Util.debug_print("type: " + tag_type); - - // if this was an invalid key and we've - // reached so far, then next iteration - if(tag_type == null) - continue; - - // now, push the previous stuff - // into the Vector - if(temp.length()>0) { - parts.addElement(temp.toString()); - temp = new StringBuffer(); - } - - // it is a valid template tag - // get its properties - - Util.debug_print("Checking: " + tag); - Properties tag_props = - getTagProps(tag.toString()); - - if(tag_props.containsKey("name")) - Util.debug_print("name: " + - tag_props.getProperty("name")); - else - Util.debug_print("no name"); - - parts.addElement(tag_props); - } - } - - if(temp.length()>0) - parts.addElement(temp.toString()); - - return parts; - } - - private String cleanTag(String tag) - throws IllegalArgumentException - { - String test_tag = new String(tag); - // first remove < and > - if(test_tag.startsWith("<")) - test_tag = test_tag.substring(1); - if(test_tag.endsWith(">")) - test_tag = test_tag.substring(0, test_tag.length()-1); - else - throw new IllegalArgumentException("Tags must start " + - "and end on the same line"); - - // remove any leading !-- and trailing - // -- in case of comment style tags - if(test_tag.startsWith("!--")) { - test_tag=test_tag.substring(3); - } - if(test_tag.endsWith("--")) { - test_tag=test_tag.substring(0, test_tag.length()-2); - } - // then leading and trailing spaces - test_tag = test_tag.trim(); - - return test_tag; - } - - private String getTagType(String tag) - { - int sp = tag.indexOf(" "); - String tag_type=""; - if(sp < 0) { - tag_type = tag.toLowerCase(); - } else { - tag_type = tag.substring(0, sp).toLowerCase(); - } - if(tag_type.startsWith("tmpl_")) - tag_type=tag_type.substring(5); - - Util.debug_print("tag_type: " + tag_type); - - if(tag_type.equals("var") || - tag_type.equals("if") || - tag_type.equals("unless") || - tag_type.equals("loop") || - tag_type.equals("include") || - tag_type.equals("else")) { - return tag_type; - } else { - return null; - } - } - - private Properties getTagProps(String tag) - throws IllegalArgumentException, - NullPointerException - { - Properties p = new Properties(); - - tag = cleanTag(tag); - - Util.debug_print("clean: " + tag); - - if(tag.startsWith("/")) { - p.put("close", "true"); - tag=tag.substring(1); - } else { - p.put("close", ""); - } - - Util.debug_print("close: " + p.getProperty("close")); - - p.put("type", getTagType(tag)); - - Util.debug_print("type: " + p.getProperty("type")); - - if(p.getProperty("type").equals("else") || - p.getProperty("close").equals("true")) - return p; - - if(p.getProperty("type").equals("var")) - p.put("escape", ""); - - int sp = tag.indexOf(" "); - // if we've got so far, this must succeed - - tag = tag.substring(sp).trim(); - Util.debug_print("checking params: " + tag); - - // now, we should have either name=value pairs - // or name space escape in case of old style vars - - if(tag.indexOf("=") < 0) { - // no = means old style - // first will be var name - // second if any will be escape - - sp = tag.toLowerCase().indexOf(" escape"); - if(sp < 0) { - // no escape - p.put("name", tag); - p.put("escape", "0"); - } else { - tag = tag.substring(0, sp); - p.put("name", tag); - p.put("escape", "html"); - } - } else { - // = means name=value pairs. - // use a StringTokenizer - StringTokenizer st = new StringTokenizer(tag, " ="); - while(st.hasMoreTokens()) { - String key, value; - key = st.nextToken().toLowerCase(); - if(st.hasMoreTokens()) - value = st.nextToken(); - else if(key.equals("escape")) - value = "html"; - else - throw new NullPointerException( - "parameter " + key + " has no value"); - - if(value.startsWith("\"") && - value.endsWith("\"")) - value = value.substring(1, - value.length()-1); - else if(value.startsWith("'") && - value.endsWith("'")) - value = value.substring(1, - value.length()-1); - - if(value.length()==0) - throw new NullPointerException( - "parameter " + key + " has no value"); - - if(key.equals("escape")) - value=value.toLowerCase(); - - p.put(key, value); - } - } - - String name = p.getProperty("name"); - // if not case sensitive, and not special variable, flatten case - // never flatten case for includes - if(!case_sensitive && !p.getProperty("type").equals("include") - && !( name.startsWith("__") && name.endsWith("__") )) - { - p.put("name", name.toLowerCase()); - } - - if(!Util.isNameChar(name)) - throw new IllegalArgumentException( - "parameter name may only contain " + - "letters, digits, ., /, +, -, _"); - // __var__ is allowed in the template, but not in the - // code. this is so that people can reference __FIRST__, - // etc - - return p; - } -} diff --git a/apps/q/java/src/HTML/Tmpl/Util.java b/apps/q/java/src/HTML/Tmpl/Util.java deleted file mode 100644 index 46ad2568b..000000000 --- a/apps/q/java/src/HTML/Tmpl/Util.java +++ /dev/null @@ -1,130 +0,0 @@ -/* -* HTML.Template: A module for using HTML Templates with java -* -* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) -* -* This module is free software; you can redistribute it -* and/or modify it under the terms of either: -* -* a) the GNU General Public License as published by the Free -* Software Foundation; either version 1, or (at your option) -* any later version, or -* -* b) the "Artistic License" which comes with this module. -* -* This program is distributed in the hope that it will be -* useful, but WITHOUT ANY WARRANTY; without even the implied -* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR -* PURPOSE. See either the GNU General Public License or the -* Artistic License for more details. -* -* You should have received a copy of the Artistic License -* with this module, in the file ARTISTIC. If not, I'll be -* glad to provide one. -* -* You should have received a copy of the GNU General Public -* License along with this program; if not, write to the Free -* Software Foundation, Inc., 59 Temple Place, Suite 330, -* Boston, MA 02111-1307 USA -*/ - - -package HTML.Tmpl; - -public class Util -{ - public static boolean debug=false; - - public static String escapeHTML(String element) - { - String s = new String(element); // don't change the original - String [] metas = {"&", "<", ">", "\""}; - String [] repls = {"&", "<", ">", """}; - for(int i = 0; i < metas.length; i++) { - int pos=0; - do { - pos = s.indexOf(metas[i], pos); - if(pos<0) - break; - - s = s.substring(0, pos) + repls[i] + s.substring(pos+1); - pos++; - } while(pos >= 0); - } - - return s; - } - - public static String escapeURL(String url) - { - StringBuffer s = new StringBuffer(); - String no_escape = "./-_"; - - for(int i=0; i= 0); - } - - return s; - } - - public static boolean isNameChar(char c) - { - return true; - } - - public static boolean isNameChar(String s) - { - String alt_valid = "./+-_"; - - for(int i=0; i"+dest.toBase64()); - - start(); - - } - - /** - * run this EchoServer - */ - public void run() - { - System.out.println("Server: listening on dest:"); - - /** - try { - System.out.println(key.toDestinationBase64()); - } catch (DataFormatException e) { - e.printStackTrace(); - } - */ - - System.out.println(dest.toBase64()); - - while (true) - { - try { - I2PSocket sessSocket = serverSocket.accept(); - - System.out.println("Server: Got connection from client"); - - InputStream socketIn = sessSocket.getInputStream(); - OutputStreamWriter socketOut = new OutputStreamWriter(sessSocket.getOutputStream()); - - System.out.println("Server: created streams"); - - // read a line from input, and echo it back - String line = DataHelper.readLine(socketIn); - - System.out.println("Server: got '" + line + "'"); - - String reply = "EchoServer: got '" + line + "'\n"; - socketOut.write(reply); - socketOut.flush(); - - System.out.println("Server: sent trply"); - - sessSocket.close(); - - System.out.println("Server: closed socket"); - - } catch (ConnectException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (I2PException e) { - e.printStackTrace(); - } - - } - - } - - public Destination getDest() throws DataFormatException - { - // return key.toDestination(); - return dest; - } - - public String getDestBase64() throws DataFormatException - { - // return key.toDestinationBase64(); - return dest.toBase64(); - } - - /** - * runs EchoServer from the command shell - */ - public static void main(String [] args) - { - System.out.println("Constructing an EchoServer"); - - try { - EchoServer myServer = new EchoServer(); - System.out.println("Got an EchoServer"); - System.out.println("Here's the dest:"); - System.out.println(myServer.getDestBase64()); - - myServer.run(); - - } catch (I2PException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - } -} - diff --git a/apps/q/java/src/net/i2p/aum/EchoTest.java b/apps/q/java/src/net/i2p/aum/EchoTest.java deleted file mode 100644 index 5ba4cf56f..000000000 --- a/apps/q/java/src/net/i2p/aum/EchoTest.java +++ /dev/null @@ -1,51 +0,0 @@ -// runs EchoServer and EchoClient as threads - -package net.i2p.aum; - -import java.io.IOException; - -import net.i2p.I2PException; -import net.i2p.data.Destination; - -/** - * A simple program which runs the EchoServer and EchoClient - * demos as threads - */ - -public class EchoTest -{ - /** - * create one instance each of EchoServer and EchoClient, - * run the server as a thread, run the client in foreground, - * display detailed results - */ - public static void main(String [] args) - { - EchoServer server; - EchoClient client; - - try { - server = new EchoServer(); - Destination serverDest = server.getDest(); - - System.out.println("EchoTest: serverDest=" + serverDest.toBase64()); - - client = new EchoClient(serverDest); - - } catch (I2PException e) { - e.printStackTrace(); return; - } catch (IOException e) { - e.printStackTrace(); return; - } - - System.out.println("Starting server..."); - //server.start(); - - System.out.println("Starting client..."); - client.run(); - - } - -} - - diff --git a/apps/q/java/src/net/i2p/aum/EmbargoedQueue.java b/apps/q/java/src/net/i2p/aum/EmbargoedQueue.java deleted file mode 100644 index 384224f7b..000000000 --- a/apps/q/java/src/net/i2p/aum/EmbargoedQueue.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * SimpleScheduler.java - * - * Created on March 24, 2005, 11:14 PM - */ - -package net.i2p.aum; - -import java.util.Date; -import java.util.Random; -import java.util.Vector; - -/** - *

Implements a queue of objects, where each object is 'embargoed' - * against release until a given time. Threads which attempt to .get - * items from this queue will block if the queue is empty, or if the - * first item of the queue has a 'release time' which has not yet passed.

- * - *

Think of it like a news desk which receives media releases which are - * 'embargoed' till a certain time. These releases sit in a queue, and when - * their embargo expires, they are actioned and go to print or broadcast. - * The reporters at this news desk are the 'threads', which get blocked - * until the next item's embargo expires.

- * - *

Purpose of implementing this is to provide a mechanism for scheduling - * background jobs to be executed at precise times

. - */ -public class EmbargoedQueue extends Thread { - - /** - * items which are waiting for dispatch - stored as 2-element vectors, - * where elem 0 is Integer dispatch time, and elem 1 is the object; - * note that this list is kept in strict ascending order of time. - * Whenever an object becomes ready, it is removed from this queue - * and appended to readyItems - */ - public Vector waitingItems; - - /** - * items which are ready for dispatch (their time has come). - */ - public SimpleQueue readyItems; - - /** set this true to enable verbose debug messages */ - public boolean debug = false; - - /** Creates a new embargoed queue */ - public EmbargoedQueue() { - waitingItems = new Vector(); - readyItems = new SimpleQueue(); - - // fire up scheduler thread - start(); - } - - /** - * fetches the item at head of queue, blocking if queue is empty - */ - public Object get() - { - return readyItems.get(); - } - - /** - * adds a new object to queue without any embargo (or, an embargo that expires - * immediately) - * @param item the object to be added - */ - public synchronized void putNow(Object item) - { - putAfter(0, item); - } - - /** - * adds a new object to queue, embargoed until given number of milliseconds - * have elapsed - * @param delay number of milliseconds from now when embargo expires - * @param item the object to be added - */ - public synchronized void putAfter(long delay, Object item) - { - long now = new Date().getTime(); - putAt(now+delay, item); - } - - /** - * adds a new object to the queue, embargoed until given time - * @param time the unixtime in milliseconds when the object's embargo expires, - * and the object is to be made available - * @param item the object to be added - */ - public synchronized void putAt(long time, Object item) - { - Vector elem = new Vector(); - elem.addElement(new Long(time)); - elem.addElement(item); - - long now = new Date().getTime(); - long future = time - now; - //System.out.println("putAt: time="+time+" ("+future+"ms from now), job="+item); - - // find where to insert - int i; - int nitems = waitingItems.size(); - for (i = 0; i < nitems; i++) - { - // get item i - Vector itemI = (Vector)waitingItems.get(i); - long timeI = ((Long)(itemI.get(0))).longValue(); - if (time < timeI) - { - // new item earlier than item i, insert here and bust out - waitingItems.insertElementAt(elem, i); - break; - } - } - - // did we insert? - if (i == nitems) - { - // no - gotta append - waitingItems.addElement(elem); - } - - // debugging - if (debug) { - printWaiting(); - } - - // awaken this scheduler object's thread, so it can - // see if any jobs are ready - //notify(); - interrupt(); - } - - /** - * for debugging - prints out a list of waiting items - */ - public synchronized void printWaiting() - { - int i; - long now = new Date().getTime(); - - System.out.println("EmbargoedQueue dump:"); - - System.out.println(" Waiting items:"); - int nwaiting = waitingItems.size(); - for (i = 0; i < nwaiting; i++) - { - Vector item = (Vector)waitingItems.get(i); - long when = ((Long)item.get(0)).longValue(); - Object job = item.get(1); - int delay = (int)(when - now)/1000; - System.out.println(" "+delay+"s, t="+when+", job="+job); - } - - System.out.println(" Ready items:"); - int nready = readyItems.items.size(); - for (i = 0; i < nready; i++) - { - //Vector item = (Vector)readyItems.items.get(i); - Object item = readyItems.items.get(i); - System.out.println(" job="+item); - } - - } - - /** - * scheduling thread, which wakes up every time a new job is queued, and - * if any jobs are ready, transfers them to the readyQueue and notifies - * any waiting client threads - */ - public void run() - { - // monitor the waiting queue, waiting till one becomes ready - while (true) - { - try { - if (waitingItems.size() > 0) - { - // at least 1 waiting item - Vector item = (Vector)(waitingItems.get(0)); - long now = new Date().getTime(); - long then = ((Long)item.get(0)).longValue(); - long delay = then - now; - - // ready? - if (delay <= 0) - { - // yep, ready, remove job and stick on waiting queue - waitingItems.remove(0); // ditch from waiting - Object elem = item.get(1); - readyItems.put(elem); // and add to ready - - if (debug) - { - System.out.println("embargo expired on "+elem); - printWaiting(); - } - } - else - { - // not ready, hang about till we get woken, or the - // job becomes ready - if (debug) - { - System.out.println("waiting for "+delay+"ms"); - } - Thread.sleep(delay); - } - } - else - { - // no items yet, hang out for an interrupt - if (debug) - { - System.out.println("queue is empty"); - } - synchronized (this) { - wait(); - } - } - } catch (Exception e) { - //System.out.println("exception"); - if (debug) - { - System.out.println("exception ("+e.getClass().getName()+") "+e.getMessage()); - } - } - } - } - - private static class TestThread extends Thread { - - String id; - - EmbargoedQueue q; - - public TestThread(String id, EmbargoedQueue q) { - this.id = id; - this.q = q; - } - - public void run() { - try { - print("waiting for queue"); - - Object item = q.get(); - - print("got item: '"+item+"'"); - - } catch (Exception e) { - e.printStackTrace(); - return; - } - } - - public void print(String msg) { - System.out.println("thread '"+id+"': "+msg); - } - - } - - /** - * @param args the command line arguments - */ - public static void main(String[] args) { - - int i; - int nthreads = 7; - - Thread [] threads = new Thread[nthreads]; - - EmbargoedQueue q = new EmbargoedQueue(); - SimpleSemaphore threadPool = new SimpleSemaphore(nthreads); - - // populate the queue with some stuff - q.putAfter(10000, "red"); - q.putAfter(3000, "orange"); - q.putAfter(6000, "yellow"); - - // populate threads array - for (i = 0; i < nthreads; i++) { - threads[i] = new TestThread("thread"+i, q); - } - - // and launch the threads - for (i = 0; i < nthreads; i++) { - threads[i].start(); - } - - // wait, presumably till all these elements are actioned - try { - Thread.sleep(12000); - } catch (Exception e) { - e.printStackTrace(); - return; - } - - // add some more shit to the queue, randomly scheduled - Random r = new Random(); - String [] items = {"green", "blue", "indigo", "violet", "black", "white", "brown"}; - for (i = 0; i < items.length; i++) { - String item = items[i]; - int delay = 2000 + r.nextInt(8000); - System.out.println("main: adding '"+item+"' after "+delay+"ms ..."); - q.putAfter(delay, item); - } - - // wait, presumably for all jobs to finish - try { - Thread.sleep(12000); - } catch (Exception e) { - e.printStackTrace(); - return; - } - - System.out.println("main: terminating"); - - } - -} diff --git a/apps/q/java/src/net/i2p/aum/I2PCat.java b/apps/q/java/src/net/i2p/aum/I2PCat.java deleted file mode 100644 index 35be1ad12..000000000 --- a/apps/q/java/src/net/i2p/aum/I2PCat.java +++ /dev/null @@ -1,460 +0,0 @@ - -// I2P equivalent of 'netcat' - -package net.i2p.aum; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.net.ConnectException; -import java.net.NoRouteToHostException; -import java.util.Properties; - -import net.i2p.I2PAppContext; -import net.i2p.I2PException; -import net.i2p.client.naming.HostsTxtNamingService; -import net.i2p.client.streaming.I2PServerSocket; -import net.i2p.client.streaming.I2PSocket; -import net.i2p.client.streaming.I2PSocketManager; -import net.i2p.client.streaming.I2PSocketManagerFactory; -import net.i2p.data.DataFormatException; -import net.i2p.data.DataHelper; -import net.i2p.data.Destination; -import net.i2p.util.Log; - -/** - * A I2P equivalent of the much-beloved 'netcat' utility. - * This command-line utility can either connect to a remote - * destination, or listen on a private destination for incoming - * connections. Once a connection is established, input on stdin - * is sent to the remote peer, and anything received from the - * remote peer is printed to stdout - */ - -public class I2PCat extends Thread -{ - public I2PSocketManager socketManager; - public I2PServerSocket serverSocket; - public I2PSocket sessSocket; - - public PrivDestination key; - public Destination dest; - - public InputStream socketIn; - public OutputStream socketOutStream; - public OutputStreamWriter socketOut; - - public SockInput rxThread; - - protected static Log _log; - - public static String defaultHost = "127.0.0.1"; - public static int defaultPort = 7654; - - /** - * a thread for reading from socket and displaying on stdout - */ - private class SockInput extends Thread { - - InputStream _in; - - protected Log _log; - public SockInput(InputStream i) { - - _in = i; - } - - public void run() - { - // the thread portion, receives incoming bytes on - // the socket input stream and spits them to stdout - - byte [] ch = new byte[1]; - - print("Receiver thread listening..."); - - try { - while (true) { - - //String line = DataHelper.readLine(socketIn); - if (_in.read(ch) != 1) { - print("failed to receive from socket"); - break; - } - - //System.out.println(line); - System.out.write(ch, 0, 1); - System.out.flush(); - } - } catch (IOException e) { - e.printStackTrace(); - print("Receiver thread crashed, terminating!!"); - System.exit(1); - } - - } - - - void print(String msg) - { - System.out.println("-=- I2PCat: "+msg); - - if (_log != null) { - _log.debug(msg); - } - - } - - - } - - - public I2PCat() - { - _log = new Log("I2PCat"); - - } - - /** - * Runs I2PCat in server mode, listening on the given destination - * for one incoming connection. Once connection is established, - * copyies data between the remote peer and - * the local terminal console. - */ - public void runServer(String keyStr) throws IOException, DataFormatException - { - Properties props = new Properties(); - props.setProperty("inbound.length", "0"); - props.setProperty("outbound.length", "0"); - props.setProperty("inbound.lengthVariance", "0"); - props.setProperty("outbound.lengthVariance", "0"); - - // generate new key if needed - if (keyStr.equals("new")) { - - try { - key = PrivDestination.newKey(); - } catch (I2PException e) { - e.printStackTrace(); - return; - } catch (IOException e) { - e.printStackTrace(); - return; - } - - print("Creating new server dest..."); - - socketManager = I2PSocketManagerFactory.createManager(key.getInputStream(), props); - - print("Getting server socket..."); - - serverSocket = socketManager.getServerSocket(); - - print("Server socket created, ready to run..."); - - dest = socketManager.getSession().getMyDestination(); - - print("private key follows:"); - System.out.println(key.toBase64()); - - print("dest follows:"); - System.out.println(dest.toBase64()); - - } - - else { - - key = PrivDestination.fromBase64String(keyStr); - - String dest64Abbrev = key.toBase64().substring(0, 16); - - print("Creating server socket manager on dest "+dest64Abbrev+"..."); - - socketManager = I2PSocketManagerFactory.createManager(key.getInputStream(), props); - - serverSocket = socketManager.getServerSocket(); - - print("Server socket created, ready to run..."); - } - - print("Awaiting client connection..."); - - I2PSocket sessSocket; - - try { - sessSocket = serverSocket.accept(); - } catch (I2PException e) { - e.printStackTrace(); - return; - } catch (ConnectException e) { - e.printStackTrace(); - return; - } - - print("Got connection from client"); - - chat(sessSocket); - - } - - public void runClient(String destStr) - throws DataFormatException, IOException - { - runClient(destStr, defaultHost, defaultPort); - } - - /** - * runs I2PCat in client mode, connecting to a remote - * destination then copying data between the remote peer and - * the local terminal console - */ - public void runClient(String destStr, String host, int port) - throws DataFormatException, IOException - { - // accept 'file:' prefix - if (destStr.startsWith("file:", 0)) - { - String path = destStr.substring(5); - destStr = new SimpleFile(path, "r").read(); - } - - else if (destStr.length() < 255) { - // attempt hosts file lookup - I2PAppContext ctx = new I2PAppContext(); - HostsTxtNamingService h = new HostsTxtNamingService(ctx); - Destination dest1 = h.lookup(destStr); - if (dest1 == null) { - usage("Cannot resolve hostname: '"+destStr+"'"); - } - - // successful lookup - runClient(dest1, host, port); - } - - else { - // otherwise, bigger strings are assumed to be base64 dests - - Destination dest = new Destination(); - dest.fromBase64(destStr); - runClient(dest, host, port); - } - } - - public void runClient(Destination dest) { - runClient(dest, "127.0.0.1", 7654); - } - - /** - * An alternative constructor which accepts an I2P Destination object - */ - public void runClient(Destination dest, String host, int port) - { - this.dest = dest; - - String destAbbrev = dest.toBase64().substring(0, 16)+"..."; - - print("Connecting via i2cp "+host+":"+port+" to destination "+destAbbrev+"..."); - System.out.flush(); - - try { - // get a socket manager - socketManager = I2PSocketManagerFactory.createManager(host, port); - - // get a client socket - print("socketManager="+socketManager); - - sessSocket = socketManager.connect(dest); - - } catch (I2PException e) { - e.printStackTrace(); - return; - } catch (ConnectException e) { - e.printStackTrace(); - return; - } catch (NoRouteToHostException e) { - e.printStackTrace(); - return; - } catch (InterruptedIOException e) { - e.printStackTrace(); - return; - } - - print("Successfully connected!"); - print("(Press Control-C to quit)"); - - // Perform console interaction - chat(sessSocket); - - try { - sessSocket.close(); - - } catch (IOException e) { - e.printStackTrace(); - return; - } - } - - /** - * Launch the background thread to copy incoming data to stdout, then - * loop in foreground copying lines from stdin and sending them to remote peer - */ - public void chat(I2PSocket sessSocket) { - - try { - socketIn = sessSocket.getInputStream(); - socketOutStream = sessSocket.getOutputStream(); - socketOut = new OutputStreamWriter(socketOutStream); - - // launch receiver thread - start(); - //launchRx(); - - while (true) { - - String line = DataHelper.readLine(System.in); - print("sent: '"+line+"'"); - - socketOut.write(line+"\n"); - socketOut.flush(); - } - } catch (IOException e) { - e.printStackTrace(); - return; - } - - } - - /** - * executes in a thread, receiving incoming bytes on - * the socket input stream and spitting them to stdout - */ - public void run() - { - - byte [] ch = new byte[1]; - - print("Receiver thread listening..."); - - try { - while (true) { - - //String line = DataHelper.readLine(socketIn); - if (socketIn.read(ch) != 1) { - print("failed to receive from socket"); - break; - } - - //System.out.println(line); - System.out.write(ch, 0, 1); - System.out.flush(); - } - } catch (IOException e) { - e.printStackTrace(); - print("Receiver thread crashed, terminating!!"); - System.exit(1); - } - - } - - - public void launchRx() { - - rxThread = new SockInput(socketIn); - rxThread.start(); - - } - - static void print(String msg) - { - System.out.println("-=- I2PCat: "+msg); - - if (_log != null) { - _log.debug(msg); - } - - } - - public static void usage(String msg) - { - usage(msg, 1); - } - - public static void usage(String msg, int ret) - { - System.out.println(msg); - usage(ret); - } - - public static void usage(int ret) - { - System.out.print( - "This utility is an I2P equivalent of the standard *nix 'netcat' utility\n"+ - "usage:\n"+ - " net.i2p.aum.I2PCat [-h]\n"+ - " - display this help\n"+ - " net.i2p.aum.I2PCat dest [host [port]]\n"+ - " - run in client mode, 'dest' should be one of:\n"+ - " hostname.i2p - an I2P hostname listed in hosts.txt\n"+ - " (only works with a hosts.txt in current directory)\n"+ - " base64dest - a full base64 destination string\n"+ - " file:b64filename - filename of a file containing base64 dest\n"+ - " net.i2p.aum.I2PCat -l privkey\n"+ - " - run in server mode, 'key' should be one of:\n"+ - " base64privkey - a full base64 private key string\n"+ - " file:b64filename - filename of a file containing base64 privkey\n"+ - "\n" - ); - System.exit(ret); - } - - public static void main(String [] args) throws IOException, DataFormatException - { - int argc = args.length; - - // barf if no args - if (argc == 0) { - usage("Missing argument"); - } - - // show help on request - if (args[0].equals("-h") || args[0].equals("--help")) { - usage(0); - } - - // server or client? - if (args[0].equals("-l")) { - if (argc != 2) { - usage("Bad argument count"); - } - - new I2PCat().runServer(args[1]); - } - else { - // client mode - barf if not 1-3 args - if (argc < 1 || argc > 3) { - usage("Bad argument count"); - } - - try { - int port = defaultPort; - String host = defaultHost; - if (args.length > 1) { - host = args[1]; - if (args.length > 2) { - port = new Integer(args[2]).intValue(); - } - } - new I2PCat().runClient(args[0], host, port); - - } catch (DataFormatException e) { - e.printStackTrace(); - } - } - } - -} - - - diff --git a/apps/q/java/src/net/i2p/aum/I2PSocketHelper.java b/apps/q/java/src/net/i2p/aum/I2PSocketHelper.java deleted file mode 100644 index bc3570c5b..000000000 --- a/apps/q/java/src/net/i2p/aum/I2PSocketHelper.java +++ /dev/null @@ -1,15 +0,0 @@ - -package net.i2p.aum; - - -/** - * Class which wraps an I2PSocket object with convenient methods. - * Nothing presently implemented here. - */ - -public class I2PSocketHelper -{ - -} - - diff --git a/apps/q/java/src/net/i2p/aum/I2PTunnelXMLObject.java b/apps/q/java/src/net/i2p/aum/I2PTunnelXMLObject.java deleted file mode 100644 index 7fa823734..000000000 --- a/apps/q/java/src/net/i2p/aum/I2PTunnelXMLObject.java +++ /dev/null @@ -1,138 +0,0 @@ -package net.i2p.aum; - -import java.util.Hashtable; - -import net.i2p.i2ptunnel.I2PTunnelXMLWrapper; - -/** - * Defines the I2P tunnel management methods which will be - * exposed to XML-RPC clients - * Methods in this class are forwarded to an I2PTunnelXMLWrapper object - */ -public class I2PTunnelXMLObject -{ - protected I2PTunnelXMLWrapper tunmgr; - - /** - * Builds the interface object. You normally shouldn't have to - * instantiate this directly - leave it to I2PTunnelXMLServer - */ - public I2PTunnelXMLObject() - { - tunmgr = new I2PTunnelXMLWrapper(); - } - - /** - * Generates an I2P keypair, returning a dict with keys 'result' (usually 'ok'), - * priv' (private key as base64) and 'dest' (destination as base64) - */ - public Hashtable genkeys() - { - return tunmgr.xmlrpcGenkeys(); - } - - /** - * Get a list of active TCP tunnels currently being managed by this - * tunnel manager. - * @return a dict with keys 'status' (usually 'ok'), - * 'jobs' (a list of dicts representing each job, each with keys 'job' (int, job - * number), 'type' (string, 'server' or 'client'), port' (int, the port number). - * Also for server, keys 'host' (hostname, string) and 'ip' (IP address, string). - * For clients, key 'dest' (string, remote destination as base64). - */ - public Hashtable list() - { - return tunmgr.xmlrpcList(); - } - - /** - * Attempts to find I2P hostname in hosts.txt. - * @param hostname string, I2P hostname - * @return dict with keys 'status' ('ok' or 'fail'), - * and if successful lookup, 'dest' (base64 destination). - */ - public Hashtable lookup(String hostname) - { - return tunmgr.xmlrpcLookup(hostname); - } - - /** - * Attempt to open client tunnel - * @param port local port to listen on, int - * @param dest remote dest to tunnel to, base64 string - * @return dict with keys 'status' (string - 'ok' or 'fail'). - * If 'ok', also key 'result' with text output from tunnelmgr - */ - public Hashtable client(int port, String dest) - { - return tunmgr.xmlrpcClient(port, dest); - } - - /** - * Attempts to open server tunnel - * @param host TCP hostname of TCP server to tunnel to - * @param port number of TCP server - * @param key - base64 private key to receive I2P connections on - * @return dict with keys 'status' (string, 'ok' or 'fail'). - * if 'fail', also a key 'error' with explanatory text. - */ - public Hashtable server(String host, int port, String key) - { - return tunmgr.xmlrpcServer(host, port, key); - } - - /** - * Close an existing tunnel - * @param jobnum (int) job number of connection to close - * @return dict with keys 'status' (string, 'ok' or 'fail') - */ - public Hashtable close(int jobnum) - { - return tunmgr.xmlrpcClose(jobnum); - } - - /** - * Close an existing tunnel - * @param jobnum (string) job number of connection to close as string, - * 'all' to close all jobs. - * @return dict with keys 'status' (string, 'ok' or 'fail') - */ - public Hashtable close(String job) - { - return tunmgr.xmlrpcClose(job); - } - - /** - * Close zero or more tunnels matching given criteria - * @param criteria A dict containing zero or more of the keys: - * 'job' (job number), 'type' (string, 'server' or 'client'), - * 'host' (hostname), 'port' (port number), - * 'ip' (IP address), 'dest' (string, remote dest) - */ - public Hashtable close(Hashtable criteria) - { - return tunmgr.xmlrpcClose(criteria); - } - - /** - * simple method to help with debugging your client prog - * @param x an int - * @return x + 1 - */ - public int bar(int x) - { - System.out.println("foo invoked"); - return x + 1; - } - - /** - * as for bar(int), but returns zero if no arg given - */ - public int bar() - { - return bar(0); - } - -} - - diff --git a/apps/q/java/src/net/i2p/aum/I2PTunnelXMLServer.java b/apps/q/java/src/net/i2p/aum/I2PTunnelXMLServer.java deleted file mode 100644 index 7c9ebbd0d..000000000 --- a/apps/q/java/src/net/i2p/aum/I2PTunnelXMLServer.java +++ /dev/null @@ -1,63 +0,0 @@ - -package net.i2p.aum; - -import org.apache.xmlrpc.WebServer; - -/** - * Provides a means for programs in any language to dynamically manage - * their own I2P <-> TCP tunnels, via simple TCP XML-RPC function calls. - * This server is presently hardwired to listen on port 22322. - */ - -public class I2PTunnelXMLServer -{ - protected WebServer ws; - protected I2PTunnelXMLObject tunobj; - - public int port = 22322; - - // constructor - - public void _init() - { - ws = new WebServer(port); - tunobj = new I2PTunnelXMLObject(); - ws.addHandler("i2p.tunnel", tunobj); - - } - - - // default constructor - public I2PTunnelXMLServer() - { - super(); - _init(); - } - - // constructor which takes shell args - public I2PTunnelXMLServer(String args[]) - { - super(); - _init(); - } - - // run the server - public void run() - { - ws.start(); - System.out.println("I2PTunnel XML-RPC server listening on port "+port); - ws.run(); - - } - - public static void main(String args[]) - { - I2PTunnelXMLServer tun; - - tun = new I2PTunnelXMLServer(); - tun.run(); - } - -} - - diff --git a/apps/q/java/src/net/i2p/aum/I2PXmlRpcClient.java b/apps/q/java/src/net/i2p/aum/I2PXmlRpcClient.java deleted file mode 100644 index fdf650897..000000000 --- a/apps/q/java/src/net/i2p/aum/I2PXmlRpcClient.java +++ /dev/null @@ -1,65 +0,0 @@ - -package net.i2p.aum; - -import java.net.MalformedURLException; -import java.net.URL; - -import net.i2p.data.Destination; -import net.i2p.util.Log; - -import org.apache.xmlrpc.XmlRpcClient; - - -/** - * an object which is used to invoke methods on remote I2P XML-RPC - * servers. You should not instantiate these objects directly, but - * create them through - * {@link net.i2p.aum.I2PXmlRpcClientFactory#newClient(Destination) I2PXmlRpcClientFactory.newClient()} - * Note that this is really just a thin wrapper around XmlRpcClient, mostly for reasons - * of consistency with I2PXmlRpcServer[Factory]. - */ - -public class I2PXmlRpcClient extends XmlRpcClient -{ - public static boolean debug = false; - - protected static Log _log; - - /** - * Construct an I2P XML-RPC client with this URL. - * Note that you should not - * use this constructor directly - use I2PXmlRpcClientFactory.newClient() instead - */ - public I2PXmlRpcClient(URL url) - { - super(url); - _log = new Log("I2PXmlRpcClient"); - - } - - /** - * Construct a XML-RPC client for the URL represented by this String. - * Note that you should not - * use this constructor directly - use I2PXmlRpcClientFactory.newClient() instead - */ - public I2PXmlRpcClient(String url) throws MalformedURLException - { - super(url); - _log = new Log("I2PXmlRpcClientFactory"); - - } - - /** - * Construct a XML-RPC client for the specified hostname and port. - * Note that you should not - * use this constructor directly - use I2PXmlRpcClientFactory.newClient() instead - */ - public I2PXmlRpcClient(String hostname, int port) throws MalformedURLException - { - super(hostname, port); - _log = new Log("I2PXmlRpcClient"); - - } - -} - diff --git a/apps/q/java/src/net/i2p/aum/I2PXmlRpcClientFactory.java b/apps/q/java/src/net/i2p/aum/I2PXmlRpcClientFactory.java deleted file mode 100644 index 16edc157e..000000000 --- a/apps/q/java/src/net/i2p/aum/I2PXmlRpcClientFactory.java +++ /dev/null @@ -1,226 +0,0 @@ - -package net.i2p.aum; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Properties; -import java.util.Vector; - -import net.i2p.data.DataFormatException; -import net.i2p.data.Destination; -import net.i2p.util.Log; - -import org.apache.xmlrpc.XmlRpcClient; - - -/** - * Creates I2P XML-RPC client objects, which you can use - * to issue XML-RPC function calls over I2P. - * Instantiating this class causes the vm-wide http proxy system - * properties to be set to the address of the I2P eepProxy host/port. - * I2PXmlRpcClient objects need to communicate with the I2P - * eepProxy. If your eepProxy is at the standard localhost:4444 address, - * you can use the default constructor. Otherwise, you can set this - * eepProxy address by either (1) passing eepProxy hostname/port to the - * constructor, or (2) running the jvm with 'eepproxy.tcp.host' and - * 'eepproxy.tcp.port' system properties set. Note that (1) takes precedence. - * Failure to set up EepProxy host/port correctly will result in an IOException - * when you invoke .execute() on your client objects. - * Invoke this class from your shell to see a demo - */ - -public class I2PXmlRpcClientFactory -{ - public static boolean debug = false; - - public static String _defaultEepHost = "127.0.0.1"; - public static int _defaultEepPort = 4444; - - protected static Log _log; - - /** - * Create an I2P XML-RPC client factory, and set it to create - * clients of a given class. - * @param clientClass a class to use when creating new clients - */ - public I2PXmlRpcClientFactory() - { - this(null, 0); - } - - /** - * Create an I2P XML-RPC client factory, and set it to create - * clients of a given class, and dispatch calls through a non-standard - * eepProxy. - * @param eepHost the eepProxy TCP hostname - * @param eepPort the eepProxy TCP port number - */ - public I2PXmlRpcClientFactory(String eepHost, int eepPort) - { - String eepPortStr; - - _log = new Log("I2PXmlRpcClientFactory"); - _log.shouldLog(Log.DEBUG); - - Properties p = System.getProperties(); - - // determine what actual eepproxy host/port we're using - if (eepHost == null) { - eepHost = p.getProperty("eepproxy.tcp.host", _defaultEepHost); - } - if (eepPort > 0) { - eepPortStr = String.valueOf(eepPort); - } - else { - eepPortStr = p.getProperty("eepproxy.tcp.port"); - if (eepPortStr == null) { - eepPortStr = String.valueOf(_defaultEepPort); - } - } - - p.put("proxySet", "true"); - p.put("http.proxyHost", eepHost); - p.put("http.proxyPort", eepPortStr); - } - - /** - * Create an I2P XML-RPC client object, which is subsequently used for - * dispatching XML-RPC requests. - * @param dest - an I2P destination object, comprising the - * destination of the remote - * I2P XML-RPC server. - * @return a new XmlRpcClient object (refer org.apache.xmlrpc.XmlRpcClient). - */ - public I2PXmlRpcClient newClient(Destination dest) throws MalformedURLException { - - return newClient(new URL("http", "i2p/"+dest.toBase64(), "/")); - } - - /** - * Create an I2P XML-RPC client object, which is subsequently used for - * dispatching XML-RPC requests. - * @param hostOrDest - an I2P hostname (listed in hosts.txt) or a - * destination base64 string, for the remote I2P XML-RPC server - * @return a new XmlRpcClient object (refer org.apache.xmlrpc.XmlRpcClient). - */ - public I2PXmlRpcClient newClient(String hostOrDest) - throws DataFormatException, MalformedURLException - { - String hostname; - URL u; - - try { - // try to make a dest out of the string - Destination dest = new Destination(); - dest.fromBase64(hostOrDest); - - // converted ok, treat as valid dest, form i2p/blahblah url from it - I2PXmlRpcClient client = newClient(new URL("http", "i2p/"+hostOrDest, "/")); - client.debug = debug; - return client; - - } catch (DataFormatException e) { - - if (debug) { - e.printStackTrace(); - print("hostOrDest length="+hostOrDest.length()); - } - - // failed to load up a dest, test length - if (hostOrDest.length() < 255) { - // short-ish, assume a hostname - u = new URL("http", hostOrDest, "/"); - I2PXmlRpcClient client = newClient(u); - client.debug = debug; - return client; - } - else { - // too long for a host, barf - throw new DataFormatException("Bad I2P hostname/dest:\n"+hostOrDest); - } - } - } - - /** - * Create an I2P XML-RPC client object, which is subsequently used for - * dispatching XML-RPC requests. This method is not recommended. - * @param u - a URL object, containing the URL of the remote - * I2P XML-RPC server, for example, "http://xmlrpc.aum.i2p" (assuming - * there's a hosts.txt entry for 'xmlrpc.aum.i2p'), or - * "http://i2p/base64destblahblah...". Note that if you use this method - * directly, the created XML-RPC client object will ONLY work if you - * instantiate the URL object as 'new URL("http", "i2p/"+host-or-dest, "/")'. - */ - protected I2PXmlRpcClient newClient(URL u) - { - Object [] args = { u }; - //return new I2PXmlRpcClient(u); - - // construct and return a client object of required class - return new I2PXmlRpcClient(u); - } - - /** - * Runs a demo of an I2P XML-RPC client. Assumes you have already - * launched an I2PXmlRpcServerFactory demo, because it gets its - * dest from the file 'demo.dest64' created by I2PXmlRpcServerFactory demo. - * - * Ensure you have first launched net.i2p.aum.I2PXmlRpcServerFactory - * from your command line. - */ - public static void main(String [] args) { - - String destStr; - - debug = true; - - try { - print("Creating client factory..."); - - I2PXmlRpcClientFactory f = new I2PXmlRpcClientFactory(); - - print("Creating new client..."); - - if (args.length == 0) { - print("Reading dest from demo.dest64"); - destStr = new SimpleFile("demo.dest64", "r").read(); - } - else { - destStr = args[0]; - } - - XmlRpcClient c = f.newClient(destStr); - - print("Invoking foo..."); - - Vector v = new Vector(); - v.add("one"); - v.add("two"); - - Object res = c.execute("foo.bar", v); - - print("Got back object: " + res); - - } catch (Exception e) { - e.printStackTrace(); - } - - } - /** - * Used for internal debugging - */ - protected static void print(String msg) - { - if (debug) { - System.out.println("I2PXmlRpcClient: " + msg); - - if (_log != null) { - System.out.println("LOGGING SOME SHIT"); - _log.debug(msg); - } - } - } -} - - - diff --git a/apps/q/java/src/net/i2p/aum/I2PXmlRpcDemoClass.java b/apps/q/java/src/net/i2p/aum/I2PXmlRpcDemoClass.java deleted file mode 100644 index a8de8e791..000000000 --- a/apps/q/java/src/net/i2p/aum/I2PXmlRpcDemoClass.java +++ /dev/null @@ -1,21 +0,0 @@ - -package net.i2p.aum; - - -/** - * A simple class providing callable xmlrpc server methods, gets linked in to - * the server demo. - */ -public class I2PXmlRpcDemoClass -{ - public int add1(int n) { - return n + 1; - } - - public String bar(String arg1, String arg2) { - System.out.println("Demo: got hit to bar: arg1='"+arg1+"', arg2='"+arg2+"'"); - return "I2P demo xmlrpc server(foo.bar): arg1='"+arg1+"', arg2='"+arg2+"'"; - } - -} - diff --git a/apps/q/java/src/net/i2p/aum/I2PXmlRpcServer.java b/apps/q/java/src/net/i2p/aum/I2PXmlRpcServer.java deleted file mode 100644 index 82854b00f..000000000 --- a/apps/q/java/src/net/i2p/aum/I2PXmlRpcServer.java +++ /dev/null @@ -1,433 +0,0 @@ -package net.i2p.aum; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.util.Date; -import java.util.Properties; - -import net.i2p.I2PAppContext; -import net.i2p.I2PException; -import net.i2p.client.streaming.I2PServerSocket; -import net.i2p.client.streaming.I2PSocket; -import net.i2p.client.streaming.I2PSocketManager; -import net.i2p.client.streaming.I2PSocketManagerFactory; -import net.i2p.data.DataFormatException; -import net.i2p.data.Destination; - -import org.apache.xmlrpc.XmlRpcServer; - - -/** - * An XML-RPC server which works completely within I2P, listening - * on a dest for requests. - * You should not instantiate this class directly, but instead create - * an I2PXmlRpcServerFactory object, and use its .newServer() method - * to create a server object. - */ -public class I2PXmlRpcServer extends XmlRpcServer implements Runnable -{ - public class I2PXmlRpcServerWorkerThread extends Thread { - - I2PSocket _sock; - - public I2PXmlRpcServerWorkerThread(I2PSocket sock) { - _sock = sock; - } - - public void run() { - - try { - System.out.println("I2PXmlRpcServer.run: got inbound XML-RPC I2P conn"); - - log.info("run: Got client connection, creating streams"); - - InputStream socketIn = _sock.getInputStream(); - OutputStreamWriter socketOut = new OutputStreamWriter(_sock.getOutputStream()); - - log.info("run: reading http headers"); - - // read headers, determine size of req - int size = readHttpHeaders(socketIn); - - if (size <= 0) { - // bad news - log.info("read req failed, terminating session"); - _sock.close(); - return; - } - - log.info("run: reading request body of "+size+" bytes"); - - // get raw request body - byte [] reqBody = new byte[size]; - for (int i=0; i mimetypes - */ - -public class Mimetypes -{ - public static String [][] _map = { - - { ".bz2", "application/x-bzip2" }, - { ".csm", "application/cu-seeme" }, - { ".cu", "application/cu-seeme" }, - { ".tsp", "application/dsptype" }, - { ".xls", "application/excel" }, - { ".spl", "application/futuresplash" }, - { ".hqx", "application/mac-binhex40" }, - { ".doc", "application/msword" }, - { ".dot", "application/msword" }, - { ".bin", "application/octet-stream" }, - { ".oda", "application/oda" }, - { ".pdf", "application/pdf" }, - { ".asc", "application/pgp-keys" }, - { ".pgp", "application/pgp-signature" }, - { ".ps", "application/postscript" }, - { ".ai", "application/postscript" }, - { ".eps", "application/postscript" }, - { ".ppt", "application/powerpoint" }, - { ".rtf", "application/rtf" }, - { ".wp5", "application/wordperfect5.1" }, - { ".zip", "application/zip" }, - { ".wk", "application/x-123" }, - { ".bcpio", "application/x-bcpio" }, - { ".pgn", "application/x-chess-pgn" }, - { ".cpio", "application/x-cpio" }, - { ".deb", "application/x-debian-package" }, - { ".dcr", "application/x-director" }, - { ".dir", "application/x-director" }, - { ".dxr", "application/x-director" }, - { ".dvi", "application/x-dvi" }, - { ".pfa", "application/x-font" }, - { ".pfb", "application/x-font" }, - { ".gsf", "application/x-font" }, - { ".pcf", "application/x-font" }, - { ".pcf.Z", "application/x-font" }, - { ".gtar", "application/x-gtar" }, - { ".tgz", "application/x-gtar" }, - { ".hdf", "application/x-hdf" }, - { ".phtml", "application/x-httpd-php" }, - { ".pht", "application/x-httpd-php" }, - { ".php", "application/x-httpd-php" }, - { ".php3", "application/x-httpd-php3" }, - { ".phps", "application/x-httpd-php3-source" }, - { ".php3p", "application/x-httpd-php3-preprocessed" }, - { ".class", "application/x-java" }, - { ".latex", "application/x-latex" }, - { ".frm", "application/x-maker" }, - { ".maker", "application/x-maker" }, - { ".frame", "application/x-maker" }, - { ".fm", "application/x-maker" }, - { ".fb", "application/x-maker" }, - { ".book", "application/x-maker" }, - { ".fbdoc", "application/x-maker" }, - { ".mif", "application/x-mif" }, - { ".nc", "application/x-netcdf" }, - { ".cdf", "application/x-netcdf" }, - { ".pac", "application/x-ns-proxy-autoconfig" }, - { ".o", "application/x-object" }, - { ".pl", "application/x-perl" }, - { ".pm", "application/x-perl" }, - { ".shar", "application/x-shar" }, - { ".swf", "application/x-shockwave-flash" }, - { ".swfl", "application/x-shockwave-flash" }, - { ".sit", "application/x-stuffit" }, - { ".sv4cpio", "application/x-sv4cpio" }, - { ".sv4crc", "application/x-sv4crc" }, - { ".tar", "application/x-tar" }, - { ".gf", "application/x-tex-gf" }, - { ".pk", "application/x-tex-pk" }, - { ".PK", "application/x-tex-pk" }, - { ".texinfo", "application/x-texinfo" }, - { ".texi", "application/x-texinfo" }, - { ".~", "application/x-trash" }, - { ".%", "application/x-trash" }, - { ".bak", "application/x-trash" }, - { ".old", "application/x-trash" }, - { ".sik", "application/x-trash" }, - { ".t", "application/x-troff" }, - { ".tr", "application/x-troff" }, - { ".roff", "application/x-troff" }, - { ".man", "application/x-troff-man" }, - { ".me", "application/x-troff-me" }, - { ".ms", "application/x-troff-ms" }, - { ".ustar", "application/x-ustar" }, - { ".src", "application/x-wais-source" }, - { ".wz", "application/x-wingz" }, - { ".au", "audio/basic" }, - { ".snd", "audio/basic" }, - { ".mid", "audio/midi" }, - { ".midi", "audio/midi" }, - { ".mpga", "audio/mpeg" }, - { ".mpega", "audio/mpeg" }, - { ".mp2", "audio/mpeg" }, - { ".mp3", "audio/mpeg" }, - { ".m3u", "audio/mpegurl" }, - { ".aif", "audio/x-aiff" }, - { ".aiff", "audio/x-aiff" }, - { ".aifc", "audio/x-aiff" }, - { ".gsm", "audio/x-gsm" }, - { ".ra", "audio/x-pn-realaudio" }, - { ".rm", "audio/x-pn-realaudio" }, - { ".ram", "audio/x-pn-realaudio" }, - { ".rpm", "audio/x-pn-realaudio-plugin" }, - { ".wav", "audio/x-wav" }, - { ".gif", "image/gif" }, - { ".ief", "image/ief" }, - { ".jpeg", "image/jpeg" }, - { ".jpg", "image/jpeg" }, - { ".jpe", "image/jpeg" }, - { ".png", "image/png" }, - { ".tiff", "image/tiff" }, - { ".tif", "image/tiff" }, - { ".ras", "image/x-cmu-raster" }, - { ".bmp", "image/x-ms-bmp" }, - { ".pnm", "image/x-portable-anymap" }, - { ".pbm", "image/x-portable-bitmap" }, - { ".pgm", "image/x-portable-graymap" }, - { ".ppm", "image/x-portable-pixmap" }, - { ".rgb", "image/x-rgb" }, - { ".xbm", "image/x-xbitmap" }, - { ".xpm", "image/x-xpixmap" }, - { ".xwd", "image/x-xwindowdump" }, - { ".csv", "text/comma-separated-values" }, - { ".html", "text/html" }, - { ".htm", "text/html" }, - { ".mml", "text/mathml" }, - { ".txt", "text/plain" }, - { ".rtx", "text/richtext" }, - { ".tsv", "text/tab-separated-values" }, - { ".h++", "text/x-c++hdr" }, - { ".hpp", "text/x-c++hdr" }, - { ".hxx", "text/x-c++hdr" }, - { ".hh", "text/x-c++hdr" }, - { ".c++", "text/x-c++src" }, - { ".cpp", "text/x-c++src" }, - { ".cxx", "text/x-c++src" }, - { ".cc", "text/x-c++src" }, - { ".h", "text/x-chdr" }, - { ".csh", "text/x-csh" }, - { ".c", "text/x-csrc" }, - { ".java", "text/x-java" }, - { ".moc", "text/x-moc" }, - { ".p", "text/x-pascal" }, - { ".pas", "text/x-pascal" }, - { ".etx", "text/x-setext" }, - { ".sh", "text/x-sh" }, - { ".tcl", "text/x-tcl" }, - { ".tk", "text/x-tcl" }, - { ".tex", "text/x-tex" }, - { ".ltx", "text/x-tex" }, - { ".sty", "text/x-tex" }, - { ".cls", "text/x-tex" }, - { ".vcs", "text/x-vCalendar" }, - { ".vcf", "text/x-vCard" }, - { ".dl", "video/dl" }, - { ".fli", "video/fli" }, - { ".gl", "video/gl" }, - { ".mpeg", "video/mpeg" }, - { ".mpg", "video/mpeg" }, - { ".mpe", "video/mpeg" }, - { ".qt", "video/quicktime" }, - { ".mov", "video/quicktime" }, - { ".asf", "video/x-ms-asf" }, - { ".asx", "video/x-ms-asf" }, - { ".avi", "video/x-msvideo" }, - { ".movie", "video/x-sgi-movie" }, - { ".vrm", "x-world/x-vrml" }, - { ".vrml", "x-world/x-vrml" }, - { ".wrl", "x-world/x-vrml" }, - - }; - - /** - * Attempts to determine a mimetype - * @param path - either a file extension string (containing the - * leading '.') or a full file pathname (in which case, the extension - * will be extracted). - * @return the mimetype that corresponds to the file extension, if the - * file extension is known, or "application/octet-stream" if the - * file extension is not known. - */ - public static String guessType(String path) { - // rip the file extension from the path - // first - split 'directories', and get last part - String [] dirs = path.split("/"); - String filename = dirs[dirs.length-1]; - String [] bits = filename.split("\\."); - String extension = "." + bits[bits.length-1]; - - // default mimetype applied to unknown file extensions - String type = "application/octet-stream"; - - for (int i=0; i<_map.length; i++) { - String [] rec = _map[i]; - if (rec[0].equals(extension)) { - type = rec[1]; - break; - } - } - return type; - } - - /** - * Attempts to guess the file extension corresponding to a given - * mimetype. - * @param type a mimetype string - * @return a file extension commonly used for storing files of this type, - * or defaults to ".bin" if mimetype not known - */ - public static String guessExtension(String type) { - // default extension applied to unknown mimetype - String extension = ".bin"; - for (int i=0; i<_map.length; i++) { - String [] rec = _map[i]; - if (rec[1].equals(type)) { - extension = rec[0]; - break; - } - } - return extension; - } - -} - -/** - -suffix_map = { - '.tgz': '.tar.gz', - '.taz': '.tar.gz', - '.tz': '.tar.gz', - } - -encodings_map = { - '.gz': 'gzip', - '.Z': 'compress', - } - -# Before adding new types, make sure they are either registered with IANA, at -# http://www.isi.edu/in-notes/iana/assignments/media-types -# or extensions, i.e. using the x- prefix - -# If you add to these, please keep them sorted! -types_map = { - '.a' : 'application/octet-stream', - '.ai' : 'application/postscript', - '.aif' : 'audio/x-aiff', - '.aifc' : 'audio/x-aiff', - '.aiff' : 'audio/x-aiff', - '.au' : 'audio/basic', - '.avi' : 'video/x-msvideo', - '.bat' : 'text/plain', - '.bcpio' : 'application/x-bcpio', - '.bin' : 'application/octet-stream', - '.bmp' : 'image/x-ms-bmp', - '.c' : 'text/plain', - # Duplicates :( - '.cdf' : 'application/x-cdf', - '.cdf' : 'application/x-netcdf', - '.cpio' : 'application/x-cpio', - '.csh' : 'application/x-csh', - '.css' : 'text/css', - '.dll' : 'application/octet-stream', - '.doc' : 'application/msword', - '.dot' : 'application/msword', - '.dvi' : 'application/x-dvi', - '.eml' : 'message/rfc822', - '.eps' : 'application/postscript', - '.etx' : 'text/x-setext', - '.exe' : 'application/octet-stream', - '.gif' : 'image/gif', - '.gtar' : 'application/x-gtar', - '.h' : 'text/plain', - '.hdf' : 'application/x-hdf', - '.htm' : 'text/html', - '.html' : 'text/html', - '.ief' : 'image/ief', - '.jpe' : 'image/jpeg', - '.jpeg' : 'image/jpeg', - '.jpg' : 'image/jpeg', - '.js' : 'application/x-javascript', - '.ksh' : 'text/plain', - '.latex' : 'application/x-latex', - '.m1v' : 'video/mpeg', - '.man' : 'application/x-troff-man', - '.me' : 'application/x-troff-me', - '.mht' : 'message/rfc822', - '.mhtml' : 'message/rfc822', - '.mif' : 'application/x-mif', - '.mov' : 'video/quicktime', - '.movie' : 'video/x-sgi-movie', - '.mp2' : 'audio/mpeg', - '.mp3' : 'audio/mpeg', - '.mpa' : 'video/mpeg', - '.mpe' : 'video/mpeg', - '.mpeg' : 'video/mpeg', - '.mpg' : 'video/mpeg', - '.ms' : 'application/x-troff-ms', - '.nc' : 'application/x-netcdf', - '.nws' : 'message/rfc822', - '.o' : 'application/octet-stream', - '.obj' : 'application/octet-stream', - '.oda' : 'application/oda', - '.p12' : 'application/x-pkcs12', - '.p7c' : 'application/pkcs7-mime', - '.pbm' : 'image/x-portable-bitmap', - '.pdf' : 'application/pdf', - '.pfx' : 'application/x-pkcs12', - '.pgm' : 'image/x-portable-graymap', - '.pl' : 'text/plain', - '.png' : 'image/png', - '.pnm' : 'image/x-portable-anymap', - '.pot' : 'application/vnd.ms-powerpoint', - '.ppa' : 'application/vnd.ms-powerpoint', - '.ppm' : 'image/x-portable-pixmap', - '.pps' : 'application/vnd.ms-powerpoint', - '.ppt' : 'application/vnd.ms-powerpoint', - '.ps' : 'application/postscript', - '.pwz' : 'application/vnd.ms-powerpoint', - '.py' : 'text/x-python', - '.pyc' : 'application/x-python-code', - '.pyo' : 'application/x-python-code', - '.qt' : 'video/quicktime', - '.ra' : 'audio/x-pn-realaudio', - '.ram' : 'application/x-pn-realaudio', - '.ras' : 'image/x-cmu-raster', - '.rdf' : 'application/xml', - '.rgb' : 'image/x-rgb', - '.roff' : 'application/x-troff', - '.rtx' : 'text/richtext', - '.sgm' : 'text/x-sgml', - '.sgml' : 'text/x-sgml', - '.sh' : 'application/x-sh', - '.shar' : 'application/x-shar', - '.snd' : 'audio/basic', - '.so' : 'application/octet-stream', - '.src' : 'application/x-wais-source', - '.sv4cpio': 'application/x-sv4cpio', - '.sv4crc' : 'application/x-sv4crc', - '.swf' : 'application/x-shockwave-flash', - '.t' : 'application/x-troff', - '.tar' : 'application/x-tar', - '.tcl' : 'application/x-tcl', - '.tex' : 'application/x-tex', - '.texi' : 'application/x-texinfo', - '.texinfo': 'application/x-texinfo', - '.tif' : 'image/tiff', - '.tiff' : 'image/tiff', - '.tr' : 'application/x-troff', - '.tsv' : 'text/tab-separated-values', - '.txt' : 'text/plain', - '.ustar' : 'application/x-ustar', - '.vcf' : 'text/x-vcard', - '.wav' : 'audio/x-wav', - '.wiz' : 'application/msword', - '.xbm' : 'image/x-xbitmap', - '.xlb' : 'application/vnd.ms-excel', - # Duplicates :( - '.xls' : 'application/excel', - '.xls' : 'application/vnd.ms-excel', - '.xml' : 'text/xml', - '.xpm' : 'image/x-xpixmap', - '.xsl' : 'application/xml', - '.xwd' : 'image/x-xwindowdump', - '.zip' : 'application/zip', - } - -# These are non-standard types, commonly found in the wild. They will only -# match if strict=0 flag is given to the API methods. - -# Please sort these too -common_types = { - '.jpg' : 'image/jpg', - '.mid' : 'audio/midi', - '.midi': 'audio/midi', - '.pct' : 'image/pict', - '.pic' : 'image/pict', - '.pict': 'image/pict', - '.rtf' : 'application/rtf', - '.xul' : 'text/xul' - } -**/ - diff --git a/apps/q/java/src/net/i2p/aum/OOTest.java b/apps/q/java/src/net/i2p/aum/OOTest.java deleted file mode 100644 index f7a6fffed..000000000 --- a/apps/q/java/src/net/i2p/aum/OOTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.i2p.aum; - - -public class OOTest -{ - public int add(int a, int b) - { - return (a + b); - } - - public static void main(String[] args) - { - OOTest mytest = new OOTest(); - System.out.println(mytest.add(3,3)); - } -} - - diff --git a/apps/q/java/src/net/i2p/aum/PrivDestination.java b/apps/q/java/src/net/i2p/aum/PrivDestination.java deleted file mode 100644 index 2d09bb0af..000000000 --- a/apps/q/java/src/net/i2p/aum/PrivDestination.java +++ /dev/null @@ -1,236 +0,0 @@ - -package net.i2p.aum; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; - -import net.i2p.I2PException; -import net.i2p.client.I2PClient; -import net.i2p.client.I2PClientFactory; -import net.i2p.data.Base64; -import net.i2p.data.DataFormatException; -import net.i2p.data.DataStructureImpl; -import net.i2p.data.Destination; -import net.i2p.data.PrivateKey; -import net.i2p.data.PublicKey; -import net.i2p.data.SigningPrivateKey; -import net.i2p.data.SigningPublicKey; -import net.i2p.util.Log; - -/** - * A convenience class for encapsulating and manipulating I2P private keys - */ - -public class PrivDestination - //extends ByteArrayInputStream - extends DataStructureImpl -{ - protected byte [] _bytes; - - protected Destination _dest; - protected PrivateKey _privKey; - protected SigningPrivateKey _signingPrivKey; - - protected static Log _log; - - /** - * Create a PrivDestination object. - * In most cases, you'll probably want to skip this constructor, - * and create PrivDestination objects by invoking the desired static methods - * of this class. - * @param raw an array of bytes containing the raw binary private key - */ - public PrivDestination(byte [] raw) throws DataFormatException, IOException - { - //super(raw); - _log = new Log("PrivDestination"); - - _bytes = raw; - readBytes(getInputStream()); - } - - /** - * reconstitutes a PrivDestination from previously exported Base64 - */ - public PrivDestination(String b64) throws DataFormatException, IOException { - this(Base64.decode(b64)); - } - - /** - * generates a new PrivDestination with random keys - */ - public PrivDestination() throws I2PException, IOException - { - I2PClient client = I2PClientFactory.createClient(); - - ByteArrayOutputStream streamOut = new ByteArrayOutputStream(); - - // create a dest - client.createDestination(streamOut); - - _bytes = streamOut.toByteArray(); - readBytes(getInputStream()); - - // construct from the stream - //return new PrivDestination(streamOut.toByteArray()); - } - - /** return the public Destination object for this private dest */ - public Destination getDestination() { - return _dest; - } - - /** return a PublicKey (encryption public key) object for this priv dest */ - public PublicKey getPublicKey() { - return getDestination().getPublicKey(); - } - - /** return a PrivateKey (encryption private key) object for this priv dest */ - public PrivateKey getPrivateKey() { - return _privKey; - } - - /** return a SigningPublicKey object for this priv dest */ - public SigningPublicKey getSigningPublicKey() { - return getDestination().getSigningPublicKey(); - } - - /** return a SigningPrivateKey object for this priv dest */ - public SigningPrivateKey getSigningPrivateKey() { - return _signingPrivKey; - } - - // static methods returning an instance - - /** - * Creates a PrivDestination object - * @param base64 a string containing the base64 private key data - * @return a PrivDestination object encapsulating that key - */ - public static PrivDestination fromBase64String(String base64) - throws DataFormatException, IOException - { - return new PrivDestination(Base64.decode(base64)); - } - - /** - * Creates a PrivDestination object, from the base64 key data - * stored in a file. - * @param path the pathname of the file from which to read the base64 private key data - * @return a PrivDestination object encapsulating that key - */ - public static PrivDestination fromBase64File(String path) - throws FileNotFoundException, IOException, DataFormatException - { - return fromBase64String(new SimpleFile(path, "r").read()); - /* - File f = new File(path); - char [] rawchars = new char[(int)(f.length())]; - byte [] rawbytes = new byte[(int)(f.length())]; - FileReader fr = new FileReader(f); - fr.read(rawchars); - String raw64 = new String(rawchars); - return PrivDestination.fromBase64String(raw64); - */ - } - - /** - * Creates a PrivDestination object, from the binary key data - * stored in a file. - * @param path the pathname of the file from which to read the binary private key data - * @return a PrivDestination object encapsulating that key - */ - public static PrivDestination fromBinFile(String path) - throws FileNotFoundException, IOException, DataFormatException - { - byte [] raw = new SimpleFile(path, "r").readBytes(); - return new PrivDestination(raw); - } - - /** - * Generate a new random I2P private key - * @return a PrivDestination object encapsulating that key - */ - public static PrivDestination newKey() throws I2PException, IOException - { - return new PrivDestination(); - } - - public ByteArrayInputStream getInputStream() - { - return new ByteArrayInputStream(_bytes); - } - - /** - * Exports the key's full contents to a string - * @return A base64-format string containing the full contents - * of this private key. The string can be used in any subsequent - * call to the .fromBase64String static constructor method. - */ -/* - public String toBase64() - { - return Base64.encode(_bytes); - } -*/ - - /** - * Exports the key's full contents to a byte array - * @return A byte array containing the full contents - * of this private key. - */ -/* - public byte [] toBytes() - { - return _bytes; - } -*/ - - /** - * Converts this key to a public destination. - * @return a standard I2P Destination object containing the - * public portion of this private key. - */ - /* - public Destination toDestination() throws DataFormatException - { - Destination dest = new Destination(); - dest.readBytes(_bytes, 0); - return dest; - } - */ - - /** - * Converts this key to a base64 string representing a public destination - * @return a string containing a base64 representation of the destination - * corresponding to this private key. - */ - public String getDestinationBase64() throws DataFormatException - { - return getDestination().toBase64(); - } - - public void readBytes(java.io.InputStream strm) - throws net.i2p.data.DataFormatException, java.io.IOException - { - _dest = new Destination(); - _privKey = new PrivateKey(); - _signingPrivKey = new SigningPrivateKey(); - - _dest.readBytes(strm); - _privKey.readBytes(strm); - _signingPrivKey.readBytes(strm); - } - - public void writeBytes(java.io.OutputStream outputStream) - throws net.i2p.data.DataFormatException, java.io.IOException - { - _dest.writeBytes(outputStream); - _privKey.writeBytes(outputStream); - _signingPrivKey.writeBytes(outputStream); - } - -} - diff --git a/apps/q/java/src/net/i2p/aum/PropertiesFile.java b/apps/q/java/src/net/i2p/aum/PropertiesFile.java deleted file mode 100644 index 2d1891549..000000000 --- a/apps/q/java/src/net/i2p/aum/PropertiesFile.java +++ /dev/null @@ -1,217 +0,0 @@ -/* - * PropertiesFile.java - * - * Created on 20 March 2005, 19:30 - */ - -package net.i2p.aum; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.NoSuchElementException; -import java.util.Properties; - -/** - * builds on Properties with methods to load/save directly to/from file - */ -public class PropertiesFile extends Properties { - - public String _path; - public File _file; - public boolean _fileExists; - - /** - * Creates a new instance of PropertiesFile - * @param path Absolute pathname of file where properties are to be stored - */ - public PropertiesFile(String path) throws IOException { - super(); - _path = path; - _file = new File(path); - _fileExists = _file.isFile(); - - if (_file.canRead()) { - loadFromFile(); - } - } - - /** - * Creates new PropertiesFile, updating its content with the - * keys/values in given hashtable - * @param path absolute pathname where properties file is located in filesystem - * @param h instance of Hashtable (or subclass). its content - * will be written to this object (note that string representations of keys/vals - * will be used) - */ - public PropertiesFile(String path, Hashtable h) throws IOException - { - this(path); - Enumeration keys = h.keys(); - Object key; - while (true) - { - try { - key = keys.nextElement(); - } catch (NoSuchElementException e) { - break; - } - setProperty(key.toString(), h.get(key).toString()); - } - } - - /** - * Loads this object from the file - */ - public void loadFromFile() throws IOException, FileNotFoundException { - if (_file.canRead()) { - InputStream fis = new FileInputStream(_file); - load(fis); - } - } - - /** - * Saves this object to the file - */ - public void saveToFile() throws IOException, FileNotFoundException { - - if (!_fileExists) { - _file.createNewFile(); - _fileExists = true; - } - OutputStream fos = new FileOutputStream(_file); - store(fos, null); - } - - /** - * Stores attribute - */ - public Object setProperty(String key, String value) { - Object o = super.setProperty(key, value); - try { - saveToFile(); - } catch (Exception e) { - e.printStackTrace(); - } - return o; - } - - /** - * return a property as an int, fall back on default if not found or invalid - */ - public int getIntProperty(String key, int dflt) { - try { - return new Integer((String)getProperty(key)).intValue(); - } catch (Exception e) { - setIntProperty(key, dflt); - return dflt; - } - } - - /** - * return a property as an int - */ - public int getIntProperty(String key) { - return new Integer((String)getProperty(key)).intValue(); - } - - /** - * set a property as an int - */ - public void setIntProperty(String key, int value) { - setProperty(key, String.valueOf(value)); - } - - /** - * return a property as a long, fall back on default if not found or invalid - */ - public long getIntProperty(String key, long dflt) { - try { - return new Long((String)getProperty(key)).longValue(); - } catch (Exception e) { - setLongProperty(key, dflt); - return dflt; - } - } - - /** - * return a property as an int - */ - public long getLongProperty(String key) { - return new Long((String)getProperty(key)).longValue(); - } - - /** - * set a property as an int - */ - public void setLongProperty(String key, long value) { - setProperty(key, String.valueOf(value)); - } - - /** - * return a property as a float - */ - public double getFloatProperty(String key) { - return new Float((String)getProperty(key)).floatValue(); - } - - /** - * return a property as a float, fall back on default if not found or invalid - */ - public double getFloatProperty(String key, float dflt) { - try { - return new Float((String)getProperty(key)).floatValue(); - } catch (Exception e) { - setFloatProperty(key, dflt); - return dflt; - } - } - - /** - * set a property as a float - */ - public void setFloatProperty(String key, float value) { - setProperty(key, String.valueOf(value)); - } - - /** - * return a property as a double - */ - public double getDoubleProperty(String key) { - return new Double((String)getProperty(key)).doubleValue(); - } - - /** - * return a property as a double, fall back on default if not found - */ - public double getDoubleProperty(String key, double dflt) { - try { - return new Double((String)getProperty(key)).doubleValue(); - } catch (Exception e) { - setDoubleProperty(key, dflt); - return dflt; - } - } - - /** - * set a property as a double - */ - public void setDoubleProperty(String key, double value) { - setProperty(key, String.valueOf(value)); - } - - /** - * increment an integer property value - */ - public void incrementIntProperty(String key) { - setIntProperty(key, getIntProperty(key)+1); - } - -} - diff --git a/apps/q/java/src/net/i2p/aum/SimpleFile.java b/apps/q/java/src/net/i2p/aum/SimpleFile.java deleted file mode 100644 index a6f0438de..000000000 --- a/apps/q/java/src/net/i2p/aum/SimpleFile.java +++ /dev/null @@ -1,118 +0,0 @@ -package net.i2p.aum; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; - -/** - * SimpleFile - subclass of File which adds some python-like - * methods. Cuts out a lot of the red tape involved with reading - * from and writing to files - */ -public class SimpleFile { - - public RandomAccessFile _file; - public String _path; - - public SimpleFile(String path, String mode) throws FileNotFoundException { - - _path = path; - _file = new RandomAccessFile(path, mode); - } - - public byte [] readBytes() throws IOException { - return readBytes((int)_file.length()); - } - - public byte[] readBytes(int n) throws IOException { - byte [] buf = new byte[n]; - _file.readFully(buf); - return buf; - } - - public char [] readChars() throws IOException { - return readChars((int)_file.length()); - } - - public char[] readChars(int n) throws IOException { - char [] buf = new char[n]; - //_file.readFully(buf); - return buf; - } - - /** - * Reads all remaining content from the file - * @return the content as a String - * @throws IOException - */ - public String read() throws IOException { - - return read((int)_file.length()); - } - - /** - * Reads one or more bytes of data from the file - * @return the content as a String - * @throws IOException - */ - public String read(int nbytes) throws IOException { - - return new String(readBytes(nbytes)); - } - - /** - * Writes one or more bytes of data to a file - * @param buf a String containing the data to write - * @return the number of bytes written, as an int - * @throws IOException - */ - public int write(String buf) throws IOException { - - return write(buf.getBytes()); - } - - public int write(byte [] buf) throws IOException { - - _file.write(buf); - return buf.length; - } - - /** - * convenient one-hit write - * @param path pathname of file to write to - * @param buf data to write - */ - public static int write(String path, String buf) throws IOException { - return new SimpleFile(path, "rws").write(buf); - } - - /** - * tests if argument refers to an actual file - * @param path pathname to test - * @return true if a file, false if not - */ - public boolean isFile() { - return new File(_path).isFile(); - } - - /** - * tests if argument refers to a directory - * @param path pathname to test - * @return true if a directory, false if not - */ - public boolean isDir() { - return new File(_path).isDirectory(); - } - - /** - * tests if a file or directory exists - * @param path pathname to test - * @return true if exists, or false - */ - public boolean exists() { - return new File(_path).exists(); - } - -} - diff --git a/apps/q/java/src/net/i2p/aum/SimpleFile_old.java b/apps/q/java/src/net/i2p/aum/SimpleFile_old.java deleted file mode 100644 index ac74b58ac..000000000 --- a/apps/q/java/src/net/i2p/aum/SimpleFile_old.java +++ /dev/null @@ -1,121 +0,0 @@ -package net.i2p.aum; - -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; - -/** - * SimpleFile - subclass of File which adds some python-like - * methods. Cuts out a lot of the red tape involved with reading - * from and writing to files - */ -public class SimpleFile_old extends File { - - public FileReader _reader; - public FileWriter _writer; - - public SimpleFile_old(String path) { - - super(path); - - _reader = null; - _writer = null; - } - - /** - * Reads all remaining content from the file - * @return the content as a String - * @throws IOException - */ - public String read() throws IOException { - - return read((int)length()); - } - - /** - * Reads one or more bytes of data from the file - * @return the content as a String - * @throws IOException - */ - public String read(int nbytes) throws IOException { - - // get a reader, if we don't already have one - if (_reader == null) { - _reader = new FileReader(this); - } - - char [] cbuf = new char[nbytes]; - - int nread = _reader.read(cbuf); - - if (nread == 0) { - return ""; - } - - return new String(cbuf, 0, nread); - - } - - /** - * Writes one or more bytes of data to a file - * @param buf a String containing the data to write - * @return the number of bytes written, as an int - * @throws IOException - */ - public int write(String buf) throws IOException { - - // get a reader, if we don't already have one - if (_writer == null) { - _writer = new FileWriter(this); - } - - _writer.write(buf); - _writer.flush(); - return buf.length(); - } - - public int write(byte [] buf) throws IOException { - - return write(new String(buf)); - } - - /** - * convenient one-hit write - * @param path pathname of file to write to - * @param buf data to write - */ - public static int write(String path, String buf) throws IOException { - SimpleFile_old f = new SimpleFile_old(path); - return f.write(buf); - } - - /** - * tests if argument refers to an actual file - * @param path pathname to test - * @return true if a file, false if not - */ - public static boolean isFile(String path) { - return new File(path).isFile(); - } - - /** - * tests if argument refers to a directory - * @param path pathname to test - * @return true if a directory, false if not - */ - public static boolean isDir(String path) { - return new File(path).isDirectory(); - } - - /** - * tests if a file or directory exists - * @param path pathname to test - * @return true if exists, or false - */ - public static boolean exists(String path) { - return new File(path).exists(); - } - -} - diff --git a/apps/q/java/src/net/i2p/aum/SimpleQueue.java b/apps/q/java/src/net/i2p/aum/SimpleQueue.java deleted file mode 100644 index 15da498ec..000000000 --- a/apps/q/java/src/net/i2p/aum/SimpleQueue.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * SimpleQueue.java - * - * Created on March 24, 2005, 11:14 PM - */ - -package net.i2p.aum; - -import java.util.Vector; - -/** - * Implements simething similar to python's 'Queue' class - */ -public class SimpleQueue { - - public Vector items; - - /** Creates a new instance of SimpleQueue */ - public SimpleQueue() { - items = new Vector(); - } - - /** - * fetches the item at head of queue, blocking if queue is empty - */ - public synchronized Object get() - { - while (true) - { - try { - if (items.size() == 0) - wait(); - - // someone has added - Object item = items.get(0); - items.remove(0); - return item; - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - /** - * adds a new object to the queue - */ - public synchronized void put(Object item) - { - items.addElement(item); - notify(); - } - - private static class TestThread extends Thread { - - String id; - - SimpleQueue q; - - public TestThread(String id, SimpleQueue q) { - this.id = id; - this.q = q; - } - - public void run() { - try { - print("waiting for queue"); - - Object item = q.get(); - - print("got item: '"+item+"'"); - - } catch (Exception e) { - e.printStackTrace(); - return; - } - } - - public void print(String msg) { - System.out.println("thread '"+id+"': "+msg); - } - - } - - /** - * @param args the command line arguments - */ - public static void main(String[] args) { - - int i; - int nthreads = 7; - - Thread [] threads = new Thread[nthreads]; - - SimpleQueue q = new SimpleQueue(); - - // populate the queue with some stuff - q.put("red"); - q.put("orange"); - q.put("yellow"); - - // populate threads array - for (i = 0; i < nthreads; i++) { - threads[i] = new TestThread("thread"+i, q); - } - - // and launch the threads - for (i = 0; i < nthreads; i++) { - threads[i].start(); - } - - try { - Thread.sleep(3000); - } catch (Exception e) { - e.printStackTrace(); - return; - } - - // wait a bit and see what happens - String [] items = {"green", "blue", "indigo", "violet", "black", "white", "brown"}; - for (i = 0; i < items.length; i++) { - String item = items[i]; - System.out.println("main: adding '"+item+"'..."); - q.put(item); - try { - Thread.sleep(3000); - } catch (Exception e) { - e.printStackTrace(); - return; - } - } - - System.out.println("main: terminating"); - - } - -} diff --git a/apps/q/java/src/net/i2p/aum/SimpleSemaphore.java b/apps/q/java/src/net/i2p/aum/SimpleSemaphore.java deleted file mode 100644 index 83a736acd..000000000 --- a/apps/q/java/src/net/i2p/aum/SimpleSemaphore.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SimpleSemaphore.java - * - * Created on March 24, 2005, 11:51 PM - */ - -package net.i2p.aum; - -/** - * Simple implementation of semaphores - */ -public class SimpleSemaphore { - - protected int count; - - /** Creates a new instance of SimpleSemaphore */ - public SimpleSemaphore(int size) { - count = size; - } - - public synchronized void acquire() throws InterruptedException - { - if (count == 0) - { - wait(); - } - count -= 1; - } - - public synchronized void release() - { - count += 1; - notify(); - } - - private static class TestThread extends Thread - { - String id; - SimpleSemaphore sem; - - public TestThread(String id, SimpleSemaphore sem) - { - this.id = id; - this.sem = sem; - } - - public void run() - { - try { - print("waiting for semaphore"); - sem.acquire(); - - print("got semaphore"); - - Thread.sleep(1000); - - print("releasing semaphore"); - - sem.release(); - - print("terminating"); - - } catch (Exception e) { - e.printStackTrace(); - return; - } - } - - public void print(String msg) { - System.out.println("thread '"+id+"': "+msg); - } - } - - /** - * @param args the command line arguments - */ - public static void main(String[] args) { - - int i; - - Thread [] threads = new Thread[10]; - - SimpleSemaphore sem = new SimpleSemaphore(3); - - // populate threads array - for (i = 0; i < 10; i++) { - threads[i] = new TestThread("thread"+i, sem); - } - - // and launch the threads - for (i = 0; i < 10; i++) { - threads[i].start(); - } - - // wait a bit and see what happens - System.out.println("main: threads launched, waiting 20 secs"); - - try { - Thread.sleep(20000); - } catch (Exception e) { - e.printStackTrace(); - } - - System.out.println("main: terminating"); - - } - -} diff --git a/apps/q/java/src/net/i2p/aum/helloworld.java b/apps/q/java/src/net/i2p/aum/helloworld.java deleted file mode 100644 index 2a8ce30e9..000000000 --- a/apps/q/java/src/net/i2p/aum/helloworld.java +++ /dev/null @@ -1,17 +0,0 @@ - -public class helloworld -{ - public static void main(String [] args) - { - helloworld h = new helloworld(); - h.greet(); - } - - public void greet() - { - System.out.println("Hi, this is your greeting"); - } -} - - - diff --git a/apps/q/java/src/net/i2p/aum/http/HtmlPage.java b/apps/q/java/src/net/i2p/aum/http/HtmlPage.java deleted file mode 100644 index 67ede7207..000000000 --- a/apps/q/java/src/net/i2p/aum/http/HtmlPage.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * HtmlPage.java - * - * Created on April 8, 2005, 8:22 PM - */ - -package net.i2p.aum.http; - -import java.util.Enumeration; - -import net.i2p.aum.DupHashtable; - -/** - * Framework for building up a page of HTML by method calls alone, breaking - * every design rule by enmeshing content, presentation and logic - */ -public class HtmlPage { - - public String dtd = ""; - - public Tag page; - public Tag head; - public Tag body; - DupHashtable cssSettings; - - /** Creates a new HtmlPage object */ - public HtmlPage() { - page = new Tag("html"); - head = new Tag(page, "head"); - body = new Tag(page, "body"); - cssSettings = new DupHashtable(); - } - - /** renders out the whole page into a single string */ - public String toString() { - - // embed stylesheet, if non-empty - if (cssSettings.size() > 0) { - Tag t1 = head.nest("style type=\"text/css\""); - t1.raw("\n"); - Enumeration elems = cssSettings.keys(); - while (elems.hasMoreElements()) { - String name = (String)elems.nextElement(); - cssTag.raw(name + " { "); - Enumeration items = cssSettings.get(name).elements(); - while (items.hasMoreElements()) { - String item = (String)items.nextElement(); - cssTag.raw(item+";"); - } - cssTag.raw(" }\n"); - } - } - - // now render out the whole page - return dtd + "\n" + page; - } - - /** adds a setting to the page's embedded stylesheet */ - public HtmlPage css(String tag, String item, String val) { - return css(tag, item+":"+val); - } - - /** adds a setting to the page's embedded stylesheet */ - public HtmlPage css(String tag, String setting) { - cssSettings.put(tag, setting); - return this; - } -} diff --git a/apps/q/java/src/net/i2p/aum/http/I2PHttpRequestHandler.java b/apps/q/java/src/net/i2p/aum/http/I2PHttpRequestHandler.java deleted file mode 100644 index 5ab1911be..000000000 --- a/apps/q/java/src/net/i2p/aum/http/I2PHttpRequestHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * I2PHttpRequestHandler.java - * - * Created on April 8, 2005, 11:57 PM - */ - -package net.i2p.aum.http; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; - -import net.i2p.client.streaming.I2PSocket; - -/** - * - * @author david - */ -public abstract class I2PHttpRequestHandler extends MiniHttpRequestHandler -{ - /** Creates a new instance of I2PHttpRequestHandler */ - public I2PHttpRequestHandler(MiniHttpServer server, Object sock, Object arg) - throws Exception - { - super(server, sock, arg); - } - - /** Extracts a readable InputStream from own socket */ - public InputStream getInputStream() throws IOException { - try { - return ((I2PSocket)socket).getInputStream(); - } catch (Exception e) { - return ((Socket)socket).getInputStream(); - } - } - - /** Extracts a writeable OutputStream from own socket */ - public OutputStream getOutputStream() throws IOException { - try { - return ((I2PSocket)socket).getOutputStream(); - } catch (Exception e) { - return ((Socket)socket).getOutputStream(); - } - } - -} diff --git a/apps/q/java/src/net/i2p/aum/http/I2PHttpServer.java b/apps/q/java/src/net/i2p/aum/http/I2PHttpServer.java deleted file mode 100644 index 0e693dac0..000000000 --- a/apps/q/java/src/net/i2p/aum/http/I2PHttpServer.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * I2PHttpServer.java - * - * Created on April 8, 2005, 11:39 PM - */ - -package net.i2p.aum.http; - -import java.io.IOException; -import java.util.Properties; - -import net.i2p.I2PException; -import net.i2p.aum.PrivDestination; -import net.i2p.client.streaming.I2PServerSocket; -import net.i2p.client.streaming.I2PSocket; -import net.i2p.client.streaming.I2PSocketManager; -import net.i2p.client.streaming.I2PSocketManagerFactory; -import net.i2p.data.DataFormatException; - -/** - * - * @author david - */ -public class I2PHttpServer extends MiniHttpServer { - - PrivDestination privKey; - I2PSocketManager socketMgr; - - public I2PHttpServer(PrivDestination key) - throws DataFormatException, IOException, I2PException - { - this(key, I2PHttpRequestHandler.class, null, null); - } - - public I2PHttpServer(PrivDestination key, Class hdlrClass) - throws DataFormatException, IOException, I2PException - { - this(key, hdlrClass, null, null); - } - - public I2PHttpServer(PrivDestination key, Class hdlrClass, Properties props) - throws DataFormatException, IOException, I2PException - { - this(key, hdlrClass, null, props); - } - - /** Creates a new instance of I2PHttpServer */ - public I2PHttpServer(PrivDestination key, Class hdlrClass, Object hdlrArg, Properties props) - throws DataFormatException, IOException, I2PException - { - super(hdlrClass, hdlrArg); - - if (key != null) { - privKey = key; - } else { - privKey = new PrivDestination(); - } - - // get a socket manager - // socketManager = I2PSocketManagerFactory.createManager(key); - if (props == null) { - socketMgr = I2PSocketManagerFactory.createManager(privKey.getInputStream()); - } else { - socketMgr = I2PSocketManagerFactory.createManager(privKey.getInputStream(), props); - } - - if (socketMgr == null) { - throw new I2PException("I2PHttpServer: Failed to create socketManager"); - } - - String d = privKey.getDestination().toBase64(); - System.out.println("Server: getting server socket for dest "+d); - - // get a server socket - //serverSocket = socketManager.getServerSocket(); - } - - public void getServerSocket() throws IOException { - - I2PServerSocket sock; - sock = socketMgr.getServerSocket(); - serverSocket = sock; - System.out.println("listening on dest: "+privKey.getDestination().toBase64()); - } - - /** - * Listens on our 'serverSocket' object for an incoming connection, - * and returns a connected socket object. You should override this - * if you're using non-standard socket objects - */ - public Object acceptConnection() throws IOException { - - I2PSocket sock; - - try { - sock = ((I2PServerSocket)serverSocket).accept(); - } catch (I2PException e) { - throw new IOException(e.toString()); - } - - System.out.println("Got connection from: "+sock.getPeerDestination().toBase64()); - - //System.out.println("New connection accepted" + - // sock.getInetAddress() + - // ":" + sock.getPort()); - return sock; - } - - public static void main(String [] args) { - try { - System.out.println("I2PHttpServer: starting up with new random key"); - I2PHttpServer server = new I2PHttpServer((PrivDestination)null); - System.out.println("I2PHttpServer: running server"); - server.run(); - } catch (Exception e) { - e.printStackTrace(); - } - } -} - diff --git a/apps/q/java/src/net/i2p/aum/http/MiniDemoXmlRpcHandler.java b/apps/q/java/src/net/i2p/aum/http/MiniDemoXmlRpcHandler.java deleted file mode 100644 index f75dfa94e..000000000 --- a/apps/q/java/src/net/i2p/aum/http/MiniDemoXmlRpcHandler.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * MiniDemoXmlRpcHandler.java - * - * Created on April 13, 2005, 3:20 PM - */ - -package net.i2p.aum.http; - - -public class MiniDemoXmlRpcHandler { - - MiniHttpServer server; - - public MiniDemoXmlRpcHandler(MiniHttpServer server) { - this.server = server; - } - - public String bar(String arg) { - return "bar: got '"+arg+"'"; - } -} - diff --git a/apps/q/java/src/net/i2p/aum/http/MiniHttpRequestHandler.java b/apps/q/java/src/net/i2p/aum/http/MiniHttpRequestHandler.java deleted file mode 100644 index 42317a39a..000000000 --- a/apps/q/java/src/net/i2p/aum/http/MiniHttpRequestHandler.java +++ /dev/null @@ -1,574 +0,0 @@ -/* - * MiniHttpRequestHandler.java - * Adapted from pont.net's httpRequestHandler (httpServer.java) - * - * Created on April 8, 2005, 3:15 PM - */ - -package net.i2p.aum.http; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.Socket; -import java.net.URLDecoder; -import java.util.Enumeration; -import java.util.Vector; - -import net.i2p.aum.DupHashtable; -import net.i2p.aum.Mimetypes; - -public abstract class MiniHttpRequestHandler implements Runnable { - final static String CRLF = "\r\n"; - - /** server which created this handler */ - protected MiniHttpServer server; - - /** socket through which client is connected to us */ - protected Object socket; - - /** stored constructor arg */ - protected Object serverArg; - - /** input sent from client in request */ - protected InputStream input; - - /** we use this to read from client */ - protected BufferedReader br; - - /** output sent to client in reply */ - protected OutputStream output; - - /** http request type - GET, POST etc */ - protected String reqType; - - /** the request pathname */ - protected String reqFile; - - /** the request protocol (eg 'HTTP/1.0') */ - protected String reqProto; - - /** http headers */ - protected DupHashtable headerVars; - - /** variable settings from POST data */ - public DupHashtable postVars; - - /** variable settings from URL (?name1=val1&name2=val2...) */ - public DupHashtable urlVars; - - /** consolidated variable settings from URL or POST data */ - public DupHashtable allVars; - - /** first line of response we send back to client, set this - * with 'setStatus' - */ - private String status = "HTTP/1.0 200 OK"; - private String contentType = "text/plain"; - private String reqContentType = null; - protected String serverName = "aum's MiniHttpServer"; - - protected byte [] rawContentBytes = null; - - /** - * raw data sent by client in post req - */ - protected char [] postData; - - /** if a POST, this holds the full POST data as a string */ - public String postDataStr; - - // Constructors - public MiniHttpRequestHandler(MiniHttpServer server, Object socket) throws Exception { - this(server, socket, null); - } - - public MiniHttpRequestHandler(MiniHttpServer server, Object socket, Object arg) throws Exception { - this.server = server; - this.socket = socket; - this.serverArg = arg; - this.input = getInputStream(); - this.output = getOutputStream(); - this.br = new BufferedReader(new InputStreamReader(input)); - } - - // ------------------------------------------- - // START OF OVERRIDEABLES - // ------------------------------------------- - - // override these methods in subclass if your socket-type thang is not - // a genuine Socket objct - - /** Extracts a readable InputStream from own socket */ - public InputStream getInputStream() throws IOException { - return ((Socket)socket).getInputStream(); - } - - /** Extracts a writeable OutputStream from own socket */ - public OutputStream getOutputStream() throws IOException { - return ((Socket)socket).getOutputStream(); - } - - /** closes the socket (or our socket-ish object) */ - public void closeSocket() throws IOException { - ((Socket)socket).close(); - } - - /** method which gets called upon receipt of a GET. - * You should override this - */ - public abstract void on_GET() throws Exception; - - /** method which gets called upon receipt of a POST. - * You should override this - */ - public abstract void on_POST() throws Exception; - - // ------------------------------------------- - // END OF OVERRIDEABLES - // ------------------------------------------- - - /** Sets the HTTP status line (default 'HTTP/1.0 200 OK') */ - public void setStatus(String status) { - this.status = status; - } - - /** Sets the Content=Type header (default "text/plain") */ - public void setContentType(String contentType) { - this.contentType = contentType; - } - - /** Sets the 'Server' header (default "aum's MiniHttpServer") */ - public void setServer(String serverType) { - this.serverName = serverType; - } - - /** Sets the full body of raw output to be written, replacing - * the generated html tags - */ - public void setRawOutput(String raw) { - setRawOutput(raw.getBytes()); - } - - /** Sets the full body of raw output to be written, replacing - * the generated html tags - */ - public void setRawOutput(byte [] raw) { - rawContentBytes = raw; - } - - /** writes a String to output - normally you shouldn't need to call - * this directly - */ - public void write(String raw) { - write(raw.getBytes()); - } - - /** writes a byte array to output - normally you shouldn't need to call - * this directly - */ - public void write(byte [] raw) { - try { - output.write(raw); - } catch (Exception e) { - System.out.print(e); - } - } - - /** processes the request, sends back response */ - public void run() { - try { - processRequest(); - } - catch(Exception e) { - e.printStackTrace(); - System.out.println(e); - } - } - - /** does all the work of processing the request */ - protected void processRequest() throws Exception { - - headerVars = new DupHashtable(); - urlVars = new DupHashtable(); - postVars = new DupHashtable(); - allVars = new DupHashtable(); - - String line; - - // basic parsing of first req line - String reqLine = br.readLine(); - printReq(reqLine); - String [] reqBits = reqLine.split("\\s+", 3); - reqType = reqBits[0]; - String [] reqFileBits = reqBits[1].split("[?]", 2); - reqFile = reqFileBits[0]; - - // check for URL variables - if (reqFileBits.length > 1) { - urlVars = parseVars(reqFileBits[1]); - } - - // extract the 'request protocol', default to HTTP/1.0 - try { - reqProto = reqBits[2]; - } catch (Exception e) { - // workaround eepproxy bug - reqFile = "/"; - reqProto = "HTTP/1.0"; - } - - // suck the headers - while (true) { - line = br.readLine(); - //System.out.println("Got header line: "+line); - if (line.equals("")) { - break; - } - String [] lineBits = line.split(":\\s+", 2); - headerVars.put(lineBits[0], lineBits[1]); - } - //br.close(); - - // GET is simple, all the work is already done - if (reqType.equals("GET")) { - on_GET(); - } - - // POST is more involved - need to read POST data and - // break it up into fields - else if (reqType.equals("POST")) { - int postLen; - String postLenStr; - try { - reqContentType = headerVars.get("Content-Type", 0, ""); - - try { - postLenStr = headerVars.get("Content-Length", 0); - } catch (Exception e) { - // damn opera - postLenStr = headerVars.get("Content-length", 0); - } - - postLen = new Integer(postLenStr).intValue(); - postData = new char[postLen]; - - //System.out.println("postLen="+postLen); - for (int i=0; i"; - } - - if (tagBits.length > 1) { - attribs.addElement(tagBits[1]); - } - - breakBefore = nlOnOpen.contains(open); - breakAfter = breakBefore || nlOnClose.contains(open); - } - - // ----------------------------------------------------- - // METHODS FOR ADDING SPECIFIC HTML TAGS - // ----------------------------------------------------- - - /** insert a <br> on the fly */ - public Tag br() { - return add("br/"); - } - - /** insert a <hr> on the fly */ - public Tag hr() { - return add("hr/"); - } - - public Tag center() { - return nest("center"); - } - - public Tag center(String attr) { - return nest("center "+attr); - } - - public Tag big() { - return nest("big"); - } - - public Tag big(String attr) { - return nest("big "+attr); - } - - public Tag small() { - return nest("small"); - } - - public Tag small(String attr) { - return nest("small "+attr); - } - - public Tag i() { - return nest("i"); - } - - public Tag i(String attr) { - return nest("i "+attr); - } - - public Tag strong() { - return nest("strong"); - } - - public Tag strong(String attr) { - return nest("big "+attr); - } - - public Tag table() { - return nest("table"); - } - - public Tag table(String attr) { - return nest("table "+attr); - } - - public Tag tr() { - return nest("tr"); - } - - public Tag tr(String attr) { - return nest("tr "+attr); - } - - public Tag td() { - return nest("td"); - } - - public Tag td(String attr) { - return nest("td "+attr); - } - - public Tag form() { - return nest("form"); - } - - public Tag form(String attr) { - return nest("form "+attr); - } - - // ----------------------------------------------------- - // METHODS FOR ADDING GENERAL CONTENT - // ----------------------------------------------------- - - /** create a new tag, embed it into this one, return this tag */ - public Tag add(String s) { - Tag t = new Tag(s); - content.addElement(t); - return this; - } - - /** add a tag to this one, returning this tag */ - public Tag add(Tag t) { - content.addElement(t); - return this; - } - - /** create a new tag, nest it into this one, return the new tag */ - public Tag nest(String opentag) { - Tag t = new Tag(this, opentag); - t.parent = this; - return t; - } - public Tag nest() { - Tag t = new Tag(this); - t.parent = this; - return t; - } - - /** insert object into this tag, return this tag */ - public Tag raw(Object o) { - content.addElement(o); - return this; - } - - /** set an attribute of this tag, return this tag */ - public Tag set(String name, String val) { - return set(name + "=\"" + val + "\""); - } - - /** set an attribute of this tag, return this tag */ - public Tag set(String setting) { - attribs.addElement(setting); - return this; - } - - public Tag style(String name, String val) { - return style(name+":"+val); - } - - public Tag style(String setting) { - styles.addElement(setting); - return this; - } - - // ----------------------------------------------------- - // METHODS FOR RENDERING - // ----------------------------------------------------- - - public void render(OutputStream out) throws IOException { - - //System.out.print("{render:"+open+"}"); - //System.out.flush(); - - if (open != null) { - out.write("<".getBytes()); - out.write(open.getBytes()); - - // add in attributes, if any - for (int i=0; i 0) { - out.write((" style=\"").getBytes()); - Enumeration elems = styles.elements(); - while (elems.hasMoreElements()) { - String s = (String)elems.nextElement()+";"; - out.write(s.getBytes()); - } - out.write("\"".getBytes()); - } - - if (close.equals("")) { - out.write("/".getBytes()); - } - out.write(">".getBytes()); - - if (breakBefore) { - out.write("\n".getBytes()); - } - } - - for (int i=0; i < content.size(); i++) { - Object item = content.get(i); - if (item.getClass().isAssignableFrom(Tag.class)) { - ((Tag)item).render(out); - } else { - out.write(item.toString().getBytes()); - } - } - - if (open != null) { - out.write(close.getBytes()); - //buf.append(close); - - if (breakAfter) { - out.write("\n".getBytes()); - } - } - } - - public String render() { - ByteArrayOutputStream s = new ByteArrayOutputStream(); - try { - render(s); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - return s.toString(); - } - - public String toString() { - return render(); - } -} - diff --git a/apps/q/java/src/net/i2p/aum/q/Favicon.java b/apps/q/java/src/net/i2p/aum/q/Favicon.java deleted file mode 100644 index f5f6ab6c9..000000000 --- a/apps/q/java/src/net/i2p/aum/q/Favicon.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.i2p.aum.q; -public class Favicon { - public static byte [] image = { - 0, 0, 1, 0, 1, 0, 16, 16, 0, 0, 1, 0, 24, 0, 104, 3, - 0, 0, 22, 0, 0, 0, 40, 0, 0, 0, 16, 0, 0, 0, 32, 0, - 0, 0, 1, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 72, 0, - 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, - 19, 19, -127, -127, -127, 12, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, -120, - -120, -120, -49, -49, -49, 116, 116, 116, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 90, 90, -80, -80, -80, - -18, -18, -18, -55, -55, -55, -122, -122, -122, 68, 68, 68, 107, 107, 107, -62, - -62, -62, -20, -20, -20, -59, -59, -59, 4, 4, 4, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, -109, -109, -109, -30, -30, -30, -8, -8, -8, - -25, -25, -25, -2, -2, -2, -28, -28, -28, -49, -49, -49, -2, -2, -2, -14, - -14, -14, -36, -36, -36, 33, 33, 33, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 72, 72, 72, -1, -1, -1, -1, -1, -1, -28, -28, -28, - -34, -34, -34, 118, 118, 118, -124, -124, -124, -1, -1, -1, -1, -1, -1, -6, - -6, -6, 68, 68, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -98, -98, -98, -1, -1, -1, -38, -38, -38, -80, -80, -80, - 13, 13, 13, 0, 0, 0, 100, 100, 100, -11, -11, -11, -9, -9, -9, -3, - -3, -3, -90, -90, -90, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -49, -49, -49, -4, -4, -4, -57, -57, -57, 63, 63, 63, - 0, 0, 0, 26, 26, 26, -74, -74, -74, -56, -56, -56, -35, -35, -35, -29, - -29, -29, -13, -13, -13, 104, 104, 104, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -28, -28, -28, -46, -46, -46, -22, -22, -22, 2, 2, 2, - 0, 0, 0, 2, 2, 2, 41, 41, 41, 108, 108, 108, 37, 37, 37, -32, - -32, -32, -29, -29, -29, -60, -60, -60, 1, 1, 1, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -60, -60, -60, -60, -60, -60, -44, -44, -44, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, -56, - -56, -56, -49, -49, -49, -43, -43, -43, 24, 24, 24, 0, 0, 0, 0, 0, - 0, 0, 0, 0, -117, -117, -117, -70, -70, -70, -48, -48, -48, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 50, 50, -93, - -93, -93, -12, -12, -12, -47, -47, -47, 32, 32, 32, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 54, 54, 54, -42, -42, -42, -79, -79, -79, 28, 28, 28, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 110, 110, 110, -70, - -70, -70, -4, -4, -4, -64, -64, -64, 3, 3, 3, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 121, 121, 121, -51, -51, -51, -87, -87, -87, - 10, 10, 10, 0, 0, 0, 37, 37, 37, -119, -119, -119, -106, -106, -106, -20, - -20, -20, -33, -33, -33, 95, 95, 95, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, -124, -124, -124, -23, -23, -23, - -33, -33, -33, -107, -107, -107, -75, -75, -75, -68, -68, -68, -15, -15, -15, -16, - -16, -16, -111, -111, -111, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 55, 55, 55, - -97, -97, -97, -26, -26, -26, -29, -29, -29, -31, -31, -31, -61, -61, -61, 121, - 121, 121, 11, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - }; -} diff --git a/apps/q/java/src/net/i2p/aum/q/QClientAPI.java b/apps/q/java/src/net/i2p/aum/q/QClientAPI.java deleted file mode 100644 index a431283df..000000000 --- a/apps/q/java/src/net/i2p/aum/q/QClientAPI.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * QClientAPI.java - * - * Created on March 31, 2005, 5:19 PM - */ - -package net.i2p.aum.q; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.util.Hashtable; -import java.util.Vector; - -import org.apache.xmlrpc.XmlRpcClient; -import org.apache.xmlrpc.XmlRpcException; - -/** - *

The official Java API for client applications wishing to access the Q - * network

- *

This API is just a thin wrapper that hides the XMLRPC details, and exposes - a simple set of methods.

- *

Note to app developers - I'm only implementing this API in Java - * and Python at present. If you've got some time and knowledge of other - * languages and their available XML-RPC client libs, we'd really appreciate - * it if you can port this API into other languages - such as Perl, C++, - * Ruby, OCaml, C#, etc. You can take this API implementation as the reference - * code for porting to your own language.

- */ - -public class QClientAPI { - - XmlRpcClient node; - - /** - * Creates a new instance of QClientAPI talking on given xmlrpc port - */ - public QClientAPI(int port) throws MalformedURLException { - node = new XmlRpcClient("http://127.0.0.1:"+port); - } - - /** - * Creates a new instance of QClientAPI talking on default xmlrpc port - */ - public QClientAPI() throws MalformedURLException { - node = new XmlRpcClient("http://127.0.0.1:"+QClientNode.defaultXmlRpcServerPort); - } - - /** - * Pings a Q client node, gets back a bunch of useful stats - */ - public Hashtable ping() throws XmlRpcException, IOException { - return (Hashtable)node.execute("i2p.q.ping", new Vector()); - } - - /** - * Retrieves an update of content catalog - * @param since a unixtime in seconds. The content list returned will - * be a differential update since this time. - */ - public Hashtable getUpdate(int since) - throws XmlRpcException, IOException - { - Vector args = new Vector(); - args.addElement(new Integer(since)); - args.addElement(new Integer(1)); - args.addElement(new Integer(1)); - return (Hashtable)node.execute("i2p.q.getUpdate", args); - } - - /** - * Retrieves an item of content from the network, given its key - * @param key the key to retrieve - */ - public Hashtable getItem(String key) throws XmlRpcException, IOException { - Vector args = new Vector(); - args.addElement(key); - return (Hashtable)node.execute("i2p.q.getItem", args); - } - - /** - * Inserts a single item of data, without metadata. A default metadata set - * will be generated. - * @param data a byte[] of data to insert - * @return a Hashtable containing results, including: - *
    - *
  • result - either "ok" or "error"
  • - *
  • error - (only if result != "ok") - terse error label
  • - *
  • key - the key under which this item has been inserted
  • - *
- */ - public Hashtable putItem(byte [] data) throws XmlRpcException, IOException { - Vector args = new Vector(); - args.addElement(data); - return (Hashtable)node.execute("i2p.q.putItem", args); - } - - /** - * Inserts a single item of data, with metadata - * @param metadata a Hashtable of metadata to insert - * @param data a byte[] of data to insert - * @return a Hashtable containing results, including: - *
    - *
  • result - either "ok" or "error"
  • - *
  • error - (only if result != "ok") - terse error label
  • - *
  • key - the key under which this item has been inserted
  • - *
- */ - public Hashtable putItem(Hashtable metadata, byte [] data) - throws XmlRpcException, IOException - { - Vector args = new Vector(); - args.addElement(metadata); - args.addElement(data); - return (Hashtable)node.execute("i2p.q.putItem", args); - } - - /** - * Generates a new keypair for inserting signed-space items - * @return a struct with the keys: - *
    - *
  • status - "ok"
  • - *
  • publicKey - base64-encoded signed space public key
  • - *
  • privateKey - base64-encoded signed space private key
  • - *
- * When inserting an item using the privateKey, the resulting uri - * will be Q:publicKey/path - */ - public Hashtable newKeys() throws XmlRpcException, IOException - { - Vector args = new Vector(); - return (Hashtable)node.execute("i2p.q.newKeys", args); - } - - - /** - * Adds a new noderef to node - * @param dest - the base64 i2p destination for the remote peer - * @return a Hashtable containing results, including: - *
    - *
  • result - either "ok" or "error"
  • - *
  • error - (only if result != "ok") - terse error label
  • - *
- */ - public Hashtable hello(String dest) throws XmlRpcException, IOException { - Vector args = new Vector(); - args.addElement(dest); - return (Hashtable)node.execute("i2p.q.hello", args); - } - - /** - * Shuts down a running node - * If the shutdown succeeds, then this call will fail with an exception. But - * if the call succeeds, then the shutdown has failed (sorry if this is a tad - * counter-intuitive). - * @param privKey - the base64 i2p private key for this node. - * @return a Hashtable containing results, including: - *
    - *
  • result - "error"
  • - *
  • error - terse error label
  • - *
- */ - public Hashtable shutdown(String privKey) throws XmlRpcException, IOException { - Vector args = new Vector(); - args.addElement(privKey); - return (Hashtable)node.execute("i2p.q.shutdown", args); - } - - /** - * Search the node for catalog entries matching a set of criteria - * @param criteria a Hashtable of metadata criteria to match, and whose - * values are regular expressions - * @return a Hashtable containing results, including: - *
    - *
  • result - "ok" or "error"
  • - *
  • error - if result != "ok", a terse error label
  • - *
  • items - a Vector of items found which match the given search - * criteria. If no available matching items were found, this vector - * will come back empty. - *
- */ - public Hashtable search(Hashtable criteria) throws XmlRpcException, IOException { - Vector args = new Vector(); - args.addElement(criteria); - return (Hashtable)node.execute("i2p.q.search", args); - } -} - diff --git a/apps/q/java/src/net/i2p/aum/q/QClientNode.java b/apps/q/java/src/net/i2p/aum/q/QClientNode.java deleted file mode 100644 index 4cd7b37c3..000000000 --- a/apps/q/java/src/net/i2p/aum/q/QClientNode.java +++ /dev/null @@ -1,608 +0,0 @@ -/* - * QClient.java - * - * Created on 20 March 2005, 23:22 - */ - -package net.i2p.aum.q; - -import java.io.File; -import java.io.IOException; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.Properties; -import java.util.Vector; - -import net.i2p.I2PException; -import net.i2p.aum.Mimetypes; -import net.i2p.aum.http.I2PHttpServer; -import net.i2p.aum.http.MiniHttpServer; -import net.i2p.data.DataFormatException; - -/** - * Implements Q client nodes. - */ - -public class QClientNode extends QNode { - - static String defaultStoreDir = ".quartermaster_client"; - I2PHttpServer webServer; - MiniHttpServer webServerTcp; - Properties httpProps; - - public String nodeType = "Client"; - - // ------------------------------------------------------- - // CONSTRUCTORS - // ------------------------------------------------------- - - /** - * Creates a new instance of QClient, using default - * datastore location - * @throws IOException, DataFormatException, I2PException - */ - public QClientNode() throws IOException, DataFormatException, I2PException - { - super(System.getProperties().getProperty("user.home") + sep + defaultStoreDir); - log.debug("TEST CLIENT DEBUG MSG1"); - } - - /** - * Creates a new instance of QClient, using specified - * datastore location - * @param path of node's datastore directory - * @throws IOException, DataFormatException, I2PException - */ - public QClientNode(String dataDir) throws IOException, DataFormatException, I2PException - { - super(dataDir); - - log.error("TEST CLIENT DEBUG MSG"); - } - - // ------------------------------------------------------- - // METHODS - XML-RPC PRIMITIVE OVERRIDES - // ------------------------------------------------------- - - /** - * hello cmds to client nodes are illegal! - */ - /** - public Hashtable localHello(String destBase64) - { - Hashtable h = new Hashtable(); - h.put("status", "error"); - h.put("error", "unimplemented"); - return h; - } - **/ - - /** perform client-specific setup */ - public void setup() - { - updateCatalogFromPeers = 1; - isClient = true; - - // allow a port change for xmlrpc client app conns - String xmlPortStr = System.getProperty("q.xmlrpc.tcp.port"); - if (xmlPortStr != null) { - xmlRpcServerPort = new Integer(xmlPortStr).intValue(); - conf.setIntProperty("xmlRpcServerPort", xmlRpcServerPort); - } - - // ditto for listening host - String xmlHostStr = System.getProperty("q.xmlrpc.tcp.host"); - if (xmlHostStr != null) { - xmlRpcServerHost = xmlHostStr; - conf.setProperty("xmlRpcServerHost", xmlRpcServerHost); - } - - // --------------------------------------------------- - // now fire up the HTTP interface - // listening only within I2P on client node's dest - - // set up a properties object for short local tunnel - httpProps = new Properties(); - httpProps.setProperty("inbound.length", "0"); - httpProps.setProperty("outbound.length", "0"); - httpProps.setProperty("inbound.lengthVariance", "0"); - httpProps.setProperty("outbound.lengthVariance", "0"); - Properties sysProps = System.getProperties(); - String i2cpHost = sysProps.getProperty("i2cp.tcp.host", "127.0.0.1"); - String i2cpPort = sysProps.getProperty("i2cp.tcp.port", "7654"); - httpProps.setProperty("i2cp.tcp.host", i2cpHost); - httpProps.setProperty("i2cp.tcp.port", i2cpPort); - } - - public void run() { - - // then do all the parent stuff - super.run(); - } - - /** - *

Sets up and launches an http server for servicing requests - * to this node.

- *

For server nodes, the xml-rpc server listens within I2P on the - * node's destination.

- *

For client nodes, the xml-rpc server listens on a local TCP - * port (according to attributes xmlRpcServerHost and xmlRpcServerPort)

- */ - public void startExternalInterfaces(QServerMethods methods) throws Exception - { - System.out.println("Creating http interface..."); - try { - // create tcp http server for xmlrpc and browser access - webServerTcp = new MiniHttpServer(QClientWebInterface.class, xmlRpcServerPort, this); - webServerTcp.addXmlRpcHandler(baseXmlRpcServiceName, methods); - System.out.println("started in-i2p http/xmlrpc server listening on port:" + xmlRpcServerPort); - webServerTcp.start(); - - // create in-i2p http server for xmlrpc and browser access - webServer - = new I2PHttpServer(privKey, - QClientWebInterface.class, - this, - httpProps - ); - webServer.addXmlRpcHandler(baseXmlRpcServiceName, methods); - webServer.start(); - System.out.println("Started in-i2p http/xmlrpc server listening on dest:"); - String dest = privKey.getDestination().toBase64(); - System.out.println(dest); - - - System.out.println("web interfaces created"); - - } catch (Exception e) { - e.printStackTrace(); - System.out.println("Failed to create client web interfaces"); - System.exit(1); - } - -/** - WebServer serv = new WebServer(xmlRpcServerPort); - // if host is non-null, add as a listen host - if (xmlRpcServerHost.length() > 0) { - serv.setParanoid(true); - serv.acceptClient(xmlRpcServerHost); - } - serv.addHandler(baseXmlRpcServiceName, methods); - serv.start(); - log.info("Client XML-RPC server listening on port "+xmlRpcServerPort+" as service"+baseXmlRpcServiceName); -**/ - - } - - // ----------------------------------------------------- - // client-specific customisations of xmlRpc methods - // ----------------------------------------------------- - - /** - * Insert an item of content, with metadata. Then (since this is the client's - * override) schedules a job to insert this item to a selection of remote peers. - * @param metadata Hashtable of item's metadata - * @param data raw data to insert - */ - public Hashtable putItem(Hashtable metadata, byte [] data) throws QException - { - Hashtable resp = new Hashtable(); - QDataItem item; - String uri; - - // do the local insert first - try { - item = new QDataItem(metadata, data); - item.processAndValidate(true); - localPutItem(item); - uri = (String)item.get("uri"); - - } catch (QException e) { - resp.put("status", "error"); - resp.put("error", "qexception"); - resp.put("summary", e.getLocalizedMessage()); - return resp; - } - - // now schedule remote insertion - schedulePeerUploadJob(item); - - // and return success, rest will happen automatically in background - resp.put("status", "ok"); - resp.put("uri", uri); - return resp; - } - - /** - * Search datastore and catalog for a given item of content - * @param criteria Hashtable of criteria to match in metadata - */ - public Hashtable search(Hashtable criteria) - { - Hashtable result = new Hashtable(); - Vector matchingItems = new Vector(); - Iterator items; - Hashtable item; - Hashtable foundUris = new Hashtable(); - String uri; - - // get an iterator for all catalog items - try { - // test all local content - items = contentIdx.getItemsSince(0); - while (items.hasNext()) { - String uriHash = (String)items.next(); - item = getLocalMetadataForHash(uriHash); - uri = (String)item.get("uri"); - //System.out.println("search: testing "+metadata+" against "+criteria); - if (metadataMatchesCriteria(item, criteria)) { - matchingItems.addElement(item); - foundUris.put(uri, item); - } - } - - // now test remote catalog - items = catalogIdx.getItemsSince(0); - while (items.hasNext()) { - String uriHash = (String)items.next(); - item = getLocalCatalogMetadataForHash(uriHash); - uri = (String)item.get("uri"); - //System.out.println("search: testing "+metadata+" against "+criteria); - if (metadataMatchesCriteria(item, criteria)) { - if (!foundUris.containsKey("uri")) { - matchingItems.addElement(item); - } - } - } - - } catch (Exception e) { - e.printStackTrace(); - result.put("status", "error"); - result.put("error", e.getMessage()); - return result; - } - - result.put("status", "ok"); - result.put("items", matchingItems); - return result; - - } - - - /** - * retrieves a peers/catalog update - executes on base class, then - * adds in our catalog entries - */ - public Hashtable getUpdate(int since, int includePeers, int includeCatalog) - { - Hashtable h = localGetUpdate(since, includePeers, includeCatalog); - - if (includeCatalog != 0) { - - // must extend v with remote catalog entries - Vector vCat = (Vector)(h.get("items")); - Iterator items; - - // get an iterator for all new catalog items since given unixtime - try { - items = catalogIdx.getItemsSince(since); - - // pick through the iterator, and fetch metadata for each item - while (items.hasNext()) { - String key = (String)(items.next()); - Hashtable pf = getLocalCatalogMetadata(key); - log.error("getUpdate(client): key="+key+", pf="+pf); - System.out.println("getUpdate(client): key="+key+", pf="+pf); - if (pf != null) { - // clone this metadata, add in the key - Hashtable pf1 = (Hashtable)pf.clone(); - pf1.put("key", key); - vCat.addElement(pf1); - } - } - - - } catch (IOException e) { - e.printStackTrace(); - } - } - - return h; - } - - /** - *

Retrieve an item of content.

- *

This client override tries the local datastore first, then - * attempts to get the data from remote servers believed to have the data

- */ - public Hashtable getItem(String uri) throws IOException, QException - { - Hashtable res; - - log.info("getItem: uri='"+uri+"'"); - - if (localHasItem(uri)) { - - class Fred { - } - - Fred xxx = new Fred(); - - // got it locally, send it back - return localGetItem(uri); - } - - // ain't got it locally - try remote sources in turn till we - // either get it or fail - Vector sources = getItemLocation(uri); - - // send back an error if not in local catalog - if (sources == null || sources.size() == 0) { - Hashtable dnf = new Hashtable(); - dnf.put("status", "error"); - dnf.put("error", "notfound"); - dnf.put("comment", "uri not known locally or remotely"); - return dnf; - } - - // ok, got at least one remote source, go through them till - // we get data that checks out - int i; - int npeers = sources.size(); - int numCmdFail = 0; - int numDnf = 0; - int numBadData = 0; - for (i=0; i - *
  • status - String - either "ok" or "error"
  • - *
  • error - String - short summary of error, only present if - * status is "error"
  • - *
  • uri - the full Q URI for the top level of the site - * - */ - public Hashtable insertQSite(String privKey64, - String siteName, - String rootPath, - Hashtable metadata - ) - throws Exception - { - // for results - Hashtable result = new Hashtable(); - String uri = null; // uri under which this site will be reachable - String pubKey64; - - File dir = new File(rootPath); - - // barf if no such directory - if (!dir.isDirectory()) { - result.put("status", "error"); - result.put("error", "nosuchdir"); - result.put("detail", "Path '"+rootPath+"' is not a directory"); - return result; - } - - // barf if not readable - if (!dir.canRead()) { - result.put("status", "error"); - result.put("error", "cantread"); - result.put("detail", "Path '"+rootPath+"' is not readable"); - return result; - } - - // barf if missing or invalid site name - siteName = siteName.trim(); - if (!siteName.matches("[a-zA-Z0-9_-]+")) { - result.put("status", "error"); - result.put("error", "badsitename"); - result.put("detail", "QSite name should be only alphanumerics, '-' and '_'"); - return result; - } - - String defaultPath = rootPath + sep + "index.html"; - File defaultFile = new File(defaultPath); - - // barf if index.html not present and readable - if (!(defaultFile.isFile() && defaultFile.canRead())) { - result.put("status", "error"); - result.put("error", "noindex"); - result.put("detail", "Required file index.html missing or unreadable"); - return result; - } - - // derive public key and uri for site, barf if bad key - try { - pubKey64 = QUtil.privateToPubHash(privKey64); - } catch (Exception e) { - result.put("status", "error"); - result.put("error", "badprivkey"); - return result; - } - uri = "Q:" + pubKey64 + "/" + siteName + "/"; - - // now the fun recursive bit - insertQSiteDir(privKey64, siteName, rootPath, ""); - - // queue up an insert of default file - metadata.put("type", "qsite"); - metadata.put("path", siteName+"/"); - metadata.put("mimetype", "text/html"); - - //System.out.println("insertQSite: privKey='"+privKey64+"'"); - //System.out.println("insertQSite: siteName='"+siteName+"'"); - //System.out.println("insertQSite: rootDir='"+rootPath+"'"); - //System.out.println("insertQSite: metadata="+metadata); - //System.out.println("insertQSite: default="+defaultPath); - - insertQSiteFile(privKey64, siteName, defaultPath, "", metadata); - - result.put("status", "ok"); - result.put("uri", uri); - return result; - } - - /** - * recursively queues jobs for the insertion of a directory's contents, for - * a qsite. - * @param privKey64 - private 'signed space' key, base64 format - * @param siteName - short text name for the site - * @param absPath - physical pathname of the subdirectory to insert - * @param relPath - qsite-relative pathname of this item - */ - protected void insertQSiteDir(String privKey64, String siteName, String absPath, String relPath) - throws Exception - { - File dir = new File(absPath); - - // fail gracefully if not a readable directory - if (!(dir.isDirectory() && dir.canRead())) { - System.out.println("insertQSiteDir: not a readable directory "+absPath); - return; - } - - //System.out.println("insertQSiteDir: entry - abs='"+absPath+"' rel='"+relPath+"'"); - - // loop through the contents - String [] contents = dir.list(); - for (int i=0; i 0) { - node = new QClientNode(args[0]); - } - else { - node = new QClientNode(); - } - node.log.info("QClientNode: running node..."); - node.run(); - } - - public void foo1() { - System.out.println("QClientNode.foo: isClient="+isClient); - } - - -} diff --git a/apps/q/java/src/net/i2p/aum/q/QClientWebInterface.java b/apps/q/java/src/net/i2p/aum/q/QClientWebInterface.java deleted file mode 100644 index 72b5bd857..000000000 --- a/apps/q/java/src/net/i2p/aum/q/QClientWebInterface.java +++ /dev/null @@ -1,755 +0,0 @@ -/* - * QClientWebInterface.java - * - * Created on April 9, 2005, 1:10 PM - */ - -package net.i2p.aum.q; - -import java.io.ByteArrayOutputStream; -import java.io.PrintStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.Socket; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Vector; - -import net.i2p.aum.http.HtmlPage; -import net.i2p.aum.http.I2PHttpRequestHandler; -import net.i2p.aum.http.MiniHttpServer; -import HTML.Template; - - -/** - * Request handler for Q Client nodes that listens within I2P - * on the client node's destination. Intended for access via - * eepProxy, and by adding a hosts.txt entry for this dest - * under the hostname 'q'. - */ -public class QClientWebInterface extends I2PHttpRequestHandler { - - /** set this to true when debugging html layout */ - public static boolean loadTemplateWithEachHit = true; - - public QNode node = null; - - // refs to main page template, and components of main page - static Template tmplt; - static Vector tabRow; - static Vector pageItems; - - /** - * for security - disables direct-uri GETs of content if running directly over TCP; - * we need to coerce users to use their eepproxy browser instead - */ - public boolean isRunningOverTcp = true; - - /** Creates a new instance of QClientWebInterface */ - public QClientWebInterface(MiniHttpServer server, Object socket, Object node) - throws Exception - { - super(server, socket, node); - this.node = (QNode)node; - isRunningOverTcp = socket.getClass() == Socket.class; - } - - static String [] tabNames = { - "home", "search", "insert", "tools", "status", "jobs", "help", "about" - }; - - /** - * Loads a template of a given name. Invokes method on node - * to resolve this to an absolute pathname, so 'name' -> '/path/to/html/name.html' - */ - public Template loadTemplate(String name) throws Exception { - - String fullPath = node.getResourcePath("html"+node.sep+name)+".html"; - //System.out.println("fullPath='"+fullPath+"'"); - String [] args = new String [] { - "filename", fullPath, - "case_sensitive", "true", - "max_includes", "5" - }; - return new Template(args); - } - - // ---------------------------------------------------- - // FRONT-END METHODS - // ---------------------------------------------------- - - /** GET and POST both go through .safelyHandleReq() */ - public void on_GET() { - - safelyHandleReq(); - } - - /** GET and POST both go through .safelyHandleReq() */ - public void on_POST() { - - safelyHandleReq(); - } - - public void on_RPC() { - - } - - /** - * wrap .handleReq() - on exception, call dump_error() to - * generate a 400 error page with diagnostics - */ - public void safelyHandleReq() { - try { - handleReq(); - } catch (Exception e) { - dump_error(e); - } - } - - /** - *

    Forwards hits to either a path handler method, or generic get method.

    - * - *

    Detects hits to paths for which we have a handler (ie, methods - * of this class with name 'hdlr_<somepath>', (such as 'hdlr_help' - * for handling hits to '/help').

    - * - *

    If we have a handler, forward to it, otherwise forward to standard - * getItem() method

    - */ - public void handleReq() throws Exception { - - Class [] noArgs; - Method hdlrMethod; - - // strip useless leading slash from reqFile - reqFile = reqFile.substring(1); - - // default to 'home' - if (reqFile.equals("")) { - reqFile = "home"; - } - //print("handleReq: reqFile='"+reqFile+"'"); - - // Set up the main page template - try { - tmplt = loadTemplate("main"); - pageItems = new Vector(); - tmplt.setParam("items", pageItems); - tmplt.setParam("nodeType", node.nodeType); - - } catch (Exception e) { - e.printStackTrace(); - throw e; - } - //print("handleReq: loaded template"); - - // execute if a command - if (allVars.containsKey("cmd")) { - do_cmd(); - } - - // -------------------------------------------------------- - // intercept magic paths for which we have a handler method - noArgs = new Class[0]; - try { - // extract top dir of path and make it method-name-safe - String methodName = "hdlr_"+reqFile.split("/")[0].replace('.','_'); - hdlrMethod = this.getClass().getMethod(methodName, null); - - // now dispatch the method - hdlrMethod.invoke(this, null); - - // spit out html, if no raw content inserted - sendPageIfNoContent(); - - // done - return; - - } catch (NoSuchMethodException e) { - // routinely fails if we dont' have handler, so assume it's - // a GET - } - - // if we get here, client is requesting a specific uri - allVars.put("uri", reqFile); - if (!cmd_get()) { - hdlr_home(); - } - sendPageIfNoContent(); - } - - /** - * as name implies, generates standard html page - * if setRawOutput hasnt' been called - */ - public void sendPageIfNoContent() { - - if (rawContentBytes == null) { - - // we're spitting out html - setContentType("text/html"); - - // set up tab row style vector - setupTabRow(); - - // finally, render out our filled-out template - setRawOutput(tmplt.output()); - } - } - - /** - * Inserts an item into main pane - */ - public Object addToMainPane(Object item) { - - Hashtable h = new Hashtable(); - h.put("item", item); - pageItems.addElement(h); - return item; - } - - /** - * Generates a set of tabs and adds these to the page, - * marking as active the tab whose name is in the current URL - */ - public void setupTabRow() - { - Hashtable h; - tabRow = new Vector(); - for (int i=0; i< tabNames.length; i++) { - String name = tabNames[i]; - h = new Hashtable(); - h.put("name", name); - h.put("label", name.substring(0,1).toUpperCase()+name.substring(1)); - if (name.equals(reqFile)) { - h.put("active", "1"); - } - tabRow.addElement(h); - tmplt.setParam("tabs", tabRow); - } - } - - // ----------------------------------------------------- - // METHODS FOR HANDLING MAGIC PATHS - // ---------------------------------------------------- - - /** Display home page */ - public void hdlr_home() throws Exception { - - // stick in 'getitem' form - addToMainPane(loadTemplate("getform")); - - } - - /** Display status page */ - public void hdlr_status() throws Exception { - - // ping the node, extract status items - Vector statusItems = new Vector(); - Hashtable h = node.ping(); - for (Enumeration e = h.keys(); e.hasMoreElements();) { - String key = (String)e.nextElement(); - String val = h.get(key).toString(); - if (val.length() > 60) { - // too big for table, stick into a readonly text field - val = ""; - } - Hashtable rec = new Hashtable(); - rec.put("key", key); - rec.put("value", val); - //print("key='"+key+"' val='"+val+"'"); - statusItems.addElement(rec); - } - - // get status form template insert the items, stick onto main pane - Template tmpltStatus = loadTemplate("status"); - tmpltStatus.setParam("items", statusItems); - addToMainPane(tmpltStatus); - - } - - /** display current node jobs list */ - public void hdlr_jobs() throws Exception { - - // get jobs list, add to jobs list template, add that to main pane - Template tmpltJobs = loadTemplate("jobs"); - tmpltJobs.setParam("items", node.getJobsList()); - addToMainPane(tmpltJobs); - } - - /** Display search form */ - public void hdlr_search() throws Exception { - addToMainPane(loadTemplate("searchform")); - } - - /** Display insert page */ - public void hdlr_insert() throws Exception { - - String formName = allVars.get("mode", 0, "file").equals("site") ? "putsiteform" : "putform"; - Template tmpltPut = loadTemplate(formName); - addToMainPane(tmpltPut); - } - - /** Display settings screen */ - public void hdlr_settings() throws Exception { - addToMainPane(loadTemplate("settings")); - } - - /** Display tools screen */ - public void hdlr_tools() throws Exception { - - addToMainPane(loadTemplate("tools")); - addToMainPane(loadTemplate("genkeysform")); - addToMainPane(loadTemplate("addrefform")); - } - - /** Display help screen */ - public void hdlr_help() throws Exception { - addToMainPane(loadTemplate("help")); - } - - /** Display about screen */ - public void hdlr_about() throws Exception { - addToMainPane(loadTemplate("about")); - } - - /** handle /favicon.ico hits */ - public void hdlr_favicon_ico() { - - System.out.println("Sending favicon image"); - setContentType("image/x-icon"); - setRawOutput(Favicon.image); - } - - /** dummy handler, causes an exception (for testing error dump pages */ - public void hdlr_shit() throws Exception { - throw new Exception("this method is shit"); - } - - // ---------------------------------------------------- - // METHODS FOR HANDLING COMMANDS - // ---------------------------------------------------- - - /** - * invoked if GET or POST vars contain 'cmd'. - * attempts to dispatch command handler method 'cmd_xxxx' - */ - public void do_cmd() throws Exception { - - // this whole method could be done in python with the statement: - // getattr(self, 'cmd_'+urlVars['cmd'], lambda:None)() - String cmd = allVars.get("cmd", 0); - try { - // extract top dir of path and make it method-name-safe - String methodName = "cmd_"+cmd; - Method hdlrMethod = this.getClass().getMethod(methodName, null); - - // now dispatch the method - hdlrMethod.invoke(this, null); - } catch (NoSuchMethodException e) {} - } - - - /** - * executes a 'get' cmd - */ - public boolean cmd_get() throws Exception { - - Hashtable result = null; - String status = null; - Hashtable metadata = null; - String mimetype = null; - - // bail if node offline - if (node == null) { - return false; - } - - // bail if no 'url' arg - if (!allVars.containsKey("uri")) { - return false; - } - - // get uri, prepend 'Q:' if needed - String uri = allVars.get("uri", 0); - if (!uri.startsWith("Q:")) { - uri = "Q:" + uri; - } - - // attempt the fetch - result = node.getItem(uri); - status = (String)result.get("status"); - - // how'd we go? - if (status.equals("ok")) { - // got it - send it back - metadata = (Hashtable)result.get("metadata"); - mimetype = (String)metadata.get("mimetype"); - - // forbid content retrieval via MSIE - boolean isIE = false; - for (Enumeration e = headerVars.get("User-Agent").elements(); e.hasMoreElements();) { - String val = ((String)e.nextElement()).toLowerCase(); - if (val.matches(".*(msie|windows|\\.net).*")) { - Template warning = loadTemplate("msiealert"); - addToMainPane(warning); - return false; - } - } - - // forbid direct delivery of text/* content via direct tcp - if (isRunningOverTcp) { - // security feature - set to application/octet-stream if req arrives via tcp. - // this prevents people surfing the q web interface directly over TCP and - // falling prey to anonymity attacks (eg gif bugs) - - // if user is trying to hit an html page, we can send back a warning - if (mimetype.startsWith("text")) { - Template warning = loadTemplate("anonalert"); - warning.setParam("dest", node.destStr); - addToMainPane(warning); - return false; - } - setContentType("application/octet-stream"); - } else { - // got this conn via I2P and eeproxy - safer to obey the mimetype - setContentType(mimetype); - } - - setRawOutput((byte [])result.get("data")); - return true; - } else { - // 404 - tmplt.setParam("show_404", "1"); - tmplt.setParam("404_uri", uri); - return false; - } - } - - /** executes genkeys command */ - public void cmd_genkeys() throws Exception { - - Hashtable res = node.newKeys(); - String pubKey = (String)res.get("publicKey"); - String privKey = (String)res.get("privateKey"); - Template keysWidget = loadTemplate("genkeysresult"); - keysWidget.setParam("publickey", pubKey); - keysWidget.setParam("privatekey", privKey); - addToMainPane(keysWidget); - } - - /** adds a noderef */ - public void cmd_addref() throws Exception { - - String ref = allVars.get("noderef", 0).trim(); - node.hello(ref); - } - - /** executes 'put' command */ - public void cmd_put() throws Exception { - - // barf if user posted both data and rawdata - if (allVars.containsKey("data") - && ((String)allVars.get("data", 0)).length() > 0 - && allVars.containsKey("rawdata") - && ((String)allVars.get("rawdata", 0)).length() > 0 - ) - { - Template t = loadTemplate("puterror"); - t.setParam("error", "you specified a file as well as 'rawdata'"); - addToMainPane(t); - addToMainPane(dumpVars().toString()); - return; - } - - Hashtable metadata = new Hashtable(); - byte [] data = new byte[0]; - - // stick in some defaults - String [] keys = { - "data", "rawdata", - "mimetype", "keywords", "privkey", "abstract", "type", "title", - "path" - }; - - //System.out.println("allVars='"+allVars+"'"); - - // extract all items from form, add to metadata ones that - // have non-zero length. Take 'data' or 'rawdata' and stick their - // bytes into data. - for (int i=0; i 0) { - data = dataval; - } - } else if (key.equals("rawdata")) { - byte [] dataval = allVars.get("rawdata", 0).getBytes(); - if (dataval.length > 0) { - data = dataval; - } - } else if (key.equals("privkey")) { - String k = allVars.get("privkey", 0); - if (k.length() > 0) { - metadata.put("privateKey", k); - } - } else { - String val = allVars.get(key, 0); - //System.out.println("'"+key+"'='"+val+"'"); - if (val.length() > 0) { - metadata.put(key, allVars.get(key, 0)); - } - } - } - } - - //System.out.println("metadata="+metadata); - - if (metadata.size() == 0) { - Template err = loadTemplate("puterror"); - err.setParam("error", "No metadata!"); - addToMainPane(err); - addToMainPane(dumpVars().toString()); - return; - } - - if (data.length == 0) { - Template err = loadTemplate("puterror"); - err.setParam("error", "No data!"); - addToMainPane(err); - addToMainPane(dumpVars().toString()); - return; - } - - // phew! ready to put - System.out.println("WEB:cmd_put: inserting"); - - Hashtable result = node.putItem(metadata, data); - - System.out.println("WEB:cmd_put: got"+result); - - String status = (String)result.get("status"); - if (!status.equals("ok")) { - String errTxt = (String)result.get("error"); - if (result.containsKey("summary")) { - errTxt = errTxt + ":" + result.get("summary").toString(); - } - Template err = loadTemplate("puterror"); - err.setParam("error", (String)result.get("error")); - addToMainPane(err); - addToMainPane(dumpVars().toString()); - return; - } - - // success, yay! - Template success = loadTemplate("putok"); - success.setParam("uri", (String)result.get("uri")); - addToMainPane(success); - - //System.out.println("cmd_put: debug on page??"); - //addToMainPane(dumpVars().toString()); - } - - /** executes 'putsite' command */ - public void cmd_putsite() throws Exception { - - Hashtable metadata = new Hashtable(); - String privKey = allVars.get("privkey", 0, ""); - String name = allVars.get("name", 0, ""); - String dir = allVars.get("dir", 0, ""); - - // pick up optional metadata items - String [] keys = { - "title", "keywords", "abstract", - }; - - // extract all items from form, add to metadata ones that - // have non-zero length. - for (int i=0; i 0) { - metadata.put(key, allVars.get(key, 0)); - } - } - } - - //System.out.println("metadata="+metadata); - - if (metadata.size() == 0) { - cmd_putsite_error("No metadata!"); - return; - } - - // phew! ready to put - Hashtable result = node.insertQSite(privKey, name, dir, metadata); - String status = (String)result.get("status"); - if (!status.equals("ok")) { - cmd_putsite_error((String)result.get("error")); - return; - } - - // success, yay! - Template success = loadTemplate("putok"); - success.setParam("is_site", "1"); - success.setParam("uri", (String)result.get("uri")); - addToMainPane(success); - - //System.out.println("cmd_put: debug on page??"); - //addToMainPane(dumpVars().toString()); - } - - protected void cmd_putsite_error(String msg) throws Exception { - - Template err = loadTemplate("puterror"); - err.setParam("error", msg); - err.setParam("is_site", "1"); - addToMainPane(err); - addToMainPane(dumpVars().toString()); - } - - /** performs a search */ - public void cmd_search() throws Exception { - - Hashtable criteria = new Hashtable(); - String [] fields = { - "type", "title", "path", "mimetype", "keywords", - "summary", "searchmode" - }; - - for (int i=0; iGET and POST methods. - * @param request servlet request - * @param response servlet response - */ - protected void processRequest(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - findNode(); - determineIfNodeIsRunning(); - - Hashtable vars = parseVars(request.getQueryString()); - - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - out.println("QConsole"); - out.println(""); - out.println(""); - - out.println("

    Q Node Manager

    "); - - //out.println("debug: vars='"+vars+"'

    "); - - if (vars.containsKey("startnode") && !nodeIsRunning) { - startNode(); - if (!nodeIsRunning) { - out.println("Failed to start node :(

    "); - } - - } else if (vars.containsKey("stopnode") && nodeIsRunning) { - stopNode(); - nodeIsRunning = false; - } - - if (nodeIsRunning) { - out.println("Q Node is running

    "); - out.print("Node Console"); - out.print(" | "); - out.println("Stop Node"); - } else { - out.println("Q Node is not running

    "); - out.println("Start Node"); - } - - out.println(""); - out.println(""); - /* */ - out.close(); - } - - /** Handles the HTTP GET method. - * @param request servlet request - * @param response servlet response - */ - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - processRequest(request, response); - } - - /** Handles the HTTP POST method. - * @param request servlet request - * @param response servlet response - */ - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - processRequest(request, response); - } - - /** Returns a short description of the servlet. - */ - public String getServletInfo() { - return "Short description"; - } - - /** try to find node */ - public void findNode() { - - try { - nodeDirStr = System.getProperties().getProperty("user.home") - + sep + ".quartermaster_client"; - - // yay, found a node (we hope), create an xmlrpc client for talking - // to that node - String propPath = nodeDirStr + sep + "node.conf"; - File propFile = new File(propPath); - FileInputStream propIn = new FileInputStream(propPath); - Properties prop = new Properties(); - prop.load(propIn); - - nodePrivKey = prop.getProperty("privKey"); - - // presence of private key indicates node exists - nodeExists = nodePrivKey != null; - - } catch (Exception e) { - // node doesn't exist - } - - } - - public void startNode() { - - int i; - String [] jars = { - "i2p", "mstreaming", "aum", - }; - - String cp = ""; - - String jarsDir = "lib"; - - for (i=0; i 0) { - cp += cpsep; - } - cp += jarsDir + sep + jars[i] + ".jar"; - } - - System.out.println("cp='"+cp+"'"); - - // build up args - int nopts = options.size(); - String args = ""; - args += "java"; - for (Enumeration e = options.propertyNames(); e.hasMoreElements();) { - String opt = (String)e.nextElement(); - String arg = "-D" + opt + "=" + options.getProperty(opt); - System.out.println(arg); - args += " " + arg; - } - - args += " -cp " + cp; - args += " net.i2p.aum.q.QMgr"; - args += " foreground"; - - Runtime runtime = Runtime.getRuntime(); - - // and spawn the start job - try { - //runtime.exec(startForegroundArgs, propLines); - System.out.println("args='"+args+"'"); - runtime.exec(args, null); - } catch (IOException e) { - e.printStackTrace(); - } - - // wait a bit - sleep(3); - - // try for 10s to contact node - for (i=0; i<10; i++) { - sleep(1); - determineIfNodeIsRunning(); - if (nodeIsRunning) { - break; - } - } - } - - public void stopNode() { - - Vector args = new Vector(); - args.addElement(nodePrivKey); - try { - System.out.println("stopping node..."); - nodeProxy.execute("i2p.q.shutdown", args); - } catch (Exception e) { - - } - System.out.println("node terminated"); - } - - /** returns true if node is up */ - public void determineIfNodeIsRunning() { - try { - nodeProxy.execute("i2p.q.ping", new Vector()); - nodeIsRunning = true; - } catch (Exception e) { - nodeIsRunning = false; - return; - } - } - - public void sleep(int n) { - try { - Thread.sleep(n * 1000); - } catch (Exception e) {} - } - - public Hashtable parseVars(String raw) { - Hashtable h = new Hashtable(); - - if (raw == null) { - return h; - } - - URLDecoder u = new URLDecoder(); - String [] items = raw.split("[&]"); - String dec; - for (int i=0; i 0) { - if (!_path.startsWith("/")) { - _path = "/" + _path; - put("path", _path); - } - } - - // determine file extension - String [] bits = _path.split("/"); - String name = bits[bits.length-1]; - bits = name.split("\\.", 2); - ext = "." + bits[bits.length-1]; - } - else { - // path is empty - set to '/.ext' where 'ext' is the - // file extension guessed from present mimetype value, and dataHash - // is a shortened hash of the content - String mime = (String)get("mimetype"); - if (mime == null) { - mime = "application/octet-stream"; - put("mimetype", mime); - } - - // determine file extension - ext = Mimetypes.guessExtension(mime); - - // and determine final path - _path = "/" + ((String)get("dataHash")).substring(0, 10) + ext; - put("path", _path); - } - - // ----------------------------------------- - // default the mimetype - if (!containsKey("mimetype")) { - String mimetype = Mimetypes.guessType(ext); - put("mimetype", mimetype); - } - - // ------------------------------------------ - // barf if contains mutually-exclusive signed space keys - if (containsKey("privateKey") && (containsKey("publicKey") || containsKey("signature"))) { - throw new QException("Metadata must NOT contain privateKey and one of publicKey or signature"); - } - - // ------------------------------------------ - // barf if exactly one of publicKey and signature are present - if (containsKey("publicKey") ^ containsKey("signature")) { - throw new QException("Either both or neither of 'publicKey' and 'signature' must be present"); - } - - // ----------------------------------------- - // now discern between plain hash items and - // signed space items - if (containsKey("privateKey") || containsKey("publicKey")) { - - DSAEngine dsa = DSAEngine.getInstance(); - - // process/validate remaining data in signed space context - - if (containsKey("privateKey")) { - // only private key given - uplift, remove, replace with public key - _privKey = new SigningPrivateKey(); - String priv64 = get("privateKey").toString(); - try { - _privKey.fromBase64(priv64); - } catch (Exception e) { - throw new QException("Invalid privateKey", e); - } - - // ok, got valid privateKey - - // expunge privKey from metadata, replace with publicKey - this.remove("privateKey"); - _pubKey = _privKey.toPublic(); - put("publicKey", _pubKey.toBase64()); - - // create and insert a signature - QUtil.debug("before sig, asSortedString give:\n"+asSortedString()); - - Signature sig = dsa.sign(asSortedString().getBytes(), _privKey); - String sigBase64 = sig.toBase64(); - put("signature", sigBase64); - } - else { - // barf if not both signature and pubkey present - if (!(containsKey("publicKey") && containsKey("signature"))) { - throw new QException("need both publicKey and signature"); - } - _pubKey = new SigningPublicKey(); - String pub64 = get("publicKey").toString(); - try { - _pubKey.fromBase64(pub64); - } catch (Exception e) { - throw new QException("Invalid publicKey", e); - } - } - - // now, whether we just signed or not, validate the signature/pubkey - byte [] thisAsBytes = asSortedString().getBytes(); - - String sig64 = get("signature").toString(); - Signature sig1 = new Signature(); - try { - sig1.fromBase64(sig64); - } catch (DataFormatException e) { - throw new QException("Invalid signature string", e); - } - - if (!dsa.verifySignature(sig1, thisAsBytes, _pubKey)) { - throw new QException("Invalid signature"); - } - - // last step - determine the correct URI - String pubHash = QUtil.hashPubKey(_pubKey); - uri = "Q:"+pubHash+_path; - - } // end of 'signed space' mode processing - else { - // ----------------------------------------------------- - // process/validate remaining data in plain hash context - String thisHashed = QUtil.sha64(asSortedString()); - uri = "Q:"+ thisHashed + ext; - - } // end of plain hash mode processing - - - // ----------------------------------------------------- - // final step - add or validate uri - if (containsKey("uri")) { - if (!get("uri").toString().equals(uri)) { - throw new QException("Invalid URI"); - } - } else { - put("uri", uri); - } - - } - - /** - * returns a filename under which this item should be stored - */ - public String getStoreFilename() throws QException { - if (!containsKey("uri")) { - throw new QException("Missing URI"); - } - return QUtil.sha64((String)get("uri")); - } - - /** - * Hashes this set of metadata, excluding any 'signature' key - * @return Base64 hash of metadata - */ - public String hashThisAsBase64() { - - return QUtil.sha64(asSortedString()); - } - - public byte [] hashThis() { - - return QUtil.sha(asSortedString()); - } - - /** - * alphabetise thie metadata to a single string, containing one - * 'key=value' entry per line. Excludes keys 'uri' and 'signature' - */ - public String asSortedString() { - - TreeSet t = new TreeSet(keySet()); - Iterator keys = t.iterator(); - int nkeys = t.size(); - int i; - String metaStr = ""; - for (i = 0; i < nkeys; i++) - { - String metaKey = (String)keys.next(); - if (!(metaKey.equals("signature") || metaKey.equals("uri"))) { - metaStr += metaKey + "=" + get(metaKey) + "\n"; - } - } - return metaStr; - } - -} - diff --git a/apps/q/java/src/net/i2p/aum/q/QException.java b/apps/q/java/src/net/i2p/aum/q/QException.java deleted file mode 100644 index fcb58aee0..000000000 --- a/apps/q/java/src/net/i2p/aum/q/QException.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * QException.java - * - * Created on April 6, 2005, 2:05 PM - */ - -package net.i2p.aum.q; - -import java.io.PrintStream; -import java.io.PrintWriter; - -/** - * Base class of Q exceptions - * @author jrandom (shamelessly rebadged by aum) - */ - -public class QException extends Exception { - private Throwable _source; - - public QException() { - this(null, null); - } - - public QException(String msg) { - this(msg, null); - } - - public QException(String msg, Throwable source) { - super(msg); - _source = source; - } - - public void printStackTrace() { - if (_source != null) _source.printStackTrace(); - super.printStackTrace(); - } - - public void printStackTrace(PrintStream ps) { - if (_source != null) _source.printStackTrace(ps); - super.printStackTrace(ps); - } - - public void printStackTrace(PrintWriter pw) { - if (_source != null) _source.printStackTrace(pw); - super.printStackTrace(pw); - } -} - diff --git a/apps/q/java/src/net/i2p/aum/q/QIndexFile.java b/apps/q/java/src/net/i2p/aum/q/QIndexFile.java deleted file mode 100644 index 0df2411a4..000000000 --- a/apps/q/java/src/net/i2p/aum/q/QIndexFile.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * QIndexFile.java - * - * Created on March 24, 2005, 11:55 AM - */ - -package net.i2p.aum.q; - -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.Date; -import java.util.Iterator; - -/** - *

    Implements a binary-searchable file for storing (time, hash) records. - * This makes it faster for server nodes to determine which content entries, - * catalog entries and peer entries have appeared since time t.

    - * - *

    To ease inter-operation with other programs, as well as human troubleshooting, - * The file is implemented as a plain text file, with records in the - * following format: - *

      - *
    • time unixtime, as 10-byte decimal string
    • - *
    • = single-char delimiter
    • - *
    • hash - a 44-byte Base64 representation of an sha256 hash
    • - *
    - *

    - */ -public class QIndexFile { - - public String path; - File fileObj; - RandomAccessFile file; - public long rawLength; - public int numRecs; - FileReader reader; - FileWriter writer; - - /** length of base64 representation of sha256 hash */ - static public int hashLen = 43; - - /** length of unixtime milliseconds in decimal format */ - static public int timeLen = 13; - - /** - * length of records, allowing for time field, delimter (,), - * hash field and terminating newline - */ - static public int recordLen = hashLen + timeLen + 2; - - /** - * Create a new index file - * @param path absolute pathname on filesystem - */ - public QIndexFile(String path) throws IOException { - this.path = path; - fileObj = new File(path); - - // if file doesn't exist, ensure parent dir exists, so subsequent - // file creation will (hopefully) succeed - if (!fileObj.exists()) - { - // create parent directory if not already existing - String parentDir = fileObj.getParent(); - File parentFile = new File(parentDir); - if (!parentFile.isDirectory()) - { - parentFile.mkdirs(); - } - } - - // get a random access object, creating file if not yet existing - file = new RandomAccessFile(fileObj, "rws"); - - // barf if file's length is not a multiple of record length - rawLength = file.length(); - if (rawLength % recordLen != 0) { - throw new IOException("File size not a multiple of record length ("+recordLen+")"); - } - - // note record count - numRecs = (int)(rawLength / recordLen); - } - - /** - * fetch an iterator for items after a given time - */ - public synchronized Iterator getItemsSince(int time) throws IOException - { - //System.out.println("getItemsSince: time="+time); - - // if no records, return an empty iterator - if (numRecs == 0) - { - return new QIndexFileIterator(this, 0); - } - - // otherwise, binary search till we find an item time-stamped - // after given time - long mtime = ((long)time) * 1000; - int lo = 0; - int hi = numRecs; - int lastguess = -1; - while (hi - lo > 0) - { - int guess = (hi + lo) / 2; - //System.out.println("getItemsSince: lo="+lo+" guess="+guess+" hi="+hi); - if (guess == lastguess) // && hi - lo == 1) - { - break; - } - lastguess = guess; - - Object [] rec = getRecord(guess); - long t = ((Long)rec[0]).longValue(); - if (t <= mtime) - { - // guess too low, go for upper range - lo = guess; - continue; - } - else - { - // guess too high, pick lower range - hi = guess; - continue; - } - } - - // found - return new QIndexFileIterator(this, hi); - } - - /** - * adds a new base64 hash value record, saving it with current time - */ - public synchronized void add(String h) throws IOException - { - // barf if hash is incorrect length - if (h.length() != hashLen) - { - System.out.println("hash="+h); - throw new IOException("Incorrect hash length ("+h.length()+"), should be "+hashLen); - } - - // format current date/time as decimal string, pad with leading zeroes - Date d = new Date(); - String ds = String.valueOf(d.getTime()); - while (ds.length() < timeLen) - { - ds = "0" + ds; - } - - // now can construct record - String rec = ds + "," + h + "\n"; - - // append it to file - file.seek(numRecs * recordLen); - file.writeBytes(rec); - - // and update count - numRecs += 1; - rawLength += recordLen; - } - - public long getRecordTime(int n) throws IOException - { - Object [] rec = getRecord(n); - - return ((Long)rec[0]).longValue(); - } - - /** return number of records currently within file */ - public int length() - { - return numRecs; - } - - /** - * returns the hash field of record n - */ - public String getRecordHash(int n) throws IOException - { - Object [] rec = getRecord(n); - return (String)rec[1]; - } - - public synchronized Object [] getRecord(int n) throws IOException - { - Object [] rec = new Object[2]; - - String recStr = getRecordStr(n); - String [] flds = recStr.split(","); - Long t = new Long(flds[0]); - String h = flds[1]; - rec[0] = t; - rec[1] = h; - return rec; - } - - protected synchronized String getRecordStr(int n) throws IOException - { - // barf if over or under-reaching - if (n < 0 || n > numRecs - 1) - { - throw new IOException("Record number ("+n+") out of range"); - } - - // position to location of the record - file.seek(n * recordLen); - - // read, trim and return - return file.readLine().trim(); - } - - /** - * @param args the command line arguments - */ - public static void main(String[] args) { - try { - QIndexFile q = new QIndexFile("/home/david/.quartermaster_client/content/index.dat"); - Iterator i = q.getItemsSince((int)(new Date().getTime() / 1000)); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/apps/q/java/src/net/i2p/aum/q/QIndexFileIterator.java b/apps/q/java/src/net/i2p/aum/q/QIndexFileIterator.java deleted file mode 100644 index 95a7df845..000000000 --- a/apps/q/java/src/net/i2p/aum/q/QIndexFileIterator.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * QIndexFileIterator.java - * - * Created on March 24, 2005, 1:49 PM - */ - -package net.i2p.aum.q; - -import java.util.Iterator; -import java.util.NoSuchElementException; - -/** - * Implements an Iterator for index files - */ -public class QIndexFileIterator implements Iterator -{ - public QIndexFile file; - int recNum; - - /** Creates an iterator starting from beginning of index file */ - public QIndexFileIterator(QIndexFile qif) - { - this(qif, 0); - } - - /** Creates a new instance of QIndexFileIterator */ - public QIndexFileIterator(QIndexFile qif, int recNum) - { - file = qif; - this.recNum = recNum; - } - - public boolean hasNext() - { - return recNum < file.length(); - } - - public Object next() throws NoSuchElementException - { - String rec; - try { - rec = file.getRecordHash(recNum); - } - catch (Exception e) { - throw new NoSuchElementException("Reached end of index"); - } - recNum += 1; - return rec; - } - - public void remove() - { - } - -} - diff --git a/apps/q/java/src/net/i2p/aum/q/QKademliaComparator.java b/apps/q/java/src/net/i2p/aum/q/QKademliaComparator.java deleted file mode 100644 index 2b949b08d..000000000 --- a/apps/q/java/src/net/i2p/aum/q/QKademliaComparator.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * QKademliaComparator.java - * - * Created on March 30, 2005, 12:30 PM - */ - -package net.i2p.aum.q; - -import java.math.BigInteger; -import java.util.Comparator; - -/** - * implements a Comparator class which compares two QPeerRec objects - * for kademlia-closeness to a given base64 sha hash value - */ -public class QKademliaComparator implements Comparator { - - QNode node; - BigInteger hashed; - - /** - * Creates a kademlia comparator, which given a base64 sha256 hash - * of something, can compare two nodes for their kademlia-closeness to - * that hash - * @param node a QNode object - needed for access to its base64 routines - * @param base64hash - string - a base64 representation of the sha256 hash - * of anything - */ - public QKademliaComparator(QNode node, String base64hash) { - - this.node = node; - hashed = new BigInteger(node.base64Dec(base64hash).getBytes()); - } - - /** - * compares two given QPeerRec objects for how close each one's ID - * is to the stored hash - */ - public int compare(Object o1, Object o2) { - - QPeer peer1 = (QPeer)o1; - QPeer peer2 = (QPeer)o2; - - String id1 = peer1.getId(); - String id2 = peer2.getId(); - - BigInteger i1 = new BigInteger(id1.getBytes()); - BigInteger i2 = new BigInteger(id2.getBytes()); - - BigInteger xor1 = i1.xor(hashed); - BigInteger xor2 = i2.xor(hashed); - - return xor1.compareTo(xor2); - } - -} - diff --git a/apps/q/java/src/net/i2p/aum/q/QMgr.java b/apps/q/java/src/net/i2p/aum/q/QMgr.java deleted file mode 100644 index a9e048a64..000000000 --- a/apps/q/java/src/net/i2p/aum/q/QMgr.java +++ /dev/null @@ -1,927 +0,0 @@ -/* - * QLaunch.java - * - * Created on March 30, 2005, 10:09 PM - */ - -package net.i2p.aum.q; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Properties; -import java.util.Vector; - -import net.i2p.aum.I2PXmlRpcClientFactory; -import net.i2p.aum.PropertiesFile; -import net.i2p.aum.SimpleFile; -import net.i2p.data.Destination; - -import org.apache.xmlrpc.XmlRpcClient; - -/** - *

    Command Line Interface (CLI) for starting/stopping Q nodes, - * and also, executing commands on Q nodes such as inserting, retrieving - * and searching for content.

    - * - *

    Commands include: - *

  • \n"); - - if (_files.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailAttachment")).append("Attachments: "); - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("
    \n"); - } - - if (_blogs.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailBlog")).append("Blog references: "); - for (int i = 0; i < _blogs.size(); i++) { - Blog b = (Blog)_blogs.get(i); - boolean expanded = (_user != null ? _user.getShowExpanded() : false); - boolean images = (_user != null ? _user.getShowImages() : false); - _postBodyBuffer.append("").append(sanitizeString(b.name)).append(" "); - } - _postBodyBuffer.append("
    \n"); - } - - if (_links.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailExternal")).append("External links: "); - for (int i = 0; i < _links.size(); i++) { - Link l = (Link)_links.get(i); - String schema = l.schema; - _postBodyBuffer.append("").append(sanitizeString(l.location)); - _postBodyBuffer.append(getSpan("summDetailExternalNet")).append(" (").append(sanitizeString(l.schema)).append(") "); - } - _postBodyBuffer.append("
    \n"); - } - - if (_addresses.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailAddr")).append("Addresses:"); - for (int i = 0; i < _addresses.size(); i++) { - Address a = (Address)_addresses.get(i); - importAddress(a); - PetName pn = null; - if (_user != null) - pn = _user.getPetNameDB().getByLocation(a.location); - if (pn != null) { - _postBodyBuffer.append(' ').append(getSpan("summDetailAddrKnown")); - _postBodyBuffer.append(sanitizeString(pn.getName())).append(""); - } else { - _postBodyBuffer.append(" ").append(sanitizeString(a.name)).append(""); - } - } - _postBodyBuffer.append("
    \n"); - } - - if (_archives.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailArchive")).append("Archives:"); - for (int i = 0; i < _archives.size(); i++) { - ArchiveRef a = (ArchiveRef)_archives.get(i); - _postBodyBuffer.append(" ").append(sanitizeString(a.name)).append(""); - if (a.description != null) - _postBodyBuffer.append(": ").append(getSpan("summDetailArchiveDesc")).append(sanitizeString(a.description)).append(""); - if (null == _user.getPetNameDB().getByLocation(a.location)) { - _postBodyBuffer.append(" bookmark"); - } - } - _postBodyBuffer.append("
    \n"); - } - - _postBodyBuffer.append("
    \n"); - } - - protected void renderMetaCell() { _preBodyBuffer.append(""); } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java b/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java deleted file mode 100644 index 3c0346ab9..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java +++ /dev/null @@ -1,1076 +0,0 @@ -package net.i2p.syndie.sml; - -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.StringTokenizer; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.data.Base64; -import net.i2p.data.DataHelper; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.Attachment; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.EntryContainer; -import net.i2p.syndie.data.SafeURL; -import net.i2p.syndie.web.AddressesServlet; -import net.i2p.syndie.web.ArchiveViewerBean; -import net.i2p.syndie.web.PostServlet; -import net.i2p.syndie.web.SyndicateServlet; -import net.i2p.util.Log; - -/** - * - */ -public class HTMLRenderer extends EventReceiverImpl { - private Log _log; - protected SMLParser _parser; - protected Writer _out; - protected User _user; - protected Archive _archive; - protected EntryContainer _entry; - protected boolean _showImages; - protected boolean _cutBody; - protected boolean _cutReached; - protected int _cutSize; - protected int _lastNewlineAt; - protected Map _headers; - protected List _addresses; - protected List _links; - protected List _blogs; - protected List _archives; - protected StringBuffer _preBodyBuffer; - protected StringBuffer _bodyBuffer; - protected StringBuffer _postBodyBuffer; - - public HTMLRenderer(I2PAppContext ctx) { - super(ctx); - _log = ctx.logManager().getLog(HTMLRenderer.class); - _parser = new SMLParser(ctx); - } - - /** - * Usage: HTMLRenderer smlFile outputFile - */ - public static void main(String args[]) { - if (args.length != 2) { - System.err.println("Usage: HTMLRenderer smlFile outputFile"); - return; - } - HTMLRenderer renderer = new HTMLRenderer(I2PAppContext.getGlobalContext()); - Writer out = null; - try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(1024*512); - FileInputStream in = new FileInputStream(args[0]); - byte buf[] = new byte[1024]; - int read = 0; - while ( (read = in.read(buf)) != -1) - baos.write(buf, 0, read); - out = new OutputStreamWriter(new FileOutputStream(args[1]), "UTF-8"); - renderer.render(new User(), BlogManager.instance().getArchive(), null, DataHelper.getUTF8(baos.toByteArray()), out, false, true); - } catch (IOException ioe) { - ioe.printStackTrace(); - } finally { - if (out != null) try { out.close(); } catch (IOException ioe) {} - } - } - - /** - * Retrieve: class="s_summary_$element" or class="s_detail_$element ss_$style_detail_$element" - */ - protected String getClass(String element) { - StringBuffer rv = new StringBuffer(64); - rv.append(" class=\"s_"); - if (_cutBody) - rv.append("summary_"); - else - rv.append("detail_"); - rv.append(element); - if (_entry != null) { - String style = sanitizeStyle(_entry.getHeader(HEADER_STYLE)); - if (style != null) { - rv.append(" ss_").append(style); - if (_cutBody) - rv.append("summary_"); - else - rv.append("detail_"); - rv.append(element); - } - } - rv.append("\" "); - return rv.toString(); - } - protected String getSpan(String element) { - return ""; - } - - public void renderUnknownEntry(User user, Archive archive, BlogURI uri, Writer out) throws IOException { - BlogInfo info = archive.getBlogInfo(uri); - if (info == null) - out.write("
    The blog " + uri.getKeyHash().toBase64() + " is not known locally. " - + "Please get it from an archive and try again"); - else - out.write("
    The blog " + info.getProperty(BlogInfo.NAME) + " is known, but the entry " + uri.getEntryId() + " is not. " - + "Please get it from an archive and try again"); - } - - public void render(User user, Archive archive, EntryContainer entry, Writer out, boolean cutBody, boolean showImages) throws IOException { - if (entry == null) - return; - render(user, archive, entry, entry.getEntry().getText(), out, cutBody, showImages); - } - public void render(User user, Archive archive, EntryContainer entry, String rawSML, Writer out, boolean cutBody, boolean showImages) throws IOException { - prepare(user, archive, entry, rawSML, out, cutBody, showImages); - - _out.write(_preBodyBuffer.toString()); - _out.write(_bodyBuffer.toString()); - _out.write(_postBodyBuffer.toString()); - //int len = _preBodyBuffer.length() + _bodyBuffer.length() + _postBodyBuffer.length(); - //System.out.println("Wrote " + len); - } - protected void prepare(User user, Archive archive, EntryContainer entry, String rawSML, Writer out, boolean cutBody, boolean showImages) throws IOException { - _user = user; - _archive = archive; - _entry = entry; - _out = out; - _headers = new HashMap(); - _preBodyBuffer = new StringBuffer(1024); - _bodyBuffer = new StringBuffer(1024); - _postBodyBuffer = new StringBuffer(1024); - _addresses = new ArrayList(); - _links = new ArrayList(); - _blogs = new ArrayList(); - _archives = new ArrayList(); - _cutBody = cutBody; - _showImages = showImages; - _cutReached = false; - _cutSize = 1024; - _parser.parse(rawSML, this); - } - - public void receivePlain(String text) { - if (!continueBody()) { return; } - if (_log.shouldLog(Log.DEBUG)) _log.debug("receive plain [" + text + "]"); - _bodyBuffer.append(sanitizeString(text)); - } - - public void receiveBold(String text) { - if (!continueBody()) { return; } - _bodyBuffer.append("").append(sanitizeString(text)).append(""); - } - public void receiveItalic(String text) { - if (!continueBody()) { return; } - _bodyBuffer.append("").append(sanitizeString(text)).append(""); - } - public void receiveUnderline(String text) { - if (!continueBody()) { return; } - _bodyBuffer.append("").append(sanitizeString(text)).append(""); - } - public void receiveHR() { - if (!continueBody()) { return; } - if (_log.shouldLog(Log.DEBUG)) _log.debug("receive HR"); - _bodyBuffer.append(getSpan("hr")).append("


    "); - } - public void receiveH1(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append("

    ").append(sanitizeString(body)).append("

    "); - } - public void receiveH2(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append("

    ").append(sanitizeString(body)).append("

    "); - } - public void receiveH3(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append("

    ").append(sanitizeString(body)).append("

    "); - } - public void receiveH4(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append("

    ").append(sanitizeString(body)).append("

    "); - } - public void receiveH5(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append("
    ").append(sanitizeString(body)).append("
    "); - } - public void receivePre(String body) { - if (!continueBody()) { return; } - if (_log.shouldLog(Log.DEBUG)) _log.debug("receive pre: [" + sanitizeString(body) + "]"); - _bodyBuffer.append("
    ").append(sanitizeString(body)).append("
    "); - } - - public void receiveQuote(String text, String whoQuoted, String quoteLocationSchema, String quoteLocation) { - if (!continueBody()) { return; } - _bodyBuffer.append("").append(sanitizeString(text)).append(""); - } - public void receiveCode(String text, String codeLocationSchema, String codeLocation) { - if (!continueBody()) { return; } - _bodyBuffer.append("").append(sanitizeString(text)).append(""); - } - public void receiveImage(String alternateText, int attachmentId) { - if (!continueBody()) { return; } - if (_showImages) { - _bodyBuffer.append("\"").append(sanitizeTagParam(alternateText)).append("\"");"); - } else { - _bodyBuffer.append(getSpan("imgSummary")).append("[image: ").append(getSpan("imgSummaryAttachment")).append(" attachment ").append(attachmentId); - _bodyBuffer.append(": ").append(getSpan("imgSummaryAlt")).append(sanitizeString(alternateText)); - _bodyBuffer.append(" view images]"); - } - } - - public void receiveCut(String summaryText) { - if (!continueBody()) { return; } - _cutReached = true; - if (_cutBody) { - _bodyBuffer.append(""); - if ( (summaryText != null) && (summaryText.length() > 0) ) - _bodyBuffer.append(sanitizeString(summaryText)); - else - _bodyBuffer.append("more inside..."); - _bodyBuffer.append("\n"); - } else { - if (summaryText != null) - _bodyBuffer.append(getSpan("cutIgnore")).append(sanitizeString(summaryText)).append("\n"); - } - } - - /** are we either before the cut or rendering without cutting? */ - protected boolean continueBody() { - boolean rv = ( (!_cutReached) && (_bodyBuffer.length() <= _cutSize) ) || (!_cutBody); - //if (!rv) - // System.out.println("rv: " + rv + " Cut reached: " + _cutReached + " bodyBufferSize: " + _bodyBuffer.length() + " cutBody? " + _cutBody); - if (!rv && !_cutReached) { - // exceeded the allowed size - _bodyBuffer.append("more inside...\n"); - _cutReached = true; - } - return rv; - } - - public void receiveNewline() { - if (!continueBody()) { return; } - if (_log.shouldLog(Log.DEBUG)) _log.debug("receive NL"); - if (true || (_lastNewlineAt >= _bodyBuffer.length())) - _bodyBuffer.append(getSpan("nl")).append("
    \n"); - else - _lastNewlineAt = _bodyBuffer.length(); - } - public void receiveLT() { - if (!continueBody()) { return; } - _bodyBuffer.append(getSpan("lt")).append("<"); - } - public void receiveGT() { - if (!continueBody()) { return; } - _bodyBuffer.append(getSpan("gt")).append(">"); - } - public void receiveBegin() {} - public void receiveLeftBracket() { - if (!continueBody()) { return; } - if (_log.shouldLog(Log.DEBUG)) _log.debug("receive ["); - _bodyBuffer.append(getSpan("lb")).append("["); - } - public void receiveRightBracket() { - if (!continueBody()) { return; } - if (_log.shouldLog(Log.DEBUG)) _log.debug("receive ]"); - _bodyBuffer.append(getSpan("rb")).append("]"); - } - - /** - * when we see a link to a blog, we may want to: - * = view the blog entry - * = view all entries in that blog - * = view all entries in that blog with the given tag - * = view the blog's metadata - * = [fetch the blog from other locations] - * = [add the blog's locations to our list of known locations] - * = [shitlist the blog] - * = [add the blog to one of our groups] - * - * [blah] implies *later*. - * - * Currently renders to: - * $description - * [blog: $name (meta) - * [tag: $tag] - * archived at $location*] - * - */ - public void receiveBlog(String name, String hash, String tag, long entryId, List locations, String description) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Receiving the blog: " + name + "/" + hash + "/" + tag + "/" + entryId +"/" + locations + ": "+ description); - byte blogData[] = Base64.decode(hash); - if ( (blogData == null) || (blogData.length != Hash.HASH_LENGTH) ) - return; - - Blog b = new Blog(); - b.name = name; - b.hash = hash; - b.tag = tag; - b.entryId = entryId; - b.locations = locations; - if (!_blogs.contains(b)) - _blogs.add(b); - - if (!continueBody()) { return; } - if (hash == null) return; - - Hash blog = new Hash(blogData); - if (entryId > 0) { - String pageURL = getPageURL(blog, tag, entryId, -1, -1, true, (_user != null ? _user.getShowImages() : false)); - _bodyBuffer.append(""); - if ( (description != null) && (description.trim().length() > 0) ) { - _bodyBuffer.append(sanitizeString(description)); - } else if ( (name != null) && (name.trim().length() > 0) ) { - _bodyBuffer.append(sanitizeTagParam(name)); - } else { - _bodyBuffer.append("[view entry]"); - } - _bodyBuffer.append(""); - } else if ( (description != null) && (description.trim().length() > 0) ) { - _bodyBuffer.append(sanitizeString(description)); - } - - //String url = getPageURL(blog, null, -1, -1, -1, (_user != null ? _user.getShowExpanded() : false), (_user != null ? _user.getShowImages() : false)); - String url = getMetadataURL(blog); - _bodyBuffer.append(getSpan("blogEntrySummary")); - _bodyBuffer.append(" ["); - if ( (name != null) && (name.trim().length() > 0) ) - _bodyBuffer.append(sanitizeTagParam(name)); - else - _bodyBuffer.append("view"); - _bodyBuffer.append(""); - //_bodyBuffer.append(" (meta)"); - if ( (tag != null) && (tag.trim().length() > 0) ) { - url = getPageURL(blog, tag, -1, -1, -1, false, false); - _bodyBuffer.append(" Tag: ").append(sanitizeString(tag)).append(""); - } - if ( (locations != null) && (locations.size() > 0) ) { - _bodyBuffer.append(getSpan("blogArchive")).append(" Archives: "); - for (int i = 0; i < locations.size(); i++) { - SafeURL surl = (SafeURL)locations.get(i); - if (_user.getAuthenticated() && BlogManager.instance().authorizeRemote(_user) ) - _bodyBuffer.append(" ").append(sanitizeString(surl.toString())).append(" "); - else - _bodyBuffer.append(getSpan("blogArchiveURL")).append(sanitizeString(surl.toString())).append(" "); - } - _bodyBuffer.append(""); - } - _bodyBuffer.append("] "); - } - - public void receiveArchive(String name, String description, String locationSchema, String location, - String postingKey, String anchorText) { - ArchiveRef a = new ArchiveRef(); - a.name = name; - a.description = description; - a.locationSchema = locationSchema; - a.location = location; - if (!_archives.contains(a)) - _archives.add(a); - - if (!continueBody()) { return; } - - _bodyBuffer.append(getSpan("archive")).append(sanitizeString(anchorText)).append(""); - _bodyBuffer.append(getSpan("archiveSummary")).append(" [Archive "); - if (name != null) - _bodyBuffer.append(getSpan("archiveSummaryName")).append(sanitizeString(name)).append(""); - if (location != null) { - _bodyBuffer.append(" at "); - SafeURL surl = new SafeURL(locationSchema + "://" + location); - if (BlogManager.instance().authorizeRemote(_user)) { - _bodyBuffer.append("").append(sanitizeString(surl.toString())).append(""); - } else { - _bodyBuffer.append(sanitizeString(surl.getLocation())); - } - if (_user.getAuthenticated()) { - _bodyBuffer.append(" bookmark it"); - } - } - if (description != null) - _bodyBuffer.append(": ").append(getSpan("archiveSummaryDesc")).append(sanitizeString(description)).append(""); - _bodyBuffer.append("]"); - } - - public void receiveLink(String schema, String location, String text) { - Link l = new Link(); - l.schema = schema; - l.location = location; - if (!_links.contains(l)) - _links.add(l); - if (!continueBody()) { return; } - if ( (schema == null) || (location == null) ) return; - _bodyBuffer.append(""). - append(sanitizeString(text)). - append(""); - } - - public void importAddress(Address a) { - BlogPostInfoRenderer.importAddress(a, _user); - } - - public void receiveAddress(String name, String schema, String protocol, String location, String anchorText) { - Address a = new Address(); - a.name = name; - a.schema = schema; - a.location = location; - a.protocol = protocol; - if (!_addresses.contains(a)) - _addresses.add(a); - if (!continueBody()) { return; } - if ( (schema == null) || (location == null) ) return; - PetName pn = null; - if (_user != null) - pn = _user.getPetNameDB().getByLocation(location); - if (pn != null) { - _bodyBuffer.append(getSpan("addr")).append(sanitizeString(anchorText)).append(""); - _bodyBuffer.append(getSpan("addrKnownName")).append("(").append(sanitizeString(pn.getName())).append(")"); - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Receiving address [" + location + "]"); - _bodyBuffer.append("(view parent)\n"); - - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("\n"); - } else { - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("
    \n"); - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("\n"); - - if ( (_entry != null) && (_entry.getAttachments() != null) && (_entry.getAttachments().length > 0) ) { - _postBodyBuffer.append(getSpan("summDetailAttachment")).append("Attachments: "); - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("
    \n"); - } - - if (_blogs.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailBlog")).append("Blog references:"); - for (int i = 0; i < _blogs.size(); i++) { - Blog b = (Blog)_blogs.get(i); - _postBodyBuffer.append("").append(sanitizeString(b.name)).append(" "); - } - _postBodyBuffer.append("
    \n"); - } - - if (_links.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailExternal")).append("External links: "); - for (int i = 0; i < _links.size(); i++) { - Link l = (Link)_links.get(i); - String schema = l.schema; - _postBodyBuffer.append("").append(sanitizeString(l.location, 30)); - _postBodyBuffer.append(getSpan("summDetailExternalNet")).append(" (").append(sanitizeString(l.schema)).append(") "); - } - _postBodyBuffer.append("
    \n"); - } - - if (_addresses.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailAddr")).append("Addresses:"); - for (int i = 0; i < _addresses.size(); i++) { - Address a = (Address)_addresses.get(i); - importAddress(a); - PetName pn = null; - if (_user != null) - pn = _user.getPetNameDB().getByLocation(a.location); - if (pn != null) { - _postBodyBuffer.append(' ').append(getSpan("summDetailAddrKnown")); - _postBodyBuffer.append(sanitizeString(pn.getName())).append(""); - } else { - _postBodyBuffer.append(" ").append(sanitizeString(a.name)).append(""); - } - } - _postBodyBuffer.append("
    \n"); - } - - if (_archives.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailArchive")).append("Archives:"); - for (int i = 0; i < _archives.size(); i++) { - ArchiveRef a = (ArchiveRef)_archives.get(i); - _postBodyBuffer.append(" ").append(sanitizeString(a.name)).append(""); - if (a.description != null) - _postBodyBuffer.append(": ").append(getSpan("summDetailArchiveDesc")).append(sanitizeString(a.description)).append(""); - if (null == _user.getPetNameDB().getByLocation(a.location)) { - _postBodyBuffer.append(" bookmark it"); - } - } - _postBodyBuffer.append("
    \n"); - } - - if (_entry != null) { - List replies = _archive.getIndex().getReplies(_entry.getURI()); - if ( (replies != null) && (replies.size() > 0) ) { - _postBodyBuffer.append(getSpan("summDetailReplies")).append("Replies: "); - for (int i = 0; i < replies.size(); i++) { - BlogURI reply = (BlogURI)replies.get(i); - _postBodyBuffer.append(""); - _postBodyBuffer.append(getSpan("summDetailReplyAuthor")); - BlogInfo replyAuthor = _archive.getBlogInfo(reply); - if (replyAuthor != null) { - _postBodyBuffer.append(sanitizeString(replyAuthor.getProperty(BlogInfo.NAME))); - } else { - _postBodyBuffer.append(reply.getKeyHash().toBase64().substring(0,16)); - } - _postBodyBuffer.append(" on "); - _postBodyBuffer.append(getSpan("summDetailReplyDate")); - _postBodyBuffer.append(getEntryDate(reply.getEntryId())); - _postBodyBuffer.append(" "); - } - _postBodyBuffer.append("
    "); - } - } - - String inReplyTo = (String)_headers.get(HEADER_IN_REPLY_TO); - if ( (inReplyTo != null) && (inReplyTo.trim().length() > 0) ) { - _postBodyBuffer.append(" (view parent)\n"); - } - - _postBodyBuffer.append("\n
    \n\n"); - _postBodyBuffer.append("\n"); - } - _postBodyBuffer.append("\n"); - } - - public void receiveHeader(String header, String value) { - //System.err.println("Receive header [" + header + "] = [" + value + "]"); - if (HEADER_PETNAME.equals(header)) { - StringTokenizer tok = new StringTokenizer(value, "\t\n"); - if (tok.countTokens() != 4) - return; - String name = tok.nextToken(); - String net = tok.nextToken(); - String proto = tok.nextToken(); - String loc = tok.nextToken(); - Address a = new Address(); - a.name = sanitizeString(name, false); - a.schema = sanitizeString(net, false); - a.protocol = sanitizeString(proto, false); - a.location = sanitizeString(loc, false); - _addresses.add(a); - } else { - _headers.put(header, value); - } - } - - public void receiveHeaderEnd() { - _preBodyBuffer.append("\n"); - renderSubjectCell(); - renderMetaCell(); - renderPreBodyCell(); - } - - public static final String HEADER_SUBJECT = "Subject"; - public static final String HEADER_BGCOLOR = "bgcolor"; - public static final String HEADER_IN_REPLY_TO = "InReplyTo"; - public static final String HEADER_STYLE = "Style"; - public static final String HEADER_PETNAME = "PetName"; - public static final String HEADER_TAGS = "Tags"; - /** if set to true, don't display the message in the same thread, though keep a parent reference */ - public static final String HEADER_FORCE_NEW_THREAD = "ForceNewThread"; - /** if set to true, don't let anyone else reply in the same thread (but let the original author reply) */ - public static final String HEADER_REFUSE_REPLIES = "RefuseReplies"; - - private void renderSubjectCell() { - _preBodyBuffer.append(""); - _preBodyBuffer.append(""); - _preBodyBuffer.append("\n"); - } - - private void renderPreBodyCell() { - _preBodyBuffer.append(""); - String bgcolor = (String)_headers.get(HEADER_BGCOLOR); - _preBodyBuffer.append(""); - _preBodyBuffer.append(""); - _preBodyBuffer.append("\n"); - } - - private final SimpleDateFormat _dateFormat = new SimpleDateFormat("yyyy/MM/dd", Locale.UK); - public final String getEntryDate(long when) { - synchronized (_dateFormat) { - try { - String str = _dateFormat.format(new Date(when)); - long dayBegin = _dateFormat.parse(str).getTime(); - return str + " [" + (when - dayBegin) + "]"; - } catch (ParseException pe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error formatting", pe); - // wtf - return "unknown"; - } - } - } - - public static final String sanitizeString(String str) { return sanitizeString(str, true); } - public static final String sanitizeString(String str, int maxLen) { return sanitizeString(str, true, maxLen); } - public static final String sanitizeString(String str, boolean allowNL) { return sanitizeString(str, allowNL, -1); } - public static final String sanitizeString(String str, boolean allowNL, int maxLen) { - if (str == null) return null; - boolean unsafe = false; - unsafe = unsafe || str.indexOf('<') >= 0; - unsafe = unsafe || str.indexOf('>') >= 0; - if (!allowNL) { - unsafe = unsafe || str.indexOf('\n') >= 0; - unsafe = unsafe || str.indexOf('\r') >= 0; - unsafe = unsafe || str.indexOf('\f') >= 0; - } - if (unsafe) { - //str = str.replace('<', '_'); // this should be < - //str = str.replace('>', '-'); // this should be > - str = str.replaceAll("<", "<"); - str = str.replaceAll(">", ">"); - if (!allowNL) { - //str = str.replace('\n', ' '); - //str = str.replace('\r', ' '); - //str = str.replace('\f', ' '); - str = str.replaceAll("\n", "
    "); // no class - str = str.replaceAll("\r", "
    "); // no class - str = str.replaceAll("\f", "
    "); // no class - } - } - if ( (maxLen > 0) && (str.length() > maxLen) ) - return str.substring(0, maxLen) + "..."; - else - return str; - } - - public static final String sanitizeURL(String str) { - if (str == null) return ""; - return Base64.encode(DataHelper.getUTF8(str)); - } - public static final String sanitizeTagParam(String str) { - if (str == null) return ""; - //str = str.replace('&', '_'); // this should be & - str = str.replaceAll("&", "&"); - - if (str.indexOf("\"") < 0 && str.indexOf("'") < 0) - return sanitizeString(str); - - str = str.replaceAll("\"", """); - str = str.replaceAll("'", "'"); // as ', but supported by IE - - return sanitizeString(str); - } - - public static final String sanitizeXML(String orig) { - if (orig == null) return ""; - if (orig.indexOf('&') < 0) return orig; - if (true) return orig.replaceAll("&", "&"); - StringBuffer rv = new StringBuffer(orig.length()+32); - for (int i = 0; i < orig.length(); i++) { - if (orig.charAt(i) == '&') - rv.append("&"); - else - rv.append(orig.charAt(i)); - } - return rv.toString(); - } - public static final String sanitizeXML(StringBuffer orig) { - if (orig == null) return ""; - if (orig.indexOf("&") >= 0) - return orig.toString().replaceAll("&", "&"); - else - return orig.toString(); - } - public static final String sanitizeStrippedXML(String orig) { - if (orig == null) return ""; - orig = orig.replaceAll("&", "&"); - orig = orig.replaceAll("<", "<"); - orig = orig.replaceAll(">", ">"); - return orig; - } - - private static final String STYLE_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; - public static String sanitizeStyle(String style) { - if ( (style == null) || (style.trim().length() <= 0) ) return null; - char c[] = style.toCharArray(); - for (int i = 0; i < c.length; i++) - if (STYLE_CHARS.indexOf(c[i]) < 0) - c[i] = '_'; - return new String(c); - } - - protected String getEntryURL() { return getEntryURL(_user != null ? _user.getShowImages() : false); } - protected String getEntryURL(boolean showImages) { - if (_entry == null) return "unknown"; - return "threads.jsp?" + ThreadedHTMLRenderer.PARAM_AUTHOR + '=' + - Base64.encode(_entry.getURI().getKeyHash().getData()) + '&' + - ThreadedHTMLRenderer.PARAM_VIEW_POST + '=' + _entry.getURI().getKeyHash().toBase64() + '/' + - _entry.getURI().getEntryId(); - } - - protected String getAttachmentURLBase() { return "viewattachment.jsp?"; } - protected String getAttachmentURL(int id) { - if (_entry == null) return "unknown"; - return getAttachmentURLBase() + - ArchiveViewerBean.PARAM_BLOG + "=" + - Base64.encode(_entry.getURI().getKeyHash().getData()) + - "&" + ArchiveViewerBean.PARAM_ENTRY + "=" + _entry.getURI().getEntryId() + - "&" + ArchiveViewerBean.PARAM_ATTACHMENT + "=" + id; - } - - public String getMetadataURL() { - if (_entry == null) return "unknown"; - return getMetadataURL(_entry.getURI().getKeyHash()); - } - public String getMetadataURL(Hash blog) { - return "viewmetadata.jsp?" + ArchiveViewerBean.PARAM_BLOG + "=" + - Base64.encode(blog.getData()); - } - - public String getPostURL(Hash blog) { - return "post.jsp?" + ArchiveViewerBean.PARAM_BLOG + "=" + Base64.encode(blog.getData()); - } - public String getPostURL(Hash blog, boolean asReply, String subject, String tags) { - if (asReply && _entry != null) { - StringBuffer rv = new StringBuffer(128); - rv.append("post.jsp?").append(ArchiveViewerBean.PARAM_BLOG).append("=").append(Base64.encode(blog.getData())); - rv.append('&').append(PostServlet.PARAM_PARENT).append('='); - rv.append(Base64.encode("entry://" + _entry.getURI().getKeyHash().toBase64() + "/" + _entry.getURI().getEntryId())); - if (subject != null) - rv.append('&').append(ArchiveViewerBean.PARAM_SUBJECT).append('=').append(Base64.encode(subject)); - if (tags != null) - rv.append('&').append(ArchiveViewerBean.PARAM_TAGS).append('=').append(Base64.encode(tags)); - rv.append('&').append(ArchiveViewerBean.PARAM_PARENT).append('=').append(Base64.encode(_entry.getURI().toString())); - return rv.toString(); - } else { - return getPostURL(blog); - } - } - - /** - * entry may take the form of "base64/messageId", "entry://base64/messageId", or "blog://base64/messageId" - * - */ - public String getPageURL(String entry) { - StringBuffer buf = new StringBuffer(128); - buf.append("threads.jsp?"); - if (entry != null) { - if (entry.startsWith("entry://")) - entry = entry.substring("entry://".length()); - else if (entry.startsWith("blog://")) - entry = entry.substring("blog://".length()); - if (entry.length() > 0) { - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(entry).append('&'); - buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=').append(entry).append('&'); - } - } - return buf.toString(); - } - - public String getPageURL(Hash blog, String tag, long entryId, int numPerPage, int pageNum, boolean expandEntries, boolean showImages) { - return getPageURL(blog, tag, entryId, null, numPerPage, pageNum, expandEntries, showImages); - } - public String getPageURL(Hash blog, String tag, long entryId, String group, int numPerPage, int pageNum, boolean expandEntries, boolean showImages) { - StringBuffer buf = new StringBuffer(128); - buf.append("threads.jsp?"); - if (blog != null) - buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(blog.toBase64()).append('&'); - if (tag != null) - buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(sanitizeTagParam(tag)).append('&'); - String entry = null; - if (entryId >= 0) { - entry = blog.toBase64() + '/' + entryId; - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(entry).append('&'); - buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=').append(entry).append('&'); - } - if ( (pageNum >= 0) && (numPerPage > 0) ) - buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(pageNum*numPerPage).append('&'); - return buf.toString(); - } - public static String getArchiveURL(Hash blog, SafeURL archiveLocation) { - return "syndicate.jsp?" - //+ "action=Continue..." // should this be the case? - + "&" + SyndicateServlet.PARAM_SCHEMA + "=" + sanitizeTagParam(archiveLocation.getSchema()) - + "&" + SyndicateServlet.PARAM_LOCATION + "=" + sanitizeTagParam(archiveLocation.getLocation()); - } - public static String getBookmarkURL(String name, String location, String schema, String protocol) { - return "addresses.jsp?" + AddressesServlet.PARAM_NAME + '=' + sanitizeTagParam(name) - + "&" + AddressesServlet.PARAM_NET + '=' + sanitizeTagParam(schema) - + "&" + AddressesServlet.PARAM_PROTO + '=' + sanitizeTagParam(protocol) - + "&" + AddressesServlet.PARAM_LOC + '=' + sanitizeTagParam(location); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/Link.java b/apps/syndie/java/src/net/i2p/syndie/sml/Link.java deleted file mode 100644 index c343216f7..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/sml/Link.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.i2p.syndie.sml; - -import net.i2p.data.DataHelper; - -/** contains intermediary rendering state */ -class Link { - public String schema; - public String location; - public int hashCode() { return -1; } - public boolean equals(Object o) { - Link l = (Link)o; - return DataHelper.eq(schema, l.schema) && DataHelper.eq(location, l.location); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/RSSRenderer.java b/apps/syndie/java/src/net/i2p/syndie/sml/RSSRenderer.java deleted file mode 100644 index 8e99a50bf..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/sml/RSSRenderer.java +++ /dev/null @@ -1,341 +0,0 @@ -package net.i2p.syndie.sml; - -import java.io.IOException; -import java.io.Writer; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.data.Base64; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.User; -import net.i2p.syndie.data.Attachment; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.EntryContainer; - -/** - * - */ -public class RSSRenderer extends HTMLRenderer { - - public RSSRenderer(I2PAppContext ctx) { - super(ctx); - } - - private static final boolean RSS_EXCERPT_ONLY = false; - - public void render(User user, Archive archive, EntryContainer entry, String urlPrefix, Writer out) throws IOException { - if (entry == null) return; - prepare(user, archive, entry, entry.getEntry().getText(), out, RSS_EXCERPT_ONLY, false); - BlogInfo info = archive.getBlogInfo(entry.getURI()); - - out.write(" \n"); - String subject = sanitizeXML(sanitizeString((String)_headers.get(HEADER_SUBJECT))); - if ( (subject == null) || (subject.length() <= 0) ) - subject = "not specified"; - out.write(" " + subject + "\n"); - out.write(" " + urlPrefix + BlogRenderer.getEntryURL(entry, info, true) + "\n"); - out.write(" syndie://" + entry.getURI().toString() + "\n"); - out.write(" " + getRFC822Date(entry.getURI().getEntryId()) + "\n"); - PetName pn = user.getPetNameDB().getByLocation(entry.getURI().getKeyHash().toBase64()); - String author = null; - if (pn != null) - author = pn.getName(); - if (author == null) { - if (info != null) - author = info.getProperty(BlogInfo.NAME); - } - if (author == null) - author = entry.getURI().getKeyHash().toBase64(); - out.write(" " + sanitizeXML(sanitizeString(author)) + "@syndie.invalid\n"); - String tags[] = entry.getTags(); - if (tags != null) - for (int i = 0; i < tags.length; i++) - out.write(" " + sanitizeXML(sanitizeString(tags[i])) + "\n"); - - out.write(" " + sanitizeXML(_bodyBuffer.toString()) + "\n"); - - renderEnclosures(user, entry, urlPrefix, out); - - out.write(" \n"); - } - - - public void receiveBold(String text) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(text)); - } - public void receiveItalic(String text) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(text)); - } - public void receiveUnderline(String text) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(text)); - } - public void receiveHR() { - if (!continueBody()) { return; } - } - public void receiveH1(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(body)); - } - public void receiveH2(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(body)); - } - public void receiveH3(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(body)); - } - public void receiveH4(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(body)); - } - public void receiveH5(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(body)); - } - public void receivePre(String body) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(body)); - } - - public void receiveQuote(String text, String whoQuoted, String quoteLocationSchema, String quoteLocation) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(text)); - } - public void receiveCode(String text, String codeLocationSchema, String codeLocation) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(text)); - } - public void receiveImage(String alternateText, int attachmentId) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(alternateText)); - } - public void receiveCut(String summaryText) { - if (!continueBody()) { return; } - _cutReached = true; - if (_cutBody) { - if ( (summaryText != null) && (summaryText.length() > 0) ) - _bodyBuffer.append(sanitizeString(summaryText)); - else - _bodyBuffer.append("more inside..."); - } else { - if (summaryText != null) - _bodyBuffer.append(sanitizeString(summaryText)); - } - } - /** are we either before the cut or rendering without cutting? */ - protected boolean continueBody() { - boolean rv = ( (!_cutReached) && (_bodyBuffer.length() <= _cutSize) ) || (!_cutBody); - //if (!rv) - // System.out.println("rv: " + rv + " Cut reached: " + _cutReached + " bodyBufferSize: " + _bodyBuffer.length() + " cutBody? " + _cutBody); - if (!rv && !_cutReached) { - // exceeded the allowed size - _bodyBuffer.append("more inside..."); - _cutReached = true; - } - return rv; - } - public void receiveNewline() { - if (!continueBody()) { return; } - if (true || (_lastNewlineAt >= _bodyBuffer.length())) - _bodyBuffer.append("\n"); - else - _lastNewlineAt = _bodyBuffer.length(); - } - public void receiveBlog(String name, String hash, String tag, long entryId, List locations, String description) { - byte blogData[] = Base64.decode(hash); - if ( (blogData == null) || (blogData.length != Hash.HASH_LENGTH) ) - return; - - Blog b = new Blog(); - b.name = name; - b.hash = hash; - b.tag = tag; - b.entryId = entryId; - b.locations = locations; - if (!_blogs.contains(b)) - _blogs.add(b); - - if (!continueBody()) { return; } - if (hash == null) return; - - Hash blog = new Hash(blogData); - if ( (description != null) && (description.trim().length() > 0) ) { - _bodyBuffer.append(sanitizeString(description)); - } else if ( (name != null) && (name.trim().length() > 0) ) { - _bodyBuffer.append(sanitizeTagParam(name)); - } else { - _bodyBuffer.append("[view entry]"); - } - } - public void receiveArchive(String name, String description, String locationSchema, String location, - String postingKey, String anchorText) { - ArchiveRef a = new ArchiveRef(); - a.name = name; - a.description = description; - a.locationSchema = locationSchema; - a.location = location; - if (!_archives.contains(a)) - _archives.add(a); - - if (!continueBody()) { return; } - - _bodyBuffer.append(sanitizeString(anchorText)); - } - public void receiveLink(String schema, String location, String text) { - Link l = new Link(); - l.schema = schema; - l.location = location; - if (!_links.contains(l)) - _links.add(l); - if (!continueBody()) { return; } - if ( (schema == null) || (location == null) ) return; - _bodyBuffer.append(sanitizeString(text)); - } - public void receiveAddress(String name, String schema, String protocol, String location, String anchorText) { - Address a = new Address(); - a.name = name; - a.schema = schema; - a.location = location; - a.protocol = protocol; - if (!_addresses.contains(a)) - _addresses.add(a); - if (!continueBody()) { return; } - if ( (schema == null) || (location == null) ) return; - PetName pn = null; - if (_user != null) - pn = _user.getPetNameDB().getByLocation(location); - if (pn != null) { - _bodyBuffer.append(sanitizeString(anchorText)); - } else { - _bodyBuffer.append(sanitizeString(anchorText)); - } - } - public void receiveAttachment(int id, int thumb, String anchorText) { - if (!continueBody()) { return; } - _bodyBuffer.append(sanitizeString(anchorText)); - } - - // Mon, 03 Jun 2005 13:04:11 +0000 - private static final SimpleDateFormat _rfc822Date = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z"); - private static final String getRFC822Date(long when) { - synchronized (_rfc822Date) { - return _rfc822Date.format(new Date(when)); - } - } - - private void renderEnclosures(User user, EntryContainer entry, String urlPrefix, Writer out) throws IOException { - int included = 0; - if (entry.getAttachments() != null) { - for (int i = 0; i < _entry.getAttachments().length; i++) { - Attachment a = _entry.getAttachments()[i]; - String url = urlPrefix + sanitizeXML(getAttachmentURL(i)) - + "#" + sanitizeTagParam(a.getName()); // tacked on for readability - out.write(" "); - // we can do neat stuff with Media RSS (http://search.yahoo.com/mrss) here, such as - // include descriptions, titles, keywords, thumbnails, etc - out.write(" \n"); - - if (included == 0) // plain RSS enclosures can only have one enclosure per entry, unlike Media RSS - out.write(" \n"); - included++; - } - } - - /* - if (_blogs.size() > 0) { - for (int i = 0; i < _blogs.size(); i++) { - Blog b = (Blog)_blogs.get(i); - out.write(" \n"); - } - } - - if (_links.size() > 0) { - for (int i = 0; i < _links.size(); i++) { - Link l = (Link)_links.get(i); - StringBuffer url = new StringBuffer(128); - url.append("externallink.jsp?schema="); - url.append(sanitizeURL(l.schema)).append("&location="); - url.append(sanitizeURL(l.location)); - out.write(" \n"); - } - } - - if (_addresses.size() > 0) { - for (int i = 0; i < _addresses.size(); i++) { - Address a = (Address)_addresses.get(i); - - PetName pn = null; - if (_user != null) - pn = _user.getPetNameDB().getByLocation(a.location); - if (pn == null) { - StringBuffer url = new StringBuffer(128); - url.append("addresses.jsp?").append(AddressesServlet.PARAM_NAME).append('='); - url.append(sanitizeTagParam(a.schema)).append("&").append(AddressesServlet.PARAM_LOC).append("="); - url.append(sanitizeTagParam(a.location)).append("&").append(AddressesServlet.PARAM_NAME).append("="); - url.append(sanitizeTagParam(a.name)).append("&").append(AddressesServlet.PARAM_PROTO).append("="); - url.append(sanitizeTagParam(a.protocol)); - out.write(" \n"); - } - } - } - - if (_archives.size() > 0) { - for (int i = 0; i < _archives.size(); i++) { - ArchiveRef a = (ArchiveRef)_archives.get(i); - String url = getArchiveURL(null, new SafeURL(a.locationSchema + "://" + a.location)); - out.write(" \n"); - } - } - - if (_entry != null) { - List replies = _archive.getIndex().getReplies(_entry.getURI()); - if ( (replies != null) && (replies.size() > 0) ) { - for (int i = 0; i < replies.size(); i++) { - BlogURI reply = (BlogURI)replies.get(i); - String url = getPageURL(reply.getKeyHash(), null, reply.getEntryId(), -1, -1, true, _user.getShowImages()); - out.write(" \n"); - } - } - } - - String inReplyTo = (String)_headers.get(HEADER_IN_REPLY_TO); - if ( (inReplyTo != null) && (inReplyTo.trim().length() > 0) ) { - String url = getPageURL(sanitizeTagParam(inReplyTo)); - out.write(" \n"); - } - */ - } - - public void receiveHeaderEnd() {} - public void receiveEnd() {} - - public static void main(String args[]) { - test(""); - test("&"); - test("a&"); - test("&a"); - test("a&a"); - test("aa&aa"); - } - private static final void test(String str) { - StringBuffer t = new StringBuffer(str); - String sanitized = sanitizeXML(t); - System.out.println("[" + str + "] --> [" + sanitized + "]"); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/SMLParser.java b/apps/syndie/java/src/net/i2p/syndie/sml/SMLParser.java deleted file mode 100644 index fd9d64387..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/sml/SMLParser.java +++ /dev/null @@ -1,472 +0,0 @@ -package net.i2p.syndie.sml; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import net.i2p.I2PAppContext; -import net.i2p.syndie.data.SafeURL; -import net.i2p.util.Log; - -/** - * Parse out the SML from the text, firing off info to the receiver whenever certain - * elements are available. This is a very simple parser, with no support for nested - * tags. A simple stack would be good to add, but DTSTTCPW. - * - * - */ -public class SMLParser { - private Log _log; - private static final char TAG_BEGIN = '['; - private static final char TAG_END = ']'; - private static final char LT = '<'; - private static final char GT = '>'; - private static final char EQ = '='; - private static final char DQUOTE = '"'; - private static final char QUOTE = '\''; - private static final String WHITESPACE = " \t\n\r"; - private static final char NL = '\n'; - private static final char CR = '\n'; - private static final char LF = '\f'; - - public SMLParser(I2PAppContext ctx) { - _log = ctx.logManager().getLog(SMLParser.class); - } - - public void parse(String rawSML, EventReceiver receiver) { - receiver.receiveBegin(); - int off = 0; - off = parseHeaders(rawSML, off, receiver); - receiver.receiveHeaderEnd(); - parseBody(rawSML, off, receiver); - receiver.receiveEnd(); - } - - private int parseHeaders(String rawSML, int off, EventReceiver receiver) { - if (rawSML == null) return off; - int len = rawSML.length(); - if (len == off) return off; - int keyBegin = off; - int valBegin = -1; - while (off < len) { - char c = rawSML.charAt(off); - if ( (c == ':') && (valBegin < 0) ) { - // moving on to the value - valBegin = off + 1; - } else if (c == '\n') { - if (valBegin < 0) { - // end of the headers - off++; - break; - } else { - String key = rawSML.substring(keyBegin, valBegin-1); - String val = rawSML.substring(valBegin, off); - receiver.receiveHeader(key.trim(), val.trim()); - valBegin = -1; - keyBegin = off + 1; - } - } - off++; - } - if ( (off >= len) && (valBegin > 0) ) { - String key = rawSML.substring(keyBegin, valBegin-1); - String val = rawSML.substring(valBegin, len); - receiver.receiveHeader(key.trim(), val.trim()); - } - return off; - } - - private void parseBody(String rawSMLBody, int off, EventReceiver receiver) { - if (rawSMLBody == null) return; - int begin = off; - int len = rawSMLBody.length(); - if (len <= off) return; - int openTagBegin = -1; - int openTagEnd = -1; - int closeTagBegin = -1; - int closeTagEnd = -1; - while (off < len) { - char c = rawSMLBody.charAt(off); - if ( (c == NL) || (c == CR) || (c == LF) ) { - // we only deal with newlines outside of a tag, since this is a ghetto parser - // without a stack, and the tag event is fired only when the tag is completed. - if (openTagBegin < 0) { - if (begin < off) - receiver.receivePlain(rawSMLBody.substring(begin, off)); - receiver.receiveNewline(); - off++; - begin = off; - continue; - } - } else if (c == TAG_BEGIN) { - if ( (off + 1 < len) && (TAG_BEGIN == rawSMLBody.charAt(off+1))) { - if (begin < off) - receiver.receivePlain(rawSMLBody.substring(begin, off)); - receiver.receiveLeftBracket(); - off += 2; - begin = off; - continue; - } else if (openTagBegin < 0) { - // push everything seen and not accounted for into a plain area - if (closeTagEnd < 0) { - if (begin < off) - receiver.receivePlain(rawSMLBody.substring(begin, off)); - } else { - if (closeTagEnd + 1 < off) - receiver.receivePlain(rawSMLBody.substring(closeTagEnd+1, off)); - } - openTagBegin = off; - closeTagBegin = -1; - begin = off + 1; - } else { - // ok, we are at the end of the tag, process it - closeTagBegin = off; - while ( (c != TAG_END) && (off < len) ) { - off++; - c = rawSMLBody.charAt(off); - } - parseTag(rawSMLBody, openTagBegin, openTagEnd, closeTagBegin, off, receiver); - begin = off + 1; - openTagBegin = -1; - openTagEnd = -1; - closeTagBegin = -1; - closeTagEnd = -1; - } - } else if (c == TAG_END) { - if ( (openTagBegin > 0) && (closeTagBegin < 0) ) { - openTagEnd = off; - } else if ( (off + 1 < len) && (TAG_END == rawSMLBody.charAt(off+1))) { - if (begin < off) - receiver.receivePlain(rawSMLBody.substring(begin, off)); - receiver.receiveRightBracket(); - off += 2; - begin = off; - continue; - } - } else if (c == LT) { - // see above re: newlines inside tags for why we check openTagBegin<0 - if (openTagBegin < 0) { - if (begin < off) - receiver.receivePlain(rawSMLBody.substring(begin, off)); - receiver.receiveLT(); - off++; - begin = off; - continue; - } - } else if (c == GT) { - // see above re: newlines inside tags for why we check openTagBegin<0 - if (openTagBegin < 0) { - if (begin < off) - receiver.receivePlain(rawSMLBody.substring(begin, off)); - receiver.receiveGT(); - off++; - begin = off; - continue; - } - } - - off++; - } - if ( (off >= len) && (openTagBegin < 0) ) { - if (closeTagEnd < 0) { - if (begin < off) - receiver.receivePlain(rawSMLBody.substring(begin, off)); - } else { - if (closeTagEnd + 1 < off) - receiver.receivePlain(rawSMLBody.substring(closeTagEnd+1, off)); - } - } - } - - private void parseTag(String source, int openTagBegin, int openTagEnd, int closeTagBegin, int closeTagEnd, EventReceiver receiver) { - String tagName = getTagName(source, openTagBegin+1); - Map attributes = getAttributes(source, openTagBegin+1+tagName.length(), openTagEnd); - String body = null; - if (openTagEnd + 1 >= closeTagBegin) - body = ""; - else - body = source.substring(openTagEnd+1, closeTagBegin); - - //System.out.println("Receiving tag [" + tagName + "] w/ open [" + source.substring(openTagBegin+1, openTagEnd) - // + "], close [" + source.substring(closeTagBegin+1, closeTagEnd) + "] body [" - // + body + "] attributes: " + attributes); - parseTag(tagName, attributes, body, receiver); - } - - private static final String T_BOLD = "b"; - private static final String T_ITALIC = "i"; - private static final String T_UNDERLINE = "u"; - private static final String T_CUT = "cut"; - private static final String T_IMAGE = "img"; - private static final String T_QUOTE = "quote"; - private static final String T_CODE = "code"; - private static final String T_BLOG = "blog"; - private static final String T_LINK = "link"; - private static final String T_ADDRESS = "address"; - private static final String T_H1 = "h1"; - private static final String T_H2 = "h2"; - private static final String T_H3 = "h3"; - private static final String T_H4 = "h4"; - private static final String T_H5 = "h5"; - private static final String T_HR = "hr"; - private static final String T_PRE = "pre"; - private static final String T_ATTACHMENT = "attachment"; - private static final String T_ARCHIVE = "archive"; - - private static final String P_THUMBNAIL = "thumbnail"; - private static final String P_ATTACHMENT = "attachment"; - private static final String P_WHO_QUOTED = "author"; - private static final String P_QUOTE_LOCATION = "location"; - private static final String P_CODE_LOCATION = "location"; - private static final String P_BLOG_NAME = "name"; - private static final String P_BLOG_HASH = "bloghash"; - private static final String P_BLOG_TAG = "blogtag"; - private static final String P_BLOG_ENTRY = "blogentry"; - private static final String P_LINK_LOCATION = "location"; - private static final String P_LINK_SCHEMA = "schema"; - private static final String P_ADDRESS_NAME = "name"; - private static final String P_ADDRESS_LOCATION = "location"; - private static final String P_ADDRESS_SCHEMA = "schema"; - private static final String P_ADDRESS_PROTOCOL = "proto"; - private static final String P_ATTACHMENT_ID = "id"; - private static final String P_ARCHIVE_NAME = "name"; - private static final String P_ARCHIVE_DESCRIPTION = "description"; - private static final String P_ARCHIVE_LOCATION_SCHEMA = "schema"; - private static final String P_ARCHIVE_LOCATION = "location"; - private static final String P_ARCHIVE_POSTING_KEY = "postingkey"; - - private void parseTag(String tagName, Map attr, String body, EventReceiver receiver) { - tagName = tagName.toLowerCase(); - if (T_BOLD.equals(tagName)) { - receiver.receiveBold(body); - } else if (T_ITALIC.equals(tagName)) { - receiver.receiveItalic(body); - } else if (T_UNDERLINE.equals(tagName)) { - receiver.receiveUnderline(body); - } else if (T_CUT.equals(tagName)) { - receiver.receiveCut(body); - } else if (T_IMAGE.equals(tagName)) { - receiver.receiveImage(body, getInt(P_ATTACHMENT, attr)); - } else if (T_QUOTE.equals(tagName)) { - receiver.receiveQuote(body, getString(P_WHO_QUOTED, attr), getSchema(P_QUOTE_LOCATION, attr), getLocation(P_QUOTE_LOCATION, attr)); - } else if (T_CODE.equals(tagName)) { - receiver.receiveCode(body, getSchema(P_CODE_LOCATION, attr), getLocation(P_CODE_LOCATION, attr)); - } else if (T_BLOG.equals(tagName)) { - List locations = new ArrayList(); - int i = 0; - while (true) { - String s = getString("archive" + i, attr); - if (s != null) - locations.add(new SafeURL(s)); - else - break; - i++; - } - receiver.receiveBlog(getString(P_BLOG_NAME, attr), getString(P_BLOG_HASH, attr), getString(P_BLOG_TAG, attr), - getLong(P_BLOG_ENTRY, attr), locations, body); - } else if (T_ARCHIVE.equals(tagName)) { - receiver.receiveArchive(getString(P_ARCHIVE_NAME, attr), getString(P_ARCHIVE_DESCRIPTION, attr), - getString(P_ARCHIVE_LOCATION_SCHEMA, attr), getString(P_ARCHIVE_LOCATION, attr), - getString(P_ARCHIVE_POSTING_KEY, attr), body); - } else if (T_LINK.equals(tagName)) { - receiver.receiveLink(getString(P_LINK_SCHEMA, attr), getString(P_LINK_LOCATION, attr), body); - } else if (T_ADDRESS.equals(tagName)) { - receiver.receiveAddress(getString(P_ADDRESS_NAME, attr), getString(P_ADDRESS_SCHEMA, attr), getString(P_ADDRESS_PROTOCOL, attr), getString(P_ADDRESS_LOCATION, attr), body); - } else if (T_H1.equals(tagName)) { - receiver.receiveH1(body); - } else if (T_H2.equals(tagName)) { - receiver.receiveH2(body); - } else if (T_H3.equals(tagName)) { - receiver.receiveH3(body); - } else if (T_H4.equals(tagName)) { - receiver.receiveH4(body); - } else if (T_H5.equals(tagName)) { - receiver.receiveH5(body); - } else if (T_HR.equals(tagName)) { - receiver.receiveHR(); - } else if (T_PRE.equals(tagName)) { - receiver.receivePre(body); - } else if (T_ATTACHMENT.equals(tagName)) { - receiver.receiveAttachment( - (int)getLong(P_ATTACHMENT_ID, attr), - (int)getLong(P_THUMBNAIL, attr), - body); - } else { - if (_log.shouldLog(Log.WARN)) - _log.warn("need to learn how to parse the tag [" + tagName + "]"); - } - } - - private String getString(String param, Map attributes) { return (String)attributes.get(param); } - private String getSchema(String param, Map attributes) { - String url = getString(param, attributes); - if (url != null) { - SafeURL u = new SafeURL(url); - return u.getSchema(); - } else { - return null; - } - } - - private String getLocation(String param, Map attributes) { - String url = getString(param, attributes); - if (url != null) { - SafeURL u = new SafeURL(url); - return u.getLocation(); - } else { - return null; - } - } - - private int getInt(String attributeName, Map attributes) { - String val = (String)attributes.get(attributeName.toLowerCase()); - if (val != null) { - try { - return Integer.parseInt(val.trim()); - } catch (NumberFormatException nfe) { - //nfe.printStackTrace(); - return -1; - } - } else { - return -1; - } - } - - private long getLong(String attributeName, Map attributes) { - String val = (String)attributes.get(attributeName.toLowerCase()); - if (val != null) { - try { - return Long.parseLong(val.trim()); - } catch (NumberFormatException nfe) { - //nfe.printStackTrace(); - return -1; - } - } else { - return -1; - } - } - - private String getTagName(String source, int nameStart) { - int off = nameStart; - while (true) { - char c = source.charAt(off); - if ( (c == TAG_END) || (WHITESPACE.indexOf(c) >= 0) ) - return source.substring(nameStart, off); - off++; - } - } - private Map getAttributes(String source, int attributesStart, int openTagEnd) { - Map rv = new HashMap(); - int off = attributesStart; - int nameStart = -1; - int nameEnd = -1; - int valStart = -1; - int valEnd = -1; - while (true) { - char c = source.charAt(off); - if ( (c == TAG_END) || (off >= openTagEnd) ) - break; - if (WHITESPACE.indexOf(c) < 0) { - if (nameStart < 0) { - nameStart = off; - } else if (c == EQ) { - if (nameEnd < 0) - nameEnd = off; - } else if ( c == DQUOTE ) { - if (valStart < 0) { - valStart = off; - } else { - valEnd = off; - - if ( ( nameStart >= 0 ) && - ( nameEnd >= 0 ) && - ( valStart >= 0 ) && - ( valEnd >= 0 )) { - String name = source.substring(nameStart, nameEnd); - String val = source.substring(valStart+1, valEnd); - rv.put(name.trim(), val.trim()); - } - nameStart = -1; - nameEnd = -1; - valStart = -1; - valEnd = -1; - } - } - } - off++; - } - return rv; - } - - public interface EventReceiver { - public void receiveHeader(String header, String value); - public void receiveLink(String schema, String location, String text); - /** @param blogArchiveLocations list of SafeURL */ - public void receiveBlog(String name, String blogKeyHash, String blogPath, long blogEntryId, - List blogArchiveLocations, String anchorText); - public void receiveArchive(String name, String description, String locationSchema, String location, - String postingKey, String anchorText); - public void receiveImage(String alternateText, int attachmentId); - public void receiveAddress(String name, String schema, String protocol, String location, String anchorText); - public void receiveAttachment(int id, int thumb, String anchorText); - public void receiveBold(String text); - public void receiveItalic(String text); - public void receiveUnderline(String text); - public void receiveH1(String text); - public void receiveH2(String text); - public void receiveH3(String text); - public void receiveH4(String text); - public void receiveH5(String text); - public void receivePre(String text); - public void receiveHR(); - public void receiveQuote(String text, String whoQuoted, String quoteLocationSchema, String quoteLocation); - public void receiveCode(String text, String codeLocationSchema, String codeLocation); - public void receiveCut(String summaryText); - public void receivePlain(String text); - public void receiveNewline(); - public void receiveLT(); - public void receiveGT(); - public void receiveLeftBracket(); - public void receiveRightBracket(); - public void receiveBegin(); - public void receiveEnd(); - public void receiveHeaderEnd(); - } - - public static void main(String args[]) { - test(null); - test(""); - test("A: B"); - test("A: B\n"); - test("A: B\nC: D"); - test("A: B\nC: D\n"); - test("A: B\nC: D\n\n"); - - test("A: B\nC: D\n\nblah"); - test("A: B\nC: D\n\nblah[["); - test("A: B\nC: D\n\nblah]]"); - test("A: B\nC: D\n\nblah]]blah"); - test("A: B\nC: D\n\nfoo[a]b[/a]bar"); - test("A: B\nC: D\n\nfoo[a]b[/a]bar[b][/b]"); - test("A: B\nC: D\n\nfoo[a]b[/a]bar[b][/b]baz"); - - test("A: B\nC: D\n\nhi"); - - test("A: B\n\n[a b=\"c\"]d[/a]"); - test("A: B\n\n[a b=\"c\" d=\"e\" f=\"g\"]h[/a]"); - test("A: B\n\n[a b=\"c\" d=\"e\" f=\"g\"]h[/a][a b=\"c\" d=\"e\" f=\"g\"]h[/a][a b=\"c\" d=\"e\" f=\"g\"]h[/a]"); - - test("A: B\n\n[a b=\"plural c's\" ]d[/a]"); - test("A: B\n\n[a b=\"c\" ]d[/a]"); - - test("A: B\n\n[b]This[/b] is [i]special[/i][cut]why?[/cut][u]because I say so[/u].\neven if you dont care"); - test("A: B\n\nHi\n[pre]>foo&bar<>blah!blah\nblah\nblah[/pre]foo![pre]bar[/pre]"); - //(openTagEnd seems wrong) test("A: B\n\n[link schema=\"web\" location=\"http://w.i2p?i2paddr...\"] Try it [[i2p]] [/link]"); - } - private static void test(String rawSML) { - I2PAppContext ctx = I2PAppContext.getGlobalContext(); - SMLParser parser = new SMLParser(ctx); - parser.parse(rawSML, new EventReceiverImpl(ctx)); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/ThreadedHTMLRenderer.java b/apps/syndie/java/src/net/i2p/syndie/sml/ThreadedHTMLRenderer.java deleted file mode 100644 index 70137b6f6..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/sml/ThreadedHTMLRenderer.java +++ /dev/null @@ -1,601 +0,0 @@ -package net.i2p.syndie.sml; - -import java.io.IOException; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.data.Base64; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.Attachment; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.EntryContainer; -import net.i2p.syndie.data.SafeURL; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.data.ThreadNode; -import net.i2p.syndie.web.AddressesServlet; -import net.i2p.syndie.web.ArchiveViewerBean; -import net.i2p.syndie.web.BaseServlet; -import net.i2p.syndie.web.PostServlet; -import net.i2p.util.Log; - -/** - * - */ -public class ThreadedHTMLRenderer extends HTMLRenderer { - private Log _log; - private String _baseURI; - private boolean _inlineReply; - - public ThreadedHTMLRenderer(I2PAppContext ctx) { - super(ctx); - _log = ctx.logManager().getLog(ThreadedHTMLRenderer.class); - } - - /** what, if any, post should be rendered */ - public static final String PARAM_VIEW_POST = "post"; - /** what, if any, thread should be rendered in its entirety */ - public static final String PARAM_VIEW_THREAD = "thread"; - /** what post should be visible in the nav tree */ - public static final String PARAM_VISIBLE = "visible"; - public static final String PARAM_ADD_TO_GROUP_LOCATION = "addLocation"; - public static final String PARAM_ADD_TO_GROUP_NAME = "addGroup"; - /** name of the bookmarked entry to remove */ - public static final String PARAM_REMOVE_FROM_GROUP_NAME = "removeName"; - /** group to remove from the bookmarked entry, or if blank, remove the entry itself */ - public static final String PARAM_REMOVE_FROM_GROUP = "removeGroup"; - /** add the specified tag to the favorites list */ - public static final String PARAM_ADD_TAG = "addTag"; - /** index into the nav tree to start displaying */ - public static final String PARAM_OFFSET = "offset"; - public static final String PARAM_TAGS = "tags"; - /** only show threads that the given author participates in */ - public static final String PARAM_AUTHOR = "author"; - /** only show threads started by the given author */ - public static final String PARAM_THREAD_AUTHOR = "threadAuthorOnly"; - /** search back through the blog for entries this many days */ - public static final String PARAM_DAYS_BACK = "daysBack"; - // parameters for editing one's profile - public static final String PARAM_PROFILE_NAME = "profileName"; - public static final String PARAM_PROFILE_DESC = "profileDesc"; - public static final String PARAM_PROFILE_URL = "profileURL"; - public static final String PARAM_PROFILE_OTHER = "profileOther"; - - public static String getFilterByTagLink(String uri, ThreadNode node, User user, String tag, String author) { - StringBuffer buf = new StringBuffer(64); - buf.append(uri).append('?'); - if (node != null) { - buf.append(PARAM_VIEW_POST).append('='); - buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); - buf.append(node.getEntry().getEntryId()).append('&'); - } - - if (!empty(tag)) - buf.append(PARAM_TAGS).append('=').append(tag).append('&'); - - if (!empty(author)) - buf.append(PARAM_AUTHOR).append('=').append(author).append('&'); - - return buf.toString(); - } - - public static String getAddTagToFavoritesLink(String uri, String tag, String author, String visible, String viewPost, - String viewThread, String offset) { - //protected String getAddToGroupLink(User user, Hash author, String group, String uri, String visible, - // String viewPost, String viewThread, String offset, String tags, String filteredAuthor) { - StringBuffer buf = new StringBuffer(64); - buf.append(uri); - buf.append('?'); - if (!empty(visible)) - buf.append(PARAM_VISIBLE).append('=').append(visible).append('&'); - buf.append(PARAM_ADD_TAG).append('=').append(sanitizeTagParam(tag)).append('&'); - - if (!empty(viewPost)) - buf.append(PARAM_VIEW_POST).append('=').append(viewPost).append('&'); - else if (!empty(viewThread)) - buf.append(PARAM_VIEW_THREAD).append('=').append(viewThread).append('&'); - - if (!empty(offset)) - buf.append(PARAM_OFFSET).append('=').append(offset).append('&'); - - if (!empty(author)) - buf.append(PARAM_AUTHOR).append('=').append(author).append('&'); - - BaseServlet.addAuthActionParams(buf); - return buf.toString(); - } - - public static String getNavLink(String uri, String viewPost, String viewThread, String tags, String author, boolean authorOnly, int offset) { - StringBuffer buf = new StringBuffer(64); - buf.append(uri); - buf.append('?'); - if (!empty(viewPost)) - buf.append(PARAM_VIEW_POST).append('=').append(viewPost).append('&'); - else if (!empty(viewThread)) - buf.append(PARAM_VIEW_THREAD).append('=').append(viewThread).append('&'); - - if (!empty(tags)) - buf.append(PARAM_TAGS).append('=').append(tags).append('&'); - - if (!empty(author)) { - buf.append(PARAM_AUTHOR).append('=').append(author).append('&'); - if (authorOnly) - buf.append(PARAM_THREAD_AUTHOR).append("=true&"); - } - - buf.append(PARAM_OFFSET).append('=').append(offset).append('&'); - - return buf.toString(); - } - - public static String getViewPostLink(String uri, ThreadNode node, User user, boolean isPermalink, - String offset, String tags, String author, boolean authorOnly) { - if (isPermalink) { - // link to the blog view of the original poster - BlogURI rootBlog = null; - ThreadNode parent = node; - while (parent != null) { - if (parent.getParent() != null) { - parent = parent.getParent(); - } else { - rootBlog = parent.getEntry(); - break; - } - } - BlogInfo root = BlogManager.instance().getArchive().getBlogInfo(rootBlog.getKeyHash()); - return BlogRenderer.getEntryURL(parent.getEntry(), root, node.getEntry(), true); - } else { - StringBuffer buf = new StringBuffer(64); - buf.append(uri); - if (node.getChildCount() > 0) { - buf.append('?').append(PARAM_VISIBLE).append('='); - ThreadNode child = node.getChild(0); - buf.append(child.getEntry().getKeyHash().toBase64()).append('/'); - buf.append(child.getEntry().getEntryId()).append('&'); - } else { - buf.append('?').append(PARAM_VISIBLE).append('='); - buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); - buf.append(node.getEntry().getEntryId()).append('&'); - } - buf.append(PARAM_VIEW_POST).append('='); - buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); - buf.append(node.getEntry().getEntryId()).append('&'); - - if (!isPermalink) { - if (!empty(offset)) - buf.append(PARAM_OFFSET).append('=').append(offset).append('&'); - if (!empty(tags)) - buf.append(PARAM_TAGS).append('=').append(tags).append('&'); - } - - if (authorOnly && !empty(author)) { - buf.append(PARAM_AUTHOR).append('=').append(author).append('&'); - buf.append(PARAM_THREAD_AUTHOR).append("=true&"); - } else if (!isPermalink && !empty(author)) - buf.append(PARAM_AUTHOR).append('=').append(author).append('&'); - - return buf.toString(); - } - } - - public static String getViewPostLink(String uri, BlogURI post, User user, boolean isPermalink, - String offset, String tags, String author, boolean authorOnly) { - StringBuffer buf = new StringBuffer(64); - buf.append(uri); - buf.append('?').append(PARAM_VISIBLE).append('='); - buf.append(post.getKeyHash().toBase64()).append('/'); - buf.append(post.getEntryId()).append('&'); - buf.append(PARAM_VIEW_POST).append('='); - buf.append(post.getKeyHash().toBase64()).append('/'); - buf.append(post.getEntryId()).append('&'); - - if (!isPermalink) { - if (!empty(offset)) - buf.append(PARAM_OFFSET).append('=').append(offset).append('&'); - if (!empty(tags)) - buf.append(PARAM_TAGS).append('=').append(tags).append('&'); - if (!empty(author)) { - buf.append(PARAM_AUTHOR).append('=').append(author).append('&'); - if (authorOnly) - buf.append(PARAM_THREAD_AUTHOR).append("=true&"); - } - } - - return buf.toString(); - } - - private static final boolean empty(String val) { return (val == null) || (val.trim().length() <= 0); } - - /** - * @param replyHiddenFields HTML of hidden input fields necessary for the reply form to be honored - */ - public void render(User user, Writer out, Archive archive, BlogURI post, - boolean inlineReply, ThreadIndex index, String baseURI, String replyHiddenFields, - String offset, String requestTags, String filteredAuthor, boolean authorOnly) throws IOException { - EntryContainer entry = archive.getEntry(post); - if (entry == null) return; - ThreadNode node = index.getNode(post); - if (node == null) { - _log.error("Post is not in the index: " + post.toString()); - return; - } - _entry = entry; - - _baseURI = baseURI; - _user = user; - _out = out; - _archive = archive; - _cutBody = false; - _showImages = true; - _inlineReply = inlineReply; - _headers = new HashMap(); - _bodyBuffer = new StringBuffer(1024); - _postBodyBuffer = new StringBuffer(1024); - _addresses = new ArrayList(); - _links = new ArrayList(); - _blogs = new ArrayList(); - _archives = new ArrayList(); - - _parser.parse(entry.getEntry().getText(), this); - - out.write("\n"); - out.write("\n"); - out.write("\n"); - - String subject = (String)_headers.get(HTMLRenderer.HEADER_SUBJECT); - if (subject == null) - subject = ""; - out.write(" \n"); - out.write("\n\n"); - out.write("\n"); - out.write("\n"); - out.write("\n"); - out.write("\n\n"); - out.write("\n"); - out.write("\n"); - out.write(_postBodyBuffer.toString()); -/* -"\n" + -" \n" + -" \n" + -" \n" + -"\n" + - */ - out.write("\n"); - if (inlineReply && user.getAuthenticated() ) { - String refuseReplies = (String)_headers.get(HTMLRenderer.HEADER_REFUSE_REPLIES); - // show the reply form if we are the author or replies have not been explicitly rejected - if ( (user.getBlog().equals(post.getKeyHash())) || - (refuseReplies == null) || (!Boolean.valueOf(refuseReplies).booleanValue()) ) { - out.write("\n"); - out.write("\n"); - out.write(replyHiddenFields); - out.write(""); - out.write(""); - out.write("\n"); - out.write("\n\n"); - out.write("\n"); - out.write("\n"); - out.write("\n"); - out.write("\n"); - out.write(" \n\n\n"); - out.write("\n"); - } - } - out.write("\n"); - } - - public void receiveEnd() { - _postBodyBuffer.append("\n"); - _postBodyBuffer.append(" \n"); - _postBodyBuffer.append(" \n"); - _postBodyBuffer.append(" \n"); - _postBodyBuffer.append("\n"); - } - - public void receiveHeaderEnd() { - //_preBodyBuffer.append("
    "); - String subject = (String)_headers.get(HEADER_SUBJECT); - if (subject == null) - subject = "[no subject]"; - _preBodyBuffer.append(getSpan("subjectText")).append(sanitizeString(subject)); - _preBodyBuffer.append("
    "); - } - - private void renderMetaPetname(PetName pn, BlogInfo info) { - if (info != null) { - _preBodyBuffer.append(""); - if (pn != null) { - _preBodyBuffer.append(getSpan("metaKnown")).append(sanitizeString(pn.getName())).append(""); - } else { - String nameStr = info.getProperty("Name"); - if (nameStr == null) - _preBodyBuffer.append(getSpan("metaUnknown")).append("[no name]"); - else - _preBodyBuffer.append(getSpan("metaUnknown")).append(sanitizeString(nameStr)).append(""); - } - _preBodyBuffer.append(""); - } else { - _preBodyBuffer.append(getSpan("metaUnknown")).append("[unknown blog]"); - } - } - - protected void renderMetaCell() { - String tags[] = (_entry != null ? _entry.getTags() : null); - _preBodyBuffer.append("\n"); - - PetName pn = null; - if ( (_entry != null) && (_user != null) ) - pn = _user.getPetNameDB().getByLocation(_entry.getURI().getKeyHash().toBase64()); - //if (knownName != null) - // _preBodyBuffer.append("Pet name: ").append(sanitizeString(knownName)).append(" "); - - BlogInfo info = null; - if (_entry != null) - info = _archive.getBlogInfo(_entry.getURI()); - renderMetaPetname(pn, info); - - if ( (_user != null) && (_user.getAuthenticated()) && (_entry != null) ) { - if ( (pn == null) || (!pn.isMember("Favorites")) ) - _preBodyBuffer.append(" "); - if ( (pn == null) || (!pn.isMember("Ignore")) ) - _preBodyBuffer.append(" "); - else - _preBodyBuffer.append(" "); - _preBodyBuffer.append(" "); - if (info != null) - _preBodyBuffer.append(" "); - } - - - if ( (tags != null) && (tags.length > 0) ) { - _preBodyBuffer.append(getSpan("metaTags")).append(" Tags: "); - _preBodyBuffer.append(""); - _preBodyBuffer.append("\n"); - //_preBodyBuffer.append(""); - } - _preBodyBuffer.append(" "); - /* - String inReplyTo = (String)_headers.get(HEADER_IN_REPLY_TO); - if ( (inReplyTo != null) && (inReplyTo.trim().length() > 0) ) - _preBodyBuffer.append(" In reply to\n"); - */ - - _preBodyBuffer.append(getSpan("metaDate")); - if (_entry != null) - _preBodyBuffer.append(getEntryDate(_entry.getURI().getEntryId())); - else - _preBodyBuffer.append(getEntryDate(new Date().getTime())); - _preBodyBuffer.append(""); - - if ( (_user != null) && (_user.getAuthenticated()) ) { - _preBodyBuffer.append(" 0) ) - for (int i = 0; i < tags.length; i++) - tagStr.append(tags[i]).append('\t'); - String replyURL = getPostURL(_user.getBlog(), true, subject, tagStr.toString()); - _preBodyBuffer.append(" href=\"").append(replyURL).append("\">Reply\n"); - } - _preBodyBuffer.append("\n
    "); - out.write(subject); - out.write("
    \n"); - out.write(""); - - String author = null; - PetName pn = user.getPetNameDB().getByLocation(post.getKeyHash().toBase64()); - if (pn == null) { - BlogInfo info = archive.getBlogInfo(post.getKeyHash()); - if (info != null) - author = info.getProperty(BlogInfo.NAME); - } else { - author = pn.getName(); - } - if ( (author == null) || (author.trim().length() <= 0) ) - author = post.getKeyHash().toBase64().substring(0,6); - - out.write(author); - out.write(" @ "); - out.write(getEntryDate(post.getEntryId())); - - Collection tags = node.getTags(); - if ( (tags != null) && (tags.size() > 0) ) { - out.write("\nTags: \n"); - for (Iterator tagIter = tags.iterator(); tagIter.hasNext(); ) { - String tag = (String)tagIter.next(); - out.write(""); - out.write(" " + tag); - out.write("\n"); - if (user.getAuthenticated() && (!user.getFavoriteTags().contains(tag)) && (!"[none]".equals(tag)) ) { - out.write(""); - out.write("\":)\""); - out.write("\n"); - } - } - } - - out.write("\npermalink\n"); - - if (true || (!inlineReply) ) { - String refuseReply = (String)_headers.get(HEADER_REFUSE_REPLIES); - boolean allowReply = false; - if ( (refuseReply != null) && (Boolean.valueOf(refuseReply).booleanValue()) ) { - if (_entry == null ) - allowReply = false; - else if ( (_user == null) || (_user.getBlog() == null) ) - allowReply = false; - else if (_entry.getURI().getKeyHash().equals(_user.getBlog())) - allowReply = true; - else - allowReply = false; - } else { - allowReply = true; - } - if (allowReply && (_entry != null) ) { - out.write("Reply
    \n"); - } - } - - out.write("
    \n"); - out.write(_bodyBuffer.toString()); - out.write("
    \n" + -" External links:\n" + -" http://foo.i2p/\n" + -" http://bar.i2p/\n" + -"
    \n" + -" Attachments: \n" + -"
    Full thread\n" + -" Prev in thread \n" + -" Next in thread \n" + -"
    Reply: (SML reference)
    \n"); - out.write(" \n"); - out.write(" Tags: "); - BaseServlet.writeTagField(_user, "", out, "Optional tags to categorize your response", "No tags", false); - // \n"); - out.write(" in a new thread? \n"); - out.write(" refuse replies? \n"); - out.write(" attachment: \n"); - out.write("
    \n"); - - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("\n"); - - //_postBodyBuffer.append("\n"); - - if ( (_entry != null) && (_entry.getAttachments() != null) && (_entry.getAttachments().length > 0) ) { - _postBodyBuffer.append(getSpan("summDetailAttachment")).append("Attachments: "); - _postBodyBuffer.append("\n"); - _postBodyBuffer.append("
    \n"); - } - - if (_blogs.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailBlog")).append("Blog references:"); - for (int i = 0; i < _blogs.size(); i++) { - Blog b = (Blog)_blogs.get(i); - _postBodyBuffer.append("").append(sanitizeString(b.name, 30)).append(" "); - } - _postBodyBuffer.append("
    \n"); - } - - if (_links.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailExternal")).append("External links: "); - for (int i = 0; i < _links.size(); i++) { - Link l = (Link)_links.get(i); - String schema = l.schema; - _postBodyBuffer.append("").append(sanitizeString(l.location, 30)); - _postBodyBuffer.append(getSpan("summDetailExternalNet")).append(" (").append(sanitizeString(l.schema)).append(") "); - } - _postBodyBuffer.append("
    \n"); - } - - if (_addresses.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailAddr")).append("Addresses:"); - for (int i = 0; i < _addresses.size(); i++) { - Address a = (Address)_addresses.get(i); - importAddress(a); - PetName pn = null; - if (_user != null) - pn = _user.getPetNameDB().getByLocation(a.location); - if (pn != null) { - _postBodyBuffer.append(' ').append(getSpan("summDetailAddrKnown")); - _postBodyBuffer.append(sanitizeString(pn.getName())).append(""); - } else { - _postBodyBuffer.append(" ").append(sanitizeString(a.name, 30)).append(""); - } - } - _postBodyBuffer.append("
    \n"); - } - - if (_archives.size() > 0) { - _postBodyBuffer.append(getSpan("summDetailArchive")).append("Archives:"); - for (int i = 0; i < _archives.size(); i++) { - ArchiveRef a = (ArchiveRef)_archives.get(i); - _postBodyBuffer.append(" ").append(sanitizeString(a.name)).append(""); - if (a.description != null) - _postBodyBuffer.append(": ").append(getSpan("summDetailArchiveDesc")).append(sanitizeString(a.description)).append(""); - if (null == _user.getPetNameDB().getByLocation(a.location)) { - _postBodyBuffer.append(" bookmark it"); - } - } - _postBodyBuffer.append("
    \n"); - } - - if (_entry != null) { - List replies = _archive.getIndex().getReplies(_entry.getURI()); - if ( (replies != null) && (replies.size() > 0) ) { - _postBodyBuffer.append(getSpan("summDetailReplies")).append("Replies: "); - for (int i = 0; i < replies.size(); i++) { - BlogURI reply = (BlogURI)replies.get(i); - _postBodyBuffer.append(""); - _postBodyBuffer.append(getSpan("summDetailReplyAuthor")); - BlogInfo replyAuthor = _archive.getBlogInfo(reply); - if (replyAuthor != null) { - _postBodyBuffer.append(sanitizeString(replyAuthor.getProperty(BlogInfo.NAME))); - } else { - _postBodyBuffer.append(reply.getKeyHash().toBase64().substring(0,16)); - } - _postBodyBuffer.append(" on "); - _postBodyBuffer.append(getSpan("summDetailReplyDate")); - _postBodyBuffer.append(getEntryDate(reply.getEntryId())); - _postBodyBuffer.append(" "); - } - _postBodyBuffer.append("
    "); - } - } - - String inReplyTo = (String)_headers.get(HEADER_IN_REPLY_TO); - if ( (inReplyTo != null) && (inReplyTo.trim().length() > 0) ) { - BlogURI replyURI = new BlogURI(inReplyTo); - if (replyURI.getEntryId() > 0) { - _postBodyBuffer.append(" (view parent)
    \n"); - } - } - - _postBodyBuffer.append("
    \n"); - //renderSubjectCell(); - //renderMetaCell(); - //renderPreBodyCell(); - } - - public String getMetadataURL(Hash blog) { - return buildProfileURL(blog); - } - public static String buildProfileURL(Hash blog) { - if ( (blog != null) && (blog.getData() != null) ) - return "profile.jsp?" + ThreadedHTMLRenderer.PARAM_AUTHOR + "=" + - Base64.encode(blog.getData()); - else - return "profile.jsp"; - } - protected String getEntryURL() { return getEntryURL(_user != null ? _user.getShowImages() : false); } - protected String getEntryURL(boolean showImages) { - if (_entry == null) - return _baseURI; - else - return _baseURI + '?' + PARAM_VIEW_POST + '=' + - Base64.encode(_entry.getURI().getKeyHash().getData()) + '/' - + _entry.getURI().getEntryId() + '&'; - } - - public String getPageURL(User user, String selector, int numPerPage, int pageNum) { return _baseURI; } - - public String getPageURL(Hash blog, String tag, long entryId, String group, int numPerPage, int pageNum, boolean expandEntries, boolean showImages) { - StringBuffer buf = new StringBuffer(128); - buf.append(_baseURI).append('?'); - String entry = null; - if ( (blog != null) && (entryId > 0) ) { - entry = blog.toBase64() + '/' + entryId; - buf.append(PARAM_VIEW_THREAD).append('=').append(entry).append('&'); - buf.append(PARAM_VISIBLE).append('=').append(Base64.encode(blog.getData())).append('/').append(entryId).append('&'); - } else if (blog != null) { - buf.append(PARAM_AUTHOR).append('=').append(blog.toBase64()).append('&'); - } - if (tag != null) - buf.append(PARAM_TAGS).append('=').append(sanitizeTagParam(tag)).append('&'); - if ( (blog != null) && (entryId > 0) ) - buf.append("#blog://").append(entry); - return buf.toString(); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/AddressesServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/AddressesServlet.java deleted file mode 100644 index ff1a49ccc..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/AddressesServlet.java +++ /dev/null @@ -1,504 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Iterator; -import java.util.TreeSet; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.client.naming.PetName; -import net.i2p.client.naming.PetNameDB; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.FilteredThreadIndex; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.sml.ThreadedHTMLRenderer; - -/** - * Show the user's addressbook - * - */ -public class AddressesServlet extends BaseServlet { - public static final String PARAM_IS_PUBLIC = "addrPublic"; - public static final String PARAM_NAME = "addrName"; - public static final String PARAM_LOC = "addrLoc"; - public static final String PARAM_FAVORITE = "addrFavorite"; - public static final String PARAM_IGNORE = "addrIgnore"; - public static final String PARAM_NET = "addrNet"; - public static final String PARAM_PROTO = "addrProto"; - public static final String PARAM_SYNDICATE = "addrSyndicate"; - public static final String PARAM_TAG = "addrTag"; - public static final String PARAM_ACTION = "action"; - - public static final String PROTO_BLOG = "syndieblog"; - public static final String PROTO_ARCHIVE = "syndiearchive"; - public static final String PROTO_I2PHEX = "i2phex"; - public static final String PROTO_EEPSITE = "eep"; - public static final String PROTO_TAG = "syndietag"; - - public static final String NET_SYNDIE = "syndie"; - public static final String NET_I2P = "i2p"; - public static final String NET_IP = "ip"; - public static final String NET_FREENET = "freenet"; - public static final String NET_TOR = "tor"; - - public static final String ACTION_DELETE_BLOG = "Delete author"; - public static final String ACTION_UPDATE_BLOG = "Update author"; - public static final String ACTION_ADD_BLOG = "Add author"; - public static final String ACTION_PURGE_AND_BAN_BLOG = "Purge and ban author"; - - public static final String ACTION_DELETE_ARCHIVE = "Delete archive"; - public static final String ACTION_UPDATE_ARCHIVE = "Update archive"; - public static final String ACTION_ADD_ARCHIVE = "Add archive"; - - public static final String ACTION_DELETE_PEER = "Delete peer"; - public static final String ACTION_UPDATE_PEER = "Update peer"; - public static final String ACTION_ADD_PEER = "Add peer"; - - public static final String ACTION_DELETE_EEPSITE = "Delete eepsite"; - public static final String ACTION_UPDATE_EEPSITE = "Update eepsite"; - public static final String ACTION_ADD_EEPSITE = "Add eepsite"; - - public static final String ACTION_DELETE_TAG = "Delete tag"; - public static final String ACTION_UPDATE_TAG = "Update tag"; - public static final String ACTION_ADD_TAG = "Add tag"; - - public static final String ACTION_DELETE_OTHER = "Delete address"; - public static final String ACTION_UPDATE_OTHER = "Update address"; - public static final String ACTION_ADD_OTHER = "Add other address"; - - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - if (!user.getAuthenticated()) { - out.write("\n"); - } else { - PetNameDB db = user.getPetNameDB(); - String uri = req.getRequestURI(); - - PetName pn = buildNewName(req, PROTO_BLOG); - _log.debug("pn for protoBlog [" + req.getParameter(PARAM_PROTO) + "]: " + pn); - renderBlogs(user, db, uri, pn, out); - pn = buildNewName(req, PROTO_ARCHIVE); - _log.debug("pn for protoArchive [" + req.getParameter(PARAM_PROTO) + "]: " + pn); - renderArchives(user, db, uri, pn, out); - pn = buildNewName(req, PROTO_TAG); - _log.debug("pn for protoTag [" + req.getParameter(PARAM_TAG) + "]: " + pn); - renderTags(user, db, uri, pn, out); - pn = buildNewName(req, PROTO_I2PHEX); - _log.debug("pn for protoPhex [" + req.getParameter(PARAM_PROTO) + "]: " + pn); - renderI2Phex(user, db, uri, pn, out); - pn = buildNewName(req, PROTO_EEPSITE); - _log.debug("pn for protoEep [" + req.getParameter(PARAM_PROTO) + "]: " + pn); - renderEepsites(user, db, uri, pn, out); - pn = buildNewName(req); - _log.debug("pn for proto other [" + req.getParameter(PARAM_PROTO) + "]: " + pn); - renderOther(user, db, uri, pn, out); - } - } - - private void renderBlogs(User user, PetNameDB db, String baseURI, PetName newName, PrintWriter out) throws IOException { - TreeSet names = new TreeSet(); - for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - PetName pn = db.getByName(name); - if (PROTO_BLOG.equals(pn.getProtocol())) - names.add(name); - } - out.write("\n"); - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - PetName pn = db.getByName((String)iter.next()); - out.write(""); - out.write(""); - out.write(""); - writeAuthActionFields(out); - out.write("\n"); - out.write("\n"); - - out.write("\n"); - } - - private void renderArchives(User user, PetNameDB db, String baseURI, PetName newName, PrintWriter out) throws IOException { - TreeSet names = new TreeSet(); - for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - PetName pn = db.getByName(name); - if (PROTO_ARCHIVE.equals(pn.getProtocol())) - names.add(name); - } - out.write("\n"); - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - PetName pn = db.getByName((String)iter.next()); - out.write(""); - writeAuthActionFields(out); - out.write(""); - out.write(""); - out.write("\n"); - out.write("\n"); - } - - out.write(""); - writeAuthActionFields(out); - out.write(""); - out.write(""); - out.write("\n"); - out.write("\n"); - - out.write("\n"); - } - - private void renderI2Phex(User user, PetNameDB db, String baseURI, PetName newName, PrintWriter out) throws IOException { - TreeSet names = new TreeSet(); - for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - PetName pn = db.getByName(name); - if (PROTO_I2PHEX.equals(pn.getProtocol())) - names.add(name); - } - out.write("\n"); - - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - PetName pn = db.getByName((String)iter.next()); - out.write(""); - writeAuthActionFields(out); - out.write(""); - out.write(""); - out.write("\n"); - out.write("\n"); - } - - out.write(""); - writeAuthActionFields(out); - out.write(""); - out.write(""); - out.write("\n"); - out.write("\n"); - - out.write("\n"); - } - private void renderEepsites(User user, PetNameDB db, String baseURI, PetName newName, PrintWriter out) throws IOException { - TreeSet names = new TreeSet(); - for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - PetName pn = db.getByName(name); - if (PROTO_EEPSITE.equals(pn.getProtocol())) - names.add(name); - } - out.write("\n"); - - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - PetName pn = db.getByName((String)iter.next()); - out.write(""); - writeAuthActionFields(out); - out.write(""); - out.write(""); - out.write("\n"); - out.write("\n"); - } - - out.write(""); - writeAuthActionFields(out); - out.write(""); - out.write(""); - out.write("\n"); - out.write("\n"); - - out.write("\n"); - } - - private void renderTags(User user, PetNameDB db, String baseURI, PetName newName, PrintWriter out) throws IOException { - TreeSet names = new TreeSet(); - for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - PetName pn = db.getByName(name); - if (PROTO_TAG.equals(pn.getProtocol())) - names.add(name); - } - out.write("\n"); - - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - PetName pn = db.getByName((String)iter.next()); - out.write(""); - writeAuthActionFields(out); - out.write(""); - out.write(""); - out.write("\n"); - out.write("\n"); - } - - out.write(""); - writeAuthActionFields(out); - out.write(""); - out.write(""); - out.write("\n"); - out.write("\n"); - - out.write("\n"); - } - - private void renderOther(User user, PetNameDB db, String baseURI, PetName newName, PrintWriter out) throws IOException { - TreeSet names = new TreeSet(); - for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - PetName pn = db.getByName(name); - if (isRightProtocol(pn.getProtocol())) - names.add(name); - } - out.write("\n"); - - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - PetName pn = db.getByName((String)iter.next()); - out.write(""); - writeAuthActionFields(out); - out.write("\n"); - out.write("\n"); - } - - out.write(""); - writeAuthActionFields(out); - - out.write("\n"); - out.write("\n"); - - out.write("\n"); - } - - /** build the 'other' name passed in */ - private PetName buildNewName(HttpServletRequest req) { return buildNewName(req, null); } - /** build a petname based by the request passed in, if the new entry is of the given protocol */ - private PetName buildNewName(HttpServletRequest req, String protocol) { - PetName pn = new PetName(); - if (!isRightProtocol(req, protocol)) { - pn.setIsPublic(true); - pn.setName(""); - pn.setLocation(""); - if (protocol == null) - pn.setProtocol(""); - else - pn.setProtocol(protocol); - pn.setNetwork(""); - return pn; - } else { - pn = buildNewAddress(req); - } - return pn; - } - - private String getParam(HttpServletRequest req, String param) { - if (empty(req, param)) { - return ""; - } else { - String val = req.getParameter(param); - return val; - } - } - - - private boolean isRightProtocol(HttpServletRequest req, String protocol) { - // if they hit submit, they are actually updating stuff, so don't include a 'new' one - if (!empty(req, PARAM_ACTION)) - return false; - - return isRightProtocol(protocol, req.getParameter(PARAM_PROTO)); - } - private boolean isRightProtocol(String proto) { return isRightProtocol((String)null, proto); } - private boolean isRightProtocol(String proto, String reqProto) { - if (empty(reqProto)) - return false; - if (proto == null) { - if (PROTO_ARCHIVE.equals(reqProto) || - PROTO_BLOG.equals(reqProto) || - PROTO_EEPSITE.equals(reqProto) || - PROTO_TAG.equals(reqProto) || - PROTO_I2PHEX.equals(reqProto)) - return false; - else // its something other than the four default types - return true; - } else { - return proto.equals(reqProto); - } - } - - protected String getTitle() { return "Syndie :: Addressbook"; } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/AdminServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/AdminServlet.java deleted file mode 100644 index fa9b852ba..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/AdminServlet.java +++ /dev/null @@ -1,79 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.ThreadIndex; - -/** - * Admin form - * - */ -public class AdminServlet extends BaseServlet { - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - if (BlogManager.instance().authorizeRemote(user)) { - displayForm(user, req, out); - } else { - out.write("\n"); - } - } - - private void displayForm(User user, HttpServletRequest req, PrintWriter out) throws IOException { - out.write("\n"); - writeAuthActionFields(out); - out.write("\n"); - out.write("\n"); - } - - protected String getTitle() { return "Syndie :: Configuration"; } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ArchiveServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ArchiveServlet.java deleted file mode 100644 index afd701b9c..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ArchiveServlet.java +++ /dev/null @@ -1,226 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import net.i2p.I2PAppContext; -import net.i2p.data.Base64; -import net.i2p.data.DataHelper; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.data.ArchiveIndex; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; - -/** - * - */ -public class ArchiveServlet extends HttpServlet { - - public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - handle(req, resp); - } - - public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - handle(req, resp); - } - public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - handle(req, resp); - } - public void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - handle(req, resp); - } - - public void handle(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String path = req.getPathInfo(); - if ( (path == null) || (path.trim().length() <= 1) ) { - renderRootIndex(resp); - return; - } else if (path.endsWith(Archive.INDEX_FILE)) { - renderSummary(req.getHeader("If-None-Match"), resp); - } else if (path.indexOf("export.zip") != -1) { - ExportServlet.export(req, resp); - } else { - String blog = getBlog(path); - if (path.endsWith(Archive.METADATA_FILE)) { - renderMetadata(blog, resp); - } else if (path.endsWith(".snd")) { - renderEntry(blog, getEntry(path), resp); - } else { - renderBlogIndex(blog, resp); - } - } - } - - private String getBlog(String path) { - //System.err.println("Blog: [" + path + "]"); - int start = 0; - int end = -1; - int len = path.length(); - for (int i = 0; i < len; i++) { - if (path.charAt(i) != '/') { - start = i; - break; - } - } - for (int j = start + 1; j < len; j++) { - if (path.charAt(j) == '/') { - end = j; - break; - } - } - if (end < 0) end = len; - String rv = path.substring(start, end); - //System.err.println("Blog: [" + path + "] rv: [" + rv + "]"); - return rv; - } - - private long getEntry(String path) { - int start = path.lastIndexOf('/'); - if (start < 0) return -1; - if (!(path.endsWith(".snd"))) return -1; - String rv = path.substring(start+1, path.length()-".snd".length()); - //System.err.println("Entry: [" + path + "] rv: [" + rv + "]"); - try { - return Long.parseLong(rv); - } catch (NumberFormatException nfe) { - return -1; - } - } - - private void renderRootIndex(HttpServletResponse resp) throws ServletException, IOException { - resp.setContentType("text/html;charset=utf-8"); - //resp.setCharacterEncoding("UTF-8"); - OutputStream out = resp.getOutputStream(); - out.write(DataHelper.getUTF8("archive.txt
    \n")); - ArchiveIndex index = BlogManager.instance().getArchive().getIndex(); - Set blogs = index.getUniqueBlogs(); - for (Iterator iter = blogs.iterator(); iter.hasNext(); ) { - Hash blog = (Hash)iter.next(); - String s = blog.toBase64(); - out.write(DataHelper.getUTF8("" + s + "
    \n")); - } - out.close(); - } - - public static final String HEADER_EXPORT_CAPABLE = "X-Syndie-Export-Capable"; - - private void renderSummary(String etag, HttpServletResponse resp) throws ServletException, IOException { - resp.setContentType("text/plain;charset=utf-8"); - //resp.setCharacterEncoding("UTF-8"); - ArchiveIndex index = BlogManager.instance().getArchive().getIndex(); - byte[] indexUTF8 = DataHelper.getUTF8(index.toString()); - String newEtag = "\"" + I2PAppContext.getGlobalContext().sha().calculateHash(indexUTF8).toBase64() + "\""; - if (etag != null && etag.equals(newEtag)) { - resp.sendError(304, "Archive not modified"); - return; - } - resp.setHeader(HEADER_EXPORT_CAPABLE, "true"); - resp.setHeader("ETag", newEtag); - OutputStream out = resp.getOutputStream(); - out.write(indexUTF8); - out.close(); - } - - private void renderMetadata(String blog, HttpServletResponse resp) throws ServletException, IOException { - byte b[] = Base64.decode(blog); - if ( (b == null) || (b.length != Hash.HASH_LENGTH) ) { - resp.sendError(404, "Invalid blog requested"); - return; - } - Hash h = new Hash(b); - BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(h); - if (info == null) { - resp.sendError(404, "Blog does not exist"); - return; - } - resp.setContentType("application/x-syndie-meta"); - OutputStream out = resp.getOutputStream(); - info.write(out); - out.close(); - } - - private void renderBlogIndex(String blog, HttpServletResponse resp) throws ServletException, IOException { - byte b[] = Base64.decode(blog); - if ( (b == null) || (b.length != Hash.HASH_LENGTH) ) { - resp.sendError(404, "Invalid blog requested"); - return; - } - Hash h = new Hash(b); - - BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(h); - if (info == null) { - resp.sendError(404, "Blog does not exist"); - return; - } - resp.setContentType("text/html;charset=utf-8"); - //resp.setCharacterEncoding("UTF-8"); - OutputStream out = resp.getOutputStream(); - out.write(DataHelper.getUTF8("..
    \n")); - out.write(DataHelper.getUTF8("" + Archive.METADATA_FILE + "
    \n")); - List entries = new ArrayList(64); - BlogManager.instance().getArchive().getIndex().selectMatchesOrderByEntryId(entries, h, null); - for (int i = 0; i < entries.size(); i++) { - BlogURI entry = (BlogURI)entries.get(i); - out.write(DataHelper.getUTF8("" + entry.getEntryId() + ".snd
    \n")); - } - out.close(); - } - - private void renderEntry(String blog, long entryId, HttpServletResponse resp) throws ServletException, IOException { - byte b[] = Base64.decode(blog); - if ( (b == null) || (b.length != Hash.HASH_LENGTH) ) { - resp.sendError(404, "Invalid blog requested"); - return; - } - Hash h = new Hash(b); - BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(h); - if (info == null) { - resp.sendError(404, "Blog does not exist"); - return; - } - File root = BlogManager.instance().getArchive().getArchiveDir(); - File blogDir = new File(root, blog); - if (!blogDir.exists()) { - resp.sendError(404, "Blog does not exist"); - return; - } - File entry = new File(blogDir, entryId + ".snd"); - if (!entry.exists()) { - resp.sendError(404, "Entry does not exist"); - return; - } - resp.setContentType("application/x-syndie-post"); - dump(entry, resp); - } - - private void dump(File source, HttpServletResponse resp) throws ServletException, IOException { - FileInputStream in = null; - OutputStream out = null; - try { - in = new FileInputStream(source); - out = resp.getOutputStream(); - byte buf[] = new byte[1024]; - int read = 0; - while ( (read = in.read(buf)) != -1) - out.write(buf, 0, read); - out.close(); - in.close(); - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - if (out != null) try { out.close(); } catch (IOException ioe) {} - } - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ArchiveViewerBean.java b/apps/syndie/java/src/net/i2p/syndie/web/ArchiveViewerBean.java deleted file mode 100644 index 56ed1b60b..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ArchiveViewerBean.java +++ /dev/null @@ -1,822 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.TreeMap; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.client.naming.PetNameDB; -import net.i2p.data.Base64; -import net.i2p.data.DataHelper; -import net.i2p.data.Hash; -import net.i2p.data.SigningPublicKey; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.ArchiveIndex; -import net.i2p.syndie.data.Attachment; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.EntryContainer; -import net.i2p.syndie.sml.HTMLRenderer; -import net.i2p.syndie.sml.ThreadedHTMLRenderer; - -/** - * - */ -public class ArchiveViewerBean { - public static String getBlogName(String keyHash) { - BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(new Hash(Base64.decode(keyHash))); - if (info == null) - return HTMLRenderer.sanitizeString(keyHash); - else - return HTMLRenderer.sanitizeString(info.getProperty("Name")); - } - - /** base64 encoded hash of the blog's public key, or null for no filtering by blog */ - public static final String PARAM_BLOG = "blog"; - /** base64 encoded tag to filter by, or blank for no filtering by tags */ - public static final String PARAM_TAG = "tag"; - /** entry id within the blog if we only want to see that one */ - public static final String PARAM_ENTRY = "entry"; - /** base64 encoded group within the user's filters */ - public static final String PARAM_GROUP = "group"; - /** how many entries per page to show at once */ - public static final String PARAM_NUM_PER_PAGE = "pageSize"; - /** which page of entries to render */ - public static final String PARAM_PAGE_NUMBER = "pageNum"; - /** should we expand each entry to show the full contents */ - public static final String PARAM_EXPAND_ENTRIES = "expand"; - /** should entries be rendered with the images shown inline */ - public static final String PARAM_SHOW_IMAGES = "images"; - /** should we regenerate an index to the archive before rendering */ - public static final String PARAM_REGENERATE_INDEX = "regenerateIndex"; - /** which attachment should we serve up raw */ - public static final String PARAM_ATTACHMENT = "attachment"; - /** we are replying to a particular blog/tag/entry/whatever (value == base64 encoded selector) */ - public static final String PARAM_IN_REPLY_TO = "inReplyTo"; - - /** prepopulate the subject field with the given value */ - public static final String PARAM_SUBJECT = "replySubject"; - /** prepopulate the tags with the given value */ - public static final String PARAM_TAGS = "replyTags"; - /** prepopulate the body with the given value */ - public static final String PARAM_PARENT = "parentURI"; - - /** - * Drop down multichooser: - * blog://base64(key) - * tag://base64(tag) - * blogtag://base64(key)/base64(tag) - * entry://base64(key)/entryId - * group://base64(groupName) - * ALL - */ - public static final String PARAM_SELECTOR = "selector"; - public static final String SEL_ALL = "ALL"; - public static final String SEL_BLOG = "blog://"; - public static final String SEL_TAG = "tag://"; - public static final String SEL_BLOGTAG = "blogtag://"; - public static final String SEL_ENTRY = "entry://"; - public static final String SEL_GROUP = "group://"; - /** submit field for the selector form */ - public static final String PARAM_SELECTOR_ACTION = "action"; - public static final String SEL_ACTION_SET_AS_DEFAULT = "Set as default"; - - public static void renderBlogSelector(User user, Map parameters, Writer out) throws IOException { - String sel = getString(parameters, PARAM_SELECTOR); - String action = getString(parameters, PARAM_SELECTOR_ACTION); - if ( (sel != null) && (action != null) && (SEL_ACTION_SET_AS_DEFAULT.equals(action)) ) { - user.setDefaultSelector(HTMLRenderer.sanitizeString(sel, false)); - BlogManager.instance().saveUser(user); - } - - out.write(""); - - int numPerPage = getInt(parameters, PARAM_NUM_PER_PAGE, 5); - int pageNum = getInt(parameters, PARAM_PAGE_NUMBER, 0); - boolean expandEntries = getBool(parameters, PARAM_EXPAND_ENTRIES, (user != null ? user.getShowExpanded() : false)); - boolean showImages = getBool(parameters, PARAM_SHOW_IMAGES, (user != null ? user.getShowImages() : false)); - - out.write(""); - out.write(""); - out.write(""); - out.write(""); - - } - - private static String getDefaultSelector(User user, Map parameters) { - if ( (user == null) || (user.getDefaultSelector() == null) ) - return BlogManager.instance().getArchive().getDefaultSelector(); - else - return user.getDefaultSelector(); - } - - public static void renderBlogs(User user, Map parameters, Writer out, String afterPagination) throws IOException { - String blogStr = getString(parameters, PARAM_BLOG); - Hash blog = null; - if (blogStr != null) blog = new Hash(Base64.decode(blogStr)); - if ( (blog != null) && (blog.getData() == null) ) blog = null; - String tag = getString(parameters, PARAM_TAG); - if (tag != null) tag = DataHelper.getUTF8(Base64.decode(tag)); - - long entryId = -1; - if (blog != null) { - String entryIdStr = getString(parameters, PARAM_ENTRY); - try { - entryId = Long.parseLong(entryIdStr); - } catch (NumberFormatException nfe) {} - } - String group = getString(parameters, PARAM_GROUP); - if (group != null) group = DataHelper.getUTF8(Base64.decode(group)); - - String sel = getString(parameters, PARAM_SELECTOR); - - if (getString(parameters, "action") != null) { - tag = null; - blog = null; - sel = null; - group = null; - } - - if ( (sel == null) && (blog == null) && (group == null) && (tag == null) ) - sel = getDefaultSelector(user, parameters); - if (sel != null) { - Selector s = new Selector(sel); - blog = s.blog; - tag = s.tag; - entryId = s.entry; - group = s.group; - } - - int numPerPage = getInt(parameters, PARAM_NUM_PER_PAGE, 5); - int pageNum = getInt(parameters, PARAM_PAGE_NUMBER, 0); - boolean expandEntries = getBool(parameters, PARAM_EXPAND_ENTRIES, (user != null ? user.getShowExpanded() : false)); - boolean showImages = getBool(parameters, PARAM_SHOW_IMAGES, (user != null ? user.getShowImages() : false)); - boolean regenerateIndex = getBool(parameters, PARAM_REGENERATE_INDEX, false); - try { - renderBlogs(user, blog, tag, entryId, group, numPerPage, pageNum, expandEntries, showImages, regenerateIndex, sel, out, afterPagination); - } catch (IOException ioe) { - ioe.printStackTrace(); - throw ioe; - } catch (RuntimeException re) { - re.printStackTrace(); - throw re; - } - } - - public static class Selector { - public Hash blog; - public String tag; - public long entry; - public String group; - public Selector(String selector) { - entry = -1; - blog = null; - tag = null; - if (selector != null) { - if (selector.startsWith(SEL_BLOG)) { - String blogStr = selector.substring(SEL_BLOG.length()); - //System.out.println("Selector [" + selector + "] blogString: [" + blogStr + "]"); - byte h[] = Base64.decode(blogStr); - if (h != null) - blog = new Hash(h); - //else - // System.out.println("blog string does not decode properly: [" + blogStr + "]"); - } else if (selector.startsWith(SEL_BLOGTAG)) { - int tagStart = selector.lastIndexOf('/'); - String blogStr = selector.substring(SEL_BLOGTAG.length(), tagStart); - blog = new Hash(Base64.decode(blogStr)); - if (blog.getData() == null) { - System.out.println("Blog string [" + blogStr + "] does not decode"); - blog = null; - return; - } - tag = selector.substring(tagStart+1); - String origTag = tag; - byte rawDecode[] = null; - if (tag != null) { - rawDecode = Base64.decode(tag); - tag = DataHelper.getUTF8(rawDecode); - } - //System.out.println("Selector [" + selector + "] blogString: [" + blogStr + "] tag: [" + tag + "]"); - if (false && tag != null) { - StringBuffer b = new StringBuffer(tag.length()*2); - for (int j = 0; j < tag.length(); j++) { - b.append((int)tag.charAt(j)); - if (rawDecode.length > j) - b.append('.').append((int)rawDecode[j]); - b.append(' '); - } - b.append("encoded as "); - for (int j = 0; j < origTag.length(); j++) { - b.append((int)origTag.charAt(j)).append(' '); - } - //System.out.println("selected tag: " + b.toString()); - } - } else if (selector.startsWith(SEL_TAG)) { - tag = selector.substring(SEL_TAG.length()); - byte rawDecode[] = null; - if (tag != null) { - rawDecode = Base64.decode(tag); - tag = DataHelper.getUTF8(rawDecode); - } - //System.out.println("Selector [" + selector + "] tag: [" + tag + "]"); - if (false && tag != null) { - StringBuffer b = new StringBuffer(tag.length()*2); - for (int j = 0; j < tag.length(); j++) { - b.append((int)tag.charAt(j)); - if (rawDecode.length > j) - b.append('.').append((int)rawDecode[j]); - b.append(' '); - } - //System.out.println("selected tag: " + b.toString()); - } - } else if (selector.startsWith(SEL_ENTRY)) { - int entryStart = selector.lastIndexOf('/'); - String blogStr = blogStr = selector.substring(SEL_ENTRY.length(), entryStart); - String entryStr = selector.substring(entryStart+1); - try { - entry = Long.parseLong(entryStr); - Hash h = new Hash(Base64.decode(blogStr)); - if (h.getData() != null) - blog = h; - //else - // System.out.println("Blog does not decode [" + blogStr + "]"); - //System.out.println("Selector [" + selector + "] blogString: [" + blogStr + "] entry: [" + entry + "]"); - } catch (NumberFormatException nfe) {} - } else if (selector.startsWith(SEL_GROUP)) { - group = DataHelper.getUTF8(Base64.decode(selector.substring(SEL_GROUP.length()))); - //System.out.println("Selector [" + selector + "] group: [" + group + "]"); - } - } - } - } - - private static void renderBlogs(User user, Hash blog, String tag, long entryId, String group, int numPerPage, int pageNum, - boolean expandEntries, boolean showImages, boolean regenerateIndex, String selector, Writer out, String afterPagination) throws IOException { - Archive archive = BlogManager.instance().getArchive(); - if (regenerateIndex) - archive.regenerateIndex(); - ArchiveIndex index = archive.getIndex(); - List entries = pickEntryURIs(user, index, blog, tag, entryId, group); - //System.out.println("Searching for " + blog + "/" + tag + "/" + entryId + "/" + pageNum + "/" + numPerPage + "/" + group); - //System.out.println("Entry URIs: " + entries); - - HTMLRenderer renderer = new HTMLRenderer(I2PAppContext.getGlobalContext()); - int start = pageNum * numPerPage; - int end = start + numPerPage; - int pages = 1; - if (entries.size() <= 1) { - // just one, so no pagination, etc - start = 0; - end = 1; - } else { - if (end >= entries.size()) - end = entries.size(); - if ( (pageNum < 0) || (numPerPage <= 0) ) { - start = 0; - end = entries.size() - 1; - } else { - HTMLRenderer rend = new ThreadedHTMLRenderer(I2PAppContext.getGlobalContext()); - pages = entries.size() / numPerPage; - if (numPerPage * pages < entries.size()) - pages++; - if (pageNum > 0) { - String prevURL = null; - prevURL = rend.getPageURL(blog, tag, entryId, group, numPerPage, pageNum-1, expandEntries, showImages); - //System.out.println("prevURL: " + prevURL); - out.write(" <<"); - } else { - out.write(" << "); - } - out.write("Page " + (pageNum+1) + " of " + pages + ""); - if (pageNum + 1 < pages) { - String nextURL = null; - nextURL = rend.getPageURL(blog, tag, entryId, group, numPerPage, pageNum+1, expandEntries, showImages); - //System.out.println("nextURL: " + nextURL); - out.write(" >>"); - } else { - out.write(" >>"); - } - } - } - - /* - out.write(" "); - - if (showImages) - out.write("Hide images"); - else - out.write("Show images"); - - if (expandEntries) - out.write(" Hide details"); - else - out.write(" Expand details"); - - out.write(""); - */ - - if (afterPagination != null) - out.write(afterPagination); - - if (entries.size() <= 0) end = -1; - //System.out.println("Entries.size: " + entries.size() + " start=" + start + " end=" + end); - for (int i = start; i < end; i++) { - BlogURI uri = (BlogURI)entries.get(i); - EntryContainer c = archive.getEntry(uri); - try { - if (c == null) - renderer.renderUnknownEntry(user, archive, uri, out); - else - renderer.render(user, archive, c, out, !expandEntries, showImages); - } catch (RuntimeException e) { - e.printStackTrace(); - throw e; - } - } - } - - public static List pickEntryURIs(User user, ArchiveIndex index, Hash blog, String tag, long entryId, String group) { - if ( (blog != null) && ( (blog.getData() == null) || (blog.getData().length != Hash.HASH_LENGTH) ) ) - blog = null; - List rv = new ArrayList(16); - if ( (blog != null) && (entryId >= 0) ) { - rv.add(new BlogURI(blog, entryId)); - return rv; - } - - if ( (group != null) && (user != null) ) { - List selectors = (List)user.getBlogGroups().get(group); - if (selectors != null) { - //System.out.println("Selectors for group " + group + ": " + selectors); - for (int i = 0; i < selectors.size(); i++) { - String sel = (String)selectors.get(i); - Selector s = new Selector(sel); - if ( (s.entry >= 0) && (s.blog != null) && (s.group == null) && (s.tag == null) ) - rv.add(new BlogURI(s.blog, s.entry)); - else - index.selectMatchesOrderByEntryId(rv, s.blog, s.tag); - } - } - PetNameDB db = user.getPetNameDB(); - for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - PetName pn = db.getByName(name); - if ("syndie".equals(pn.getNetwork()) && "syndieblog".equals(pn.getProtocol()) && pn.isMember(group)) { - byte pnLoc[] = Base64.decode(pn.getLocation()); - if (pnLoc != null) { - Hash pnHash = new Hash(pnLoc); - index.selectMatchesOrderByEntryId(rv, pnHash, null); - } - } - } - sort(rv); - if (rv.size() > 0) - return rv; - } - index.selectMatchesOrderByEntryId(rv, blog, tag); - filterIgnored(user, rv); - return rv; - } - - private static void filterIgnored(User user, List uris) { - for (int i = 0; i < uris.size(); i++) { - BlogURI uri = (BlogURI)uris.get(i); - Hash k = uri.getKeyHash(); - if (k == null) continue; - PetName pn = user.getPetNameDB().getByLocation(k.toBase64()); - if ( (pn != null) && (pn.isMember("Ignore")) ) { - uris.remove(i); - i--; - } - } - } - - private static void sort(List uris) { - TreeMap ordered = new TreeMap(); - while (uris.size() > 0) { - BlogURI uri = (BlogURI)uris.remove(0); - int off = 0; - while (ordered.containsKey(new Long(0 - off - uri.getEntryId()))) - off++; - ordered.put(new Long(0-off-uri.getEntryId()), uri); - } - for (Iterator iter = ordered.values().iterator(); iter.hasNext(); ) - uris.add(iter.next()); - } - - public static final String getString(Map parameters, String param) { - if ( (parameters == null) || (parameters.get(param) == null) ) - return null; - Object vals = parameters.get(param); - if (vals.getClass().isArray()) { - String v[] = (String[])vals; - if (v.length > 0) - return ((String[])vals)[0]; - else - return null; - } else if (vals instanceof Collection) { - Collection c = (Collection)vals; - if (c.size() > 0) - return (String)c.iterator().next(); - else - return null; - } else if (vals instanceof String) { - return (String)vals; - } else { - return null; - } - } - public static final String[] getStrings(Map parameters, String param) { - if ( (parameters == null) || (parameters.get(param) == null) ) - return null; - Object vals = parameters.get(param); - if (vals.getClass().isArray()) { - return (String[])vals; - } else if (vals instanceof Collection) { - Collection c = (Collection)vals; - if (c.size() <= 0) return null; - String rv[] = new String[c.size()]; - int i = 0; - for (Iterator iter = c.iterator(); iter.hasNext(); i++) - rv[i] = (String)iter.next(); - return rv; - } else { - return null; - } - } - - private static final int getInt(Map param, String key, int defaultVal) { - String val = getString(param, key); - if (val != null) { - try { return Integer.parseInt(val); } catch (NumberFormatException nfe) {} - } - return defaultVal; - } - - private static final boolean getBool(Map param, String key, boolean defaultVal) { - String val = getString(param, key); - if (val != null) { - return ("true".equals(val) || "yes".equals(val)); - } - return defaultVal; - } - - public static void renderAttachment(Map parameters, OutputStream out) throws IOException { - renderAttachment(getAttachment(parameters), out); - } - public static void renderAttachment(Attachment a, OutputStream out) throws IOException { - if (a == null) { - renderInvalidAttachment(out); - } else { - InputStream data = a.getDataStream(); - byte buf[] = new byte[1024]; - int read = 0; - while ( (read = data.read(buf)) != -1) - out.write(buf, 0, read); - data.close(); - } - } - - public static final String getAttachmentContentType(Map parameters) { - return getAttachmentContentType(getAttachment(parameters)); - } - public static final String getAttachmentContentType(Attachment attachment) { - if (attachment == null) - return "text/html"; - String mime = attachment.getMimeType(); - if ( (mime != null) && ((mime.startsWith("image/") || mime.startsWith("text/plain"))) ) - return mime; - return "application/octet-stream"; - } - - public static final boolean getAttachmentShouldShowInline(Map parameters) { - return getAttachmentShouldShowInline(getAttachment(parameters)); - } - public static final boolean getAttachmentShouldShowInline(Attachment a) { - if (a == null) - return true; - String mime = a.getMimeType(); - if ( (mime != null) && ((mime.startsWith("image/") || mime.startsWith("text/plain"))) ) - return true; - else - return false; - } - - public static final int getAttachmentContentLength(Map parameters) { - return getAttachmentContentLength(getAttachment(parameters)); - } - public static final int getAttachmentContentLength(Attachment a) { - if (a != null) - return a.getDataLength(); - else - return -1; - } - - public static final String getAttachmentFilename(Map parameters) { - return getAttachmentFilename(getAttachment(parameters)); - } - public static final String getAttachmentFilename(Attachment a) { - if (a != null) - return a.getName(); - else - return "attachment.dat"; - } - - private static final Attachment getAttachment(Map parameters) { - String blogStr = getString(parameters, PARAM_BLOG); - Hash blog = null; - if (blogStr != null) blog = new Hash(Base64.decode(blogStr)); - long entryId = -1; - if (blogStr != null) { - String entryIdStr = getString(parameters, PARAM_ENTRY); - try { - entryId = Long.parseLong(entryIdStr); - } catch (NumberFormatException nfe) {} - } - int attachment = getInt(parameters, PARAM_ATTACHMENT, -1); - - Archive archive = BlogManager.instance().getArchive(); - EntryContainer entry = archive.getEntry(new BlogURI(blog, entryId)); - if ( (entry != null) && (attachment >= 0) && (attachment < entry.getAttachments().length) ) { - return entry.getAttachments()[attachment]; - } - return null; - } - - private static void renderInvalidAttachment(OutputStream out) throws IOException { - out.write(DataHelper.getUTF8("No such entry, or no such attachment")); - } - - private static String getURL(String uri, Map parameters) { - StringBuffer rv = new StringBuffer(128); - rv.append(uri); - rv.append('?'); - if (parameters != null) { - for (Iterator iter = parameters.keySet().iterator(); iter.hasNext(); ) { - String key = (String)iter.next(); - String vals[] = getStrings(parameters, key); - // we are already looking at the page with the given parameters, no need to further sanitize - if ( (key != null) && (vals != null) ) - for (int i = 0; i < vals.length; i++) - rv.append(key).append('=').append(vals[i]).append('&'); - } - } - return rv.toString(); - } - - private static void updateMetadata(User viewer, Map parameters, Writer out) throws IOException { - if ( (viewer == null) || (!viewer.getAuthenticated()) ) - return; - String blogStr = getString(parameters, PARAM_BLOG); - if (blogStr != null) { - Hash blog = new Hash(Base64.decode(blogStr)); - Archive archive = BlogManager.instance().getArchive(); - BlogInfo info = archive.getBlogInfo(blog); - if (info != null) { - boolean isUser = viewer.getBlog().equals(info.getKey().calculateHash()); - if (!isUser) - return; - Properties toSave = new Properties(); - String existing[] = info.getProperties(); - for (int i = 0; i < existing.length; i++) { - String newVal = getString(parameters, existing[i]); - if ( (newVal != null) && (newVal.length() > 0) ) - toSave.setProperty(existing[i], newVal.trim()); - else - toSave.setProperty(existing[i], info.getProperty(existing[i])); - } - boolean saved = BlogManager.instance().updateMetadata(viewer, blog, toSave); - if (saved) - out.write("

    Blog metadata saved

    \n"); - else - out.write("

    Blog metadata could not be saved

    \n"); - } - } - } - - /** - * @param currentURI URI of the with current page without any parameters tacked on - */ - public static void renderMetadata(User viewer, String currentURI, Map parameters, Writer out) throws IOException { - if (parameters.get("action") != null) { - updateMetadata(viewer, parameters, out); - } - String blogStr = getString(parameters, PARAM_BLOG); - if (blogStr != null) { - Hash blog = new Hash(Base64.decode(blogStr)); - Archive archive = BlogManager.instance().getArchive(); - BlogInfo info = archive.getBlogInfo(blog); - if (info == null) { - out.write("Blog " + blog.toBase64() + " does not exist"); - return; - } - boolean isUser = ( (viewer != null) && (viewer.getAuthenticated()) && (viewer.getBlog().equals(info.getKey().calculateHash())) ); - String props[] = info.getProperties(); - if (isUser) { - out.write("\n"); - out.write("\n"); - } - out.write("
    You must log in to view your addressbook
    Syndie authors
    "); - out.write("\n"); - out.write("Name: " + pn.getName() + " "); - out.write("Location: "); - if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) - out.write("Favorite? "); - else - out.write("Favorite? "); - - if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) { - out.write("Ignored? "); - if (BlogManager.instance().authorizeRemote(user)) - out.write(" "); - } else { - out.write("Ignored? "); - out.write(" "); - out.write("Location: "); - if (newName.isMember(FilteredThreadIndex.GROUP_FAVORITE)) - out.write("Favorite? "); - else - out.write("Favorite? "); - - if (newName.isMember(FilteredThreadIndex.GROUP_IGNORE)) { - out.write("Ignored? "); - } else { - out.write("Ignored? "); - } - - out.write(" "); - out.write("

    Syndie archives
    "); - out.write("\n"); - out.write("Name: " + pn.getName() + " "); - out.write("Location: "); - if (BlogManager.instance().authorizeRemote(user)) { - if (BlogManager.instance().syndicationScheduled(pn.getLocation())) - out.write("Syndicate? "); - else - out.write("Syndicate? "); - - out.write("Sync manually "); - } else { - out.write("You are not authorized to syndicate with the archive "); - } - out.write(" "); - out.write(" "); - out.write("
    "); - out.write("\n"); - out.write("Name: "); - out.write("Location: "); - if (BlogManager.instance().authorizeRemote(user)) { - if (BlogManager.instance().syndicationScheduled(newName.getLocation())) - out.write("Syndicate? "); - else - out.write("Syndicate? "); - } - - out.write(" "); - out.write("

    I2Phex peers
    "); - out.write("\n"); - out.write("Name: " + pn.getName() + " "); - out.write("Location: "); - - out.write(" "); - out.write(" "); - out.write("
    "); - out.write("\n"); - out.write("Name: "); - out.write("Location: "); - - out.write(" "); - out.write("

    Eepsites
    "); - out.write("\n"); - out.write("Name: " + pn.getName() + " "); - out.write("Location: "); - - out.write(" "); - out.write(" "); - out.write("
    "); - out.write("\n"); - out.write("Name: "); - out.write("Location: "); - - out.write(" "); - out.write("

    Favorite tags
    "); - out.write("\n"); - out.write("Name: " + pn.getName() + " "); - out.write(" "); - - out.write(" "); - out.write("
    "); - out.write("\n"); - out.write("Name: "); - - out.write(" "); - out.write("

    Other addresses
    "); - out.write("\n"); - out.write("Network: "); - out.write("Protocol: "); - out.write("Name: " + pn.getName() +" "); - out.write("Location: "); - - out.write(" "); - out.write(" "); - out.write("
    "); - out.write("\n"); - out.write("Network: "); - out.write("Protocol: "); - out.write("Name: "); - out.write("Location: "); - - out.write(" "); - out.write("

    You are not authorized to configure this Syndie instance
    "); - - // stop people from shooting themselves in the foot - only geeks can enable multiuser mode - // (by adding the single user flag to their syndie.config) - if (BlogManager.instance().isSingleUser()) - out.write("\n"); - /* - out.write("Single user?
    \n"); - - out.write("If this is checked, the registration, admin, and remote passwords are unnecessary - anyone"); - out.write("can register and administer Syndie, as well as use any remote functionality. This should not be checked if untrusted"); - out.write("parties can access this web interface.
    \n"); - */ - out.write("Default user: \n"); - out.write("pass:
    \n"); - out.write("If Syndie is in single user mode, it will create a new 'default' user automatically and use that "); - out.write("whenever you access Syndie unless you explicitly log in to another account. If you want Syndie to use an existing account as "); - out.write("your default account, you can specify them here, in which case it will automatically log you in under that account.
    \n"); - out.write("Registration password:
    \n"); - out.write("Users must specify this password on the registration form to proceed. If this is "); - out.write("blank, anyone can register.
    \n"); - out.write("Remote password:
    \n"); - out.write("To access remote archives, users must first provide this password on their "); - out.write("metadata page. Remote access is 'dangerous', as it allows the user to instruct "); - out.write("this Syndie instance to establish HTTP connections with arbitrary locations. If "); - out.write("this field is not specified, no one can use remote archives.
    \n"); - out.write("Default remote proxy host:
    \n"); - out.write("Default remote proxy port:
    \n"); - out.write("This is the default HTTP proxy shown on the remote archive page.
    \n"); - out.write("
    \n"); - out.write("\n"); - - out.write("
    "); - for (int i = 0; i < props.length; i++) { - if (props[i].equals(BlogInfo.OWNER_KEY)) { - out.write(""); - out.write("\n"); - } else if (props[i].equals(BlogInfo.SIGNATURE)) { - continue; - } else if (props[i].equals(BlogInfo.POSTERS)) { - SigningPublicKey keys[] = info.getPosters(); - if ( (keys != null) && (keys.length > 0) ) { - out.write(""); - out.write("\n"); - } - } else { - String field = HTMLRenderer.sanitizeString(props[i]); - String val = HTMLRenderer.sanitizeString(info.getProperty(props[i])); - out.write("\n"); - - if (isUser && (!field.equals("Edition"))) - out.write(""); - } - } - List tags = BlogManager.instance().getArchive().getIndex().getBlogTags(blog); - if ( (tags != null) && (tags.size() > 0) ) { - out.write(""); - } - if (isUser) - out.write("\n"); - out.write("
    Blog:" + Base64.encode(blog.getData()) + "
    Allowed authors:"); - for (int j = 0; j < keys.length; j++) { - out.write("" + keys[j].calculateHash().toBase64() + ""); - if (j + 1 < keys.length) - out.write("
    \n"); - } - out.write("
    " + field - + ":" + val + "
     
    Known tags:"); - for (int i = 0; i < tags.size(); i++) { - String tag = (String)tags.get(i); - HTMLRenderer rend = new HTMLRenderer(I2PAppContext.getGlobalContext()); - out.write("" + - HTMLRenderer.sanitizeString(tag) + " "); - } - out.write("
    "); - } else { - out.write("Blog not specified"); - } - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/BaseServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/BaseServlet.java deleted file mode 100644 index aba868a15..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/BaseServlet.java +++ /dev/null @@ -1,1302 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.io.Reader; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; -import java.util.Properties; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.TreeSet; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.client.naming.PetNameDB; -import net.i2p.data.Base64; -import net.i2p.data.DataFormatException; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.FilteredThreadIndex; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.data.ThreadNode; -import net.i2p.syndie.sml.HTMLRenderer; -import net.i2p.syndie.sml.ThreadedHTMLRenderer; -import net.i2p.util.FileUtil; -import net.i2p.util.Log; - -/** - * Base servlet for handling request and rendering the templates - * - */ -public abstract class BaseServlet extends HttpServlet { - protected static final String PARAM_AUTH_ACTION = "syndie.auth"; - protected static long _authNonce; - protected I2PAppContext _context; - protected Log _log; - - public void init() throws ServletException { - super.init(); - _context = I2PAppContext.getGlobalContext(); - _log = _context.logManager().getLog(getClass()); - _authNonce = _context.random().nextLong(); - } - - protected boolean authAction(HttpServletRequest req) { - return authAction(req.getParameter(PARAM_AUTH_ACTION)); - } - protected boolean authAction(String auth) { - if (auth == null) { - return false; - } else { - try { - boolean rv = (Long.valueOf(auth).longValue() == _authNonce); - return rv; - } catch (NumberFormatException nfe) { - return false; - } - } - } - - /** - * write out hidden fields for params that need to be tacked onto an http request that updates - * data, to prevent spoofing - */ - protected void writeAuthActionFields(Writer out) throws IOException { - out.write(""); - } - protected String getAuthActionFields() throws IOException { - return ""; - } - /** - * key=value& of params that need to be tacked onto an http request that updates data, to - * prevent spoofing - */ - protected static String getAuthActionParams() { return PARAM_AUTH_ACTION + '=' + _authNonce + "&"; } - /** - * key=value& of params that need to be tacked onto an http request that updates data, to - * prevent spoofing - */ - public static void addAuthActionParams(StringBuffer buf) { - buf.append(PARAM_AUTH_ACTION).append('=').append(_authNonce).append("&"); - } - - public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - req.setCharacterEncoding("UTF-8"); - resp.setCharacterEncoding("UTF-8"); - resp.setContentType("text/html;charset=UTF-8"); - resp.setHeader("cache-control", "no-cache"); - resp.setHeader("pragma", "no-cache"); - - User user = (User)req.getSession().getAttribute("user"); - String login = req.getParameter("login"); - String pass = req.getParameter("password"); - String action = req.getParameter("action"); - boolean forceNewIndex = false; - - boolean authAction = authAction(req); - - if (req.getParameter("regenerateIndex") != null) - forceNewIndex = true; - - User oldUser = user; - if (authAction) - user = handleRegister(user, req); - if (oldUser != user) - forceNewIndex = true; - - if (user == null) { - if ("Login".equals(action)) { - user = BlogManager.instance().login(login, pass); // ignore failures - user will just be unauthorized - if (!user.getAuthenticated()) { - user = BlogManager.instance().getDefaultUser(); - if (_log.shouldLog(Log.INFO)) - _log.info("Explicit login failed for [" + login + "], using default login"); - } else { - if (_log.shouldLog(Log.INFO)) - _log.info("Explicit login successful for [" + login + "]"); - } - } else { - user = BlogManager.instance().getDefaultUser(); - if (_log.shouldLog(Log.INFO)) - _log.info("Implicit login for the default user"); - } - forceNewIndex = true; - } else if (authAction && "Login".equals(action)) { - user = BlogManager.instance().login(login, pass); // ignore failures - user will just be unauthorized - if (!user.getAuthenticated()) { - if (_log.shouldLog(Log.INFO)) - _log.info("Explicit relogin failed for [" + login + "] from [" + user.getUsername() + "], using default user"); - user = BlogManager.instance().getDefaultUser(); - } else { - if (_log.shouldLog(Log.INFO)) - _log.info("Explicit relogin successful for [" + login + "] from [" + user.getUsername() + "]"); - } - forceNewIndex = true; - } else if (authAction && "Logout".equals(action)) { - if (_log.shouldLog(Log.INFO)) - _log.info("Explicit logout successful for [" + user.getUsername() + "], using default login"); - user = BlogManager.instance().getDefaultUser(); - forceNewIndex = true; - } - - req.getSession().setAttribute("user", user); - - if (authAction) { - handleAdmin(user, req); - - forceNewIndex = handleAddressbook(user, req) || forceNewIndex; - forceNewIndex = handleBookmarking(user, req) || forceNewIndex; - forceNewIndex = handleManageTags(user, req) || forceNewIndex; - handleUpdateProfile(user, req); - req.setAttribute(BaseServlet.class.getName() + ".auth", "true"); - } - - // the 'dataImported' flag is set by successful fetches in the SyndicateServlet/RemoteArchiveBean - if (user.resetDataImported()) { - forceNewIndex = true; - if (_log.shouldLog(Log.INFO)) - _log.info("Data imported, force regenerate"); - } - - FilteredThreadIndex index = (FilteredThreadIndex)req.getSession().getAttribute("threadIndex"); - - boolean authorOnly = Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue(); - if ( (index != null) && (authorOnly != index.getFilterAuthorsByRoot()) ) - forceNewIndex = true; - - Collection tags = getFilteredTags(req); - Collection filteredAuthors = getFilteredAuthors(req); - boolean tagsChanged = ( (index != null) && (!index.getFilteredTags().equals(tags)) ); - boolean authorsChanged = ( (index != null) && (!index.getFilteredAuthors().equals(filteredAuthors)) ); - - if (_log.shouldLog(Log.DEBUG)) - _log.debug("authorOnly=" + authorOnly + " forceNewIndex? " + forceNewIndex + " authors=" + filteredAuthors); - - if (forceNewIndex || (index == null) || (tagsChanged) || (authorsChanged) ) { - index = new FilteredThreadIndex(user, BlogManager.instance().getArchive(), getFilteredTags(req), filteredAuthors, authorOnly); - req.getSession().setAttribute("threadIndex", index); - if (_log.shouldLog(Log.INFO)) - _log.info("New filtered index created (forced? " + forceNewIndex + ", tagsChanged? " + tagsChanged + ", authorsChanged? " + authorsChanged + ")"); - } - - render(user, req, resp, index); - } - protected void render(User user, HttpServletRequest req, HttpServletResponse resp, ThreadIndex index) throws IOException, ServletException { - render(user, req, resp.getWriter(), index); - } - protected boolean isAuthed(HttpServletRequest req) { - String auth = (String)req.getAttribute(BaseServlet.class.getName() + ".auth"); - return (auth != null) && (Boolean.valueOf(auth).booleanValue()); - } - - private boolean handleBookmarking(User user, HttpServletRequest req) { - if (!user.getAuthenticated()) - return false; - - boolean rv = false; - - String loc = req.getParameter(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_LOCATION); - String group = req.getParameter(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_NAME); - if ( (loc != null) && (group != null) && (group.trim().length() > 0) ) { - try { - Hash key = new Hash(); - key.fromBase64(loc); - PetNameDB db = user.getPetNameDB(); - PetName pn = db.getByLocation(loc); - boolean isNew = false; - if (pn == null) { - isNew = true; - BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(key); - String name = null; - if (info != null) - name = info.getProperty(BlogInfo.NAME); - else - name = loc.substring(0,6); - - if (db.containsName(name)) { - int i = 0; - while (db.containsName(name + i)) - i++; - name = name + i; - } - - pn = new PetName(name, AddressesServlet.NET_SYNDIE, AddressesServlet.PROTO_BLOG, loc); - } - pn.addGroup(group); - if (isNew) - db.add(pn); - BlogManager.instance().saveUser(user); - // if we are ignoring someone, we need to recalculate the filters - if (FilteredThreadIndex.GROUP_IGNORE.equals(group)) - rv = true; - } catch (DataFormatException dfe) { - // bad loc, ignore - } - } - - String name = req.getParameter(ThreadedHTMLRenderer.PARAM_REMOVE_FROM_GROUP_NAME); - group = req.getParameter(ThreadedHTMLRenderer.PARAM_REMOVE_FROM_GROUP); - if ( (name != null) && (name.trim().length() > 0) ) { - PetNameDB db = user.getPetNameDB(); - PetName pn = db.getByName(name); - boolean changed = false; - if (pn != null) { - if ( (group != null) && (group.trim().length() > 0) ) { - // just remove them from the group - changed = pn.isMember(group); - pn.removeGroup(group); - if ( (changed) && (FilteredThreadIndex.GROUP_IGNORE.equals(group)) ) - rv = true; - } else { - // remove it completely - if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) - rv = true; - db.remove(pn); - changed = true; - } - } - if (changed) - BlogManager.instance().saveUser(user); - } - - if (rv) - _log.debug("Bookmarking required rebuild"); - return rv; - } - - private boolean handleManageTags(User user, HttpServletRequest req) { - if (!user.getAuthenticated()) - return false; - - boolean rv = false; - - String tag = req.getParameter(ThreadedHTMLRenderer.PARAM_ADD_TAG); - if ( (tag != null) && (tag.trim().length() > 0) ) { - tag = HTMLRenderer.sanitizeString(tag, false); - String name = tag; - PetNameDB db = user.getPetNameDB(); - PetName pn = db.getByLocation(tag); - if (pn == null) { - if (db.containsName(name)) { - int i = 0; - while (db.containsName(name + i)) - i++; - name = tag + i; - } - - pn = new PetName(name, AddressesServlet.NET_SYNDIE, AddressesServlet.PROTO_TAG, tag); - db.add(pn); - BlogManager.instance().saveUser(user); - } - } - - return false; - } - - private boolean handleAddressbook(User user, HttpServletRequest req) { - if ( (!user.getAuthenticated()) || (empty(AddressesServlet.PARAM_ACTION)) ) { - return false; - } - - String action = req.getParameter(AddressesServlet.PARAM_ACTION); - - if (AddressesServlet.ACTION_ADD_TAG.equals(action)) { - String name = req.getParameter(AddressesServlet.PARAM_NAME); - if ((name != null) && (name.trim().length() > 0) && (!user.getPetNameDB().containsName(name)) ) { - PetName pn = new PetName(name, AddressesServlet.NET_SYNDIE, AddressesServlet.PROTO_TAG, name); - user.getPetNameDB().add(pn); - BlogManager.instance().saveUser(user); - } - return false; - } else if ( (AddressesServlet.ACTION_ADD_ARCHIVE.equals(action)) || - (AddressesServlet.ACTION_ADD_BLOG.equals(action)) || - (AddressesServlet.ACTION_ADD_EEPSITE.equals(action)) || - (AddressesServlet.ACTION_ADD_OTHER.equals(action)) || - (AddressesServlet.ACTION_ADD_PEER.equals(action)) ) { - PetName pn = buildNewAddress(req); - if ( (pn != null) && (pn.getName() != null) && (pn.getName().trim().length() > 0) && (pn.getLocation() != null) && - (!user.getPetNameDB().containsName(pn.getName())) ) { - user.getPetNameDB().add(pn); - BlogManager.instance().saveUser(user); - - updateSyndication(user, pn.getLocation(), !empty(req, AddressesServlet.PARAM_SYNDICATE)); - - if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE) || - pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) - return true; - else - return false; - } else { - // not valid, ignore - return false; - } - } else if ( (AddressesServlet.ACTION_UPDATE_ARCHIVE.equals(action)) || - (AddressesServlet.ACTION_UPDATE_BLOG.equals(action)) || - (AddressesServlet.ACTION_UPDATE_EEPSITE.equals(action)) || - (AddressesServlet.ACTION_UPDATE_OTHER.equals(action)) || - (AddressesServlet.ACTION_UPDATE_PEER.equals(action)) ) { - return updateAddress(user, req); - } else if (AddressesServlet.ACTION_PURGE_AND_BAN_BLOG.equals(action)) { - String name = req.getParameter(AddressesServlet.PARAM_NAME); - PetName pn = user.getPetNameDB().getByName(name); - if (pn != null) { - boolean purged = false; - if (BlogManager.instance().authorizeRemote(user)) { - Hash h = null; - BlogURI uri = new BlogURI(pn.getLocation()); - if (uri.getKeyHash() != null) { - h = uri.getKeyHash(); - } - if (h == null) { - byte b[] = Base64.decode(pn.getLocation()); - if ( (b != null) && (b.length == Hash.HASH_LENGTH) ) - h = new Hash(b); - } - if (h != null) { - BlogManager.instance().purgeAndBan(h); - purged = true; - } - } - if (purged) // force a new thread index - return true; - else - return false; - } else { - return false; - } - } else if ( (AddressesServlet.ACTION_DELETE_ARCHIVE.equals(action)) || - (AddressesServlet.ACTION_DELETE_BLOG.equals(action)) || - (AddressesServlet.ACTION_DELETE_EEPSITE.equals(action)) || - (AddressesServlet.ACTION_DELETE_OTHER.equals(action)) || - (AddressesServlet.ACTION_DELETE_TAG.equals(action)) || - (AddressesServlet.ACTION_DELETE_PEER.equals(action)) ) { - String name = req.getParameter(AddressesServlet.PARAM_NAME); - PetName pn = user.getPetNameDB().getByName(name); - if (pn != null) { - user.getPetNameDB().remove(pn); - BlogManager.instance().saveUser(user); - updateSyndication(user, pn.getLocation(), false); - if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE) || - pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) - return true; - else - return false; - } else { - return false; - } - } else { - // not an addressbook op - return false; - } - } - - private boolean updateAddress(User user, HttpServletRequest req) { - PetName pn = user.getPetNameDB().getByName(req.getParameter(AddressesServlet.PARAM_NAME)); - if (pn != null) { - boolean wasIgnored = pn.isMember(FilteredThreadIndex.GROUP_IGNORE); - boolean wasFavorite = pn.isMember(FilteredThreadIndex.GROUP_FAVORITE); - - pn.setIsPublic(!empty(req, AddressesServlet.PARAM_IS_PUBLIC)); - pn.setLocation(req.getParameter(AddressesServlet.PARAM_LOC)); - pn.setNetwork(req.getParameter(AddressesServlet.PARAM_NET)); - pn.setProtocol(req.getParameter(AddressesServlet.PARAM_PROTO)); - if (empty(req, AddressesServlet.PARAM_FAVORITE)) - pn.removeGroup(FilteredThreadIndex.GROUP_FAVORITE); - else - pn.addGroup(FilteredThreadIndex.GROUP_FAVORITE); - if (empty(req, AddressesServlet.PARAM_IGNORE)) - pn.removeGroup(FilteredThreadIndex.GROUP_IGNORE); - else - pn.addGroup(FilteredThreadIndex.GROUP_IGNORE); - - BlogManager.instance().saveUser(user); - - if (AddressesServlet.PROTO_ARCHIVE.equals(pn.getProtocol())) - updateSyndication(user, pn.getLocation(), !empty(req, AddressesServlet.PARAM_SYNDICATE)); - - return (wasIgnored != pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) || - (wasFavorite != pn.isMember(FilteredThreadIndex.GROUP_IGNORE)); - } else { - return false; - } - } - - protected void updateSyndication(User user, String loc, boolean shouldAutomate) { - if (BlogManager.instance().authorizeRemote(user)) { - if (shouldAutomate) - BlogManager.instance().scheduleSyndication(loc); - else - BlogManager.instance().unscheduleSyndication(loc); - } - } - - protected PetName buildNewAddress(HttpServletRequest req) { - PetName pn = new PetName(); - pn.setName(req.getParameter(AddressesServlet.PARAM_NAME)); - pn.setIsPublic(!empty(req, AddressesServlet.PARAM_IS_PUBLIC)); - pn.setLocation(req.getParameter(AddressesServlet.PARAM_LOC)); - pn.setNetwork(req.getParameter(AddressesServlet.PARAM_NET)); - pn.setProtocol(req.getParameter(AddressesServlet.PARAM_PROTO)); - if (empty(req, AddressesServlet.PARAM_FAVORITE)) - pn.removeGroup(FilteredThreadIndex.GROUP_FAVORITE); - else - pn.addGroup(FilteredThreadIndex.GROUP_FAVORITE); - if (empty(req, AddressesServlet.PARAM_IGNORE)) - pn.removeGroup(FilteredThreadIndex.GROUP_IGNORE); - else - pn.addGroup(FilteredThreadIndex.GROUP_IGNORE); - return pn; - } - - protected void handleUpdateProfile(User user, HttpServletRequest req) { - if ( (user == null) || (!user.getAuthenticated()) || (user.getBlog() == null) ) - return; - - String action = req.getParameter("action"); - if ( (action == null) || !("Update profile".equals(action)) ) - return; - - String name = req.getParameter(ThreadedHTMLRenderer.PARAM_PROFILE_NAME); - String desc = req.getParameter(ThreadedHTMLRenderer.PARAM_PROFILE_DESC); - String url = req.getParameter(ThreadedHTMLRenderer.PARAM_PROFILE_URL); - String other = req.getParameter(ThreadedHTMLRenderer.PARAM_PROFILE_OTHER); - - Properties opts = new Properties(); - if (!empty(name)) - opts.setProperty(BlogInfo.NAME, name.trim()); - if (!empty(desc)) - opts.setProperty(BlogInfo.DESCRIPTION, desc.trim()); - if (!empty(url)) - opts.setProperty(BlogInfo.CONTACT_URL, url.trim()); - if (!empty(other)) { - StringBuffer key = new StringBuffer(); - StringBuffer val = null; - for (int i = 0; i < other.length(); i++) { - char c = other.charAt(i); - if ( (c == ':') || (c == '=') ) { - if (val != null) { - val.append(c); - } else { - val = new StringBuffer(); - } - } else if ( (c == '\n') || (c == '\r') ) { - String k = key.toString().trim(); - String v = (val != null ? val.toString().trim() : ""); - if ( (k.length() > 0) && (v.length() > 0) ) { - opts.setProperty(k, v); - } - key.setLength(0); - val = null; - } else if (val != null) { - val.append(c); - } else { - key.append(c); - } - } - // now finish the last of it - String k = key.toString().trim(); - String v = (val != null ? val.toString().trim() : ""); - if ( (k.length() > 0) && (v.length() > 0) ) { - opts.setProperty(k, v); - } - } - - String pass0 = req.getParameter("password"); - String pass1 = req.getParameter("passwordConfirm"); - String oldPass = req.getParameter("oldPassword"); - - if ( (pass0 != null) && (pass1 != null) && (pass0.equals(pass1)) ) { - BlogManager.instance().changePasswrd(user, oldPass, pass0, pass1); - } - - if (user.getAuthenticated() && !BlogManager.instance().authorizeRemote(user)) { - String adminPass = req.getParameter("adminPass"); - if (adminPass != null) { - boolean authorized = BlogManager.instance().authorizeRemote(adminPass); - if (authorized) { - user.setAllowAccessRemote(authorized); - BlogManager.instance().saveUser(user); - } - } - } - - boolean updated = BlogManager.instance().updateMetadata(user, user.getBlog(), opts); - } - - private User handleRegister(User user, HttpServletRequest req) { - String l = req.getParameter("login"); - String p = req.getParameter("password"); - String name = req.getParameter("accountName"); - String desc = req.getParameter("description"); - String contactURL = req.getParameter("url"); - String regPass = req.getParameter("registrationPass"); - String action = req.getParameter("action"); - - if ( (action != null) && ("Register".equals(action)) && !empty(l) ) { - return BlogManager.instance().register(l, p, regPass, name, desc, contactURL); - } else { - return user; - } - } - - private void handleAdmin(User user, HttpServletRequest req) throws IOException { - if (BlogManager.instance().authorizeRemote(user)) { - String action = req.getParameter("action"); - if ( (action != null) && ("Save config".equals(action)) ) { - boolean wantSingle = !empty(req, "singleuser"); - String defaultUser = req.getParameter("defaultUser"); - String defaultPass = req.getParameter("defaultPass"); - String regPass = req.getParameter("regpass"); - String remotePass = req.getParameter("remotepass"); - String proxyHost = req.getParameter("proxyhost"); - String proxyPort = req.getParameter("proxyport"); - - // default user cannot be empty, but the rest can be blank - if ( (!empty(defaultUser)) && (defaultPass != null) && (regPass != null) && (remotePass != null) && - (proxyHost != null) && (proxyPort != null) ) { - int port = 4444; - try { port = Integer.parseInt(proxyPort); } catch (NumberFormatException nfe) {} - BlogManager.instance().configure(regPass, remotePass, null, null, proxyHost, port, wantSingle, - null, defaultUser, defaultPass); - } - } - } - } - - - protected void render(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws ServletException, IOException { - Archive archive = BlogManager.instance().getArchive(); - int numThreads = 10; - int threadOffset = getOffset(req); - if (threadOffset == -1) { - threadOffset = index.getRootCount() - numThreads; - } - if (threadOffset < 0) { - threadOffset = 0; - } - - BlogURI visibleEntry = getVisible(req); - - int offset = 0; - if ( empty(req, ThreadedHTMLRenderer.PARAM_OFFSET) && (visibleEntry != null) ) { - // we're on a permalink, so jump the tree to the given thread - threadOffset = index.getRoot(visibleEntry); - if (threadOffset < 0) - threadOffset = 0; - } - - renderBegin(user, req, out, index); - renderNavBar(user, req, out); - renderControlBar(user, req, out, index); - renderServletDetails(user, req, out, index, threadOffset, visibleEntry, archive); - renderEnd(user, req, out, index); - } - - protected void renderBegin(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException { - out.write("\n"); - out.write("\n"); - out.write("\n\n" + getTitle() + "\n"); - out.write("\n"); - out.write("\n"); - out.write("\n"); - out.write(BEGIN_HTML); - } - - protected void renderNavBar(User user, HttpServletRequest req, PrintWriter out) throws IOException { - out.write("
    \n"); - out.write("\n"); - out.write("\n"); - out.write("Threads Blogs "); - if (user.getAuthenticated() && (user.getBlog() != null) ) { - out.write("Logged in as "); - out.write(user.getUsername()); - out.write("\n"); - out.write("(switch)\n"); - out.write("Post\n"); - out.write("Addressbook\n"); - } else { - out.write("Log in\n"); - } - out.write("\n"); - out.write("About "); - if (BlogManager.instance().authorizeRemote(user)) { - out.write("Syndicate\n"); - out.write("Import RSS/Atom\n"); - out.write("Admin\n"); - } - out.write("\n
    \n"); - } - - /* - - protected void renderNavBar(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException { - //out.write("\n"); - out.write("\n"); - out.write("\n"); - out.write("Threads Blogs "); - if (user.getAuthenticated() && (user.getBlog() != null) ) { - out.write("Logged in as "); - out.write(user.getUsername()); - out.write("\n"); - out.write("(switch)\n"); - out.write("Post\n"); - out.write("Addressbook\n"); - } else { - out.write("\n"); - writeAuthActionFields(out); - out.write("Login: \n"); - out.write("Password: \n"); - out.write("\n"); - } - //out.write("\n"); - out.write("\n"); - out.write("About "); - if (BlogManager.instance().authorizeRemote(user)) { - out.write("Syndicate\n"); - out.write("Import RSS/Atom\n"); - out.write("Admin\n"); - } - out.write("\n\n"); - } - */ - - protected String getSyndicateLink(User user, String location) { - if (location != null) - return "syndicate.jsp?" + SyndicateServlet.PARAM_LOCATION + "=" + location; - return "syndicate.jsp"; - } - - protected static final ArrayList SKIP_TAGS = new ArrayList(); - static { - SKIP_TAGS.add("action"); - SKIP_TAGS.add("filter"); - // post and visible are skipped since we aren't good at filtering by tag when the offset will - // skip around randomly. at least, not yet. - SKIP_TAGS.add(ThreadedHTMLRenderer.PARAM_VISIBLE); - //SKIP_TAGS.add("post"); - //SKIP_TAGS.add("thread"); - SKIP_TAGS.add(ThreadedHTMLRenderer.PARAM_OFFSET); // if we are adjusting the filter, ignore the previous offset - SKIP_TAGS.add(ThreadedHTMLRenderer.PARAM_DAYS_BACK); - SKIP_TAGS.add("addLocation"); - SKIP_TAGS.add("addGroup"); - SKIP_TAGS.add("login"); - SKIP_TAGS.add("password"); - } - - private static final String CONTROL_TARGET = "threads.jsp"; - protected String getControlTarget() { return CONTROL_TARGET; } - protected String getPostURI() { return "post.jsp"; } - - protected void renderControlBar(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException { - out.write("
    \n"); - String tags = ""; - String author = ""; - Enumeration params = req.getParameterNames(); - while (params.hasMoreElements()) { - String param = (String)params.nextElement(); - String val = req.getParameter(param); - if (ThreadedHTMLRenderer.PARAM_TAGS.equals(param)) { - tags = val; - } else if (ThreadedHTMLRenderer.PARAM_AUTHOR.equals(param)) { - author = val; - } else if (SKIP_TAGS.contains(param)) { - // skip - } else if (param.length() <= 0) { - // skip - } else { - out.write("\n"); - } - } - out.write("\n"); - out.write("\n"); - out.write("Filter: \n"); - - out.write("Tags: "); - writeTagField(user, tags, out); - - String days = req.getParameter(ThreadedHTMLRenderer.PARAM_DAYS_BACK); - if (days == null) - days = ""; - out.write("Age: days\n"); - - out.write("\n"); - out.write(""); - - if ( (req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST) != null) || - (req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD) != null) ) - out.write("Threads"); - out.write("\n"); - out.write("\n"); - out.write("\n"); - out.write("
    \n"); - } - - protected void writeTagField(User user, String selectedTags, PrintWriter out) throws IOException { - writeTagField(user, selectedTags, out, "Threads are filtered to include only ones with posts containing these tags", "Any tags - no filtering", true); - } - public static void writeTagField(User user, String selectedTags, Writer out, String title, String blankTitle, boolean includeFavoritesTag) throws IOException { - Set favoriteTags = new TreeSet(user.getFavoriteTags()); - if (favoriteTags.size() <= 0) { - out.write("\n"); - } else { - out.write("\n"); - } - } - - protected abstract void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, - ThreadIndex index, int threadOffset, BlogURI visibleEntry, - Archive archive) throws IOException; - - protected static final int getOffset(HttpServletRequest req) { - String off = req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET); - try { - return Integer.parseInt(off); - } catch (NumberFormatException nfe) { - return 0; - } - } - protected static final BlogURI getVisible(HttpServletRequest req) { - return getAsBlogURI(req.getParameter(ThreadedHTMLRenderer.PARAM_VISIBLE)); - } - protected static final BlogURI getAsBlogURI(String uri) { - if (uri != null) { - int split = uri.indexOf('/'); - if ( (split <= 0) || (split + 1 >= uri.length()) ) - return null; - String blog = uri.substring(0, split); - String id = uri.substring(split+1); - try { - Hash hash = new Hash(); - hash.fromBase64(blog); - long msgId = Long.parseLong(id); - if (msgId > 0) - return new BlogURI(hash, msgId); - } catch (DataFormatException dfe) { - return null; - } catch (NumberFormatException nfe) { - return null; - } - } - return null; - } - - - protected String trim(String orig, int maxLen) { - if ( (orig == null) || (orig.length() <= maxLen) ) - return orig; - return orig.substring(0, maxLen) + "..."; - } - - protected static final boolean empty(HttpServletRequest req, String param) { - String val = req.getParameter(param); - return (val == null) || (val.trim().length() <= 0); - } - - protected static final boolean empty(String val) { - return (val == null) || (val.trim().length() <= 0); - } - - protected String getExpandLink(HttpServletRequest req, ThreadNode node) { - return getExpandLink(node, req.getRequestURI(), req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), - req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD), - req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), - req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), - req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR)); - } - protected static String getExpandLink(ThreadNode node, String uri, String viewPost, String viewThread, - String offset, String tags, String author) { - StringBuffer buf = new StringBuffer(64); - buf.append(uri); - buf.append('?'); - // expand node == let one of node's children be visible - if (node.getChildCount() > 0) { - ThreadNode child = node.getChild(0); - buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('='); - buf.append(child.getEntry().getKeyHash().toBase64()).append('/'); - buf.append(child.getEntry().getEntryId()).append("&"); - } - - if (!empty(viewPost)) - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append("&"); - else if (!empty(viewThread)) - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append("&"); - - if (!empty(offset)) - buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append("&"); - - if (!empty(tags)) - buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append("&"); - - if (!empty(author)) - buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append("&"); - - return buf.toString(); - } - protected String getCollapseLink(HttpServletRequest req, ThreadNode node) { - return getCollapseLink(node, req.getRequestURI(), - req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), - req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD), - req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), - req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), - req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR)); - } - - protected String getCollapseLink(ThreadNode node, String uri, String viewPost, String viewThread, - String offset, String tags, String author) { - StringBuffer buf = new StringBuffer(64); - buf.append(uri); - // collapse node == let the node be visible - buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('='); - buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); - buf.append(node.getEntry().getEntryId()).append("&"); - - if (!empty(viewPost)) - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append("&"); - else if (!empty(viewThread)) - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append("&"); - - if (!empty(offset)) - buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append("&"); - - if (!empty(tags)) - buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append("&"); - - if (!empty(author)) - buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append("&"); - - return buf.toString(); - } - protected String getProfileLink(HttpServletRequest req, Hash author) { - return getProfileLink(author); - } - protected String getProfileLink(Hash author) { return ThreadedHTMLRenderer.buildProfileURL(author); } - - protected String getAddToGroupLink(HttpServletRequest req, Hash author, User user, String group) { - return getAddToGroupLink(user, author, group, req.getRequestURI(), - req.getParameter(ThreadedHTMLRenderer.PARAM_VISIBLE), - req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), - req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD), - req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), - req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), - req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR)); - } - protected String getAddToGroupLink(User user, Hash author, String group, String uri, String visible, - String viewPost, String viewThread, String offset, String tags, String filteredAuthor) { - StringBuffer buf = new StringBuffer(64); - buf.append(uri); - buf.append('?'); - if (!empty(visible)) - buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=').append(visible).append("&"); - buf.append(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_LOCATION).append('=').append(author.toBase64()).append("&"); - buf.append(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_NAME).append('=').append(group).append("&"); - - if (!empty(viewPost)) - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append("&"); - else if (!empty(viewThread)) - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append("&"); - - if (!empty(offset)) - buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append("&"); - - if (!empty(tags)) - buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append("&"); - - if (!empty(filteredAuthor)) - buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(filteredAuthor).append("&"); - - addAuthActionParams(buf); - return buf.toString(); - } - protected String getRemoveFromGroupLink(User user, String name, String group, String uri, String visible, - String viewPost, String viewThread, String offset, String tags, String filteredAuthor) { - StringBuffer buf = new StringBuffer(64); - buf.append(uri); - buf.append('?'); - if (!empty(visible)) - buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=').append(visible).append("&"); - buf.append(ThreadedHTMLRenderer.PARAM_REMOVE_FROM_GROUP_NAME).append('=').append(name).append("&"); - buf.append(ThreadedHTMLRenderer.PARAM_REMOVE_FROM_GROUP).append('=').append(group).append("&"); - - if (!empty(viewPost)) - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append("&"); - else if (!empty(viewThread)) - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append("&"); - - if (!empty(offset)) - buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append("&"); - - if (!empty(tags)) - buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append("&"); - - if (!empty(filteredAuthor)) - buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(filteredAuthor).append("&"); - - addAuthActionParams(buf); - return buf.toString(); - } - protected String getViewPostLink(HttpServletRequest req, ThreadNode node, User user, boolean isPermalink) { - return ThreadedHTMLRenderer.getViewPostLink(req.getRequestURI(), node, user, isPermalink, - req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), - req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), - req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR), - Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue()); - } - protected String getViewPostLink(HttpServletRequest req, BlogURI post, User user) { - return ThreadedHTMLRenderer.getViewPostLink(req.getRequestURI(), post, user, false, - req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), - req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), - req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR), - Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue()); - } - protected String getViewThreadLink(HttpServletRequest req, ThreadNode node, User user) { - return getViewThreadLink(req.getRequestURI(), node, user, - req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), - req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), - req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR), - Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue()); - } - protected static String getViewThreadLink(String uri, ThreadNode node, User user, String offset, - String tags, String author, boolean authorOnly) { - StringBuffer buf = new StringBuffer(64); - buf.append(uri); - BlogURI expandTo = node.getEntry(); - if (node.getChildCount() > 0) { - if (true) { - // lets expand to the leaf - expandTo = new BlogURI(node.getMostRecentPostAuthor(), node.getMostRecentPostDate()); - } else { - // only expand one level - expandTo = node.getChild(0).getEntry(); - } - } - buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('='); - buf.append(expandTo.getKeyHash().toBase64()).append('/'); - buf.append(expandTo.getEntryId()).append("&"); - - buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('='); - buf.append(node.getEntry().getKeyHash().toBase64()).append('/'); - buf.append(node.getEntry().getEntryId()).append("&"); - - if (!empty(offset)) - buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append("&"); - - if (!empty(tags)) - buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append("&"); - - if (!empty(author)) { - buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append("&"); - if (authorOnly) - buf.append(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR).append("=true&"); - } - buf.append("#").append(node.getEntry().toString()); - return buf.toString(); - } - protected String getFilterByTagLink(HttpServletRequest req, ThreadNode node, User user, String tag, String author) { - return ThreadedHTMLRenderer.getFilterByTagLink(req.getRequestURI(), node, user, tag, author); - } - protected String getNavLink(HttpServletRequest req, int offset) { - return ThreadedHTMLRenderer.getNavLink(req.getRequestURI(), - req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), - req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD), - req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), - req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR), - Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue(), - offset); - } - - protected void renderEnd(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException { - out.write(END_HTML); - } - - protected Collection getFilteredTags(HttpServletRequest req) { - String tags = req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS); - if (tags != null) { - StringTokenizer tok = new StringTokenizer(tags, "\n\t "); - ArrayList rv = new ArrayList(); - while (tok.hasMoreTokens()) { - String tag = tok.nextToken().trim(); - if (tag.length() > 0) - rv.add(tag); - } - return rv; - } else { - return Collections.EMPTY_LIST; - } - } - - protected Collection getFilteredAuthors(HttpServletRequest req) { - List rv = new ArrayList(); - rv.addAll(getAuthors(req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR))); - //rv.addAll(getAuthors(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR))); - return rv; - } - - private Collection getAuthors(String authors) { - if (authors != null) { - StringTokenizer tok = new StringTokenizer(authors, "\n\t "); - ArrayList rv = new ArrayList(); - while (tok.hasMoreTokens()) { - try { - Hash h = new Hash(); - h.fromBase64(tok.nextToken().trim()); - rv.add(h); - } catch (DataFormatException dfe) {} - } - return rv; - } else { - return Collections.EMPTY_LIST; - } - } - - private static final String BEGIN_HTML = "\n" + -"\n" + -"\n" + -"Jump to the beginning of the first post rendered, if any\n" + -"Jump to the thread navigation\n\n" + -"\n"; - private static final String STYLE_HTML = "* {\n" + -" margin: 0;\n" + -" padding: 0;\n" + -"}\n" + -"body {\n" + -" font-family: Arial, Helvetica, sans-serif;\n" + -" font-size: 100%;\n" + -" background-color : #EEEEEE;\n" + -" color: #000000;\n" + -"}\n" + -"select {\n" + -" min-width: 1.5em;\n" + -"}\n" + -".overallTable {\n" + -" border-spacing: 0px;\n" + -" border-collapse:collapse;\n" + -" float:left;\n" + -"}\n" + -".topNav {\n" + -" background-color: #BBBBBB;\n" + -"}\n" + -".topNav_user {\n" + -" text-align: left;\n" + -" float: left;\n" + -" display: inline;\n" + -"}\n" + -".topNav_admin {\n" + -" text-align: right;\n" + -" float: right;\n" + -" margin: 0 5px 0 0;\n" + -" display: inline;\n" + -"}\n" + -".controlBar {\n" + -" background-color: #BBBBBB;\n" + -"}\n" + -".controlBarRight {\n" + -" text-align: right;\n" + -"}\n" + -".threadEven {\n" + -" background-color: #FFFFFF;\n" + -" white-space: nowrap;\n" + -"}\n" + -".threadOdd {\n" + -" background-color: #EEEEEE;\n" + -" white-space: nowrap;\n" + -"}\n" + -".threadLeft {\n" + -" text-align: left;\n" + -" align: left;\n" + -"}\n" + -".threadNav {\n" + -" background-color: #BBBBBB;\n" + -"}\n" + -".threadNavRight {\n" + -" text-align: right;\n" + -" float: right;\n" + -" background-color: #BBBBBB;\n" + -"}\n" + -".rightOffset {\n" + -" float: right;\n" + -" margin: 0 5px 0 0;\n" + -" display: inline;\n" + -"}\n" + -".threadInfoLeft {\n" + -" float: left;\n" + -" margin: 5px 0px 0 0;\n" + -" display: inline;\n" + -"}\n" + -".threadInfoRight {\n" + -" float: right;\n" + -" margin: 0 5px 0 0;\n" + -" display: inline;\n" + -"}\n" + -".postMeta {\n" + -" background-color: #BBBBFF;\n" + -"}\n" + -".postMetaSubject {\n" + -" text-align: left;\n" + -"}\n" + -".postMetaLink {\n" + -" text-align: right;\n" + -"}\n" + -".postDetails {\n" + -" background-color: #DDDDFF;\n" + -"}\n" + -".postReply {\n" + -" background-color: #BBBBFF;\n" + -"}\n" + -".postReplyText {\n" + -" background-color: #BBBBFF;\n" + -"}\n" + -".postReplyOptions {\n" + -" background-color: #BBBBFF;\n" + -"}\n" + -".syndieBlogTopNav {\n" + -" float:left;\n" + -" width: 100%;\n" + -" background-color: #BBBBBB;\n" + -"}\n" + -".syndieBlogTopNavUser {\n" + -" text-align: left;\n" + -" float: left;\n" + -"}\n" + -".syndieBlogTopNavAdmin {\n" + -" text-align: left;\n" + -" float: right;\n" + -"}\n" + -".syndieBlogFavorites {\n" + -" float: left;\n" + -" margin: 5px 0px 0 0;\n" + -" display: inline;\n" + -"}\n" + -".syndieBlogList {\n" + -" float: right;\n" + -" margin: 5px 0px 0 0;\n" + -" display: inline;\n" + -"}\n"; - - private static final String END_HTML = "
    \n" + -"\n"; - - protected String getTitle() { return "Syndie"; } - - protected static class TreeRenderState { - private int _rowsWritten; - private int _rowsSkipped; - private List _ignored; - public TreeRenderState(List ignored) { - _rowsWritten = 0; - _rowsSkipped = 0; - _ignored = ignored; - } - public int getRowsWritten() { return _rowsWritten; } - public void incrementRowsWritten() { _rowsWritten++; } - public int getRowsSkipped() { return _rowsSkipped; } - public void incrementRowsSkipped() { _rowsSkipped++; } - public List getIgnoredAuthors() { return _ignored; } - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigBean.java b/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigBean.java deleted file mode 100644 index 1652a525a..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigBean.java +++ /dev/null @@ -1,308 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Properties; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.data.DataHelper; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogInfoData; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.EntryContainer; -import net.i2p.util.Log; - -/** - * - */ -public class BlogConfigBean { - private I2PAppContext _context; - private Log _log; - private User _user; - private String _title; - private String _description; - private String _contactInfo; - /** list of list of PetNames */ - private List _groups; - private Properties _styleOverrides; - private File _logo; - private boolean _loaded; - private boolean _updated; - - public BlogConfigBean() { - _context = I2PAppContext.getGlobalContext(); - _log = _context.logManager().getLog(BlogConfigBean.class); - _groups = new ArrayList(); - _styleOverrides = new Properties(); - } - - public boolean isUpdated() { return _updated; } - - public User getUser() { return _user; } - public void setUser(User user) { - _user = user; - _title = null; - _description = null; - _contactInfo = null; - _groups.clear(); - _styleOverrides.clear(); - if (_logo != null) - _logo.delete(); - _logo = null; - _loaded = false; - _updated = false; - load(); - } - public String getTitle() { return _title; } - public void setTitle(String title) { - _title = title; - _updated = true; - } - public String getDescription() { return _description; } - public void setDescription(String desc) { - _description = desc; - _updated = true; - } - public String getContactInfo() { return _contactInfo; } - public void setContactInfo(String info) { - _contactInfo = info; - _updated = true; - } - public int getGroupCount() { return _groups.size(); } - /** gets the actual modifiable list of PetName instances */ - public List getGroup(int i) { return (List)_groups.get(i); } - /** gets the actual modifiable list of PetName instances */ - public List getGroup(String name) { - for (int i = 0; i < _groups.size(); i++) { - List grp = (List)_groups.get(i); - if (grp.size() > 0) { - PetName pn = (PetName)grp.get(0); - if ( (pn.getGroupCount() == 0) && ( (name == null) || (name.length() <= 0) ) ) - return grp; - if (pn.getGroupCount() == 0) - continue; - String curGroup = pn.getGroup(0); - if (curGroup.equals(name)) - return grp; - } - } - return null; - } - /** adds the given element to the appropriate group (creating a new one if necessary) */ - public void add(PetName pn) { - String groupName = null; - if (pn.getGroupCount() > 0) - groupName = pn.getGroup(0); - List group = getGroup(groupName); - if (group == null) { - group = new ArrayList(4); - group.add(pn); - _groups.add(group); - } else { - group.add(pn); - } - _updated = true; - } - public void remove(PetName pn) { - String groupName = null; - if (pn.getGroupCount() > 0) - groupName = pn.getGroup(0); - List group = getGroup(groupName); - if (group != null) { - group.remove(pn); - if (group.size() <= 0) - _groups.remove(group); - } - _updated = true; - } - public void remove(String name) { - for (int i = 0; i < getGroupCount(); i++) { - List group = getGroup(i); - for (int j = 0; j < group.size(); j++) { - PetName pn = (PetName)group.get(j); - if (pn.getName().equals(name)) { - group.remove(j); - if (group.size() <= 0) - _groups.remove(group); - _updated = true; - return; - } - } - } - } - /** take note that the groups have been updated in some way (reordered, etc) */ - public void groupsUpdated() { _updated = true; } - public String getStyleOverride(String prop) { return _styleOverrides.getProperty(prop); } - public void setStyleOverride(String prop, String val) { - _styleOverrides.setProperty(prop, val); - _updated = true; - } - public void unsetStyleOverride(String prop) { - _styleOverrides.remove(prop); - _updated = true; - } - public File getLogo() { return _logo; } - public void setLogo(File logo) { - if ( (logo != null) && (logo.length() > BlogInfoData.MAX_LOGO_SIZE) ) { - _log.error("Refusing a logo of size " + logo.length()); - logo.delete(); - return; - } - if (_logo != null) - _logo.delete(); - _logo = logo; - _updated = true; - } - public boolean hasPendingChanges() { return _loaded && _updated; } - - private void load() { - Archive archive = BlogManager.instance().getArchive(); - BlogInfo info = archive.getBlogInfo(_user.getBlog()); - if (info != null) { - _title = info.getProperty(BlogInfo.NAME); - _description = info.getProperty(BlogInfo.DESCRIPTION); - _contactInfo = info.getProperty(BlogInfo.CONTACT_URL); - String id = info.getProperty(BlogInfo.SUMMARY_ENTRY_ID); - if (id != null) { - BlogURI uri = new BlogURI(id); - EntryContainer entry = archive.getEntry(uri); - if (entry != null) { - BlogInfoData data = new BlogInfoData(); - try { - data.load(entry); - if (data.isLogoSpecified()) { - File logo = File.createTempFile("logo", ".png", BlogManager.instance().getTempDir()); - FileOutputStream os = null; - try { - os = new FileOutputStream(logo); - data.writeLogo(os); - _logo = logo; - } finally { - if (os != null) try { os.close(); } catch (IOException ioe) {} - } - } - for (int i = 0; i < data.getReferenceGroupCount(); i++) { - List group = (List)data.getReferenceGroup(i); - for (int j = 0; j < group.size(); j++) { - PetName pn = (PetName)group.get(j); - add(pn); - } - } - Properties overrides = data.getStyleOverrides(); - if (overrides != null) - _styleOverrides.putAll(overrides); - } catch (IOException ioe) { - _log.warn("Unable to load the blog info data from " + uri, ioe); - } - } - } - } - _loaded = true; - _updated = false; - } - - public boolean publishChanges() { - FileInputStream logo = null; - try { - if (_logo != null) { - logo = new FileInputStream(_logo); - _log.debug("Logo file is: " + _logo.length() + "bytes @ " + _logo.getAbsolutePath()); - } - InputStream styleStream = createStyleStream(); - InputStream groupStream = createGroupStream(); - - String tags = BlogInfoData.TAG; - String subject = "n/a"; - String headers = ""; - String sml = ""; - List filenames = new ArrayList(); - List filestreams = new ArrayList(); - List filetypes = new ArrayList(); - if (logo != null) { - filenames.add(BlogInfoData.ATTACHMENT_LOGO); - filestreams.add(logo); - filetypes.add("image/png"); - } - filenames.add(BlogInfoData.ATTACHMENT_STYLE_OVERRIDE); - filestreams.add(styleStream); - filetypes.add("text/plain"); - filenames.add(BlogInfoData.ATTACHMENT_REFERENCE_GROUPS); - filestreams.add(groupStream); - filetypes.add("text/plain"); - - BlogURI uri = BlogManager.instance().createBlogEntry(_user, subject, tags, headers, sml, - filenames, filestreams, filetypes); - if (uri != null) { - Archive archive = BlogManager.instance().getArchive(); - BlogInfo info = archive.getBlogInfo(_user.getBlog()); - if (info != null) { - String props[] = info.getProperties(); - Properties opts = new Properties(); - for (int i = 0; i < props.length; i++) { - if (!props[i].equals(BlogInfo.SUMMARY_ENTRY_ID)) - opts.setProperty(props[i], info.getProperty(props[i])); - } - opts.setProperty(BlogInfo.SUMMARY_ENTRY_ID, uri.toString()); - boolean updated = BlogManager.instance().updateMetadata(_user, _user.getBlog(), opts); - if (updated) { - // ok great, published locally, though should we push it to others? - _log.info("Blog summary updated for " + _user + " in " + uri.toString()); - setUser(_user); - _log.debug("Updated? " + _updated); - return true; - } - } else { - _log.error("Info is not known for " + _user.getBlog().toBase64()); - return false; - } - } else { - _log.error("Error creating the summary entry"); - return false; - } - } catch (IOException ioe) { - _log.error("Error publishing", ioe); - } finally { - if (logo != null) try { logo.close(); } catch (IOException ioe) {} - // the other streams are in-memory, drop with the scope - if (_logo != null) _logo.delete(); - } - return false; - } - private InputStream createStyleStream() throws IOException { - StringBuffer buf = new StringBuffer(1024); - if (_styleOverrides != null) { - for (Iterator iter = _styleOverrides.keySet().iterator(); iter.hasNext(); ) { - String key = (String)iter.next(); - String val = _styleOverrides.getProperty(key); - buf.append(key).append('=').append(val).append('\n'); - } - } - return new ByteArrayInputStream(DataHelper.getUTF8(buf)); - } - private InputStream createGroupStream() throws IOException { - StringBuffer buf = new StringBuffer(1024); - for (int i = 0; i < _groups.size(); i++) { - List group = (List)_groups.get(i); - for (int j = 0; j < group.size(); j++) { - PetName pn = (PetName)group.get(j); - buf.append(pn.toString()).append('\n'); - } - } - return new ByteArrayInputStream(DataHelper.getUTF8(buf)); - } - - protected void finalize() { - if (_logo != null) _logo.delete(); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigServlet.java deleted file mode 100644 index c2cfc5d14..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigServlet.java +++ /dev/null @@ -1,451 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.client.naming.PetName; -import net.i2p.client.naming.PetNameDB; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogInfoData; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.sml.HTMLRenderer; - -/** - * Display our blog config, and let us edit it through several screens - * - */ -public class BlogConfigServlet extends BaseServlet { - private static final String ATTR_CONFIG_BEAN = "__blogConfigBean"; - public static final String PARAM_CONFIG_SCREEN = "screen"; - public static final String SCREEN_REFERENCES = "references"; - public static final String SCREEN_IMAGES = "images"; - - public static BlogConfigBean getConfigBean(HttpServletRequest req, User user) { - BlogConfigBean bean = (BlogConfigBean)req.getSession().getAttribute(ATTR_CONFIG_BEAN); - if (bean == null) { - bean = new BlogConfigBean(); - bean.setUser(user); - req.getSession().setAttribute(ATTR_CONFIG_BEAN, bean); - } - return bean; - } - public static BlogConfigBean getConfigBean(HttpServletRequest req) { - return (BlogConfigBean)req.getSession().getAttribute(ATTR_CONFIG_BEAN); - } - - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - if ( (user == null) || (!user.getAuthenticated() && !BlogManager.instance().isSingleUser())) { - out.write("You must be logged in to edit your profile"); - return; - } - - BlogConfigBean bean = getConfigBean(req, user); - - String screen = req.getParameter(PARAM_CONFIG_SCREEN); - if (screen == null) - screen = SCREEN_REFERENCES; - out.write("\n"); - showConfigNav(req, out); - - if (isAuthed(req)) { - StringBuffer buf = handleOtherAuthedActions(user, req, bean); - if (buf != null) out.write(buf.toString()); - } else { - String contentType = req.getContentType(); - if (!empty(contentType) && (contentType.indexOf("boundary=") != -1)) { - StringBuffer buf = handlePost(user, req, bean); - if (buf != null) out.write(buf.toString()); - } - } - if (bean.isUpdated()) - showCommitForm(req, out); - - if (SCREEN_REFERENCES.equals(screen)) { - displayReferencesScreen(req, out, user, bean); - } else if (SCREEN_IMAGES.equals(screen)) { - displayImagesScreen(req, out, user, bean); - } else { - displayUnknownScreen(out, screen); - } - out.write("\n"); - } - private StringBuffer handlePost(User user, HttpServletRequest rawRequest, BlogConfigBean bean) throws IOException { - StringBuffer rv = new StringBuffer(64); - MultiPartRequest req = new MultiPartRequest(rawRequest); - if (authAction(req.getString(PARAM_AUTH_ACTION))) { - // read in the logo if specified - String filename = req.getFilename("newLogo"); - if ( (filename != null) && (filename.trim().length() > 0) ) { - Hashtable params = req.getParams("newLogo"); - String type = "image/png"; - for (Iterator iter = params.keySet().iterator(); iter.hasNext(); ) { - String cur = (String)iter.next(); - if ("content-type".equalsIgnoreCase(cur)) { - type = (String)params.get(cur); - break; - } - } - InputStream logoSrc = req.getInputStream("newLogo"); - - File tmpLogo = File.createTempFile("blogLogo", ".png", BlogManager.instance().getTempDir()); - FileOutputStream out = null; - try { - out = new FileOutputStream(tmpLogo); - byte buf[] = new byte[4096]; - int read = 0; - while ( (read = logoSrc.read(buf)) != -1) - out.write(buf, 0, read); - } finally { - if (out != null) try { out.close(); } catch (IOException ioe) {} - } - - long len = tmpLogo.length(); - if (len > BlogInfoData.MAX_LOGO_SIZE) { - tmpLogo.delete(); - rv.append("Proposed logo is too large (" + len + ", max of " + BlogInfoData.MAX_LOGO_SIZE + ")
    \n"); - } else { - bean.setLogo(tmpLogo); - rv.append("Logo updated
    "); - } - } else { - // logo not specified - } - } else { - // noop - } - return rv; - } - - private void showCommitForm(HttpServletRequest req, PrintWriter out) throws IOException { - out.write("
    \n"); - writeAuthActionFields(out); - out.write("Note: Uncommitted changes outstanding \n\n"); - } - - private void showConfigNav(HttpServletRequest req, PrintWriter out) throws IOException { - out.write("References " - + "Images
    \n"); - } - - private String getScreenURL(HttpServletRequest req, String screen, boolean wantAuth) { - StringBuffer buf = new StringBuffer(128); - buf.append(req.getRequestURI()).append("?").append(PARAM_CONFIG_SCREEN).append("="); - buf.append(screen).append("&"); - if (wantAuth) - buf.append(PARAM_AUTH_ACTION).append('=').append(_authNonce).append("&"); - return buf.toString(); - } - - private void displayUnknownScreen(PrintWriter out, String screen) throws IOException { - out.write("

    The screen " + HTMLRenderer.sanitizeString(screen) + " has not yet been implemented"); - } - private void displayReferencesScreen(HttpServletRequest req, PrintWriter out, User user, BlogConfigBean bean) throws IOException { - out.write("
    \n"); - writeAuthActionFields(out); - out.write("
      \n"); - boolean defaultFound = false; - for (int i = 0; i < bean.getGroupCount(); i++) { - List group = bean.getGroup(i); - String groupName = null; - PetName pn = (PetName)group.get(0); - if (pn.getGroupCount() <= 0) { - groupName = ViewBlogServlet.DEFAULT_GROUP_NAME; - defaultFound = true; - } else { - groupName = pn.getGroup(0); - } - out.write("
    1. Group: " + HTMLRenderer.sanitizeString(groupName) + "\n"); - if (i > 0) - out.write(" ^"); - if (i + 1 < bean.getGroupCount()) - out.write(" v"); - out.write(" X"); - - out.write("
        \n"); - for (int j = 0; j < group.size(); j++) { - out.write("
      1. " + ViewBlogServlet.renderLink(user.getBlog(), (PetName)group.get(j))); - if (j > 0) - out.write(" ^"); - if (j + 1 < group.size()) - out.write(" v"); - out.write(" X"); - out.write("
      2. \n"); - } - out.write("
      \n"); - out.write("
    2. \n"); - } - out.write("
    \n"); - - - out.write("Add a new element:
    Group: or
    " + - "Type:
    \n" + - "Name:
    " + - "Location: \n" + - "
    • Blogs should be specified as $base64Key
    • \n" + - "
    • Blog posts should be specified as $base64Key/$postEntryId
    • \n" + - "
    • Blog post attachments should be specified as $base64Key/$postEntryId/$attachmentNum
    • \n" + - "

    \n"); - - out.write("\n"); - out.write("
    \n"); - } - - private void writePetnameDropdown(PrintWriter out, PetNameDB db) throws IOException { - Set names = db.getNames(); - TreeSet ordered = new TreeSet(names); - for (Iterator iter = ordered.iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - PetName pn = db.getByName(name); - String proto = pn.getProtocol(); - if ("syndietag".equals(proto)) - continue; - out.write("\n"); - } - } - - private void displayImagesScreen(HttpServletRequest req, PrintWriter out, User user, BlogConfigBean bean) throws IOException { - out.write("
    \n"); - writeAuthActionFields(out); - - File logo = bean.getLogo(); - if (logo != null) - out.write("Blog logo: \"Your
    \n"); - out.write("New logo:
    \n"); - out.write("\n"); - out.write("
    \n"); - } - - protected StringBuffer handleOtherAuthedActions(User user, HttpServletRequest req, BlogConfigBean bean) { - StringBuffer buf = new StringBuffer(); - req.setAttribute(getClass().getName() + ".output", buf); - String action = req.getParameter("action"); - if ("Publish blog configuration".equals(action)) { - if (bean.publishChanges()) { - buf.append("Changes published
    \n"); - } else { - buf.append("Changes could not be published (please check the log)
    \n"); - } - } else { - if ("Save changes".equals(action)) { - String newGroup = req.getParameter("new.group"); - if ( (newGroup == null) || (newGroup.trim().length() <= 0) ) - newGroup = req.getParameter("new.groupOther"); - if ( (newGroup != null) && (newGroup.trim().length() > 0) ) { - addElement(req, user, newGroup, buf, bean); - } else { - } - } else { - } - - handleDelete(req, user, bean, buf); - handleReorderGroup(req, user, bean, buf); - handleReorderRef(req, user, bean, buf); - } - return buf; - } - - private void addElement(HttpServletRequest req, User user, String newGroup, StringBuffer actionOutputHTML, BlogConfigBean bean) { - String type = req.getParameter("new.type"); - String loc = req.getParameter("new.location"); - String name = req.getParameter("new.name"); - - if (empty(type) || empty(loc) || empty(name)) return; - - PetName pn = null; - if ("blog".equals(type)) - pn = new PetName(name, "syndie", "syndieblog", loc); - else if ("blogpost".equals(type)) - pn = new PetName(name, "syndie", "syndieblogpost", loc); - else if ("blogpostattachment".equals(type)) - pn = new PetName(name, "syndie", "syndieblogattachment", loc); - else if ("eepsite".equals(type)) - pn = new PetName(name, "i2p", "http", loc); - else if ("website".equals(type)) - pn = new PetName(name, "web", "http", loc); - else { - // unknown type - } - - if (pn != null) { - if (!ViewBlogServlet.DEFAULT_GROUP_NAME.equals(newGroup)) - pn.addGroup(newGroup); - bean.add(pn); - actionOutputHTML.append("Reference '").append(HTMLRenderer.sanitizeString(name)); - actionOutputHTML.append("' for ").append(HTMLRenderer.sanitizeString(loc)).append(" added to "); - actionOutputHTML.append(HTMLRenderer.sanitizeString(newGroup)).append("
    \n"); - } - } - - private void handleDelete(HttpServletRequest req, User user, BlogConfigBean bean, StringBuffer actionOutputHTML) { - // control parameters: - // delete=$i removes group # $i - // delete=$i.$j removes element $j in group $i - String del = req.getParameter("delete"); - if (empty(del)) return; - int split = del.indexOf('.'); - int group = -1; - int elem = -1; - if (split <= 0) { - try { group = Integer.parseInt(del); } catch (NumberFormatException nfe) {} - } else { - try { - group = Integer.parseInt(del.substring(0, split)); - elem = Integer.parseInt(del.substring(split+1)); - } catch (NumberFormatException nfe) { - group = -1; - elem = -1; - } - } - if ( (elem >= 0) && (group >= 0) ) { - List l = bean.getGroup(group); - if (elem < l.size()) { - PetName pn = (PetName)l.get(elem); - bean.remove(pn); - actionOutputHTML.append("Reference '").append(HTMLRenderer.sanitizeString(pn.getName())); - actionOutputHTML.append("' for ").append(HTMLRenderer.sanitizeString(pn.getLocation())); - actionOutputHTML.append(" removed
    \n"); - } - } else if ( (elem == -1) && (group >= 0) ) { - List l = bean.getGroup(group); - for (int i = 0; i < l.size(); i++) { - PetName pn = (PetName)l.get(i); - bean.remove(pn); - } - actionOutputHTML.append("All references in the selected group were removed
    \n"); - } else { - // noop - } - } - - private void handleReorderGroup(HttpServletRequest req, User user, BlogConfigBean bean, StringBuffer actionOutputHTML) { - // control parameters: - // moveFrom=$i & moveTo=$j moves group $i to position $j - int from = -1; - int to = -1; - try { - String str = req.getParameter("moveFrom"); - if (str != null) - from = Integer.parseInt(str); - str = req.getParameter("moveTo"); - if (str != null) - to = Integer.parseInt(str); - - if ( (from >= 0) && (to >= 0) ) { - List src = bean.getGroup(from); - List dest = bean.getGroup(to); - List orig = new ArrayList(dest); - dest.clear(); - dest.addAll(src); - src.clear(); - src.addAll(orig); - bean.groupsUpdated(); - actionOutputHTML.append("Reference group moved
    \n"); - } - } catch (NumberFormatException nfe) { - // ignore - } - } - - private void handleReorderRef(HttpServletRequest req, User user, BlogConfigBean bean, StringBuffer actionOutputHTML) { - // control parameters: - // moveRefFrom=$i.$j & moveRefTo=$k.$l moves element $j in group $i to position $l in group l - // (i == k) - int from = -1; - int fromElem = -1; - int to = -1; // ignored - int toElem = -1; - try { - String str = req.getParameter("moveRefFrom"); - if (str != null) { - int split = str.indexOf('.'); - if (split > 0) { - try { - from = Integer.parseInt(str.substring(0, split)); - fromElem = Integer.parseInt(str.substring(split+1)); - } catch (NumberFormatException nfe) { - from = -1; - fromElem = -1; - } - } - } - str = req.getParameter("moveRefTo"); - if (str != null) { - int split = str.indexOf('.'); - if (split > 0) { - try { - to = Integer.parseInt(str.substring(0, split)); - toElem = Integer.parseInt(str.substring(split+1)); - } catch (NumberFormatException nfe) { - to = -1; - toElem = -1; - } - } - } - - if ( (from >= 0) && (fromElem >= 0) && (toElem >= 0) ) { - List src = bean.getGroup(from); - PetName pn = (PetName)src.remove(fromElem); - src.add(toElem, pn); - bean.groupsUpdated(); - actionOutputHTML.append("Reference element moved
    \n"); - } - } catch (NumberFormatException nfe) { - // ignore - } - } - - - protected String getTitle() { return "Syndie :: Configure blog"; } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ExportServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ExportServlet.java deleted file mode 100644 index 5bcd2f2c6..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ExportServlet.java +++ /dev/null @@ -1,210 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import net.i2p.data.Base64; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.data.BlogURI; - -/** - * Dump out a whole series of blog metadata and entries as a zip stream. All metadata - * is written before any entries, so it can be processed in order safely. - * - * HTTP parameters: - * = meta (multiple values): base64 hash of the blog for which metadata is requested - * = entry (multiple values): blog URI of an entry being requested - */ -public class ExportServlet extends HttpServlet { - - public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - export(req, resp); - } - - public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - export(req, resp); - } - public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - export(req, resp); - } - public void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - export(req, resp); - } - - public static void export(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - try { - doExport(req, resp); - } catch (ServletException se) { - se.printStackTrace(); - throw se; - } catch (IOException ioe) { - ioe.printStackTrace(); - throw ioe; - } - } - private static void doExport(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String meta[] = null; - String entries[] = null; - String type = req.getHeader("Content-Type"); - if ( (type == null) || (type.indexOf("boundary") == -1) ) { - // it has to be POSTed with the request, name=value pairs. the export servlet doesn't allow any - // free form fields, so no worry about newlines, so lets parse 'er up - List metaList = new ArrayList(); - List entryList = new ArrayList(); - StringBuffer key = new StringBuffer(); - StringBuffer val = null; - String lenStr = req.getHeader("Content-length"); - int len = -1; - if (lenStr != null) - try { len = Integer.valueOf(lenStr).intValue(); } catch (NumberFormatException nfe) {} - - int read = 0; - int c = 0; - InputStream in = req.getInputStream(); - while ( (len == -1) || (read < len) ){ - c = in.read(); - if ( (c == '=') && (val == null) ) { - val = new StringBuffer(128); - } else if ( (c == -1) || (c == '&') ) { - String k = (key == null ? "" : key.toString()); - String v = (val == null ? "" : val.toString()); - if ("meta".equals(k)) - metaList.add(v.trim()); - else if ("entry".equals(k)) - entryList.add(v.trim()); - key.setLength(0); - val = null; - // no newlines in the export servlet - if (c == -1) - break; - } else { - if (val == null) - key.append((char)c); - else - val.append((char)c); - } - read++; - } - if (metaList != null) { - meta = new String[metaList.size()]; - for (int i = 0; i < metaList.size(); i++) - meta[i] = (String)metaList.get(i); - } - if (entryList != null) { - entries = new String[entryList.size()]; - for (int i = 0; i < entryList.size(); i++) - entries[i] = (String)entryList.get(i); - } - } else { - meta = req.getParameterValues("meta"); - entries = req.getParameterValues("entry"); - } - resp.setContentType("application/x-syndie-zip"); - resp.setStatus(200); - OutputStream out = resp.getOutputStream(); - - if (false) { - StringBuffer bbuf = new StringBuffer(1024); - bbuf.append("meta: "); - if (meta != null) - for (int i = 0; i < meta.length; i++) - bbuf.append(meta[i]).append(", "); - bbuf.append("entries: "); - if (entries != null) - for (int i = 0; i < entries.length; i++) - bbuf.append(entries[i]).append(", "); - System.out.println(bbuf.toString()); - } - - ZipOutputStream zo = null; - if ( (meta != null) && (entries != null) && (meta.length + entries.length > 0) ) - zo = new ZipOutputStream(out); - - List metaFiles = getMetaFiles(meta); - - ZipEntry ze = null; - byte buf[] = new byte[1024]; - int read = -1; - for (int i = 0; metaFiles != null && i < metaFiles.size(); i++) { - ze = new ZipEntry("meta" + i); - ze.setTime(0); - zo.putNextEntry(ze); - FileInputStream in = null; - try { - in = new FileInputStream((File)metaFiles.get(i)); - while ( (read = in.read(buf)) != -1) - zo.write(buf, 0, read); - zo.closeEntry(); - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - } - } - - List entryFiles = getEntryFiles(entries); - for (int i = 0; entryFiles != null && i < entryFiles.size(); i++) { - ze = new ZipEntry("entry" + i); - ze.setTime(0); - zo.putNextEntry(ze); - FileInputStream in = null; - try { - in = new FileInputStream((File)entryFiles.get(i)); - while ( (read = in.read(buf)) != -1) - zo.write(buf, 0, read); - zo.closeEntry(); - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - } - } - - if (zo != null) { - zo.finish(); - zo.close(); - } - } - - private static List getMetaFiles(String blogHashes[]) { - if ( (blogHashes == null) || (blogHashes.length <= 0) ) return null; - File dir = BlogManager.instance().getArchive().getArchiveDir(); - List rv = new ArrayList(blogHashes.length); - for (int i = 0; i < blogHashes.length; i++) { - byte hv[] = Base64.decode(blogHashes[i]); - if ( (hv == null) || (hv.length != Hash.HASH_LENGTH) ) - continue; - File blogDir = new File(dir, blogHashes[i]); - File metaFile = new File(blogDir, Archive.METADATA_FILE); - if (metaFile.exists()) - rv.add(metaFile); - } - return rv; - } - - private static List getEntryFiles(String blogURIs[]) { - if ( (blogURIs == null) || (blogURIs.length <= 0) ) return null; - File dir = BlogManager.instance().getArchive().getArchiveDir(); - List rv = new ArrayList(blogURIs.length); - for (int i = 0; i < blogURIs.length; i++) { - BlogURI uri = new BlogURI(blogURIs[i]); - if (uri.getEntryId() < 0) - continue; - File blogDir = new File(dir, uri.getKeyHash().toBase64()); - File entryFile = new File(blogDir, uri.getEntryId() + ".snd"); - if (entryFile.exists()) - rv.add(entryFile); - } - return rv; - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ExternalLinkServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ExternalLinkServlet.java deleted file mode 100644 index f89e58079..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ExternalLinkServlet.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.data.Base64; -import net.i2p.data.DataHelper; -import net.i2p.syndie.Archive; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.sml.HTMLRenderer; - -/** - * Confirm page before hitting a remote site - * - */ -public class ExternalLinkServlet extends BaseServlet { - protected String getTitle() { return "Syndie :: External link"; } - - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - String b64Schema = req.getParameter("schema"); - String b64Location = req.getParameter("location"); - if ( (b64Schema == null) || (b64Schema.trim().length() <= 0) || - (b64Location == null) || (b64Location.trim().length() <= 0) ) { - out.write("No location specified\n"); - } else { - byte loc[] = Base64.decode(b64Location); - if ( (loc == null) || (loc.length <= 0) ) { - out.write("Invalid location specified\n"); - } else { - String location = DataHelper.getUTF8(loc); - out.write("Are you sure you want to go to "); - out.write(HTMLRenderer.sanitizeString(location)); - out.write("\n"); - } - } - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ImportFeedServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ImportFeedServlet.java deleted file mode 100644 index 9b603bbbf..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ImportFeedServlet.java +++ /dev/null @@ -1,149 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Iterator; -import java.util.List; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.ThreadIndex; - -/** - * Schedule the import of atom/rss feeds - */ -public class ImportFeedServlet extends BaseServlet { - protected String getTitle() { return "Syndie :: Import feed"; } - - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - - if (!BlogManager.instance().authorizeRemote(user)) { - out.write("You are not authorized for remote access.\n"); - return; - } else { - out.write(""); - - String url=req.getParameter("url"); - if (url != null) - url = url.trim(); - String blog=req.getParameter("blog"); - if (blog != null) - blog=blog.trim(); - String tagPrefix = req.getParameter("tagprefix"); - if (tagPrefix != null) - tagPrefix=tagPrefix.trim(); - String action = req.getParameter("action"); - if ( (action != null) && ("Add".equals(action)) ) { - if(url==null || blog==null || tagPrefix==null) { - out.write("Please fill in all fields
    \n"); - } else { - boolean ret = BlogManager.instance().addRssFeed(url, blog, tagPrefix); - if (!ret) { - out.write("addRssFeed failure."); - } else { - out.write("RSS feed added."); - } - } - } else if ( (action != null) && ("Change".equals(action)) ) { - String lastUrl=req.getParameter("lasturl"); - String lastBlog=req.getParameter("lastblog"); - String lastTagPrefix=req.getParameter("lasttagprefix"); - - if (url == null || blog == null || tagPrefix == null || - lastUrl == null || lastBlog == null || lastTagPrefix == null) { - out.write("error, some fields were empty.
    "); - } else { - boolean ret = BlogManager.instance().deleteRssFeed(lastUrl,lastBlog,lastTagPrefix); - if (!ret) { - out.write("Could not delete while attempting to change."); - } else { - ret = BlogManager.instance().addRssFeed(url,blog,tagPrefix); - if (!ret) { - out.write("Could not add while attempting to change."); - } else { - out.write("Ok, changed successfully."); - } - } - } - } else if ( (action != null) && ("Delete".equals(action)) ) { - if (url == null || blog == null || tagPrefix == null) { - out.write("error, some fields were empty.
    "); - } else { - boolean ret = BlogManager.instance().deleteRssFeed(url,blog,tagPrefix); - if (!ret) { - out.write("error, could not delete."); - } else { - out.write("ok, deleted successfully."); - } - } - } - - String blogStr = user.getBlogStr(); - if (blogStr == null) - blogStr=""; - - out.write("

    Here you can add RSS feeds that will be periodically polled and added to your syndie.

    "); - out.write("
    "); - writeAuthActionFields(out); - out.write("RSS URL. (e.g. http://tracker.postman.i2p/rss.php)
    \n"); - out.write("url:
    \n"); - out.write("Blog hash to which the RSS entries will get posted, defaults to the one you're logged in to.
    \n"); - out.write("blog:
    \n"); - out.write("This will be prepended to any tags that the RSS feed contains. (e.g. feed.tracker)
    \n"); - out.write("tagprefix:\n"); - out.write("
    \n"); - out.write("\n"); - out.write("\n"); - out.write("
    \n"); - - List feedList = BlogManager.instance().getRssFeeds(); - if (feedList.size()>0) { - out.write("

    Subscriptions:


    \n"); - out.write("\n"); - out.write("\n"); - out.write("\n"); - out.write("\n"); - out.write("\n"); - out.write("\n"); - - Iterator iter = feedList.iterator(); - while (iter.hasNext()) { - String fields[] = (String[])iter.next(); - url = fields[0]; - blog = fields[1]; - tagPrefix = fields[2]; - StringBuffer buf = new StringBuffer(128); - - buf.append(""); - writeAuthActionFields(out); - buf.append(""); - buf.append(""); - buf.append(""); - - buf.append(""); - buf.append(""); - buf.append(""); - buf.append(""); - out.write(buf.toString()); - buf.setLength(0); - } - - out.write("
    UrlBlogTagPrefix 
    "); - - buf.append(""); - buf.append(""); - - buf.append("
    \n"); - } // end iterating over feeds - - out.write("\n"); - } - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/MultiPartRequest.java b/apps/syndie/java/src/net/i2p/syndie/web/MultiPartRequest.java deleted file mode 100644 index 22f73b40a..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/MultiPartRequest.java +++ /dev/null @@ -1,422 +0,0 @@ -// see below for license info -package net.i2p.syndie.web; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.util.Hashtable; -import java.util.List; -import java.util.Set; -import java.util.StringTokenizer; - -import javax.servlet.http.HttpServletRequest; - -import org.mortbay.http.HttpFields; -import org.mortbay.util.LineInput; -import org.mortbay.util.MultiMap; -import org.mortbay.util.StringUtil; - -/* ------------------------------------------------------------ */ -/** - * Hacked version of Jetty's MultiPartRequest handler, applying a tiny patch for - * charset handling [1]. These changes are public domain, and will hopefully - * be integrated into Jetty so we can drop this file altogether. Of course, - * until then, this file is APL2 licensed. - * - * Original code is up at [2] - * - * [1] http://article.gmane.org/gmane.comp.java.jetty.general/6031 - * [2] http://cvs.sourceforge.net/viewcvs.py/jetty/Jetty/src/org/mortbay/servlet/ - * (rev 1.15) - * - */ -public class MultiPartRequest -{ - /* ------------------------------------------------------------ */ - HttpServletRequest _request; - LineInput _in; - String _boundary; - String _encoding; - byte[] _byteBoundary; - MultiMap _partMap = new MultiMap(10); - int _char=-2; - boolean _lastPart=false; - - /* ------------------------------------------------------------ */ - /** Constructor. - * @param request The request containing a multipart/form-data - * request - * @exception IOException IOException - */ - public MultiPartRequest(HttpServletRequest request) - throws IOException - { - _request=request; - String content_type = request.getHeader(HttpFields.__ContentType); - if (!content_type.startsWith("multipart/form-data")) - throw new IOException("Not multipart/form-data request"); - - //if(log.isDebugEnabled())log.debug("Multipart content type = "+content_type); - - _encoding = request.getCharacterEncoding(); - if (_encoding != null) - _in = new LineInput(request.getInputStream(), 2048, _encoding); - else - _in = new LineInput(request.getInputStream()); - - // Extract boundary string - _boundary="--"+ - value(content_type.substring(content_type.indexOf("boundary="))); - - //if(log.isDebugEnabled())log.debug("Boundary="+_boundary); - _byteBoundary= (_boundary+"--").getBytes(StringUtil.__ISO_8859_1); - - loadAllParts(); - } - - /* ------------------------------------------------------------ */ - /** Get the part names. - * @return an array of part names - */ - public String[] getPartNames() - { - Set s = _partMap.keySet(); - return (String[]) s.toArray(new String[s.size()]); - } - - /* ------------------------------------------------------------ */ - /** Check if a named part is present - * @param name The part - * @return true if it was included - */ - public boolean contains(String name) - { - Part part = (Part)_partMap.get(name); - return (part!=null); - } - - /* ------------------------------------------------------------ */ - /** Get the data of a part as a string. - * @param name The part name - * @return The part data - */ - public String getString(String name) - { - List part = (List)_partMap.getValues(name); - if (part==null) - return null; - if (_encoding != null) - { - try - { - return new String(((Part)part.get(0))._data, _encoding); - } - catch (UnsupportedEncodingException uee) - { - //if (log.isDebugEnabled()) log.debug("Invalid character set: " + uee); - return null; - } - } - else - { - return new String(((Part)part.get(0))._data); - } - } - - /* ------------------------------------------------------------ */ - /** - * @param name The part name - * @return The parts data - */ - public String[] getStrings(String name) - { - List parts = (List)_partMap.getValues(name); - if (parts==null) - return null; - String[] strings = new String[parts.size()]; - if (_encoding == null) { - for (int i=0; i0) - value=value.substring(0,i); - } - return value; - } - - /* ------------------------------------------------------------ */ - private class Part - { - String _name=null; - String _filename=null; - Hashtable _headers= new Hashtable(10); - byte[] _data=null; - } -}; diff --git a/apps/syndie/java/src/net/i2p/syndie/web/PostBean.java b/apps/syndie/java/src/net/i2p/syndie/web/PostBean.java deleted file mode 100644 index 7e653587d..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/PostBean.java +++ /dev/null @@ -1,226 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Writer; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.StringTokenizer; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.EntryContainer; -import net.i2p.syndie.sml.HTMLPreviewRenderer; -import net.i2p.syndie.sml.HTMLRenderer; -import net.i2p.util.Log; - -/** - * - */ -public class PostBean { - private I2PAppContext _context; - private Log _log; - private User _user; - private String _subject; - private String _tags; - private String _headers; - private String _text; - private String _archive; - private List _filenames; - private List _fileStreams; - private List _localFiles; - private List _fileTypes; - private boolean _previewed; - - public PostBean() { - _context = I2PAppContext.getGlobalContext(); - _log = _context.logManager().getLog(PostBean.class); - reinitialize(); - } - - public void reinitialize() { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Reinitializing " + (_text != null ? "(with " + _text.length() + " bytes of sml!)" : "")); - _user = null; - _subject = null; - _tags = null; - _text = null; - _headers = null; - _archive = null; - _filenames = new ArrayList(); - _fileStreams = new ArrayList(); - _fileTypes = new ArrayList(); - if (_localFiles != null) - for (int i = 0; i < _localFiles.size(); i++) - ((File)_localFiles.get(i)).delete(); - - _localFiles = new ArrayList(); - _previewed = false; - } - - public User getUser() { return _user; } - public String getSubject() { return (_subject != null ? _subject : ""); } - public String getTags() { return (_tags != null ? _tags : ""); } - public String getText() { return (_text != null ? _text : ""); } - public String getHeaders() { return (_headers != null ? _headers : ""); } - public void setUser(User user) { _user = user; } - public void setSubject(String subject) { _subject = subject; } - public void setTags(String tags) { _tags = tags; } - public void setText(String text) { _text = text; } - public void setHeaders(String headers) { _headers = headers; } - public void setArchive(String archive) { _archive = archive; } - - public String getContentType(int id) { - if ( (id >= 0) && (id < _fileTypes.size()) ) - return (String)_fileTypes.get(id); - return "application/octet-stream"; - } - - public void writeAttachmentData(int id, OutputStream out) throws IOException { - FileInputStream in = null; - try { - in = new FileInputStream((File)_localFiles.get(id)); - byte buf[] = new byte[1024]; - int read = 0; - while ( (read = in.read(buf)) != -1) - out.write(buf, 0, read); - out.close(); - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - } - } - - public void addAttachment(String filename, InputStream fileStream, String mimeType) { - _filenames.add(filename); - _fileStreams.add(fileStream); - _fileTypes.add(mimeType); - } - public int getAttachmentCount() { return (_filenames != null ? _filenames.size() : 0); } - - public BlogURI postEntry() throws IOException { - if (!_previewed) return null; - List localStreams = new ArrayList(_localFiles.size()); - for (int i = 0; i < _localFiles.size(); i++) { - File f = (File)_localFiles.get(i); - localStreams.add(new FileInputStream(f)); - } - BlogURI uri = BlogManager.instance().createBlogEntry(_user, _subject, _tags, _headers, _text, - _filenames, localStreams, _fileTypes); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Posted the entry " + uri.toString() + " (archive = " + _archive + ")"); - if ( (uri != null) && BlogManager.instance().authorizeRemote(_user) ) { - PetName pn = _user.getPetNameDB().getByName(_archive); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Archive to petname? " + pn + " (protocol: " + (pn != null ? pn.getProtocol() : "") + ")"); - if ( (pn != null) && ("syndiearchive".equals(pn.getProtocol())) ) { - RemoteArchiveBean r = new RemoteArchiveBean(); - Map params = new HashMap(); - - String entries[] = null; - BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(uri); - if (info != null) { - String str = info.getProperty(BlogInfo.SUMMARY_ENTRY_ID); - if (str != null) { - entries = new String[] { uri.toString(), str }; - } - } - if (entries == null) - entries = new String[] { uri.toString() }; - - params.put("localentry", entries); - String proxyHost = BlogManager.instance().getDefaultProxyHost(); - String port = BlogManager.instance().getDefaultProxyPort(); - int proxyPort = 4444; - try { proxyPort = Integer.parseInt(port); } catch (NumberFormatException nfe) {} - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Posting the entry " + uri.toString() + " to " + pn.getLocation()); - r.postSelectedEntries(_user, params, proxyHost, proxyPort, pn.getLocation()); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Post status: " + r.getStatus()); - } - } - return uri; - } - - public void renderPreview(Writer out) throws IOException { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Subject: " + _subject); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Text: " + _text); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Headers: " + _headers); - // cache all the _fileStreams into temporary files, storing those files in _localFiles - // then render the page accordingly with an HTMLRenderer, altered to use a different - // 'view attachment' - cacheAttachments(); - String smlContent = renderSMLContent(); - HTMLPreviewRenderer r = new HTMLPreviewRenderer(_context, _filenames, _fileTypes, _localFiles); - r.render(_user, BlogManager.instance().getArchive(), null, smlContent, out, false, true); - _previewed = true; - } - - public void renderReplyPreview(Writer out, String parentURI) throws IOException { - HTMLRenderer r = new HTMLRenderer(_context); - Archive a = BlogManager.instance().getArchive(); - BlogURI uri = new BlogURI(parentURI); - if (uri.getEntryId() > 0) { - EntryContainer entry = a.getEntry(uri); - r.render(_user, a, entry, out, false, true); - } - } - - private String renderSMLContent() { - StringBuffer raw = new StringBuffer(); - raw.append("Subject: ").append(_subject).append('\n'); - raw.append("Tags: "); - StringTokenizer tok = new StringTokenizer(_tags, " \t\n"); - while (tok.hasMoreTokens()) - raw.append(tok.nextToken()).append('\t'); - raw.append('\n'); - raw.append(_headers.trim()); - raw.append("\n\n"); - raw.append(_text.trim()); - return raw.toString(); - } - - /** until we have a good filtering/preferences system, lets try to keep the content small */ - private static final int MAX_SIZE = 256*1024; - - private void cacheAttachments() throws IOException { - if (_user == null) throw new IOException("User not specified"); - File postCacheDir = new File(BlogManager.instance().getTempDir(), _user.getBlog().toBase64()); - if (!postCacheDir.exists()) - postCacheDir.mkdirs(); - for (int i = 0; i < _fileStreams.size(); i++) { - InputStream in = (InputStream)_fileStreams.get(i); - File f = File.createTempFile("attachment", ".dat", postCacheDir); - FileOutputStream o = new FileOutputStream(f); - byte buf[] = new byte[1024]; - int read = 0; - while ( (read = in.read(buf)) != -1) - o.write(buf, 0, read); - o.close(); - in.close(); - if (f.length() > MAX_SIZE) { - _log.error("Refusing to post the attachment, because it is too big: " + f.length()); - } else { - _localFiles.add(f); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Caching attachment " + i + " temporarily in " - + f.getAbsolutePath() + " w/ " + f.length() + "bytes"); - } - } - _fileStreams.clear(); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/PostServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/PostServlet.java deleted file mode 100644 index 8d7ce3814..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/PostServlet.java +++ /dev/null @@ -1,381 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.TreeSet; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.client.naming.PetName; -import net.i2p.client.naming.PetNameDB; -import net.i2p.data.Base64; -import net.i2p.data.DataHelper; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.sml.HTMLRenderer; -import net.i2p.syndie.sml.ThreadedHTMLRenderer; - -/** - * Post and preview form - * - */ -public class PostServlet extends BaseServlet { - public static final String PARAM_ACTION = "action"; - public static final String ACTION_CONFIRM = "confirm"; - - public static final String PARAM_SUBJECT = "entrysubject"; - public static final String PARAM_TAGS = ThreadedHTMLRenderer.PARAM_TAGS; - public static final String PARAM_INCLUDENAMES = "includenames"; - public static final String PARAM_TEXT = "entrytext"; - public static final String PARAM_HEADERS = "entryheaders"; - - public static final String PARAM_PARENT = "parentURI"; - public static final String PARAM_IN_NEW_THREAD = "replyInNewThread"; - public static final String PARAM_REFUSE_REPLIES = "refuseReplies"; - - public static final String PARAM_REMOTE_ARCHIVE = "archive"; - - private static final String ATTR_POST_BEAN = "post"; - - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - if (!user.getAuthenticated()) { - out.write("You must be logged in to post\n"); - } else { - PostBean post = getPostBean(user, req); - String action = req.getParameter(PARAM_ACTION); - if (!empty(action) && ACTION_CONFIRM.equals(action)) { - postEntry(user, req, archive, post, out); - post.reinitialize(); - post.setUser(user); - } else { - String contentType = req.getContentType(); - if (!empty(contentType) && (contentType.indexOf("boundary=") != -1)) { - previewPostedData(user, req, archive, contentType, post, out); - } else { - displayNewForm(user, req, post, out); - } - } - } - } - - private void previewPostedData(User user, HttpServletRequest rawRequest, Archive archive, String contentType, PostBean post, PrintWriter out) throws IOException { - MultiPartRequest req = new MultiPartRequest(rawRequest); - - if (!authAction(req.getString(PARAM_AUTH_ACTION))) { - out.write("Invalid form submission... stale data?"); - return; - } - - // not confirmed but they posted stuff... gobble up what they give - // and display it as a prview (then we show the confirm form - - out.write(""); - - //post.reinitialize(); - //post.setUser(user); - - boolean inNewThread = getInNewThread(req.getString(PARAM_IN_NEW_THREAD)); - boolean refuseReplies = getRefuseReplies(req.getString(PARAM_REFUSE_REPLIES)); - - String entrySubject = req.getString(PARAM_SUBJECT); - String entryTags = req.getString(PARAM_TAGS); - String entryText = req.getString(PARAM_TEXT); - String entryHeaders = req.getString(PARAM_HEADERS); - String style = ""; //req.getString("style"); - if ( (style != null) && (style.trim().length() > 0) ) { - if (entryHeaders == null) entryHeaders = HTMLRenderer.HEADER_STYLE + ": " + style; - else entryHeaders = entryHeaders + '\n' + HTMLRenderer.HEADER_STYLE + ": " + style; - } - String replyTo = req.getString(PARAM_PARENT); - if ( (replyTo != null) && (replyTo.trim().length() > 0) ) { - byte r[] = Base64.decode(replyTo); - if (r != null) { - replyTo = new String(r, "UTF-8"); - if (!replyTo.startsWith("entry://") && !replyTo.startsWith("blog://")) - replyTo = "entry://" + replyTo; - if (entryHeaders == null) entryHeaders = HTMLRenderer.HEADER_IN_REPLY_TO + ": " + replyTo; - else entryHeaders = entryHeaders + '\n' + HTMLRenderer.HEADER_IN_REPLY_TO + ": " + replyTo; - } else { - replyTo = null; - } - } - - if (entryTags == null) entryTags = ""; - - if ( (entryHeaders == null) || (entryHeaders.trim().length() <= 0) ) - entryHeaders = ThreadedHTMLRenderer.HEADER_FORCE_NEW_THREAD + ": " + inNewThread + '\n' + - ThreadedHTMLRenderer.HEADER_REFUSE_REPLIES + ": " + refuseReplies; - else - entryHeaders = entryHeaders.trim() + '\n' + - ThreadedHTMLRenderer.HEADER_FORCE_NEW_THREAD + ": " + inNewThread + '\n' + - ThreadedHTMLRenderer.HEADER_REFUSE_REPLIES + ": " + refuseReplies; - - String includeNames = req.getString(PARAM_INCLUDENAMES); - if ( (includeNames != null) && (includeNames.trim().length() > 0) ) { - PetNameDB db = user.getPetNameDB(); - if (entryHeaders == null) entryHeaders = ""; - for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) { - PetName pn = db.getByName((String)iter.next()); - if ( (pn != null) && (pn.getIsPublic()) ) { - entryHeaders = entryHeaders.trim() + '\n' + HTMLRenderer.HEADER_PETNAME + ": " + - pn.getName() + "\t" + pn.getNetwork() + "\t" + pn.getProtocol() + "\t" + pn.getLocation(); - } - } - } - - post.setSubject(entrySubject); - post.setTags(entryTags); - post.setText(entryText); - post.setHeaders(entryHeaders); - - for (int i = 0; i < 32; i++) { - String filename = req.getFilename("entryfile" + i); - if ( (filename != null) && (filename.trim().length() > 0) ) { - Hashtable params = req.getParams("entryfile" + i); - String type = "application/octet-stream"; - for (Iterator iter = params.keySet().iterator(); iter.hasNext(); ) { - String cur = (String)iter.next(); - if ("content-type".equalsIgnoreCase(cur)) { - type = (String)params.get(cur); - break; - } - } - post.addAttachment(filename.trim(), req.getInputStream("entryfile" + i), type); - } - } - - post.renderPreview(out); - out.write("
    \n"); - writeAuthActionFields(out); - out.write("Please confirm that the above is ok"); - if (BlogManager.instance().authorizeRemote(user)) { - out.write(", and select what additional archive you want the post transmitted to: "); - out.write("
    \n"); - out.write("If you don't push this post remotely now, you can do so later on the syndicate screen "); - out.write("by choosing an archive, verifying that they don't already have the post, and selecting which posts to push.\n"); - } - out.write("\n"); - - out.write("
    \n"); - - displayEditForm(user, req, post, out); - - out.write("\n"); - } - - private void postEntry(User user, HttpServletRequest req, Archive archive, PostBean post, PrintWriter out) throws IOException { - if (!authAction(req)) { - out.write("Invalid form submission... stale data?"); - return; - } - String remArchive = req.getParameter(PARAM_REMOTE_ARCHIVE); - post.setArchive(remArchive); - BlogURI uri = post.postEntry(); - if (uri != null) { - out.write("Entry posted!"); - } else { - out.write("There was an unknown error posting the entry..."); - } - } - - private void displayNewForm(User user, HttpServletRequest req, PostBean post, PrintWriter out) throws IOException { - // logged in and not confirmed because they didn't send us anything! - // give 'em a new form - - post.reinitialize(); - post.setUser(user); - - String parentURI = req.getParameter(PARAM_PARENT); - - String subject = getParam(req, PARAM_SUBJECT); - - out.write("
    \n"); - writeAuthActionFields(out); - out.write("\n"); - out.write("Post subject: "); - out.write("
    \n"); - out.write("Post content (in raw SML, no headers):
    \n"); - out.write("
    \n"); - out.write("SML post headers:
    \n"); - out.write("
    \n"); - - if ( (parentURI != null) && (parentURI.trim().length() > 0) ) - out.write("\n"); - - out.write(" Tags: "); - BaseServlet.writeTagField(user, getParam(req, PARAM_TAGS), out, "Optional tags to categorize your post", "No tags", false); - //
    \n"); - out.write("
    \n"); - - boolean inNewThread = getInNewThread(req); - boolean refuseReplies = getRefuseReplies(req); - - out.write("In a new thread?
    \n"); - out.write("Refuse replies?
    \n"); - - out.write("Include public names? "); - out.write("
    \n"); - - out.write(ATTACHMENT_FIELDS); - - out.write("
    \n"); - out.write(" "); - out.write("\n"); - - if (parentURI != null) { - out.write("
    "); - String decoded = DataHelper.getUTF8(Base64.decode(parentURI)); - post.renderReplyPreview(out, "entry://" + decoded); - out.write("
    \n"); - } - - out.write("\n"); - out.write("
    \n"); - } - - private void displayEditForm(User user, MultiPartRequest req, PostBean post, PrintWriter out) throws IOException { - String parentURI = req.getString(PARAM_PARENT); - - String subject = getParam(req, PARAM_SUBJECT); - - out.write("
    \n"); - out.write("
    \n"); - writeAuthActionFields(out); - out.write("\n"); - out.write("Post subject: "); - out.write("
    \n"); - out.write("Post content (in raw SML, no headers):
    \n"); - out.write("
    \n"); - out.write("SML post headers:
    \n"); - out.write("
    \n"); - - if ( (parentURI != null) && (parentURI.trim().length() > 0) ) - out.write("\n"); - - out.write(" Tags: "); - //
    \n"); - out.write(" Tags: "); - BaseServlet.writeTagField(user, getParam(req, PARAM_TAGS), out, "Optional tags to categorize your post", "No tags", false); - out.write("
    \n"); - - boolean inNewThread = getInNewThread(req); - boolean refuseReplies = getRefuseReplies(req); - - out.write("In a new thread?
    \n"); - out.write("Refuse replies?
    \n"); - - out.write("Include public names? "); - out.write("
    \n"); - - int newCount = 0; - for (int i = 0; i < 32 && newCount < 3; i++) { - String filename = req.getFilename("entryfile" + i); - if ( (filename != null) && (filename.trim().length() > 0) ) { - out.write("Attachment " + i + ": "); - out.write(HTMLRenderer.sanitizeString(filename)); - out.write("
    "); - } else { - out.write("Attachment " + i + ": "); - out.write("
    "); - newCount++; - } - } - - out.write("
    \n"); - out.write(" "); - out.write("\n"); - - out.write("
    \n"); - } - - private boolean getInNewThread(HttpServletRequest req) { - return getInNewThread(req.getParameter(PARAM_IN_NEW_THREAD)); - } - private boolean getInNewThread(MultiPartRequest req) { - return getInNewThread(getParam(req, PARAM_IN_NEW_THREAD)); - } - private boolean getInNewThread(String val) { - boolean rv = false; - String inNewThread = val; - if ( (inNewThread != null) && (Boolean.valueOf(inNewThread).booleanValue()) ) - rv = true; - return rv; - } - private boolean getRefuseReplies(HttpServletRequest req) { - return getRefuseReplies(req.getParameter(PARAM_REFUSE_REPLIES)); - } - private boolean getRefuseReplies(MultiPartRequest req) { - return getRefuseReplies(getParam(req, PARAM_REFUSE_REPLIES)); - } - private boolean getRefuseReplies(String val) { - boolean rv = false; - String refuseReplies = val; - if ( (refuseReplies != null) && (Boolean.valueOf(refuseReplies).booleanValue()) ) - rv = true; - return rv; - } - - private PostBean getPostBean(User user, HttpServletRequest req) { - PostBean bean = (PostBean)req.getSession().getAttribute(ATTR_POST_BEAN); - if (bean == null) { - bean = new PostBean(); - req.getSession().setAttribute(ATTR_POST_BEAN, bean); - } - bean.setUser(user); - return bean; - } - - private String getParam(HttpServletRequest req, String param) { - String val = req.getParameter(param); - if (val == null) val = ""; - return val; - } - private String getParam(MultiPartRequest req, String param) { - String val = req.getString(param); - if (val == null) return ""; - return val; - } - - private static final String ATTACHMENT_FIELDS = "" - + "Attachment 0:
    " - + "Attachment 1:
    " - + "Attachment 2:
    " - + "Attachment 3:
    \n"; - - protected String getTitle() { return "Syndie :: Post new content"; } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java deleted file mode 100644 index 1c961a768..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java +++ /dev/null @@ -1,232 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.client.naming.PetName; -import net.i2p.data.DataFormatException; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.FilteredThreadIndex; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.sml.HTMLRenderer; -import net.i2p.syndie.sml.ThreadedHTMLRenderer; - -/** - * Render the requested profile - * - */ -public class ProfileServlet extends BaseServlet { - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - Hash author = null; - String str = req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR); - if (str != null) { - try { - author = new Hash(); - author.fromBase64(str); - } catch (DataFormatException dfe) { - author = null; - } - } else { - author = user.getBlog(); - } - - String uri = req.getRequestURI(); - - if (author == null) { - renderInvalidProfile(out); - } else if ( (user.getBlog() != null) && (user.getBlog().equals(author)) ) { - renderMyProfile(user, uri, out, archive); - } else { - renderProfile(user, uri, out, author, archive); - } - } - - private void renderInvalidProfile(PrintWriter out) throws IOException { - out.write(INVALID_PROFILE); - } - - private void renderMyProfile(User user, String baseURI, PrintWriter out, Archive archive) throws IOException { - BlogInfo info = archive.getBlogInfo(user.getBlog()); - if (info == null) - return; - - out.write("\n"); - out.write("
    \n"); - writeAuthActionFields(out); - // now add the form to update - out.write("Your profile (configure your blog)\n"); - out.write("Name: \n"); - out.write("Account description: \n"); - out.write("Contact information: \n"); - out.write("Other attributes:
    \n"); - - if (user.getAuthenticated()) { - if ( (user.getUsername() == null) || (user.getUsername().equals(BlogManager.instance().getDefaultLogin())) ) { - // this is the default user, don't let them change the password - } else { - out.write("Old Password: \n"); - out.write("Password: \n"); - out.write("Password again: \n"); - } - if (!BlogManager.instance().authorizeRemote(user)) { - out.write("To access the remote functionality, please specify the administrative password:
    \n" + - "\n"); - } - } - - out.write("\n"); - out.write("
    \n"); - } - - private void renderProfile(User user, String baseURI, PrintWriter out, Hash author, Archive archive) throws IOException { - out.write("Profile for "); - PetName pn = user.getPetNameDB().getByLocation(author.toBase64()); - String name = null; - BlogInfo info = archive.getBlogInfo(author); - if (pn != null) { - out.write(pn.getName()); - name = null; - if (info != null) - name = info.getProperty(BlogInfo.NAME); - - if ( (name == null) || (name.trim().length() <= 0) ) - name = author.toBase64().substring(0, 6); - - out.write(" (" + name + ")"); - } else { - if (info != null) - name = info.getProperty(BlogInfo.NAME); - - if ( (name == null) || (name.trim().length() <= 0) ) - name = author.toBase64().substring(0, 6); - out.write(name); - } - out.write(""); - if (info != null) - out.write(" [edition " + info.getEdition() + "]"); - out.write("
    \n"); - out.write("View their blog or "); - out.write("\n"); - out.write("\n"); - out.write(" \n"); - out.write(" Syndie feed\n"); - String page = urlPrefix; - if (tags != null) - page = page + "threads.jsp?" + ThreadedHTMLRenderer.PARAM_TAGS + '=' + HTMLRenderer.sanitizeXML(tags); - out.write(" " + page +"\n"); - out.write(" Summary of the latest Syndie posts\n"); - out.write(" Syndie\n"); - - RSSRenderer r = new RSSRenderer(I2PAppContext.getGlobalContext()); - for (int i = 0; i < count && i < entries.size(); i++) { - BlogURI uri = (BlogURI)entries.get(i); - EntryContainer entry = archive.getEntry(uri); - r.render(user, archive, entry, urlPrefix, out); - } - - out.write(" \n"); - out.write("\n"); - out.close(); - } - - private void walkTree(List uris, ThreadNode node) { - if (node == null) - return; - if (uris.contains(node)) - return; - uris.add(node.getEntry()); - for (int i = 0; i < node.getChildCount(); i++) - walkTree(uris, node.getChild(i)); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/RemoteArchiveBean.java b/apps/syndie/java/src/net/i2p/syndie/web/RemoteArchiveBean.java deleted file mode 100644 index 3601d4b24..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/RemoteArchiveBean.java +++ /dev/null @@ -1,852 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.Writer; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.TreeSet; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.client.naming.PetNameDB; -import net.i2p.data.Base64; -import net.i2p.data.DataHelper; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.ArchiveIndex; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.EntryContainer; -import net.i2p.syndie.sml.HTMLRenderer; -import net.i2p.syndie.sml.ThreadedHTMLRenderer; -import net.i2p.util.EepGet; -import net.i2p.util.EepGetScheduler; -import net.i2p.util.EepPost; -import net.i2p.util.I2PThread; -import net.i2p.util.Log; - -/** - * - */ -public class RemoteArchiveBean { - private I2PAppContext _context; - private Log _log; - private String _remoteSchema; - private String _remoteLocation; - private String _proxyHost; - private int _proxyPort; - private ArchiveIndex _remoteIndex; - private List _statusMessages; - private boolean _fetchIndexInProgress; - private boolean _exportCapable; - - public RemoteArchiveBean() { - _context = I2PAppContext.getGlobalContext(); - _log = _context.logManager().getLog(RemoteArchiveBean.class); - reinitialize(); - } - public void reinitialize() { - _remoteSchema = null; - _remoteLocation = null; - _remoteIndex = null; - _fetchIndexInProgress = false; - _proxyHost = null; - _proxyPort = -1; - _exportCapable = false; - _statusMessages = new ArrayList(); - } - - public String getRemoteSchema() { return _remoteSchema; } - public String getRemoteLocation() { return _remoteLocation; } - public ArchiveIndex getRemoteIndex() { return _remoteIndex; } - public String getProxyHost() { return _proxyHost; } - public int getProxyPort() { return _proxyPort; } - public boolean getFetchIndexInProgress() { return _fetchIndexInProgress; } - public String getStatus() { - StringBuffer buf = new StringBuffer(); - while (_statusMessages.size() > 0) - buf.append(_statusMessages.remove(0)).append("\n"); - return buf.toString(); - } - - private boolean ignoreBlog(User user, Hash blog) { - if (BlogManager.instance().isBanned(blog)) - return true; - PetNameDB db = user.getPetNameDB(); - PetName pn = db.getByLocation(blog.toBase64()); - return ( (pn!= null) && (pn.isMember("Ignore")) ); - } - - public void fetchMetadata(User user, Map parameters) { - String meta = ArchiveViewerBean.getString(parameters, "blog"); - if (meta == null) return; - Set blogs = new HashSet(); - if ("ALL".equals(meta)) { - Set localBlogs = BlogManager.instance().getArchive().getIndex().getUniqueBlogs(); - Set remoteBlogs = _remoteIndex.getUniqueBlogs(); - for (Iterator iter = remoteBlogs.iterator(); iter.hasNext(); ) { - Hash blog = (Hash)iter.next(); - if (!localBlogs.contains(blog)) { - if (!ignoreBlog(user, blog)) - blogs.add(blog); - } - } - } else { - byte h[] = Base64.decode(meta.trim()); - if (h != null) { - Hash blog = new Hash(h); - if (!ignoreBlog(user, blog)) - blogs.add(blog); - } - } - List urls = new ArrayList(blogs.size()); - List tmpFiles = new ArrayList(blogs.size()); - for (Iterator iter = blogs.iterator(); iter.hasNext(); ) { - Hash blog = (Hash)iter.next(); - urls.add(buildMetaURL(blog)); - try { - tmpFiles.add(File.createTempFile("fetchMeta", ".txt", BlogManager.instance().getTempDir())); - } catch (IOException ioe) { - _statusMessages.add("Internal error creating temporary file to fetch " + blog.toBase64() + ": " + ioe.getMessage()); - } - } - - for (int i = 0; i < urls.size(); i++) - _statusMessages.add("Scheduling up metadata fetches for " + HTMLRenderer.sanitizeString((String)urls.get(i))); - fetch(urls, tmpFiles, user, new MetadataStatusListener()); - } - - private String buildMetaURL(Hash blog) { - String loc = _remoteLocation.trim(); - int root = loc.lastIndexOf('/'); - return loc.substring(0, root + 1) + blog.toBase64() + "/" + Archive.METADATA_FILE; - } - - public void fetchSelectedEntries(User user, Map parameters) { - String entries[] = ArchiveViewerBean.getStrings(parameters, "entry"); - if ( (entries == null) || (entries.length <= 0) ) return; - List urls = new ArrayList(entries.length); - List tmpFiles = new ArrayList(entries.length); - for (int i = 0; i < entries.length; i++) { - BlogURI uri = new BlogURI(entries[i]); - if (ignoreBlog(user, uri.getKeyHash())) - continue; - urls.add(buildEntryURL(uri)); - try { - tmpFiles.add(File.createTempFile("fetchBlog", ".txt", BlogManager.instance().getTempDir())); - } catch (IOException ioe) { - _statusMessages.add("Internal error creating temporary file to fetch " + HTMLRenderer.sanitizeString(entries[i]) + ": " + ioe.getMessage()); - } - } - - for (int i = 0; i < urls.size(); i++) - _statusMessages.add("Scheduling blog post fetching for " + HTMLRenderer.sanitizeString(entries[i])); - fetch(urls, tmpFiles, user, new BlogStatusListener(user)); - } - - public void fetchSelectedBulk(User user, Map parameters) { - fetchSelectedBulk(user, parameters, false); - } - - public void fetchSelectedBulk(User user, Map parameters, boolean shouldBlock) { - String entries[] = ArchiveViewerBean.getStrings(parameters, "entry"); - String action = ArchiveViewerBean.getString(parameters, "action"); - if ("Fetch all new entries".equals(action)) { - ArchiveIndex localIndex = BlogManager.instance().getArchive().getIndex(); - List uris = new ArrayList(); - List matches = new ArrayList(); - for (Iterator iter = _remoteIndex.getUniqueBlogs().iterator(); iter.hasNext(); ) { - Hash blog = (Hash)iter.next(); - if (ignoreBlog(user, blog)) - continue; - - _remoteIndex.selectMatchesOrderByEntryId(matches, blog, null); - for (int i = 0; i < matches.size(); i++) { - BlogURI uri = (BlogURI)matches.get(i); - if (!localIndex.getEntryIsKnown(uri)) - uris.add(uri); - } - matches.clear(); - } - entries = new String[uris.size()]; - for (int i = 0; i < uris.size(); i++) - entries[i] = ((BlogURI)uris.get(i)).toString(); - } - if ( (entries == null) || (entries.length <= 0) ) return; - if (_exportCapable) { - StringBuffer url = new StringBuffer(512); - url.append(buildExportURL()); - StringBuffer postData = new StringBuffer(512); - Set meta = new HashSet(); - for (int i = 0; i < entries.length; i++) { - BlogURI uri = new BlogURI(entries[i]); - if (uri.getEntryId() >= 0) { - postData.append("entry=").append(uri.toString()).append('&'); - meta.add(uri.getKeyHash()); - _statusMessages.add("Scheduling bulk blog post fetch of " + HTMLRenderer.sanitizeString(entries[i])); - } - } - for (Iterator iter = meta.iterator(); iter.hasNext(); ) { - Hash blog = (Hash)iter.next(); - postData.append("meta=").append(blog.toBase64()).append('&'); - _statusMessages.add("Scheduling bulk blog metadata fetch of " + blog.toBase64()); - } - try { - File tmp = File.createTempFile("fetchBulk", ".zip", BlogManager.instance().getTempDir()); - - boolean shouldProxy = (_proxyHost != null) && (_proxyPort > 0); - final EepGet get = new EepGet(_context, shouldProxy, _proxyHost, _proxyPort, 0, tmp.getAbsolutePath(), url.toString(), postData.toString()); - get.addStatusListener(new BulkFetchListener(user, tmp)); - - if (shouldBlock) { - get.fetch(); - } else { - I2PThread t = new I2PThread(new Runnable() { public void run() { get.fetch(); } }, "Syndie fetcher"); - t.setDaemon(true); - t.start(); - } - } catch (IOException ioe) { - _statusMessages.add("Internal error creating temporary file to fetch " + HTMLRenderer.sanitizeString(url.toString()) + ": " + ioe.getMessage()); - } - } else { - List urls = new ArrayList(entries.length+8); - for (int i = 0; i < entries.length; i++) { - BlogURI uri = new BlogURI(entries[i]); - if (uri.getEntryId() >= 0) { - String metaURL = buildMetaURL(uri.getKeyHash()); - if (!urls.contains(metaURL)) { - urls.add(metaURL); - _statusMessages.add("Scheduling blog metadata fetch of " + HTMLRenderer.sanitizeString(entries[i])); - } - urls.add(buildEntryURL(uri)); - _statusMessages.add("Scheduling blog post fetch of " + HTMLRenderer.sanitizeString(entries[i])); - } - } - List tmpFiles = new ArrayList(1); - try { - for (int i = 0; i < urls.size(); i++) { - File t = File.createTempFile("fetchBulk", ".dat", BlogManager.instance().getTempDir()); - tmpFiles.add(t); - } - fetch(urls, tmpFiles, user, new BlogStatusListener(user), shouldBlock); - } catch (IOException ioe) { - _statusMessages.add("Internal error creating temporary file to fetch posts: " + HTMLRenderer.sanitizeString(urls.toString())); - } - } - } - - private String buildExportURL() { - String loc = _remoteLocation.trim(); - int root = loc.lastIndexOf('/'); - return loc.substring(0, root + 1) + "export.zip?"; - } - - private String buildEntryURL(BlogURI uri) { - String loc = _remoteLocation.trim(); - int root = loc.lastIndexOf('/'); - return loc.substring(0, root + 1) + uri.getKeyHash().toBase64() + "/" + uri.getEntryId() + ".snd"; - } - - public void fetchAllEntries(User user, Map parameters) { - ArchiveIndex localIndex = BlogManager.instance().getArchive().getIndex(); - List uris = new ArrayList(); - List entries = new ArrayList(); - for (Iterator iter = _remoteIndex.getUniqueBlogs().iterator(); iter.hasNext(); ) { - Hash blog = (Hash)iter.next(); - if (ignoreBlog(user, blog)) - continue; - _remoteIndex.selectMatchesOrderByEntryId(entries, blog, null); - for (int i = 0; i < entries.size(); i++) { - BlogURI uri = (BlogURI)entries.get(i); - if (!localIndex.getEntryIsKnown(uri)) - uris.add(uri); - } - entries.clear(); - } - List urls = new ArrayList(uris.size()); - List tmpFiles = new ArrayList(uris.size()); - for (int i = 0; i < uris.size(); i++) { - urls.add(buildEntryURL((BlogURI)uris.get(i))); - try { - tmpFiles.add(File.createTempFile("fetchBlog", ".txt", BlogManager.instance().getTempDir())); - } catch (IOException ioe) { - _statusMessages.add("Internal error creating temporary file to fetch " + HTMLRenderer.sanitizeString(uris.get(i).toString()) + ": " + ioe.getMessage()); - } - } - - for (int i = 0; i < urls.size(); i++) - _statusMessages.add("Fetch all entries: " + HTMLRenderer.sanitizeString((String)urls.get(i))); - fetch(urls, tmpFiles, user, new BlogStatusListener(user)); - } - - private void fetch(List urls, List tmpFiles, User user, EepGet.StatusListener lsnr) { - fetch(urls, tmpFiles, user, lsnr, false); - } - - private void fetch(List urls, List tmpFiles, User user, EepGet.StatusListener lsnr, boolean shouldBlock) { - EepGetScheduler scheduler = new EepGetScheduler(I2PAppContext.getGlobalContext(), urls, tmpFiles, _proxyHost, _proxyPort, lsnr); - scheduler.fetch(shouldBlock); - } - - public void fetchIndex(User user, String schema, String location, String proxyHost, String proxyPort, boolean allowCaching) { - _fetchIndexInProgress = true; - _remoteIndex = null; - _remoteLocation = location; - _remoteSchema = schema; - _proxyHost = null; - _proxyPort = -1; - _exportCapable = false; - if (user == null) user = BlogManager.instance().getDefaultUser(); - - if ( (schema == null) || (schema.trim().length() <= 0) || - (location == null) || (location.trim().length() <= 0) ) { - _statusMessages.add("Location must be specified [" + location + "] [" + schema + "]"); - _fetchIndexInProgress = false; - return; - } - - if ("web".equals(schema)) { - if ( (proxyHost != null) && (proxyHost.trim().length() > 0) && - (proxyPort != null) && (proxyPort.trim().length() > 0) ) { - _proxyHost = proxyHost; - try { - _proxyPort = Integer.parseInt(proxyPort); - } catch (NumberFormatException nfe) { - _statusMessages.add("Proxy port " + HTMLRenderer.sanitizeString(proxyPort) + " is invalid"); - _fetchIndexInProgress = false; - return; - } - } - } else { - _statusMessages.add(new String("Remote schema " + HTMLRenderer.sanitizeString(schema) + " currently not supported")); - _fetchIndexInProgress = false; - return; - } - - _statusMessages.add("Fetching index from " + HTMLRenderer.sanitizeString(_remoteLocation) + - (_proxyHost != null ? " via " + HTMLRenderer.sanitizeString(_proxyHost) + ":" + _proxyPort : "")); - - File archiveFile; - if (user.getBlog() != null) { - archiveFile = new File(BlogManager.instance().getTempDir(), user.getBlog().toBase64() + "_remoteArchive.txt"); - } else { - archiveFile = new File(BlogManager.instance().getTempDir(), "remoteArchive.txt"); - } - archiveFile.delete(); - - Properties etags = new Properties(); - try { - DataHelper.loadProps(etags, new File(BlogManager.instance().getRootDir(), "etags")); - } catch (IOException ioe) { - //ignore - } - - String tag = null; - if (allowCaching) - tag = etags.getProperty(location); - EepGet eep = new EepGet(I2PAppContext.getGlobalContext(), ((_proxyHost != null) && (_proxyPort > 0)), - _proxyHost, _proxyPort, 0, archiveFile.getAbsolutePath(), location, allowCaching, tag); - eep.addStatusListener(new IndexFetcherStatusListener(archiveFile)); - eep.fetch(); - - if (eep.getETag() != null) { - etags.setProperty(location, eep.getETag()); - } - try { - DataHelper.storeProps(etags, new File(BlogManager.instance().getRootDir(), "etags")); - } catch (IOException ioe) { - //ignore - } - } - - private class IndexFetcherStatusListener implements EepGet.StatusListener { - private File _archiveFile; - public IndexFetcherStatusListener(File file) { - _archiveFile = file; - } - public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) { - _statusMessages.add("Attempt " + currentAttempt + " failed after " + bytesTransferred + (cause != null ? " " + cause.getMessage() : "")); - } - - public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {} - public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) { - _statusMessages.add("Fetch of " + HTMLRenderer.sanitizeString(url) + " successful"); - _fetchIndexInProgress = false; - ArchiveIndex i = new ArchiveIndex(I2PAppContext.getGlobalContext(), false); - if (notModified) { - _statusMessages.add("Archive unchanged since last fetch."); - _statusMessages.add("If you want to force a refetch, make a trivial modification to the URL, such as adding a \"?\""); - } else { - try { - i.load(_archiveFile); - _statusMessages.add("Archive fetched and loaded"); - _remoteIndex = i; - } catch (IOException ioe) { - _statusMessages.add("Archive is corrupt: " + ioe.getMessage()); - } - } - } - public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) { - _statusMessages.add("Fetch of " + HTMLRenderer.sanitizeString(url) + " failed after " + bytesTransferred); - _fetchIndexInProgress = false; - } - public void headerReceived(String url, int currentAttempt, String key, String val) { - if (ArchiveServlet.HEADER_EXPORT_CAPABLE.equals(key) && ("true".equals(val))) { - _statusMessages.add("Remote archive is bulk export capable"); - _exportCapable = true; - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Header received: [" + key + "] = [" + val + "]"); - } - } - } - - private class MetadataStatusListener implements EepGet.StatusListener { - public MetadataStatusListener() {} - public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) { - _statusMessages.add("Attempt " + currentAttempt + " failed after " + bytesTransferred + (cause != null ? " " + cause.getMessage() : "")); - } - - public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {} - public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) { - handleMetadata(url, outputFile); - } - public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) { - _statusMessages.add("Fetch of " + HTMLRenderer.sanitizeString(url) + " failed after " + bytesTransferred);; - } - public void headerReceived(String url, int currentAttempt, String key, String val) {} - } - - private void handleMetadata(String url, String outputFile) { - _statusMessages.add("Fetch of " + HTMLRenderer.sanitizeString(url) + " successful"); - File info = new File(outputFile); - FileInputStream in = null; - try { - BlogInfo i = new BlogInfo(); - in = new FileInputStream(info); - i.load(in); - boolean ok = BlogManager.instance().getArchive().storeBlogInfo(i); - if (ok) { - _statusMessages.add("Blog info for " + HTMLRenderer.sanitizeString(i.getProperty(BlogInfo.NAME)) + " imported"); - BlogManager.instance().getArchive().reloadInfo(); - } else { - _statusMessages.add("Blog info at " + HTMLRenderer.sanitizeString(url) + " was corrupt / invalid / forged"); - } - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error handling metadata", ioe); - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - info.delete(); - } - } - - private class BlogStatusListener implements EepGet.StatusListener { - private User _user; - public BlogStatusListener(User user) { - _user = user; - } - public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) { - _statusMessages.add("Attempt " + currentAttempt + " failed after " + bytesTransferred + (cause != null ? " " + cause.getMessage() : "")); - } - - public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {} - public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) { - if (url.endsWith(".snm")) { - handleMetadata(url, outputFile); - return; - } - _statusMessages.add("Fetch of " + HTMLRenderer.sanitizeString(url) + " successful"); - File file = new File(outputFile); - FileInputStream in = null; - try { - EntryContainer c = new EntryContainer(); - in = new FileInputStream(file); - c.load(in); - BlogURI uri = c.getURI(); - if ( (uri == null) || (uri.getKeyHash() == null) ) { - _statusMessages.add("Blog post at " + HTMLRenderer.sanitizeString(url) + " was corrupt - no URI"); - return; - } - Archive a = BlogManager.instance().getArchive(); - BlogInfo info = a.getBlogInfo(uri); - if (info == null) { - _statusMessages.add("Blog post " + uri.toString() + " cannot be imported, as we don't have their blog metadata"); - return; - } - boolean ok = a.storeEntry(c); - if (!ok) { - _statusMessages.add("Blog post at " + url + ": " + uri.toString() + " has an invalid signature"); - return; - } else { - _statusMessages.add("Blog post " + uri.toString() + " imported"); - BlogManager.instance().getArchive().regenerateIndex(); - _user.dataImported(); - } - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Error importing", ioe); - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - file.delete(); - } - } - public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) { - _statusMessages.add("Fetch of " + HTMLRenderer.sanitizeString(url) + " failed after " + bytesTransferred); - } - public void headerReceived(String url, int currentAttempt, String key, String val) {} - } - - /** - * Receive the status of a fetch for the zip containing blogs and metadata (as generated by - * the ExportServlet) - */ - private class BulkFetchListener implements EepGet.StatusListener { - private File _tmp; - private User _user; - public BulkFetchListener(User user, File tmp) { - _user = user; - _tmp = tmp; - } - public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) { - _statusMessages.add("Attempt " + currentAttempt + " failed after " + bytesTransferred + (cause != null ? " " + cause.getMessage() : "")); - } - - public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {} - public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) { - _statusMessages.add("Fetch of " + HTMLRenderer.sanitizeString(url.substring(0, url.indexOf('?'))) + " successful, importing the data"); - File file = new File(outputFile); - ZipInputStream zi = null; - try { - zi = new ZipInputStream(new FileInputStream(file)); - boolean postImported = false; - while (true) { - ZipEntry entry = zi.getNextEntry(); - if (entry == null) - break; - - ByteArrayOutputStream out = new ByteArrayOutputStream(1024); - byte buf[] = new byte[1024]; - int read = -1; - while ( (read = zi.read(buf)) != -1) - out.write(buf, 0, read); - - if (entry.getName().startsWith("meta")) { - BlogInfo i = new BlogInfo(); - i.load(new ByteArrayInputStream(out.toByteArray())); - boolean ok = BlogManager.instance().getArchive().storeBlogInfo(i); - if (ok) { - _statusMessages.add("Blog info for " + HTMLRenderer.sanitizeString(i.getProperty(BlogInfo.NAME)) + " imported"); - } else { - _statusMessages.add("Blog info at " + HTMLRenderer.sanitizeString(url) + " was corrupt / invalid / forged"); - } - } else if (entry.getName().startsWith("entry")) { - EntryContainer c = new EntryContainer(); - c.load(new ByteArrayInputStream(out.toByteArray())); - BlogURI uri = c.getURI(); - if ( (uri == null) || (uri.getKeyHash() == null) ) { - _statusMessages.add("Blog post " + HTMLRenderer.sanitizeString(entry.getName()) + " was corrupt - no URI"); - continue; - } - Archive a = BlogManager.instance().getArchive(); - BlogInfo info = a.getBlogInfo(uri); - if (info == null) { - _statusMessages.add("Blog post " + HTMLRenderer.sanitizeString(entry.getName()) + " cannot be imported, as we don't have their blog metadata"); - continue; - } - boolean ok = a.storeEntry(c); - if (!ok) { - _statusMessages.add("Blog post " + uri.toString() + " has an invalid signature"); - continue; - } else { - _statusMessages.add("Blog post " + uri.toString() + " imported"); - postImported = true; - } - } - } - - BlogManager.instance().getArchive().regenerateIndex(); - if (postImported) - _user.dataImported(); - } catch (IOException ioe) { - if (_log.shouldLog(Log.WARN)) - _log.debug("Error importing", ioe); - _statusMessages.add("Error importing from " + HTMLRenderer.sanitizeString(url) + ": " + ioe.getMessage()); - } finally { - if (zi != null) try { zi.close(); } catch (IOException ioe) {} - file.delete(); - } - } - public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) { - _statusMessages.add("Fetch of " + HTMLRenderer.sanitizeString(url) + " failed after " + bytesTransferred); - _tmp.delete(); - } - public void headerReceived(String url, int currentAttempt, String key, String val) {} - } - - public void postSelectedEntries(User user, Map parameters) { - postSelectedEntries(user, parameters, _proxyHost, _proxyPort, _remoteLocation); - } - public void postSelectedEntries(User user, Map parameters, String proxyHost, int proxyPort, String location) { - String entries[] = ArchiveViewerBean.getStrings(parameters, "localentry"); - if ( (entries == null) || (entries.length <= 0) ) return; - List uris = new ArrayList(entries.length); - for (int i = 0; i < entries.length; i++) - uris.add(new BlogURI(entries[i])); - postSelectedEntries(user, uris, proxyHost, proxyPort, location); - } - public void postSelectedEntries(User user, List uris, String location) { - postSelectedEntries(user, uris, _proxyHost, _proxyPort, location); - } - public void postSelectedEntries(User user, List uris, String proxyHost, int proxyPort, String location) { - if ( (proxyPort > 0) && (proxyHost != null) && (proxyHost.trim().length() > 0) ) { - _proxyPort = proxyPort; - _proxyHost = proxyHost; - } else { - _proxyPort = -1; - _proxyHost = null; - } - _remoteLocation = location; - post(uris, user); - } - - private void post(List blogURIs, User user) { - List files = new ArrayList(blogURIs.size()+1); - Set meta = new HashSet(4); - Map uploads = new HashMap(files.size()); - String importURL = getImportURL(); - _statusMessages.add("Uploading through " + HTMLRenderer.sanitizeString(importURL)); - for (int i = 0; i < blogURIs.size(); i++) { - BlogURI uri = (BlogURI)blogURIs.get(i); - File blogDir = new File(BlogManager.instance().getArchive().getArchiveDir(), uri.getKeyHash().toBase64()); - BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(uri); - if (!meta.contains(uri.getKeyHash())) { - uploads.put("blogmeta" + meta.size(), new File(blogDir, Archive.METADATA_FILE)); - meta.add(uri.getKeyHash()); - _statusMessages.add("Scheduling upload of the blog metadata for " + HTMLRenderer.sanitizeString(info.getProperty(BlogInfo.NAME))); - } - uploads.put("blogpost" + i, new File(blogDir, uri.getEntryId() + ".snd")); - _statusMessages.add("Scheduling upload of " + HTMLRenderer.sanitizeString(info.getProperty(BlogInfo.NAME)) - + ": " + getEntryDate(uri.getEntryId())); - } - EepPost post = new EepPost(); - post.postFiles(importURL, _proxyHost, _proxyPort, uploads, new Runnable() { public void run() { _statusMessages.add("Upload complete"); } }); - } - - private String getImportURL() { - String loc = _remoteLocation.trim(); - int archiveRoot = loc.lastIndexOf('/'); - int syndieRoot = loc.lastIndexOf('/', archiveRoot-1); - return loc.substring(0, syndieRoot + 1) + "import.jsp"; - } - - public void renderDeltaForm(User user, ArchiveIndex localIndex, Writer out) throws IOException { - Archive archive = BlogManager.instance().getArchive(); - StringBuffer buf = new StringBuffer(512); - buf.append("New blogs:
    \n"); - } - - int newEntries = 0; - int localNew = 0; - out.write("\n"); - List entries = new ArrayList(); - for (Iterator iter = remoteBlogs.iterator(); iter.hasNext(); ) { - Hash blog = (Hash)iter.next(); - if (ignoreBlog(user, blog)) - continue; - buf.setLength(0); - int shownEntries = 0; - buf.append("\n"); - buf.append(""); - buf.append(""); - buf.append(""); - buf.append("\n"); - entries.clear(); - _remoteIndex.selectMatchesOrderByEntryId(entries, blog, null); - for (int i = 0; i < entries.size(); i++) { - BlogURI uri = (BlogURI)entries.get(i); - buf.append("\n"); - if (!archive.getIndex().getEntryIsKnown(uri)) { - buf.append("\n"); - newEntries++; - shownEntries++; - } else { - String page = "threads.jsp?" + ThreadedHTMLRenderer.PARAM_VIEW_POST + '=' + blog.toBase64() + '/' + uri.getEntryId(); - buf.append("\n"); - } - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - } - if (shownEntries > 0) { - out.write(buf.toString()); - buf.setLength(0); - } - int remote = shownEntries; - - // now for posts in known blogs that we have and they don't - entries.clear(); - localIndex.selectMatchesOrderByEntryId(entries, blog, null); - buf.append("\n"); - for (int i = 0; i < entries.size(); i++) { - BlogURI uri = (BlogURI)entries.get(i); - if (!_remoteIndex.getEntryIsKnown(uri)) { - buf.append("\n"); - buf.append("\n"); - shownEntries++; - newEntries++; - localNew++; - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - } - } - - if (shownEntries > remote) // skip blogs we have already syndicated - out.write(buf.toString()); - } - - // now for posts in blogs we have and they don't - int newBefore = localNew; - buf.setLength(0); - buf.append("\n"); - for (Iterator iter = localBlogs.iterator(); iter.hasNext(); ) { - Hash blog = (Hash)iter.next(); - if (remoteBlogs.contains(blog)) { - //System.err.println("Remote index has " + blog.toBase64()); - continue; - } else if (ignoreBlog(user, blog)) { - continue; - } - - entries.clear(); - localIndex.selectMatchesOrderByEntryId(entries, blog, null); - - for (int i = 0; i < entries.size(); i++) { - BlogURI uri = (BlogURI)entries.get(i); - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - buf.append("\n"); - localNew++; - } - } - if (localNew > newBefore) - out.write(buf.toString()); - - out.write("
    \n"); - BlogInfo info = archive.getBlogInfo(blog); - if (info != null) { - buf.append(HTMLRenderer.sanitizeString(info.getProperty(BlogInfo.NAME))).append(": "); - buf.append("").append(HTMLRenderer.sanitizeString(info.getProperty(BlogInfo.DESCRIPTION))); - buf.append("\n"); - } else { - buf.append("" + blog.toBase64() + "\n"); - } - buf.append("
     "); - buf.append("Posted on#SizeTags
    (local)" + getDate(uri.getEntryId()) + "" + getId(uri.getEntryId()) + "" + _remoteIndex.getBlogEntrySizeKB(uri) + "KB"); - for (Iterator titer = new TreeSet(_remoteIndex.getBlogEntryTags(uri)).iterator(); titer.hasNext(); ) { - String tag = (String)titer.next(); - String page = "threads.jsp?" + ThreadedHTMLRenderer.PARAM_TAGS + '=' + HTMLRenderer.sanitizeTagParam(tag); - buf.append("" + HTMLRenderer.sanitizeString(tag) + " \n"); - } - buf.append("
    Entries we have, but the remote Syndie doesn't:
    " + getDate(uri.getEntryId()) + "" + getId(uri.getEntryId()) + "" + localIndex.getBlogEntrySizeKB(uri) + "KB"); - for (Iterator titer = new TreeSet(localIndex.getBlogEntryTags(uri)).iterator(); titer.hasNext(); ) { - String tag = (String)titer.next(); - String page = "threads.jsp?" + ThreadedHTMLRenderer.PARAM_TAGS + '=' + HTMLRenderer.sanitizeTagParam(tag); - buf.append("" + HTMLRenderer.sanitizeString(tag) + " \n"); - } - buf.append("
    Blogs the remote Syndie doesn't have
    " + getDate(uri.getEntryId()) + "" + getId(uri.getEntryId()) + "" + localIndex.getBlogEntrySizeKB(uri) + "KB"); - for (Iterator titer = new TreeSet(localIndex.getBlogEntryTags(uri)).iterator(); titer.hasNext(); ) { - String tag = (String)titer.next(); - String page = "threads.jsp?" + ThreadedHTMLRenderer.PARAM_TAGS + '=' + HTMLRenderer.sanitizeTagParam(tag); - buf.append("" + HTMLRenderer.sanitizeString(tag) + " \n"); - } - buf.append("
    \n"); - if (newEntries > 0) { - out.write(" \n"); - out.write(" \n"); - } else { - out.write("" + HTMLRenderer.sanitizeString(_remoteLocation) + " has no new posts to offer us\n"); - } - if (localNew > 0) { - out.write(" \n"); - } - out.write("
    \n"); - } - private final SimpleDateFormat _dateFormat = new SimpleDateFormat("yyyy/MM/dd", Locale.UK); - private String getDate(long when) { - synchronized (_dateFormat) { - return _dateFormat.format(new Date(when)); - } - } - private final String getEntryDate(long when) { - synchronized (_dateFormat) { - try { - String str = _dateFormat.format(new Date(when)); - long dayBegin = _dateFormat.parse(str).getTime(); - return str + "." + (when - dayBegin); - } catch (ParseException pe) { - pe.printStackTrace(); - // wtf - return "unknown"; - } - } - } - - private long getId(long id) { - synchronized (_dateFormat) { - try { - String str = _dateFormat.format(new Date(id)); - long dayBegin = _dateFormat.parse(str).getTime(); - return (id - dayBegin); - } catch (ParseException pe) { - pe.printStackTrace(); - // wtf - return id; - } - } - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/RunStandalone.java b/apps/syndie/java/src/net/i2p/syndie/web/RunStandalone.java deleted file mode 100644 index 778d3e41a..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/RunStandalone.java +++ /dev/null @@ -1,52 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.File; - -import net.i2p.util.FileUtil; - -import org.mortbay.jetty.Server; - -public class RunStandalone { - private Server _server; - - static { - System.setProperty("org.mortbay.http.Version.paranoid", "true"); - System.setProperty("org.mortbay.xml.XmlParser.NotValidating", "true"); - System.setProperty("syndie.rootDir", "."); - System.setProperty("syndie.defaultSingleUserArchives", "http://syndiemedia.i2p.net:8000/archive/archive.txt"); - System.setProperty("syndie.defaultProxyHost", ""); - System.setProperty("syndie.defaultProxyPort", ""); - } - - private RunStandalone(String args[]) {} - - public static void main(String args[]) { - RunStandalone runner = new RunStandalone(args); - runner.start(); - } - - public void start() { - File workDir = new File("work"); - boolean workDirRemoved = FileUtil.rmdir(workDir, false); - if (!workDirRemoved) - System.err.println("ERROR: Unable to remove Jetty temporary work directory"); - boolean workDirCreated = workDir.mkdirs(); - if (!workDirCreated) - System.err.println("ERROR: Unable to create Jetty temporary work directory"); - - try { - _server = new Server("jetty-syndie.xml"); - _server.start(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void stop() { - try { - _server.stop(); - } catch (InterruptedException ie) { - ie.printStackTrace(); - } - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/SwitchServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/SwitchServlet.java deleted file mode 100644 index 44fd4e970..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/SwitchServlet.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.syndie.Archive; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.sml.ThreadedHTMLRenderer; - -/** - * Login/register form - * - */ -public class SwitchServlet extends BaseServlet { - protected String getTitle() { return "Syndie :: Login/Register"; } - - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - out.write("
    \n"); - writeAuthActionFields(out); - out.write("Log in to an existing account\n" + - "Login: \n" + - "Password: \n" + - "\n" + - "\n" + - "\n" + - "
    \n" + - "
    \n" + - "
    \n"); - writeAuthActionFields(out); - out.write("Register a new account\n" + - "Login: (only known locally)\n" + - "Password: \n" + - "Public name: \n" + - "Description: \n" + - "Contact URL: \n" + - "Registration password: " + - " (only necessary if the Syndie administrator requires it)\n" + - "\n" + - "
    \n"); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/SyndicateServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/SyndicateServlet.java deleted file mode 100644 index 337ac5aa1..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/SyndicateServlet.java +++ /dev/null @@ -1,154 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Iterator; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.client.naming.PetName; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.sml.HTMLRenderer; - -/** - * Syndicate with another remote Syndie node - * - */ -public class SyndicateServlet extends BaseServlet { - protected String getTitle() { return "Syndie :: Syndicate"; } - - public static final String PARAM_SCHEMA = "schema"; - public static final String PARAM_LOCATION = "location"; - public static final String PARAM_PETNAME = "petname"; - - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - if (!BlogManager.instance().authorizeRemote(user)) { - out.write("Sorry, you are not authorized to access remote archives\n"); - } else { - out.write("
    "); - displayForm(user, req, out); - handleRequest(user, req, index, out); - out.write("
    \n"); - } - } - - private void handleRequest(User user, HttpServletRequest req, ThreadIndex index, PrintWriter out) throws IOException { - RemoteArchiveBean remote = getRemote(req); - String action = req.getParameter("action"); - if ("Continue...".equals(action)) { - String location = req.getParameter(PARAM_LOCATION); - String pn = req.getParameter(PARAM_PETNAME); - if ( (pn != null) && (pn.trim().length() > 0) ) { - PetName pnval = user.getPetNameDB().getByName(pn); - if (pnval != null) location = pnval.getLocation(); - } - - // dont allow caching if they explicit ask for a fetch - boolean allowCaching = false; - remote.fetchIndex(user, req.getParameter(PARAM_SCHEMA), location, - req.getParameter("proxyhost"), - req.getParameter("proxyport"), allowCaching); - } else if ("Fetch metadata".equals(action)) { - remote.fetchMetadata(user, req.getParameterMap()); - } else if ("Fetch selected entries".equals(action)) { - //remote.fetchSelectedEntries(user, request.getParameterMap()); - remote.fetchSelectedBulk(user, req.getParameterMap()); - } else if ("Fetch all new entries".equals(action)) { - //remote.fetchAllEntries(user, request.getParameterMap()); - remote.fetchSelectedBulk(user, req.getParameterMap()); - } else if ("Post selected entries".equals(action)) { - remote.postSelectedEntries(user, req.getParameterMap()); - } - String msgs = remote.getStatus(); - if ( (msgs != null) && (msgs.length() > 0) ) { - out.write("
    ");
    -            out.write(msgs);
    -            out.write("Refresh

    \n"); - } - - if (remote.getFetchIndexInProgress()) { - out.write("Please wait while the index is being fetched "); - out.write("from "); - out.write(remote.getRemoteLocation()); - out.write("."); - } else if (remote.getRemoteIndex() != null) { - // remote index is NOT null! - out.write(""); - out.write(remote.getRemoteLocation()); - out.write(""); - out.write("(refetch):
    \n"); - - remote.renderDeltaForm(user, BlogManager.instance().getArchive().getIndex(), out); - out.write(""); - } - - out.write("\n"); - } - - private void displayForm(User user, HttpServletRequest req, PrintWriter out) throws IOException { - writeAuthActionFields(out); - out.write(""); - out.write("Import from:\n"); - out.write("\n"); - out.write("Proxy\n"); - out.write("\n"); - out.write("
    \n"); - out.write("Bookmarked archives:\n"); - out.write(" or "); - out.write("\n"); - out.write("
    \n"); - out.write("
    \n"); - } - - private static final String ATTR_REMOTE = "remote"; - protected RemoteArchiveBean getRemote(HttpServletRequest req) { - RemoteArchiveBean remote = (RemoteArchiveBean)req.getSession().getAttribute(ATTR_REMOTE); - if (remote == null) { - remote = new RemoteArchiveBean(); - req.getSession().setAttribute(ATTR_REMOTE, remote); - } - return remote; - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ThreadNavServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ThreadNavServlet.java deleted file mode 100644 index abb6f852e..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ThreadNavServlet.java +++ /dev/null @@ -1,157 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.HeaderReceiver; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.EntryContainer; -import net.i2p.syndie.data.FilteredThreadIndex; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.data.ThreadNode; -import net.i2p.syndie.sml.HTMLRenderer; -import net.i2p.syndie.sml.SMLParser; - -/** - * Export the thread nav as either RDF or XML - * - */ -public class ThreadNavServlet extends BaseServlet { - public static final String PARAM_COUNT = "count"; - public static final String PARAM_OFFSET = "offset"; - public static final String PARAM_FORMAT = "format"; - - public static final String FORMAT_RDF = "rdf"; - public static final String FORMAT_XML = "xml"; - - protected void render(User user, HttpServletRequest req, HttpServletResponse resp, ThreadIndex index) throws ServletException, IOException { - int threadCount = empty(req, PARAM_COUNT) ? index.getRootCount() : getInt(req, PARAM_COUNT); - int offset = getInt(req, PARAM_OFFSET); - String uri = req.getRequestURI(); - if (uri.endsWith(FORMAT_XML)) { - resp.setContentType("text/xml; charset=UTF-8"); - render(user, index, resp.getWriter(), threadCount, offset, FORMAT_XML); - } else { - resp.setContentType("application/rdf+xml; charset=UTF-8"); - render(user, index, resp.getWriter(), threadCount, offset, FORMAT_RDF); - } - } - - private int getInt(HttpServletRequest req, String param) { - String val = req.getParameter(param); - if (val != null) { - try { - return Integer.parseInt(val); - } catch (NumberFormatException nfe) { - // ignore - } - } - return -1; - } - - private static final int DEFAULT_THREADCOUNT = 10; - private static final int DEFAULT_THREADOFFSET = 0; - - private void render(User user, ThreadIndex index, PrintWriter out, int threadCount, int offset, String format) throws IOException { - int startRoot = DEFAULT_THREADOFFSET; - if (offset >= 0) - startRoot = offset; - renderStart(out, format); - - int endRoot = startRoot + (threadCount > 0 ? threadCount : DEFAULT_THREADCOUNT); - if (endRoot >= index.getRootCount()) - endRoot = index.getRootCount() - 1; - for (int i = startRoot; i <= endRoot; i++) { - ThreadNode node = index.getRoot(i); - if (FORMAT_XML.equals(format)) - out.write(node.toString()); - else - render(user, node, out); - } - renderEnd(out, format); - } - private void renderStart(PrintWriter out, String format) throws IOException { - out.write("\n"); - if (FORMAT_XML.equals(format)) { - out.write(""); - } else { - out.write("\n"); - out.write("\n"); - } - } - private void renderEnd(PrintWriter out, String format) throws IOException { - if (FORMAT_XML.equals(format)) { - out.write(""); - } else { - out.write("\n"); - out.write("\n"); - } - } - private void render(User user, ThreadNode node, PrintWriter out) throws IOException { - Archive archive = BlogManager.instance().getArchive(); - String blog = node.getEntry().getKeyHash().toBase64(); - out.write("\n"); - out.write(""); - PetName pn = user.getPetNameDB().getByLocation(blog); - String name = null; - if (pn != null) { - if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) - out.write("\n"); - if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) - out.write("\n"); - name = pn.getName(); - } else { - BlogInfo info = archive.getBlogInfo(node.getEntry().getKeyHash()); - if (info != null) - name = info.getProperty(BlogInfo.NAME); - if ( (name == null) || (name.trim().length() <= 0) ) - name = node.getEntry().getKeyHash().toBase64().substring(0,6); - } - out.write("" + HTMLRenderer.sanitizeStrippedXML(name) + "\n"); - if ( (user.getBlog() != null) && (node.containsAuthor(user.getBlog())) ) - out.write("\n"); - - EntryContainer entry = archive.getEntry(node.getEntry()); - if (entry == null) throw new RuntimeException("Unable to fetch the entry " + node.getEntry()); - - SMLParser parser = new SMLParser(I2PAppContext.getGlobalContext()); - HeaderReceiver rec = new HeaderReceiver(); - parser.parse(entry.getEntry().getText(), rec); - String subject = rec.getHeader(HTMLRenderer.HEADER_SUBJECT); - if ( (subject == null) || (subject.trim().length() <= 0) ) - subject = "(no subject)"; - - out.write("" + HTMLRenderer.sanitizeStrippedXML(subject) + "\n"); - - long dayBegin = BlogManager.instance().getDayBegin(); - long postId = node.getEntry().getEntryId(); - int daysAgo = (int)((dayBegin - postId + 24*60*60*1000l-1l)/(24*60*60*1000l)); - out.write("" + daysAgo + "\n"); - - out.write(""); - out.write(""); - for (int i = 0; i < node.getChildCount(); i++) - render(user, node.getChild(i), out); - out.write("\n"); - out.write("\n"); - - out.write("\n"); - out.write("\n"); - } - - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - throw new UnsupportedOperationException("Not relevant..."); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogServlet.java deleted file mode 100644 index 6a254f521..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogServlet.java +++ /dev/null @@ -1,819 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import net.i2p.client.naming.PetName; -import net.i2p.data.Base64; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.User; -import net.i2p.syndie.data.ArchiveIndex; -import net.i2p.syndie.data.Attachment; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogInfoData; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.EntryContainer; -import net.i2p.syndie.data.FilteredThreadIndex; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.data.ThreadNode; -import net.i2p.syndie.sml.BlogPostInfoRenderer; -import net.i2p.syndie.sml.BlogRenderer; -import net.i2p.syndie.sml.HTMLRenderer; -import net.i2p.syndie.sml.ThreadedHTMLRenderer; -import net.i2p.util.FileUtil; -import net.i2p.util.Log; - -/** - * Render the appropriate posts for the current blog, using any blog info data available - * - */ -public class ViewBlogServlet extends BaseServlet { - public static final String PARAM_OFFSET = "offset"; - /** $blogHash */ - public static final String PARAM_BLOG = "blog"; - /** $blogHash/$entryId */ - public static final String PARAM_ENTRY = "entry"; - /** tag,tag,tag */ - public static final String PARAM_TAG = "tag"; - /** $blogHash/$entryId/$attachmentId */ - public static final String PARAM_ATTACHMENT = "attachment"; - /** image within the BlogInfoData to load (e.g. logo.png, icon_$tagHash.png, etc) */ - public static final String PARAM_IMAGE = "image"; - - public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - req.setCharacterEncoding("UTF-8"); - String attachment = req.getParameter(PARAM_ATTACHMENT); - if (attachment != null) { - // if they requested an attachment, serve it up to 'em - if (renderAttachment(req, resp, attachment)) - return; - } - String img = req.getParameter(PARAM_IMAGE); - if (img != null) { - boolean rendered = renderUpdatedImage(img, req, resp); - if (!rendered) - rendered = renderPublishedImage(img, req, resp); - if (!rendered) - rendered = renderDefaultImage(img, req, resp); - if (rendered) return; - } - super.service(req, resp); - } - - protected void render(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws ServletException, IOException { - Archive archive = BlogManager.instance().getArchive(); - - Hash blog = null; - String name = req.getParameter(PARAM_BLOG); - if ( (name == null) || (name.trim().length() <= 0) ) { - blog = user.getBlog(); - } else { - byte val[] = Base64.decode(name); - if ( (val != null) && (val.length == Hash.HASH_LENGTH) ) - blog = new Hash(val); - } - - BlogInfo info = null; - if (blog != null) - info = archive.getBlogInfo(blog); - - int offset = 0; - String off = req.getParameter(PARAM_OFFSET); - if (off != null) try { offset = Integer.parseInt(off); } catch (NumberFormatException nfe) {} - - List posts = getPosts(user, archive, info, req, index); - render(user, req, out, archive, info, posts, offset); - } - - private BlogURI getEntry(HttpServletRequest req) { - String param = req.getParameter(PARAM_ENTRY); - if (param != null) - return new BlogURI("blog://" + param); - return null; - } - - private List getPosts(User user, Archive archive, BlogInfo info, HttpServletRequest req, ThreadIndex index) { - List rv = new ArrayList(1); - if (info == null) return rv; - - String entrySelected = req.getParameter(PARAM_ENTRY); - if (entrySelected != null) { - // $blogKey/$entryId - BlogURI uri = null; - if (entrySelected.startsWith("blog://")) - uri = new BlogURI(entrySelected); - else - uri = new BlogURI("blog://" + entrySelected.trim()); - if (uri.getEntryId() >= 0) { - rv.add(uri); - return rv; - } - } - - ArchiveIndex aindex = archive.getIndex(); - - BlogURI uri = getEntry(req); - if (uri != null) { - rv.add(uri); - return rv; - } - - aindex.selectMatchesOrderByEntryId(rv, info.getKey().calculateHash(), null); - - // lets filter out any posts that are not roots - for (int i = 0; i < rv.size(); i++) { - BlogURI curURI = (BlogURI)rv.get(i); - ThreadNode node = index.getNode(curURI); - if ( (node != null) && (node.getParent() == null) ) { - // ok, its a root - Collection tags = node.getTags(); - if ( (tags != null) && (tags.contains(BlogInfoData.TAG)) ) { - // skip this, as its an info post - rv.remove(i); - i--; - } - } else { - rv.remove(i); - i--; - } - } - return rv; - } - - private void render(User user, HttpServletRequest req, PrintWriter out, Archive archive, BlogInfo info, List posts, int offset) throws IOException { - String title = null; - String desc = null; - BlogInfoData data = null; - if (info != null) { - title = info.getProperty(BlogInfo.NAME); - desc = info.getProperty(BlogInfo.DESCRIPTION); - String dataURI = info.getProperty(BlogInfo.SUMMARY_ENTRY_ID); - if (dataURI != null) { - EntryContainer entry = archive.getEntry(new BlogURI(dataURI)); - if (entry != null) { - data = new BlogInfoData(); - try { - data.load(entry); - } catch (IOException ioe) { - data = null; - if (_log.shouldLog(Log.WARN)) - _log.warn("Error loading the blog info data from " + dataURI, ioe); - } - } - } - } - String pageTitle = "Syndie :: Blogs" + (desc != null ? " :: " + desc : ""); - if (title != null) pageTitle = pageTitle + " (" + title + ")"; - pageTitle = HTMLRenderer.sanitizeString(pageTitle); - out.write("\n"); - out.write("\n"); - out.write("\n\n" + pageTitle + "\n"); - if (info != null) - out.write("\n"); - out.write(""); - renderHeader(user, req, out, info, data, title, desc); - renderReferences(user, out, info, data, req, posts); - renderBody(user, out, info, data, posts, offset, archive, req); - out.write("\n"); - } - private void renderStyle(PrintWriter out, BlogInfo info, BlogInfoData data, HttpServletRequest req) throws IOException { - // modify it based on data.getStyleOverrides()... - out.write(CSS); - Reader css = null; - try { - InputStream in = req.getSession().getServletContext().getResourceAsStream("/syndie.css"); - if (in != null) { - css = new InputStreamReader(in, "UTF-8"); - char buf[] = new char[1024]; - int read = 0; - while ( (read = css.read(buf)) != -1) - out.write(buf, 0, read); - } - } finally { - if (css != null) - css.close(); - } - String content = FileUtil.readTextFile("./docs/syndie_standard.css", -1, true); - if (content != null) out.write(content); - } - - public static String getLogoURL(Hash blog) { - if (blog == null) return ""; - return "blog.jsp?" + PARAM_BLOG + "=" + blog.toBase64() + "&" - + PARAM_IMAGE + "=" + BlogInfoData.ATTACHMENT_LOGO; - } - - private void renderHeader(User user, HttpServletRequest req, PrintWriter out, BlogInfo info, BlogInfoData data, String title, String desc) throws IOException { - out.write("\n" + - "Content\n"); - renderNavBar(user, req, out); - out.write("
    \n"); - Hash kh = null; - if ( (info != null) && (info.getKey() != null) ) - kh = info.getKey().calculateHash(); - out.write("\"\"\n"); - String name = desc; - if ( (name == null) || (name.trim().length() <= 0) ) - name = title; - if ( ( (name == null) || (name.trim().length() <= 0) ) && (info != null) && (kh != null) ) - name = kh.toBase64(); - if (name != null) { - String url = "blog.jsp?" + (info != null ? PARAM_BLOG + "=" + info.getKey().calculateHash().toBase64() : ""); - out.write("" - + HTMLRenderer.sanitizeString(name) + ""); - out.write("
    profile threads"); - } - out.write("
    \n"); - } - - public static final String DEFAULT_GROUP_NAME = "References"; - private void renderReferences(User user, PrintWriter out, BlogInfo info, BlogInfoData data, HttpServletRequest req, List posts) throws IOException { - out.write("
    \n"); - if (data != null) { - for (int i = 0; i < data.getReferenceGroupCount(); i++) { - List group = data.getReferenceGroup(i); - if (group.size() <= 0) continue; - PetName pn = (PetName)group.get(0); - String name = null; - if (pn.getGroupCount() <= 0) - name = DEFAULT_GROUP_NAME; - else - name = HTMLRenderer.sanitizeString(pn.getGroup(0)); - out.write("\n"); - out.write("
    \n"); - out.write("" + name + "\n"); - out.write("
      \n"); - for (int j = 0; j < group.size(); j++) { - pn = (PetName)group.get(j); - out.write("
    • " + renderLink(info.getKey().calculateHash(), pn) + "
    • \n"); - } - out.write("
    \n
    \n\n"); - } - } - //out.write("
    \n"); - //out.write("Custom links\n"); - //out.write("\n"); - //out.write("
    "); - - renderPostReferences(user, req, out, posts); - - out.write("
    "); - out.write("Secured by Syndie"); - out.write("
    \n"); - out.write("
    \n\n"); - } - - private void renderPostReferences(User user, HttpServletRequest req, PrintWriter out, List posts) throws IOException { - if (!empty(req, PARAM_ENTRY) && (posts.size() == 1)) { - BlogURI uri = (BlogURI)posts.get(0); - Archive archive = BlogManager.instance().getArchive(); - EntryContainer entry = archive.getEntry(uri); - if (entry != null) { - out.write("
    \n"); - - BlogPostInfoRenderer renderer = new BlogPostInfoRenderer(_context); - renderer.render(user, archive, entry, out); - - out.write("
    \n"); - } - } - } - - /** generate a link for the given petname within the scope of the given blog */ - public static String renderLink(Hash blogFrom, PetName pn) { - StringBuffer buf = new StringBuffer(64); - String type = pn.getProtocol(); - if ("syndieblog".equals(type)) { - String loc = pn.getLocation(); - if (loc != null) { - buf.append(""); - } - buf.append(HTMLRenderer.sanitizeString(pn.getName())); - if (loc != null) { - buf.append(""); - //buf.append(" \"\"\n"); - } - } else if ("syndieblogpost".equals(type)) { - String loc = pn.getLocation(); - if (loc != null) { - buf.append(""); - } - buf.append(HTMLRenderer.sanitizeString(pn.getName())); - if (loc != null) { - buf.append(""); - } - } else if ("syndieblogattachment".equals(type)) { - String loc = pn.getLocation(); - if (loc != null) { - int split = loc.lastIndexOf('/'); - try { - int attachmentId = -1; - if (split > 0) - attachmentId = Integer.parseInt(loc.substring(split+1)); - - if (attachmentId < 0) { - loc = null; - } else { - BlogURI post = null; - if (loc.startsWith("blog://")) - post = new BlogURI(loc.substring(0, split)); - else - post = new BlogURI("blog://" + loc.substring(0, split)); - - EntryContainer entry = BlogManager.instance().getArchive().getEntry(post); - if (entry != null) { - Attachment attachments[] = entry.getAttachments(); - if (attachmentId < attachments.length) { - buf.append(""); - buf.append(HTMLRenderer.sanitizeString(pn.getName())); - buf.append(""); - } else { - loc = null; - } - } else { - loc = null; - } - } - } catch (Exception e) { - e.printStackTrace(); - loc = null; - } - } - if (loc == null) - buf.append(HTMLRenderer.sanitizeString(pn.getName())); - } else if ( ("eepsite".equals(type)) || ("i2p".equals(type)) || - ("website".equals(type)) || ("http".equals(type)) || ("web".equals(type)) ) { - String loc = pn.getLocation(); - if (loc != null) { - buf.append(""); - } - buf.append(HTMLRenderer.sanitizeString(pn.getName())); - if (loc != null) { - buf.append(""); - } - } else { - buf.append(""); - buf.append(HTMLRenderer.sanitizeString(pn.getName())).append(""); - } - return buf.toString(); - } - - private static final int POSTS_PER_PAGE = 5; - private void renderBody(User user, PrintWriter out, BlogInfo info, BlogInfoData data, List posts, int offset, Archive archive, HttpServletRequest req) throws IOException { - out.write("
    \n\n\n"); - if (info == null) { - out.write("No blog specified\n"); - return; - } - - BlogRenderer renderer = new BlogRenderer(_context, info, data); - - if ( (posts.size() == 1) && (req.getParameter(PARAM_ENTRY) != null) ) { - BlogURI uri = (BlogURI)posts.get(0); - EntryContainer entry = archive.getEntry(uri); - renderer.renderPost(user, archive, entry, out, false, true); - renderComments(user, out, info, data, entry, archive, renderer); - } else { - for (int i = offset; i < posts.size() && i < offset + POSTS_PER_PAGE; i++) { - BlogURI uri = (BlogURI)posts.get(i); - EntryContainer entry = archive.getEntry(uri); - renderer.renderPost(user, archive, entry, out, true, true); - } - - renderNav(out, info, data, posts, offset, archive, req); - } - - out.write("
    \n"); - } - - private void renderComments(User user, PrintWriter out, BlogInfo info, BlogInfoData data, EntryContainer entry, - Archive archive, BlogRenderer renderer) throws IOException { - ArchiveIndex index = archive.getIndex(); - out.write("
    \n"); - renderComments(user, out, entry.getURI(), archive, index, renderer); - out.write("
    \n"); - } - private void renderComments(User user, PrintWriter out, BlogURI parentURI, Archive archive, ArchiveIndex index, BlogRenderer renderer) throws IOException { - List replies = index.getReplies(parentURI); - if (replies.size() > 0) { - out.write("
      \n"); - for (int i = 0; i < replies.size(); i++) { - BlogURI uri = (BlogURI)replies.get(i); - out.write("
    • "); - if (!shouldIgnore(user, uri)) { - EntryContainer cur = archive.getEntry(uri); - renderer.renderComment(user, archive, cur, out); - // recurse - renderComments(user, out, uri, archive, index, renderer); - } - out.write("
    • \n"); - } - out.write("
    \n"); - } - } - - private boolean shouldIgnore(User user, BlogURI uri) { - PetName pn = user.getPetNameDB().getByLocation(uri.getKeyHash().toBase64()); - return ( (pn != null) && pn.isMember(FilteredThreadIndex.GROUP_IGNORE)); - } - - private void renderNav(PrintWriter out, BlogInfo info, BlogInfoData data, List posts, int offset, Archive archive, HttpServletRequest req) throws IOException { - out.write("

    \n"); - String uri = req.getRequestURI() + "?"; - if (info != null) - uri = uri + PARAM_BLOG + "=" + info.getKey().calculateHash().toBase64() + "&"; - if (offset + POSTS_PER_PAGE >= posts.size()) - out.write(POSTS_PER_PAGE + " more older entries"); - else - out.write("" - + POSTS_PER_PAGE + " older entries"); - out.write(" | "); - if (offset <= 0) - out.write(POSTS_PER_PAGE + " more recent entries"); - else - out.write("" + POSTS_PER_PAGE + " more recent entries"); - - out.write("
    \n"); - } - - /** - * render the attachment to the browser, using the appropriate mime types, etc - * @param attachment formatted as $blogHash/$entryId/$attachmentId - * @return true if rendered - */ - private boolean renderAttachment(HttpServletRequest req, HttpServletResponse resp, String attachment) throws ServletException, IOException { - int split = attachment.lastIndexOf('/'); - if (split <= 0) - return false; - BlogURI uri = new BlogURI("blog://" + attachment.substring(0, split)); - try { - int attachmentId = Integer.parseInt(attachment.substring(split+1)); - if (attachmentId < 0) return false; - EntryContainer entry = BlogManager.instance().getArchive().getEntry(uri); - if (entry == null) { - System.out.println("Could not render the attachment [" + uri + "] / " + attachmentId); - return false; - } - Attachment attachments[] = entry.getAttachments(); - if (attachmentId >= attachments.length) { - System.out.println("Out of range attachment on " + uri + ": " + attachmentId); - return false; - } - - resp.setContentType(ArchiveViewerBean.getAttachmentContentType(attachments[attachmentId])); - boolean inline = ArchiveViewerBean.getAttachmentShouldShowInline(attachments[attachmentId]); - String filename = ArchiveViewerBean.getAttachmentFilename(attachments[attachmentId]); - if (inline) - resp.setHeader("Content-Disposition", "inline; filename=\"" + filename + "\""); - else - resp.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); - int len = ArchiveViewerBean.getAttachmentContentLength(attachments[attachmentId]); - if (len >= 0) - resp.setContentLength(len); - ArchiveViewerBean.renderAttachment(attachments[attachmentId], resp.getOutputStream()); - return true; - } catch (NumberFormatException nfe) {} - return false; - } - - - private boolean renderUpdatedImage(String requestedImage, HttpServletRequest req, HttpServletResponse resp) throws IOException { - BlogConfigBean bean = BlogConfigServlet.getConfigBean(req); - if ( (bean != null) && (bean.isUpdated()) && (bean.getLogo() != null) ) { - // the updated image only affects *our* blog... - User u = bean.getUser(); - if (u != null) { - String reqBlog = req.getParameter(PARAM_BLOG); - if ( (reqBlog == null) || (u.getBlog().toBase64().equals(reqBlog)) ) { - if (BlogInfoData.ATTACHMENT_LOGO.equals(requestedImage)) { - File logo = bean.getLogo(); - if (logo != null) { - byte buf[] = new byte[4096]; - resp.setContentType("image/png"); - resp.setContentLength((int)logo.length()); - OutputStream out = resp.getOutputStream(); - FileInputStream in = null; - try { - in = new FileInputStream(logo); - int read = 0; - while ( (read = in.read(buf)) != -1) - out.write(buf, 0, read); - _log.debug("Done writing the updated full length logo"); - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - if (out != null) try { out.close(); } catch (IOException ioe) {} - } - _log.debug("Returning from writing the updated full length logo"); - return true; - } - } else { - // ok, the blogConfigBean doesn't let people configure other things yet... fall through - } - } - } - } - return false; - } - - private boolean renderPublishedImage(String requestedImage, HttpServletRequest req, HttpServletResponse resp) throws IOException { - // nothing matched in the updated config, lets look at the current published info - String blog = req.getParameter(PARAM_BLOG); - if (blog != null) { - Archive archive = BlogManager.instance().getArchive(); - byte h[] = Base64.decode(blog); - if ( (h != null) && (h.length == Hash.HASH_LENGTH) ) { - Hash blogHash = new Hash(h); - BlogInfo info = archive.getBlogInfo(blogHash); - String entryId = info.getProperty(BlogInfo.SUMMARY_ENTRY_ID); - _log.debug("Author's entryId: " + entryId); - if (entryId != null) { - BlogURI dataURI = new BlogURI(entryId); - EntryContainer entry = archive.getEntry(dataURI); - if (entry != null) { - BlogInfoData data = new BlogInfoData(); - try { - data.load(entry); - - _log.debug("Blog info data loaded from: " + entryId); - Attachment toWrite = null; - if (BlogInfoData.ATTACHMENT_LOGO.equals(requestedImage)) { - toWrite = data.getLogo(); - } else { - toWrite = data.getOtherAttachment(requestedImage); - } - if (toWrite != null) { - resp.setContentType("image/png"); - resp.setContentLength(toWrite.getDataLength()); - InputStream in = null; - OutputStream out = null; - try { - in = toWrite.getDataStream(); - out = resp.getOutputStream(); - byte buf[] = new byte[4096]; - int read = -1; - while ( (read = in.read(buf)) != -1) - out.write(buf, 0, read); - - _log.debug("Write image from: " + entryId); - return true; - } finally { - if (in != null) try { in.close(); } catch (IOException ioe) {} - if (out != null) try { out.close(); } catch (IOException ioe) {} - } - } - } catch (IOException ioe) { - _log.debug("Error reading/writing: " + entryId, ioe); - data = null; - } - } - } - } - } - return false; - } - - /** 1px png, base64 encoded, used if they asked for an image that we dont know of */ - private static final byte BLANK_IMAGE[] = Base64.decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQI12NgYAAAAAMAASDVlMcAAAAASUVORK5CYII="); - private boolean renderDefaultImage(String requestedImage, HttpServletRequest req, HttpServletResponse resp) throws IOException { - if (requestedImage.equals("logo.png")) { - InputStream in = req.getSession().getServletContext().getResourceAsStream("/images/default_blog_logo.png"); - if (in != null) { - resp.setContentType("image/png"); - OutputStream out = resp.getOutputStream(); - try { - byte buf[] = new byte[4096]; - int read = -1; - while ( (read = in.read(buf)) != -1) - out.write(buf, 0, read); - _log.debug("Done writing default logo"); - } finally { - try { in.close(); } catch (IOException ioe) {} - try { out.close(); } catch (IOException ioe) {} - } - return true; - } - } - resp.setContentType("img.png"); - resp.setContentLength(BLANK_IMAGE.length); - OutputStream out = resp.getOutputStream(); - try { - out.write(BLANK_IMAGE); - } finally { - try { out.close(); } catch (IOException ioe) {} - } - _log.debug("Done writing default image"); - return true; - } - - private static final String CSS = "\n" + -"* {\n" + -" margin: 0px;\n" + -" padding: 0px;\n" + -"}\n" + -"body {\n" + -" font-family: Arial, Helvetica, sans-serif;\n" + -" font-size: 100%;\n" + -" background-color : #EEEEEE;\n" + -"}\n" + -"a {\n" + -" text-decoration: none;\n" + -"}\n" + -"a:hover {\n" + -" color: red;\n" + -"}\n" + -"select {\n" + -" min-width: 1.5em;\n" + -"}\n" + -".syndieBlog {\n" + -"}\n" + -".syndieBlogTopNav {\n" + -" float:left;\n" + -" width: 100%;\n" + -" background-color: #BBBBBB;\n" + -"}\n" + -".syndieBlogTopNavUser {\n" + -" text-align: left;\n" + -" float: left;\n" + -" margin: 2px;\n" + -"}\n" + -".syndieBlogTopNavAdmin {\n" + -" text-align: left;\n" + -" float: right;\n" + -" margin: 2px;\n" + -"}\n" + -".syndieBlogHeader {\n" + -" width: 100%;\n" + -" background-color: black;\n" + -" float:left;\n" + -"}\n" + -".syndieBlogHeader a {\n" + -" color: white;\n" + -" padding: 4px;\n" + -"}\n" + -".syndieBlogHeader b {\n" + -" font-size: 1.2em;\n" + -"}\n" + -".syndieBlogLogo {\n" + -" float: left;\n" + -"}\n" + -".syndieBlogLinks {\n" + -" width: 20%;\n" + -" float: left;\n" + -"}\n" + -".syndieBlogLinkGroup {\n" + -" font-size: 0.8em;\n" + -" background-color: #DDD;\n" + -" border: 1px solid black;\n" + -" margin: 5px;\n" + -" padding: 2px;\n" + -"}\n" + -".syndieBlogLinkGroup ul {\n" + -" list-style: none;\n" + -"}\n" + -".syndieBlogLinkGroup li {\n" + -" width: 100%;\n" + -" overflow: hidden;\n" + -" white-space: nowrap;\n" + -"}\n" + -".syndieBlogLinkGroupName {\n" + -" font-weight: bold;\n" + -" width: 100%;\n" + -" border-bottom: 1px dashed black;\n" + -" display: block;\n" + -" overflow: hidden;\n" + -" white-space: nowrap;\n" + -"}\n" + -".syndieBlogPostInfoGroup {\n" + -" font-size: 0.8em;\n" + -" background-color: #FFEA9F;\n" + -" border: 1px solid black;\n" + -" margin: 5px;\n" + -" padding: 2px;\n" + -"}\n" + -".syndieBlogPostInfoGroup ol {\n" + -" list-style: none;\n" + -"}\n" + -".syndieBlogPostInfoGroup li {\n" + -" white-space: nowrap;\n" + -" width: 100%;\n" + -" overflow: hidden;\n" + -"}\n" + -".syndieBlogPostInfoGroupName {\n" + -" font-weight: bold;\n" + -" width: 100%;\n" + -" border-bottom: 1px dashed black;\n" + -" display: block;\n" + -" overflow: hidden;\n" + -" white-space: nowrap;\n" + -"}\n" + -".syndieBlogMeta {\n" + -" text-align: left;\n" + -" font-size: 0.8em;\n" + -" background-color: #DDD;\n" + -" border: 1px solid black;\n" + -" margin: 5px;\n" + -" padding: 2px;\n" + -"}\n" + -".syndieBlogBody {\n" + -" width: 80%;\n" + -" float: left;\n" + -"}\n" + -".syndieBlogPost {\n" + -" border: 1px solid black;\n" + -" margin-top: 5px;\n" + -" margin-right: 5px;\n" + -" word-wrap: break-word;\n" + -"}\n" + -".syndieBlogPostHeader {\n" + -" background-color: #BBB;\n" + -" padding: 2px;\n" + -"}\n" + -".syndieBlogPostSubject {\n" + -" font-weight: bold;\n" + -"}\n" + -".syndieBlogPostFrom {\n" + -" text-align: right;\n" + -"}\n" + -".syndieBlogPostSummary {\n" + -" background-color: #FFFFFF;\n" + -" padding: 2px;\n" + -"}\n" + -".syndieBlogPostDetails {\n" + -" background-color: #DDD;\n" + -" padding: 2px;\n" + -"}\n" + -".syndieBlogNav {\n" + -" text-align: center;\n" + -"}\n" + -".syndieBlogComments {\n" + -" border: none;\n" + -" margin-top: 5px;\n" + -" margin-left: 0px;\n" + -" float: left;\n" + -"}\n" + -".syndieBlogComments ul {\n" + -" list-style: none;\n" + -" margin-left: 10px;\n" + -"}\n" + -".syndieBlogCommentInfoGroup {\n" + -" font-size: 0.8em;\n" + -" margin-right: 5px;\n" + -"}\n" + -".syndieBlogCommentInfoGroup ol {\n" + -" list-style: none;\n" + -"}\n" + -".syndieBlogCommentInfoGroup li {\n" + -"}\n" + -".syndieBlogCommentInfoGroupName {\n" + -" font-size: 0.8em;\n" + -" font-weight: bold;\n" + -"}\n"; - protected String getTitle() { return "unused"; } - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - throw new RuntimeException("unused"); - } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogsServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogsServlet.java deleted file mode 100644 index 4a1d4d258..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogsServlet.java +++ /dev/null @@ -1,168 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.TreeSet; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.client.naming.PetName; -import net.i2p.client.naming.PetNameDB; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.NewestEntryFirstComparator; -import net.i2p.syndie.User; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.FilteredThreadIndex; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.data.ThreadNode; -import net.i2p.syndie.sml.HTMLRenderer; - -/** - * List the blogs known in the archive - * - */ -public class ViewBlogsServlet extends BaseServlet { - private static final int MAX_AUTHORS_AT_ONCE = 20; - private static final int MAX_TAGS = 50; - - /** renders the posts from the last 3 days */ - private String getViewBlogLink(Hash blog, long lastPost) { - long dayBegin = BlogManager.instance().getDayBegin(); - int daysAgo = 2; - if ( (lastPost > 0) && (dayBegin - 3*24*60*60*1000l >= lastPost) ) // last post was old 3 days ago - daysAgo = (int)((dayBegin - lastPost + 24*60*60*1000l-1)/(24*60*60*1000l)); - daysAgo++; - return "blog.jsp?" + ViewBlogServlet.PARAM_BLOG + "=" + blog.toBase64(); - //return getControlTarget() + "?" + ThreadedHTMLRenderer.PARAM_AUTHOR + '=' + blog.toBase64() - // + '&' + ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR + "=true&daysBack=" + daysAgo; - } - - private String getPostDate(long when) { - String age = null; - long dayBegin = BlogManager.instance().getDayBegin(); - long postId = when; - if (postId >= dayBegin) { - age = "today"; - } else if (postId >= dayBegin - 24*60*60*1000) { - age = "yesterday"; - } else { - int daysAgo = (int)((dayBegin - postId + 24*60*60*1000-1)/(24*60*60*1000)); - age = daysAgo + " days ago"; - } - return age; - } - - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - TreeSet orderedRoots = new TreeSet(new NewestEntryFirstComparator()); - // The thread index is ordered by last updated date, as opposed to root posting date, - // so lets reorder things - int count = index.getRootCount(); - for (int i = 0; i < count; i++) { - ThreadNode node = index.getRoot(i); - orderedRoots.add(node.getEntry()); - } - - TreeSet tags = new TreeSet(); - List writtenAuthors = new ArrayList(); - - - out.write(""); - if ( (user != null) && (user.getAuthenticated()) ) { - out.write("Favorite blogs: view all
    \n"); - out.write("Your blog
    \n"); - - PetNameDB db = user.getPetNameDB(); - for (Iterator iter = orderedRoots.iterator(); iter.hasNext() && writtenAuthors.size() < MAX_AUTHORS_AT_ONCE; ) { - BlogURI uri= (BlogURI)iter.next(); - if (writtenAuthors.contains(uri.getKeyHash())) { - // skip - } else { - PetName pn = db.getByLocation(uri.getKeyHash().toBase64()); - if (pn != null) { - if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) { - out.write(""); - out.write(HTMLRenderer.sanitizeString(pn.getName(), 32)); - out.write(" (" + getPostDate(uri.getEntryId()) + ")
    \n"); - writtenAuthors.add(uri.getKeyHash()); - } else if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) { - // ignore 'em - writtenAuthors.add(uri.getKeyHash()); - } else { - // bookmarked, but not a favorite... leave them for later - } - } else { - // not bookmarked, leave them for later - } - } - } - } - out.write("
    \n"); - - // now for the non-bookmarked people - out.write(""); - out.write("Most recently updated blogs:
    \n"); - for (Iterator iter = orderedRoots.iterator(); iter.hasNext() && writtenAuthors.size() < MAX_AUTHORS_AT_ONCE; ) { - BlogURI uri= (BlogURI)iter.next(); - String curTags[] = archive.getEntry(uri).getTags(); - if (curTags != null) - for (int i = 0; i < curTags.length && tags.size() < MAX_TAGS; i++) - tags.add(curTags[i]); - if (writtenAuthors.contains(uri.getKeyHash())) { - // skip - } else { - BlogInfo info = archive.getBlogInfo(uri); - if (info == null) - continue; - String name = info.getProperty(BlogInfo.NAME); - if ( (name == null) || (name.trim().length() <= 0) ) - name = uri.getKeyHash().toBase64().substring(0,8); - String desc = info.getProperty(BlogInfo.DESCRIPTION); - if ( (desc == null) || (desc.trim().length() <= 0) ) - desc = name + "'s blog"; - String age = null; - long dayBegin = BlogManager.instance().getDayBegin(); - long postId = uri.getEntryId(); - if (postId >= dayBegin) { - age = "today"; - } else if (postId >= dayBegin - 24*60*60*1000) { - age = "yesterday"; - } else { - int daysAgo = (int)((dayBegin - postId + 24*60*60*1000-1)/(24*60*60*1000)); - age = daysAgo + " days ago"; - } - - out.write(""); - out.write(HTMLRenderer.sanitizeString(desc, 32)); - out.write(" (" + getPostDate(uri.getEntryId()) + ")
    \n"); - writtenAuthors.add(uri.getKeyHash()); - } - } - - out.write("
    \n"); - /* - out.write("Topics:\n"); - out.write(""); - for (Iterator iter = tags.iterator(); iter.hasNext(); ) { - String tag = (String)iter.next(); - out.write(""); - out.write(HTMLRenderer.sanitizeString(tag, 32)); - out.write(" "); - } - */ - out.write("\n"); - } - - protected String getTitle() { return "Syndie :: View blogs"; } -} diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java deleted file mode 100644 index 065bed377..000000000 --- a/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java +++ /dev/null @@ -1,478 +0,0 @@ -package net.i2p.syndie.web; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -import javax.servlet.http.HttpServletRequest; - -import net.i2p.I2PAppContext; -import net.i2p.client.naming.PetName; -import net.i2p.client.naming.PetNameDB; -import net.i2p.data.Base64; -import net.i2p.data.Hash; -import net.i2p.syndie.Archive; -import net.i2p.syndie.BlogManager; -import net.i2p.syndie.HeaderReceiver; -import net.i2p.syndie.User; -import net.i2p.syndie.data.ArchiveIndex; -import net.i2p.syndie.data.BlogInfo; -import net.i2p.syndie.data.BlogURI; -import net.i2p.syndie.data.EntryContainer; -import net.i2p.syndie.data.FilteredThreadIndex; -import net.i2p.syndie.data.ThreadIndex; -import net.i2p.syndie.data.ThreadNode; -import net.i2p.syndie.sml.HTMLRenderer; -import net.i2p.syndie.sml.SMLParser; -import net.i2p.syndie.sml.ThreadedHTMLRenderer; - -/** - * Render the appropriate posts and the thread tree - * - */ -public class ViewThreadedServlet extends BaseServlet { - protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, - int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException { - List posts = getPosts(user, archive, req, index); - renderBody(user, req, out, index, archive, posts); - - renderThreadNav(user, req, out, threadOffset, index); - renderThreadTree(user, req, out, threadOffset, visibleEntry, archive, index, posts); - renderThreadNav(user, req, out, threadOffset, index); - } - - private void renderBody(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, Archive archive, List posts) throws IOException { - ThreadedHTMLRenderer renderer = new ThreadedHTMLRenderer(I2PAppContext.getGlobalContext()); - - String uri = req.getRequestURI(); - String off = req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET); - String tags = req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS); - String author = req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR); - - boolean authorOnly = Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue(); - - for (int i = 0; i < posts.size(); i++) { - BlogURI post = (BlogURI)posts.get(i); - boolean inlineReply = (posts.size() == 1); - //if (true) - // inlineReply = true; - renderer.render(user, out, archive, post, inlineReply, index, uri, getAuthActionFields(), off, tags, author, authorOnly); - } - } - - private List getPosts(User user, Archive archive, HttpServletRequest req, ThreadIndex index) { - List rv = new ArrayList(1); - String author = req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR); - String tags = req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS); - String post = req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST); - String thread = req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD); - boolean threadAuthorOnly = Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR) + "").booleanValue(); - - long dayBegin = BlogManager.instance().getDayBegin(); - String daysStr = req.getParameter(ThreadedHTMLRenderer.PARAM_DAYS_BACK); - int days = 1; - try { - if (daysStr != null) - days = Integer.parseInt(daysStr); - } catch (NumberFormatException nfe) { - days = 1; - } - dayBegin -= (days-1) * 24*60*60*1000l; - - if ( (author != null) && empty(post) && empty(thread) ) { - ArchiveIndex aindex = archive.getIndex(); - PetNameDB db = user.getPetNameDB(); - if ("favorites".equals(author)) { - for (Iterator nameIter = db.getNames().iterator(); nameIter.hasNext(); ) { - PetName pn = db.getByName((String)nameIter.next()); - if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE) && AddressesServlet.PROTO_BLOG.equals(pn.getProtocol()) ) { - Hash loc = new Hash(); - byte key[] = Base64.decode(pn.getLocation()); - if ( (key != null) && (key.length == Hash.HASH_LENGTH) ) { - loc.setData(key); - aindex.selectMatchesOrderByEntryId(rv, loc, tags, dayBegin); - } - } - } - // always include ourselves... - aindex.selectMatchesOrderByEntryId(rv, user.getBlog(), tags, dayBegin); - - Collections.sort(rv, BlogURI.COMPARATOR); - } else { - Hash loc = new Hash(); - byte key[] = Base64.decode(author); - if ( (key != null) && (key.length == Hash.HASH_LENGTH) ) { - loc.setData(key); - aindex.selectMatchesOrderByEntryId(rv, loc, tags, dayBegin); - } else { - } - } - - // how inefficient can we get? - if (threadAuthorOnly && (rv.size() > 0)) { - // lets filter out any posts that are not roots - for (int i = 0; i < rv.size(); i++) { - BlogURI curURI = (BlogURI)rv.get(i); - ThreadNode node = index.getNode(curURI); - if ( (node != null) && (node.getParent() == null) ) { - // ok, its a root - } else { - rv.remove(i); - i--; - } - } - } - } - - BlogURI uri = getAsBlogURI(post); - if ( (uri != null) && (uri.getEntryId() > 0) ) { - rv.add(uri); - } else { - uri = getAsBlogURI(thread); - if ( (uri != null) && (uri.getEntryId() > 0) ) { - ThreadNode node = index.getNode(uri); - if (node != null) { - if (false) { - // entire thread, as a depth first search - while (node.getParent() != null) - node = node.getParent(); // hope the structure is loopless... - // depth first traversal - walkTree(rv, node); - } else { - // only the "current" unforked thread, as suggested by cervantes. - // e.g. - // a--b--c--d - // \-e--f--g - // \-h - // would show "a--e--f--g" if node == {e, f, or g}, - // or "a--b--c--d" if node == {a, b, c, or d}, - // or "a--e--f--h" if node == h - rv.add(node.getEntry()); - ThreadNode cur = node; - while (cur.getParent() != null) { - cur = cur.getParent(); - rv.add(0, cur.getEntry()); // parents go before children... - } - cur = node; - while ( (cur != null) && (cur.getChildCount() > 0) ) { - cur = cur.getChild(0); - rv.add(cur.getEntry()); // and children after parents - } - } - } else { - rv.add(uri); - } - } - } - - return rv; - } - - private void walkTree(List uris, ThreadNode node) { - if (node == null) - return; - if (uris.contains(node)) - return; - uris.add(node.getEntry()); - for (int i = 0; i < node.getChildCount(); i++) - walkTree(uris, node.getChild(i)); - } - private void renderThreadNav(User user, HttpServletRequest req, PrintWriter out, int threadOffset, ThreadIndex index) throws IOException { - out.write("\n"); - out.write("\n"); - if (threadOffset == 0) { - out.write("<< First Page "); - } else { - out.write("<< First Page "); - } - if (threadOffset > 0) { - out.write("< Prev Page\n"); - } else { - out.write("< Prev Page\n"); - } - out.write("\n"); - - out.write(""); - int max = index.getRootCount(); - if (threadOffset + 10 > max) { - out.write("Next Page> Last Page>>\n"); - } else { - out.write("Next Page> Last Page>>\n"); - } - out.write(""); - //out.write("\n"); - out.write("\n"); - } - - private void renderThreadTree(User user, HttpServletRequest req, PrintWriter out, int threadOffset, BlogURI visibleEntry, Archive archive, ThreadIndex index, List visibleURIs) throws IOException { - int numThreads = 10; - renderThreadTree(user, out, index, archive, req, threadOffset, numThreads, visibleEntry, visibleURIs); - } - - private void renderThreadTree(User user, PrintWriter out, ThreadIndex index, Archive archive, HttpServletRequest req, - int threadOffset, int numThreads, BlogURI visibleEntry, List visibleURIs) { - - if ( (visibleEntry != null) && (empty(req, ThreadedHTMLRenderer.PARAM_OFFSET)) ) { - // we want to jump to a specific thread in the nav - threadOffset = index.getRoot(visibleEntry); - } - - if (threadOffset < 0) - threadOffset = 0; - out.write("\n"); - if (threadOffset + numThreads > index.getRootCount()) - numThreads = index.getRootCount() - threadOffset; - TreeRenderState state = new TreeRenderState(new ArrayList()); - - int written = 0; - for (int curRoot = threadOffset; curRoot < numThreads + threadOffset; curRoot++) { - ThreadNode node = index.getRoot(curRoot); - out.write("\n"); - renderThread(user, out, index, archive, req, node, 0, visibleEntry, state, visibleURIs); - out.write("\n"); - written++; - } - - if (written <= 0) - out.write("No matching threads\n"); - - out.write("\n"); - } - - private boolean renderThread(User user, PrintWriter out, ThreadIndex index, Archive archive, HttpServletRequest req, - ThreadNode node, int depth, BlogURI visibleEntry, TreeRenderState state, List visibleURIs) { - boolean isFavorite = false; - boolean ignored = false; - boolean displayed = false; - - if ( (visibleURIs != null) && (visibleURIs.contains(node.getEntry())) ) - displayed = true; - - HTMLRenderer rend = new HTMLRenderer(I2PAppContext.getGlobalContext()); - SMLParser parser = new SMLParser(I2PAppContext.getGlobalContext()); - - PetName pn = user.getPetNameDB().getByLocation(node.getEntry().getKeyHash().toBase64()); - if (pn != null) { - if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) { - isFavorite = true; - } - if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) - ignored = true; - } - - state.incrementRowsWritten(); - if (state.getRowsWritten() % 2 == 0) - out.write("\n"); - else - out.write("\n"); - - out.write(""); - out.write(""); - //out.write(""); - out.write(getFlagHTML(user, node)); - //out.write("\n\n"); - for (int i = 0; i < depth; i++) - out.write("\"\""); - - boolean showChildren = false; - - int childCount = node.getChildCount(); - - if (childCount > 0) { - boolean allowCollapse = false; - - if (visibleEntry != null) { - if (node.getEntry().equals(visibleEntry)) { - // noop - } else if (node.containsEntry(visibleEntry)) { - showChildren = true; - allowCollapse = true; - } - } else { - // noop - } - - if (allowCollapse) { - out.write("\"collapse\"\n"); - } else { - out.write("\"expand\"\n"); - } - } else { - out.write("\"\"\n"); - } - - out.write(""); - - if (displayed) out.write(""); - - if (pn == null) { - BlogInfo info = archive.getBlogInfo(node.getEntry().getKeyHash()); - String name = null; - if (info != null) - name = info.getProperty(BlogInfo.NAME); - if ( (name == null) || (name.trim().length() <= 0) ) - name = node.getEntry().getKeyHash().toBase64().substring(0,6); - out.write(trim(name, 30)); - } else { - out.write(trim(pn.getName(), 30)); - } - - if (displayed) out.write(""); - - out.write("\n"); - - if ( (user.getBlog() != null) && (node.getEntry().getKeyHash().equals(user.getBlog())) ) { - out.write("\"You\n"); - } else if (isFavorite) { - out.write("\"favorites\"\n"); - } else if (ignored) { - out.write("\"ignored\"\n"); - } else { - if (user.getAuthenticated()) { - // give them a link to bookmark or ignore the peer - out.write("(\"friend\"\n"); - out.write("/\"ignore\")\n"); - } - } - - out.write(": "); - out.write(""); - EntryContainer entry = archive.getEntry(node.getEntry()); - if (entry == null) throw new RuntimeException("Unable to fetch the entry " + node.getEntry()); - - HeaderReceiver rec = new HeaderReceiver(); - parser.parse(entry.getEntry().getText(), rec); - String subject = rec.getHeader(HTMLRenderer.HEADER_SUBJECT); - if ( (subject == null) || (subject.trim().length() <= 0) ) - subject = "(no subject)"; - if (displayed) { - // currently being rendered - out.write(""); - out.write(trim(subject, 40)); - out.write(""); - } else { - out.write(trim(subject, 40)); - } - //out.write("\n\n"); - out.write(""); - if (false) { - out.write(" (full thread)\n"); - } - - out.write(""); - - out.write(" 0) { - cur = (ThreadNode)paths.remove(0); - if (cur.getEntry().equals(newestURI)) - break; - for (int i = cur.getChildCount() - 1; i >= 0; i--) - paths.add(cur.getChild(i)); - if (paths.size() <= 0) - cur = null; - } - if (cur != null) - out.write(getViewThreadLink(req, cur, user)); - } - out.write("\" title=\"View the most recent post\">latest - "); - - long dayBegin = BlogManager.instance().getDayBegin(); - long postId = node.getMostRecentPostDate(); - if (postId >= dayBegin) { - out.write("today"); - } else if (postId >= dayBegin - 24*60*60*1000) { - out.write("yesterday"); - } else { - int daysAgo = (int)((dayBegin - postId + 24*60*60*1000-1)/(24*60*60*1000)); - out.write(daysAgo + " days ago"); - } - - out.write("\n"); - /* - out.write(" full thread\n"); - */ - out.write(""); - out.write("\n"); - - boolean rendered = true; - - if (showChildren) { - for (int i = 0; i < node.getChildCount(); i++) { - ThreadNode child = node.getChild(i); - boolean childRendered = renderThread(user, out, index, archive, req, child, depth+1, visibleEntry, state, visibleURIs); - rendered = rendered || childRendered; - } - } - - return rendered; - } - - private String getFlagHTML(User user, ThreadNode node) { - if ( (user.getBlog() != null) && (node.containsAuthor(user.getBlog())) ) - return "\"You"; - - // grab all of the peers in the user's favorites group and check to see if - // they posted something in the given thread, flagging it if they have - boolean favoriteFound = false; - for (Iterator iter = user.getPetNameDB().getNames().iterator(); iter.hasNext(); ) { - PetName pn = user.getPetNameDB().getByName((String)iter.next()); - if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) { - Hash cur = new Hash(); - try { - cur.fromBase64(pn.getLocation()); - if (node.containsAuthor(cur)) { - favoriteFound = true; - break; - } - } catch (Exception e) {} - } - } - if (favoriteFound) - return "\"flagged"; - else - return " "; - } - - protected String getTitle() { return "Syndie :: View threads"; } -} diff --git a/apps/syndie/jetty-syndie.xml b/apps/syndie/jetty-syndie.xml deleted file mode 100644 index 4cf61e494..000000000 --- a/apps/syndie/jetty-syndie.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - 0.0.0.0 - 8001 - - - 3 - 10 - 30000 - 1000 - 8443 - 8443 - main - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - syndie - - / - syndie.war - - - - - - - - ./logs/yyyy_mm_dd.syndie-request.log - 90 - true - false - false - GMT - - - - - - - 2000 - false - - diff --git a/apps/syndie/jsp/_bodyindex.jsp b/apps/syndie/jsp/_bodyindex.jsp deleted file mode 100644 index d997bbd2c..000000000 --- a/apps/syndie/jsp/_bodyindex.jsp +++ /dev/null @@ -1,40 +0,0 @@ -<%@page contentType="text/html; charset=UTF-8" import="net.i2p.syndie.web.ArchiveViewerBean, net.i2p.syndie.*, net.i2p.client.naming.PetName" %> -<% request.setCharacterEncoding("UTF-8"); %> -<% -if (user.getAuthenticated() && (null != request.getParameter("action")) ) { - %><% - String blog = request.getParameter("blog"); - String group = null; - if (request.getParameter("action").equals("Bookmark blog")) - group = "Favorites"; - else if (request.getParameter("action").equals("Ignore blog")) - group = "Ignore"; - boolean unignore = ("Unignore blog".equals(request.getParameter("action"))); - - PetName pn = user.getPetNameDB().getByLocation(blog); - String name = null; - if (pn != null) name = pn.getName(); - if (name == null) - name = request.getParameter("name"); - if (name == null) - name = blog; - if ( (name != null) && (blog != null) && ( (group != null) || (unignore) ) ) { - if (pn != null) { - if (unignore) - pn.removeGroup("Ignore"); - else - pn.addGroup(group); - } else { - pn = new PetName(name, "syndie", "syndieblog", blog); - pn.addGroup(group); - user.getPetNameDB().add(pn); - } - BlogManager.instance().saveUser(user); - } -} -%> -
    -Blogs: <%ArchiveViewerBean.renderBlogSelector(user, request.getParameterMap(), out);%> - - -<%ArchiveViewerBean.renderBlogs(user, request.getParameterMap(), out, "
    ");%>
    \ No newline at end of file diff --git a/apps/syndie/jsp/_leftnav.jsp b/apps/syndie/jsp/_leftnav.jsp deleted file mode 100644 index 2b342a5f5..000000000 --- a/apps/syndie/jsp/_leftnav.jsp +++ /dev/null @@ -1,3 +0,0 @@ -<%@page import="net.i2p.syndie.web.ArchiveViewerBean, net.i2p.syndie.*, net.i2p.data.Base64" %> - - \ No newline at end of file diff --git a/apps/syndie/jsp/_rightnav.jsp b/apps/syndie/jsp/_rightnav.jsp deleted file mode 100644 index 70f597e4f..000000000 --- a/apps/syndie/jsp/_rightnav.jsp +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/syndie/jsp/_toplogo.jsp b/apps/syndie/jsp/_toplogo.jsp deleted file mode 100644 index 07d6f60ab..000000000 --- a/apps/syndie/jsp/_toplogo.jsp +++ /dev/null @@ -1,5 +0,0 @@ -<%@page import="net.i2p.syndie.BlogManager" %> - - \ No newline at end of file diff --git a/apps/syndie/jsp/_topnav.jsp b/apps/syndie/jsp/_topnav.jsp deleted file mode 100644 index 2f3536513..000000000 --- a/apps/syndie/jsp/_topnav.jsp +++ /dev/null @@ -1,47 +0,0 @@ -<%@page import="net.i2p.syndie.*, net.i2p.syndie.sml.*, net.i2p.syndie.web.*" %> - -
    "> - -Home -Syndie admin -Remote archives -RSS imports -Import -<% -if ("true".equals(request.getParameter("logout"))) { - user.invalidate(); - RemoteArchiveBean rem = (RemoteArchiveBean)session.getAttribute("remote"); - if (rem != null) rem.reinitialize(); - PostBean post = (PostBean)session.getAttribute("post"); - if (post != null) post.reinitialize(); -} -String login = request.getParameter("login"); -String pass = request.getParameter("password"); -String loginSubmit = request.getParameter("Login"); -if ( (login != null) && (pass != null) && (loginSubmit != null) && (loginSubmit.equals("Login")) ) { - String loginResult = BlogManager.instance().login(user, login, pass); - if (!user.getAuthenticated()) - out.write("" + loginResult + ""); -} -%> -<% if (user.getAuthenticated()) { %> -Logged in as: : -"><%=HTMLRenderer.sanitizeString(ArchiveViewerBean.getBlogName(user.getBlogStr()))%> -">Post -">Metadata -Addressbook -Logout -<%} else {%> -Login: -Pass: <% -java.util.Enumeration params = request.getParameterNames(); -while (params.hasMoreElements()) { - String p = (String)params.nextElement(); - String val = request.getParameter(p); - %><% -}%> - -Register -<% } %> - -
    \ No newline at end of file diff --git a/apps/syndie/jsp/about.html b/apps/syndie/jsp/about.html deleted file mode 100644 index 329b5a66f..000000000 --- a/apps/syndie/jsp/about.html +++ /dev/null @@ -1,31 +0,0 @@ -What is Syndie? - -

    Perhaps the best introduction to Syndie can be found in Syndie itself.

    - -

    Updates can be found by filtering for the syndie.intro tag (if you only want to -receive posts that jrandom - made with that tag, that can be achieved -as well).

    - -

    If you have any questions or problems with Syndie, just post them and -syndicate it up to syndiemedia.i2p (which should show up as the default archive -on new installs). You can also use the I2P -forums if you're having trouble getting Syndie to work, and people are -almost always around on the #i2p irc -channel.

    - -

    One FAQ which might keep people from getting more posts into their Syndie -node regards cookies. If you get "internal errors" when using the syndicate form, you probably have cookies disabled. -Syndie needs cookies to help maintain state, and while its good practice to -disable cookies in general, you should be able to tell your web browser to make -an exception and allow cookies to "localhost" (or wherever your Syndie instance -is). Further FAQs should be found -within syndie

    - diff --git a/apps/syndie/jsp/images/addToFavorites.png b/apps/syndie/jsp/images/addToFavorites.png deleted file mode 100644 index 95ded8d979a2c86a3a261636862795ab52fc2f3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 275 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyDx`7I;J! zGca%qgD@k*tT_@u!Ofm7jv*44dnf4fH3tZ^>_4tKE9Clj4i1rJD<4YCX*3oP;ar!n z)5-rsP;`KXfCW#UR@}nQXx%!c)r#4>L=AfVhj>4L=~oATm$vhQ}enqKkxb9bT5 zbG|MnzvWluYxx~FeR#6UlX4x&)eh@**5E$sodxM2j)8B2f|Zh SzK8&w&*16m=d#Wzp$PyOlxc_n diff --git a/apps/syndie/jsp/images/addToIgnored.png b/apps/syndie/jsp/images/addToIgnored.png deleted file mode 100644 index 5b87bf45bc46333e5fffc775cf475e17620a9834..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyDx`7I;J! zGca%qgD@k*tT_@u!Bw6vjv*44dnXujH9Ls7%%7)pL@!~!@|v}=QvDATb@sNn->^M6 z>E5YxhnPFgF8T4QO~A>H=l^D&&1X*EaDOFpM%PCETao;OOR1649?On-E>*D$@yfb0 z^UK{YdGBKK?)M(yaAh<+wCRQ|4}-G7c8{$pTXa^u&t1Rz?&Xpt*Q|Q?hiY#xGgEX3 zo$2%{i>1%#p7QgcuHqBbDtA*}l-b-{{_J+z?cc9E)qjis;5J%&P~6XEV++vL44$rj JF6*2UngBB|Yc>D? diff --git a/apps/syndie/jsp/images/collapse.png b/apps/syndie/jsp/images/collapse.png deleted file mode 100644 index 2ce31b36d88ff5b3a23caaeb92cd9b3a5557fcce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 917 zcmc(eziO0G5XH|LWaBO=f{h9RdmCXMKqI?m7j+3Sja^WfS}=uatrQDwNAe75MN3#!S-AdbK#>xBt(!xA^}2>&`pr(%-{} z$4kk%kwQwTWRjW8Wg$yhb>2m;m4sBTi!gyeP+L)M6!mK$C9l zfD?jI2y4I~O((SA!CH1zTZtyVB50nOvUmM{-ZV-z?4;oSdyan7?pIX&7u*4>{!`c;@beQ>bQ z>&ts9f-BEgPhLp#tNoei!}&EXr>E;j%jviIboctL&C8E(x!PSX_K&x}KRdZOx&39b Yb;jHERr9@1@Bfh0dbwF#+&g*o4`Ex`jQ{`u diff --git a/apps/syndie/jsp/images/default_blog_logo.png b/apps/syndie/jsp/images/default_blog_logo.png deleted file mode 100644 index 54ea2a91766e997c872302000f29c06e32bf64d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1580 zcmV+{2GjY8P)`gAnoa`DQ*7)zHgsRsa?Q1!`() zL^J_lI|J6M05(EedXm1DrNDff(1~qNGcy2eMH;!>^J02p>Ehetqd-SlM|zB{&5hv2M02K@dL{o5=t<*Oh0maSR z-rLvTlO9+$6^&dDgPqM|Jr?8R0Pe+xdQ>Q`xy)EH4B?wD=&@X-yWdM(dvS%S9wRP{ zq|d5v2gx5LmwQ>$-0TGg1*FF3=BiF!Hx7PTE{ci(+l2slm(I!5>GayM=>R%|T`(&( zN9ykOzRc(9xN2y0jG37L?7@4#bq2_zK7C{WrG!~+iEaP?0L=f)tN<&Mm7I2V0JgRO zY5^O-nE=nVk$UDc6D~hp8!!qI!j7VP-KU+poudrEni79zk(LypEst20M7IOGyx7gJVQG> z0_(kYzLYPjdJ>a&0ITZ)mjEXB>eRTnxw4O9A`Szyh9JBEFUfiUjgF93L;%O^`>f9EqlstSH8^jbgXa=q@c33KeFwZiLS;I{o&+h?3d#;L+{W;-<6>|tyC^&O+9WU8nr9lpi~mER$@}s0 zeR=N@iu$)XHMt#Vf9%|?t_Fl5U%w|aE4Mq7A0~ZJ4K2um6Zs@SLmWDzG%ha-a9=!Q ze1r1WHarksc-I)6%p9bUVH%R{iBKJdLZ}hCtw8v9c0OPw;cx?O&d8P&dR(?tR;(#( zdTK{!hX_=GHgZq)5h!i5F82215P{lMdo`VwNfP?#KhbpBn;}xz)Mp->1iscxRR9AOoIXR*Xu17V**2@{l19GWQt$UvSiNd8l0He5y%v8Js?SLsG;Az?pxdR7T@>{l(~XdIr^;BC0l2QxaQ+Bern*+n zYIY}OwG4YSx3yA0NqqwVuu}ysw<{EtsM=d8zP{2nRkG?>jyqp4rJ~+k5XDHnW!0N) z1)TzuWN|Hz#K`deSH$Q&LZCN~#&S#Vb(d$C_a9U}ys?LTa1HNOH1>XeD$IrPl*zh7 zuE)h|rw!BRH!jy|{n`D@kLhy#0eNFB0_hYDU2Eg!%UkTB-bArGugfpi$7|Ui_BX$+ zyF9RY7`+;h1eLkdXD&GU#3?`EI&--k$W=4wh*mf*hv}RnnrvyowA1PL+)bShjywJq e1UOsN|C!$!EfcpQY;CM81UsAYneQfiVSnbGnKS2{`M!4ttNoqHbRubIxtJaD+y5t9=lTBf>&^%1 z+~0$TM{~)gkWwm{WF~W2$Wm6Fbd$S0_9&x zPzYJK=IV`D34Zin! z2P>Q7;7)IQ5Zsgp4ZZwf4u_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv|4+?>q3(mRU+;u#niV?13PLnJPT_MO*mE@U`n|2{*n z`efWT^UZsud5=!BD0ue7{ST{N*33CEcVsOD#dogTQ5f)#;b>&Evf;+^YV+!|72Q#X z?yWy8G_UH_=PSiOJa!!64B#k_E-G0d5olW_-Vn)mZ@GT0`1bO(`3}olyqB;qzIo-- zrFU~KXMQl3m#^DaCiXq(4nSzdKfZ{_Lb(!5vKZ~ZyxX062AdKp>ie5dQ7pIE1^%oI((%<)=U`}Xwkxpth> zD;V@l{%1SKub2F`O`5UcxO)A+Uv7KPN}e@U_{3ZDYnr6sf4u_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv|4-0Xsq5+7Gwn+p`;^K@|xk+__kAi>%URLeMvfsuRB RTzQ}jgQu&X%Q~loCIIJ19ZvuN diff --git a/apps/syndie/jsp/images/self.png b/apps/syndie/jsp/images/self.png deleted file mode 100644 index f56c8a0df06c47f73c629521d8028be6ddac8970..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv|4+}tc2g5A?5gN)Jeba4!kxSagw{DBYi8#xXfaZpKU sH0;UON(wCq`)ik}#>d*)6TJ_c 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 P00000NkvXXu0mjf*sxRW diff --git a/apps/syndie/jsp/images/threadIndent.png b/apps/syndie/jsp/images/threadIndent.png deleted file mode 100644 index 3ea13d3a131c793fc3cabe91a9861d69767a2164..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv|4-0Xsq5+7Gwn+p`;^K@|xk+__kAi>%URLeMvfsuRB RTzQ}jgQu&X%Q~loCIIJ19ZvuN diff --git a/apps/syndie/jsp/import.jsp b/apps/syndie/jsp/import.jsp deleted file mode 100644 index ba482bd80..000000000 --- a/apps/syndie/jsp/import.jsp +++ /dev/null @@ -1,68 +0,0 @@ -<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="net.i2p.data.Base64, net.i2p.syndie.web.*, net.i2p.syndie.sml.*, net.i2p.syndie.data.*, net.i2p.syndie.*, org.mortbay.servlet.MultiPartRequest, java.util.*, java.io.*" %><% -request.setCharacterEncoding("UTF-8"); -%> - - -SyndieMedia import - - - - - - - - - -
    <% - -String contentType = request.getContentType(); -if ((contentType != null) && (contentType.indexOf("boundary=") != -1) ) { - MultiPartRequest req = new MultiPartRequest(request); - int metaId = 0; - while (true) { - InputStream meta = req.getInputStream("blogmeta" + metaId); - if ( (meta == null) || (meta.available() <= 0) ) - break; - if (!BlogManager.instance().importBlogMetadata(meta)) { - %>Metadata <%=metaId%> failed to be imported
    <% - break; - } - metaId++; - } - int entryId = 0; - while (true) { - InputStream entry = req.getInputStream("blogpost" + entryId); - if ( (entry == null) || (entry.available() <= 0) ) - break; - if (!BlogManager.instance().importBlogEntry(entry)) { - %>Entry <%=entryId%> failed to be imported
    <% - break; - } - entryId++; - } - - if ( (entryId > 0) || (metaId > 0) ) { - BlogManager.instance().getArchive().regenerateIndex(); - session.setAttribute("index", BlogManager.instance().getArchive().getIndex()); - } -%>Imported <%=entryId%> posts and <%=metaId%> blog metadata files. -<% -} else { %>
    -Blog metadata 0:
    -Blog metadata 1:
    -Post 0:
    -Post 1:
    -Post 2:
    -Post 3:
    -Post 4:
    -Post 5:
    -Post 6:
    -Post 7:
    -Post 8:
    -Post 9:
    -
    - -<% } %> -
    - diff --git a/apps/syndie/jsp/index.html b/apps/syndie/jsp/index.html deleted file mode 100644 index 7244e85bf..000000000 --- a/apps/syndie/jsp/index.html +++ /dev/null @@ -1,19 +0,0 @@ -Welcome to Syndie
    - -
    -Welcome to Syndie!
    - -

    Jump right in and read discussion threads or -blogs

    -

    Create a new post of your own

    -

    Learn more about Syndie

    -

    NOTE: This version of Syndie is being replaced by -the new Syndie! -The new Syndie is a standalone application under active development. -Please give the new Syndie a try, as it has lots more traffic -than this version. Don't expect anybody to see your posts here.

    -
    -
    - diff --git a/apps/syndie/jsp/register.jsp b/apps/syndie/jsp/register.jsp deleted file mode 100644 index 660735ed4..000000000 --- a/apps/syndie/jsp/register.jsp +++ /dev/null @@ -1,50 +0,0 @@ -<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="net.i2p.data.Base64, net.i2p.syndie.web.*, net.i2p.syndie.sml.*, net.i2p.syndie.*" %><% -request.setCharacterEncoding("UTF-8"); -%> - - -SyndieMedia - - - - - - - - - -
    <% -String regLogin = request.getParameter("login"); -boolean showForm = true; -if ( (regLogin != null) && ("Register".equals(request.getParameter("Register"))) ) { - String regUserPass = request.getParameter("password"); - String regPass = request.getParameter("registrationpassword"); - String blogName = request.getParameter("blogname"); - String desc = request.getParameter("description"); - String url = request.getParameter("contacturl"); - String regResult = BlogManager.instance().register(user, regLogin, regUserPass, regPass, blogName, desc, url); - if (User.LOGIN_OK.equals(regResult)) { - %>Registration successful. Continue... -<% showForm = false; - } else { - %><%=regResult%><% - } -} -if (showForm) {%> -

    To create a new blog (and Syndie user account), please fill out the following form. -You may need to enter a registration password given to you by this Syndie instance's -operator, or there may be no registration password in place (in which case you can -leave that field blank).

    -

    -Syndie login:
    -New password:
    -Registration password:
    -Blog name:
    -Brief description:
    -Contact URL: (e.g. mailto://user@mail.i2p, http://foo.i2p/, etc)
    - -

    -<% } %> -
    - \ No newline at end of file diff --git a/apps/syndie/jsp/smlref.jsp b/apps/syndie/jsp/smlref.jsp deleted file mode 100644 index ed2bd52ee..000000000 --- a/apps/syndie/jsp/smlref.jsp +++ /dev/null @@ -1,38 +0,0 @@ -<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.*" %><% -request.setCharacterEncoding("UTF-8"); -%> - - -SML Quick Reference - - - -

    SML Quick Reference:

    -
      -
    • newlines are newlines are newlines.
    • -
    • all < and > are replaced with their &symbol;
    • -
    • the [ and ] characters delimit tags, or must be quoted by doubling them up ([[ displays as [, ]] displays as ])
    • -
    • [b][/b] = <b>bold</b>
    • -
    • [i][/i] = <i>italics</i>
    • -
    • [u][/u] = <i>underline</i>
    • -
    • [pre]foo[/pre] = <pre>preformatted section</pre>
    • -
    • [cut]more inside[/cut] = <a href="#">more inside...</a>
    • -
    • [quote][/quote] = Quoted text
    • -
    • [img attachment="1"]alt[/img] = use attachment 1 as an image with 'alt' as the alt text.
    • -
    • [attachment id="0"]text[/attachment] = offer attachment 0 as a link in your post
    • -
    • [attachment thumbnail="0" id="1"]text[/attachment] = offer attachment 1 as a link around a thumbnail image using attachment 0
    • -
    • [link schema="eep" location="http://forum.i2p"]text[/link] = offer a link to an external resource (accessible with the given schema)
    • -
    • [blog name="name" bloghash="base64hash"]description[/blog] = link to all posts in the blog
    • -
    • [blog name="name" bloghash="base64hash" blogentry="1234"]description[/blog] = link to the specified post in the blog
    • -
    • [blog name="name" bloghash="base64hash" blogtag="tag"]description[/blog] = link to all posts in the blog with the specified tag
    • -
    • [blog name="name" blogtag="tag"]description[/blog] = link to all posts in all blogs with the specified tag
    • -
    • [archive name="name" description="they have good stuff" schema="eep" location="http://syndiemedia.i2p/archive/archive.txt"]foo![/archive] = offer an easy way to sync up with a new Syndie archive
    • -
    • [address name="www.i2p" location="Nf3ab-ZFkmI-LyMt7Gjg...vobM57UpqSAAAA" schema="i2p" proto="eep"]official website[/address] = share a pet name reference to the given eepsite (using fields from the addresses page)
    • -
    -SML headers are newline delimited key:value pairs. Example keys are: -
      -
    • bgcolor = background color of the post (e.g. bgcolor:#ffccaa or bgcolor=red)
    • -
    • bgimage = attachment number to place as the background image for the post (only shown if images are enabled) (e.g. bgimage=1)
    • -
    • textfont = font to put most text into
    • -
    - diff --git a/apps/syndie/jsp/style.jsp b/apps/syndie/jsp/style.jsp deleted file mode 100644 index c1b37f3ee..000000000 --- a/apps/syndie/jsp/style.jsp +++ /dev/null @@ -1,7 +0,0 @@ -<%@page contentType="text/css; charset=UTF-8" pageEncoding="UTF-8" import="net.i2p.util.FileUtil" %> -<% request.setCharacterEncoding("UTF-8"); %> -<%@include file="syndie.css" %> -<% -String content = FileUtil.readTextFile("./docs/syndie_standard.css", -1, true); -if (content != null) out.write(content); -%> \ No newline at end of file diff --git a/apps/syndie/jsp/syndie.css b/apps/syndie/jsp/syndie.css deleted file mode 100644 index 3d7ccef24..000000000 --- a/apps/syndie/jsp/syndie.css +++ /dev/null @@ -1,444 +0,0 @@ -.b_topnavUser { - text-align: right; - background-color: #CCCCDD; -} -.b_topnavHome { - background-color: #CCCCDD; - color: #000000; - width: 50px; - text-align: left; -} -.b_topnav { - background-color: #CCCCDD; -} -.b_content { -} -.s_summary_overall { -} -.s_detail_overall { -} -.s_detail_subject { - font-size: 0.8em; - text-align: left; - background-color: #BBBBFF; -} -.s_detail_quote { - margin-left: 1em; - border: 1px solid #DBDBDB; - background-color: #E0E0E0; -} -.s_detail_italic { - font-style: italic; -} -.s_detail_bold { - font-style: normal; - font-weight: bold; -} -.s_detail_underline { - font-style: normal; - text-decoration: underline; -} -.s_detail_meta { - font-size: 0.8em; - text-align: right; - background-color: #BBBBFF; -} - -.s_summary_subject { - font-size: 0.8em; - text-align: left; - background-color: #BBBBFF; -} -.s_summary_meta { - font-size: 0.8em; - text-align: right; - background-color: #BBBBFF; -} -.s_summary_quote { - margin-left: 1em; - border-width: 1px solid #DBDBDB; - background-color: #E0E0E0; -} -.s_summary_italic { - font-style: italic; -} -.s_summary_bold { - font-style: normal; - font-weight: bold; -} -.s_summary_underline { - font-style: normal; - text-decoration: underline; -} -.s_summary_summDetail { - font-size: 0.8em; -} -.s_detail_summDetail { -} -.s_detail_summDetailBlog { -} -.s_detail_summDetailBlogLink { -} -td.s_detail_summDetail { - background-color: #DDDDFF; -} -td.s_summary_summ { - font-size: 0.8em; - background-color: #DDDDFF; -} - - -body { - margin : 0px; - padding : 0px; - width: 99%; - font-family : Arial, sans-serif, Helvetica; - background-color : #FFF; - color : black; - font-size : 100%; - - /* we've avoided Tantek Hacks so far, - ** but we can't avoid using the non-w3c method of - ** box rendering. (and therefore one of mozilla's - ** proprietry -moz properties (which hopefully they'll - ** drop soon). - */ - -moz-box-sizing : border-box; - box-sizing : border-box; -} -a:link{color:#007} -a:visited{color:#606} -a:hover{color:#720} -a:active{color:#900} - -select { - min-width: 1.5em; -} -.overallTable { - border-spacing: 0px; - border-collapse: collapse; - float: left; -} -.topNav { - background-color: #BBB; -} -.topNav_user { - text-align: left; - float: left; - display: inline; -} -.topNav_admin { - text-align: right; - float: right; - margin: 0 5px 0 0; - display: inline; -} -.controlBar { - border-bottom: thick double #CCF; - border-left: medium solid #CCF; - border-right: medium solid #CCF; - background-color: #EEF; - color: inherit; - font-size: small; - clear: left; /* fixes a bug in Opera */ -} -.controlBarRight { - text-align: right; -} -.threadEven { - background-color: #FFF; - white-space: nowrap; -} -.threadOdd { - background-color: #FFC; - white-space: nowrap; -} -.threadLeft { - text-align: left; - align: left; -} -.threadNav { - background-color: #EEF; - border: medium solid #CCF; -} -.threadNavRight { - text-align: right; - float: right; - background-color: #EEF; -} -.rightOffset { - float: right; - margin: 0 5px 0 0; - display: inline; -} -.threadInfoLeft { - float: left; - margin: 5px 0px 0 0; - display: inline; -} -.threadInfoRight { - float: right; - margin: 0 5px 0 0; - display: inline; -} -.postMeta { - border-top: 1px solid black; - background-color: #FFB; -} -.postMetaSubject { - text-align: left; - font-size: large; -} -.postMetaLink { - text-align: right; -} -.postDetails { - background-color: #FFC; -} -.postReply { - background-color: #CCF; -} -.postReplyText { - background-color: #CCF; -} -.postReplyOptions { - background-color: #CCF; -} -.syndieBlogTopNav { - padding: 0.5em; - width: 98%; - border: medium solid #CCF; - background-color: #EEF; - font-size: small; -} -.syndieBlogTopNavUser { - text-align: left; -} -.syndieBlogTopNavAdmin { - text-align: right; -} -.syndieBlogHeader { - width: 100%; - font-size: 1.4em; - background-color: #000; - text-align: Left; - float: Left; -} -.syndieBlogHeader a { - color: #FFF; - padding: 4px; -} -.syndieBlogHeader a:hover { - color:#88F; - padding: 4px; -} -.syndieBlogLogo { - float: left; - display: inline; -} -.syndieBlogLinks { - width: 20%; - float: left; -} -.syndieBlogLinkGroup { - font-size: 0.8em; - background-color: #DDD; - border: 1px solid black; - margin: 5px; - padding: 2px; -} -.syndieBlogLinkGroup ul { - list-style: none; -} -.syndieBlogLinkGroup li { -} -.syndieBlogLinkGroupName { - font-weight: bold; - width: 100%; - border-bottom: 1px dashed black; - display: block; -} -.syndieBlogPostInfoGroup { - font-size: 0.8em; - background-color: #FFEA9F; - border: 1px solid black; - margin: 5px; - padding: 2px; -} -.syndieBlogPostInfoGroup ol { - list-style: none; -} -.syndieBlogPostInfoGroup li { -} -.syndieBlogPostInfoGroup li a { - display: block; -} -.syndieBlogPostInfoGroupName { - font-weight: bold; - width: 100%; - border-bottom: 1px dashed black; - display: block; -} -.syndieBlogMeta { - text-align: left; - font-size: 0.8em; - background-color: #DDD; - border: 1px solid black; - margin: 5px; - padding: 2px; -} -.syndieBlogBody { - width: 80%; - float: left; -} -.syndieBlogPost { - border: 1px solid black; - margin-top: 5px; - margin-right: 5px; -} -.syndieBlogPostHeader { - background-color: #FFB; - padding: 2px; -} -.syndieBlogPostSubject { - font-weight: bold; -} -.syndieBlogPostFrom { - text-align: right; -} -.syndieBlogPostSummary { - background-color: #FFF; - padding: 2px; -} -.syndieBlogPostDetails { - background-color: #FFC; - padding: 2px; -} -.syndieBlogNav { - text-align: center; -} -.syndieBlogComments { - border: none; - margin-top: 5px; - margin-left: 0px; - float: left; -} -.syndieBlogComments ul { - list-style: none; - margin-left: 10px; -} -.syndieBlogCommentInfoGroup { - font-size: 0.8em; - margin-right: 5px; -} -.syndieBlogCommentInfoGroup ol { - list-style: none; -} -.syndieBlogCommentInfoGroup li { -} -.syndieBlogCommentInfoGroup li a { - display: block; -} -.syndieBlogCommentInfoGroupName { - font-size: 0.8em; - font-weight: bold; -} - -.syndieBlogFavorites { - float: left; - margin: 5px 0px 0 0; - display: inline; -} -.syndieBlogList { - float: right; - margin: 5px 0px 0 0; - display: inline; -} -.b_topnavUser { - text-align: right; - background-color: #CCD; -} -.b_topnavHome { - background-color: #CCD; - color: #000; - width: 50px; - text-align: left; -} -.b_topnav { - background-color: #CCD; -} -.b_content { -} -.s_summary_overall { -} -.s_detail_overall { -} -.s_detail_subject { - font-size: 0.8em; - text-align: left; - background-color: #CCF; -} -.s_detail_quote { - margin-left: 1em; - border: 1px solid #DBDBDB; - background-color: #E0E0E0; -} -.s_detail_italic { - font-style: italic; -} -.s_detail_bold { - font-style: normal; - font-weight: bold; -} -.s_detail_underline { - font-style: normal; - text-decoration: underline; -} -.s_detail_meta { - font-size: 0.8em; - text-align: right; - background-color: #CCF; -} - -.s_summary_subject { - font-size: 0.8em; - text-align: left; - background-color: #CCF; -} -.s_summary_meta { - font-size: 0.8em; - text-align: right; - background-color: #CCF; -} -.s_summary_quote { - margin-left: 1em; - border-width: 1px solid #DBDBDB; - background-color: #E0E0E0; -} -.s_summary_italic { - font-style: italic; -} -.s_summary_bold { - font-style: normal; - font-weight: bold; -} -.s_summary_underline { - font-style: normal; - text-decoration: underline; -} -.s_summary_summDetail { - font-size: 0.8em; -} -.s_detail_summDetail { -} -.s_detail_summDetailBlog { -} -.s_detail_summDetailBlogLink { -} -td.s_detail_summDetail { - background-color: #CCF; -} -td.s_summary_summ { width: 80%; - font-size: 0.8em; - background-color: #CCF; -} \ No newline at end of file diff --git a/apps/syndie/jsp/syndie/index.jsp b/apps/syndie/jsp/syndie/index.jsp deleted file mode 100644 index 5517346b6..000000000 --- a/apps/syndie/jsp/syndie/index.jsp +++ /dev/null @@ -1 +0,0 @@ -<%response.sendRedirect("../index.jsp");%> \ No newline at end of file diff --git a/apps/syndie/jsp/viewattachment.jsp b/apps/syndie/jsp/viewattachment.jsp deleted file mode 100644 index a8e76171b..000000000 --- a/apps/syndie/jsp/viewattachment.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@page autoFlush="false" import="net.i2p.syndie.web.*" %><% - -request.setCharacterEncoding("UTF-8"); -java.util.Map params = request.getParameterMap(); -response.setContentType(ArchiveViewerBean.getAttachmentContentType(params)); -boolean inline = ArchiveViewerBean.getAttachmentShouldShowInline(params); -String filename = ArchiveViewerBean.getAttachmentFilename(params); -if (inline) - response.setHeader("Content-Disposition", "inline; filename=\"" + filename + "\""); -else - response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); -int len = ArchiveViewerBean.getAttachmentContentLength(params); -if (len >= 0) - response.setContentLength(len); -ArchiveViewerBean.renderAttachment(params, response.getOutputStream()); -%> \ No newline at end of file diff --git a/apps/syndie/jsp/viewmetadata.jsp b/apps/syndie/jsp/viewmetadata.jsp deleted file mode 100644 index 9547f40cc..000000000 --- a/apps/syndie/jsp/viewmetadata.jsp +++ /dev/null @@ -1,35 +0,0 @@ -<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="net.i2p.syndie.web.*, net.i2p.syndie.*" %><% -request.setCharacterEncoding("UTF-8"); -%> - - -SyndieMedia metadata - - - - - - - - - -
    <% -ArchiveViewerBean.renderMetadata(user, request.getRequestURI(), request.getParameterMap(), out); -if (user.getAuthenticated()) { - if ("Authorize".equals(request.getParameter("action"))) { - %><%=BlogManager.instance().authorizeRemoteAccess(user, request.getParameter("password"))%><% - } - if (!user.getAllowAccessRemote()) { - if (user.getBlog().toBase64().equals(request.getParameter("blog"))) { - %>
    -" /> -To access remote instances from this instance, please supply the Syndie administration password: - - -
    <% - } - } -} -%>
    - \ No newline at end of file diff --git a/apps/syndie/jsp/viewtempattachment.jsp b/apps/syndie/jsp/viewtempattachment.jsp deleted file mode 100644 index 0eae918f9..000000000 --- a/apps/syndie/jsp/viewtempattachment.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@page import="net.i2p.syndie.web.ArchiveViewerBean" %><% -request.setCharacterEncoding("UTF-8"); -java.util.Map params = request.getParameterMap(); -String id = request.getParameter(ArchiveViewerBean.PARAM_ATTACHMENT); -if (id != null) { - try { - int attachmentId = Integer.parseInt(id); - if ( (attachmentId < 0) || (attachmentId >= post.getAttachmentCount()) ) { - %>Attachment <%=attachmentId%> does not exist<% - } else { - response.setContentType(post.getContentType(attachmentId)); - boolean inline = ArchiveViewerBean.getAttachmentShouldShowInline(params); - String filename = ArchiveViewerBean.getAttachmentFilename(params); - if (inline) - response.setHeader("Content-Disposition", "inline; filename=" + filename); - else - response.setHeader("Content-Disposition", "attachment; filename=" + filename); - post.writeAttachmentData(attachmentId, response.getOutputStream()); - } - } catch (NumberFormatException nfe) {} -} -%> \ No newline at end of file diff --git a/apps/syndie/jsp/web.xml b/apps/syndie/jsp/web.xml deleted file mode 100644 index 3da3b8bd3..000000000 --- a/apps/syndie/jsp/web.xml +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - net.i2p.syndie.web.ArchiveServlet - net.i2p.syndie.web.ArchiveServlet - - - - net.i2p.syndie.web.RSSServlet - net.i2p.syndie.web.RSSServlet - - - - net.i2p.syndie.web.ViewThreadedServlet - net.i2p.syndie.web.ViewThreadedServlet - - - - net.i2p.syndie.web.ProfileServlet - net.i2p.syndie.web.ProfileServlet - - - - net.i2p.syndie.web.SwitchServlet - net.i2p.syndie.web.SwitchServlet - - - - net.i2p.syndie.web.AddressesServlet - net.i2p.syndie.web.AddressesServlet - - - - net.i2p.syndie.web.PostServlet - net.i2p.syndie.web.PostServlet - - - - net.i2p.syndie.web.AdminServlet - net.i2p.syndie.web.AdminServlet - - - - net.i2p.syndie.web.SyndicateServlet - net.i2p.syndie.web.SyndicateServlet - - - - net.i2p.syndie.web.ImportFeedServlet - net.i2p.syndie.web.ImportFeedServlet - - - - net.i2p.syndie.web.ExternalLinkServlet - net.i2p.syndie.web.ExternalLinkServlet - - - - net.i2p.syndie.web.ThreadNavServlet - net.i2p.syndie.web.ThreadNavServlet - - - - net.i2p.syndie.web.ViewBlogsServlet - net.i2p.syndie.web.ViewBlogsServlet - - - - net.i2p.syndie.web.BlogConfigServlet - net.i2p.syndie.web.BlogConfigServlet - - - - net.i2p.syndie.web.ViewBlogServlet - net.i2p.syndie.web.ViewBlogServlet - - - - net.i2p.syndie.UpdaterServlet - net.i2p.syndie.UpdaterServlet - 1 - - - - - - - net.i2p.syndie.web.ArchiveServlet - /archive/* - - - net.i2p.syndie.web.RSSServlet - /rss.jsp - - - net.i2p.syndie.web.ViewThreadedServlet - /threads.jsp - - - net.i2p.syndie.web.ProfileServlet - /profile.jsp - - - net.i2p.syndie.web.SwitchServlet - /switchuser.jsp - - - net.i2p.syndie.web.AddressesServlet - /addresses.jsp - - - net.i2p.syndie.web.PostServlet - /post.jsp - - - net.i2p.syndie.web.AdminServlet - /admin.jsp - - - net.i2p.syndie.web.SyndicateServlet - /syndicate.jsp - - - net.i2p.syndie.web.ImportFeedServlet - /importfeed.jsp - - - net.i2p.syndie.web.ExternalLinkServlet - /externallink.jsp - - - net.i2p.syndie.web.ThreadNavServlet - /threadnav/* - - - net.i2p.syndie.web.ViewBlogsServlet - /blogs.jsp - - - net.i2p.syndie.web.BlogConfigServlet - /configblog.jsp - - - net.i2p.syndie.web.ViewBlogServlet - /blog.jsp - - - - - 30 - - - - index.html - index.jsp - - From 2d86e7cf604b43120bd3d887312b54853149b232 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 20 Dec 2008 01:00:53 +0000 Subject: [PATCH 011/191] add router.memoryUsed stat --- .../java/src/net/i2p/router/web/StatSummarizer.java | 1 + .../java/src/net/i2p/router/web/SummaryListener.java | 2 +- router/java/src/net/i2p/router/Router.java | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java index 6fc982386..331f42d89 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java @@ -54,6 +54,7 @@ public class StatSummarizer implements Runnable { // ",udp.receivePacketSkew.60000" + // ",udp.sendConfirmTime.60000" + // ",udp.sendPacketSize.60000" + + ",router.memoryUsed.60000" + ",router.activePeers.60000"; // ",router.activeSendPeers.60000" + // ",tunnel.acceptLoad.60000" + diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryListener.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryListener.java index ef2274512..6ef9df9db 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryListener.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryListener.java @@ -184,7 +184,7 @@ class SummaryRenderer { def.setTimePeriod(start/1000, 0); String name = _listener.getRate().getRateStat().getName(); // heuristic to set K=1024 - if ((name.startsWith("bw.") || name.indexOf("Size") >= 0 || name.indexOf("Bps") >= 0) + if ((name.startsWith("bw.") || name.indexOf("Size") >= 0 || name.indexOf("Bps") >= 0 || name.indexOf("memory") >= 0) && !showEvents) def.setBaseValue(1024); String title = name; diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index 591414fd0..f7342413a 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -1215,6 +1215,8 @@ class CoalesceStatsEvent implements SimpleTimer.TimedEvent { ctx.statManager().createRateStat("router.activeSendPeers", "How many peers we've sent to this minute", "Throttle", new long[] { 60*1000, 5*60*1000, 60*60*1000 }); ctx.statManager().createRateStat("router.highCapacityPeers", "How many high capacity peers we know", "Throttle", new long[] { 5*60*1000, 60*60*1000 }); ctx.statManager().createRateStat("router.fastPeers", "How many fast peers we know", "Throttle", new long[] { 5*60*1000, 60*60*1000 }); + long max = Runtime.getRuntime().maxMemory() / (1024*1024); + ctx.statManager().createRateStat("router.memoryUsed", "(Bytes) Max is " + max + "MB", "Router", new long[] { 60*1000 }); } private RouterContext getContext() { return _ctx; } public void timeReached() { @@ -1233,6 +1235,9 @@ class CoalesceStatsEvent implements SimpleTimer.TimedEvent { getContext().statManager().addRateData("bw.sendRate", (long)getContext().bandwidthLimiter().getSendBps(), 0); getContext().statManager().addRateData("bw.recvRate", (long)getContext().bandwidthLimiter().getReceiveBps(), 0); + long used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + getContext().statManager().addRateData("router.memoryUsed", used, 0); + getContext().tunnelDispatcher().updateParticipatingStats(); getContext().statManager().coalesceStats(); From 4336dc441e483d36201fb06886a2d94685f68fd2 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 20 Dec 2008 01:04:19 +0000 Subject: [PATCH 012/191] Remove spurious UDP warning on startup --- .../java/src/net/i2p/router/web/SummaryHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java index 279650ebb..2b445a004 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java @@ -144,7 +144,7 @@ public class SummaryHelper { case CommSystemFacade.STATUS_UNKNOWN: // fallthrough default: ra = _context.router().getRouterInfo().getTargetAddress("UDP"); - if (ra == null) { + if (ra == null && _context.router().getUptime() > 5*60*1000) { if (_context.getProperty(ConfigNetHelper.PROP_I2NP_NTCP_HOSTNAME) == null || _context.getProperty(ConfigNetHelper.PROP_I2NP_NTCP_PORT) == null) return "ERR-UDP Disabled and Inbound TCP host/port not set"; From 33b43f40b99ea17efb5feeeb11a92aee045408ff Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 20 Dec 2008 02:33:57 +0000 Subject: [PATCH 013/191] try again to kill the i2psnarkurl files --- apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java | 1 + apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 7b674b0a0..f014580ec 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -211,6 +211,7 @@ public class I2PSnarkUtil { out.delete(); return null; } + out.deleteOnExit(); String fetchURL = url; if (rewrite) fetchURL = rewriteAnnounce(url); diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index 21e6bcac2..3ba2f6be6 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -346,7 +346,6 @@ public class TrackerClient extends I2PAppThread if (fetched == null) { throw new IOException("Error fetching " + s); } - fetched.deleteOnExit(); InputStream in = null; try { From d6148db4555113005eb5a2eb9263153bd9e70464 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 21 Dec 2008 14:43:09 +0000 Subject: [PATCH 014/191] * NetDb: - Expire routers with introducers after 90m. This should improve reachability to firewalled routers by keeping introducer info current. - Expire routers with no addresses after 90m. --- .../kademlia/KademliaNetworkDatabaseFacade.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java index 763005e8c..02198b0f3 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java @@ -17,6 +17,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; @@ -121,6 +122,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { * know anyone or just started up) */ private final static long ROUTER_INFO_EXPIRATION = 3*24*60*60*1000l; + private final static long ROUTER_INFO_EXPIRATION_SHORT = 90*60*1000l; private final static long EXPLORE_JOB_DELAY = 10*60*1000l; @@ -721,6 +723,16 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } else if ( (_context.router().getUptime() > 60*60*1000) && (routerInfo.getPublished() < now - 2*24*60*60*1000l) ) { long age = _context.clock().now() - routerInfo.getPublished(); return "Peer " + key.toBase64() + " published " + DataHelper.formatDuration(age) + " ago"; + } else if (!routerInfo.isCurrent(ROUTER_INFO_EXPIRATION_SHORT) && (_context.router().getUptime() > 60*60*1000) ) { + if (routerInfo.getAddresses().size() <= 0) + return "Peer " + key.toBase64() + " published > 90m ago with no addresses"; + RouterAddress ra = routerInfo.getTargetAddress("SSU"); + if (ra != null) { + // Introducers change often, introducee will ping introducer for 2 hours + Properties props = ra.getOptions(); + if (props != null && props.getProperty("ihost0") != null) + return "Peer " + key.toBase64() + " published > 90m ago with SSU Introducers"; + } } return null; } From ba8de6c565c78c6b4e19b9f2ed8b7fe19b5f74e9 Mon Sep 17 00:00:00 2001 From: sponge Date: Mon, 22 Dec 2008 23:04:41 +0000 Subject: [PATCH 015/191] Spelling error correction. --- apps/BOB/src/net/i2p/BOB/MUXlisten.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/BOB/src/net/i2p/BOB/MUXlisten.java b/apps/BOB/src/net/i2p/BOB/MUXlisten.java index 7694803d6..095481ac2 100644 --- a/apps/BOB/src/net/i2p/BOB/MUXlisten.java +++ b/apps/BOB/src/net/i2p/BOB/MUXlisten.java @@ -234,7 +234,7 @@ die: { System.out.println("BOB: MUXlisten: Please email the following dump to sponge@mail.i2p"); WrapperManager.requestThreadDump(); System.out.println("BOB: MUXlisten: Something fucked up REALLY bad!"); - System.out.println("BOB: MUXlisten: Please email the avove dump to sponge@mail.i2p"); + System.out.println("BOB: MUXlisten: Please email the above dump to sponge@mail.i2p"); } // zero out everything, just incase. try { From 841feaedff28ddceab58f823a65625cd56214a48 Mon Sep 17 00:00:00 2001 From: sponge Date: Tue, 30 Dec 2008 07:52:04 +0000 Subject: [PATCH 016/191] Bugfix for getting Properties to actually work. --- apps/BOB/bob.config | 14 + apps/BOB/nbproject/project.properties | 12 + apps/BOB/src/net/i2p/BOB/BOB.java | 4 +- apps/BOB/src/net/i2p/BOB/DoCMDS.java | 539 +++++++++++++----------- apps/BOB/src/net/i2p/BOB/Lifted.java | 56 +++ apps/BOB/src/net/i2p/BOB/MUXlisten.java | 5 +- 6 files changed, 393 insertions(+), 237 deletions(-) create mode 100644 apps/BOB/bob.config create mode 100644 apps/BOB/src/net/i2p/BOB/Lifted.java diff --git a/apps/BOB/bob.config b/apps/BOB/bob.config new file mode 100644 index 000000000..f9c28d382 --- /dev/null +++ b/apps/BOB/bob.config @@ -0,0 +1,14 @@ +#bob.config +#Tue Dec 30 00:00:00 UTC 2008 +# Please leave this file here for testing. +# Thank you, +# Sponge +i2cp.tcp.port=7654 +BOB.host=localhost +inbound.lengthVariance=0 +i2cp.messageReliability=BestEffort +BOB.port=45067 +outbound.length=1 +inbound.length=1 +outbound.lengthVariance=0 +i2cp.tcp.host=localhost diff --git a/apps/BOB/nbproject/project.properties b/apps/BOB/nbproject/project.properties index aa2df5ffd..76e318ff0 100644 --- a/apps/BOB/nbproject/project.properties +++ b/apps/BOB/nbproject/project.properties @@ -1,5 +1,11 @@ application.title=BOB application.vendor=root +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=false +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=8 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=8 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=8 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=80 +auxiliary.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project build.classes.dir=${build.dir}/classes build.classes.excludes=**/*.java,**/*.form # This directory is removed when the project is cleaned: @@ -76,6 +82,12 @@ javadoc.splitindex=true javadoc.use=true javadoc.version=false javadoc.windowtitle= +jnlp.codebase.type=local +jnlp.codebase.url=file:/root/NetBeansProjects/i2p.i2p/apps/BOB/dist/ +jnlp.descriptor=application +jnlp.enabled=false +jnlp.offline-allowed=false +jnlp.signed=false main.class=net.i2p.BOB.Main manifest.file=manifest.mf meta.inf.dir=${src.dir}/META-INF diff --git a/apps/BOB/src/net/i2p/BOB/BOB.java b/apps/BOB/src/net/i2p/BOB/BOB.java index c4a4f7539..0b7d17318 100644 --- a/apps/BOB/src/net/i2p/BOB/BOB.java +++ b/apps/BOB/src/net/i2p/BOB/BOB.java @@ -114,6 +114,8 @@ public class BOB { public final static String PROP_BOB_HOST = "BOB.host"; private static int maxConnections = 0; private static NamedDB database; + private static Properties props = new Properties(); + /** * Log a warning @@ -157,7 +159,6 @@ public class BOB { // Set up all defaults to be passed forward to other threads. // Re-reading the config file in each thread is pretty damn stupid. // I2PClient client = I2PClientFactory.createClient(); - Properties props = new Properties(); String configLocation = System.getProperty(PROP_CONFIG_LOCATION, "bob.config"); // This is here just to ensure there is no interference with our threadgroups. @@ -217,6 +218,7 @@ public class BOB { } try { + info(props.toString()); info("BOB is now running."); ServerSocket listener = new ServerSocket(Integer.parseInt(props.getProperty(PROP_BOB_PORT)), 10, InetAddress.getByName(props.getProperty(PROP_BOB_HOST))); Socket server; diff --git a/apps/BOB/src/net/i2p/BOB/DoCMDS.java b/apps/BOB/src/net/i2p/BOB/DoCMDS.java index bb29b1394..27442e35b 100644 --- a/apps/BOB/src/net/i2p/BOB/DoCMDS.java +++ b/apps/BOB/src/net/i2p/BOB/DoCMDS.java @@ -89,6 +89,7 @@ public class DoCMDS implements Runnable { private static final String C_setkeys = "setkeys"; private static final String C_setnick = "setnick"; private static final String C_show = "show"; + private static final String C_show_props = "showprops"; private static final String C_start = "start"; private static final String C_status = "status"; private static final String C_stop = "stop"; @@ -113,32 +114,34 @@ public class DoCMDS implements Runnable { {C_setkeys, C_setkeys + " BASE64_keypair * Sets the keypair for the current nickname."}, {C_setnick, C_setnick + " nickname * Create a new nickname."}, {C_show, C_show + " * Display the status of the current nickname."}, + {C_show_props, C_show_props + " * Display the properties of the current nickname."}, {C_start, C_start + " * Start the current nickname tunnel."}, {C_status, C_status + " nickname * Display status of a nicknamed tunnel."}, {C_stop, C_stop + " * Stops the current nicknamed tunnel."}, {C_verify, C_verify + " BASE64_key * Verifies BASE64 destination."}, {"", "COMMANDS: " + // this is ugly, but... - C_help + " " + - C_clear + " " + - C_getdest + " " + - C_getkeys + " " + - C_getnick + " " + - C_inhost + " " + - C_inport + " " + - C_list + " " + - C_newkeys + " " + - C_option + " " + - C_outhost + " " + - C_outport + " " + - C_quiet + " " + - C_quit + " " + - C_setkeys + " " + - C_setnick + " " + - C_show + " " + - C_start + " " + - C_status + " " + - C_stop + " " + - C_verify + C_help + " " + + C_clear + " " + + C_getdest + " " + + C_getkeys + " " + + C_getnick + " " + + C_inhost + " " + + C_inport + " " + + C_list + " " + + C_newkeys + " " + + C_option + " " + + C_outhost + " " + + C_outport + " " + + C_quiet + " " + + C_quit + " " + + C_setkeys + " " + + C_setnick + " " + + C_show + " " + + C_show_props + " " + + C_start + " " + + C_status + " " + + C_stop + " " + + C_verify }, {" ", " "} // end of list }; @@ -152,9 +155,10 @@ public class DoCMDS implements Runnable { */ DoCMDS(Socket server, Properties props, NamedDB database, Log _log) { this.server = server; - this.props = new Properties(props); + this.props = new Properties(); this.database = database; this._log = _log; + Lifted.copyProperties(props, this.props); } private void rlock() throws Exception { @@ -204,17 +208,17 @@ public class DoCMDS implements Runnable { private void trypnt(PrintStream out, NamedDB info, Object key) throws Exception { try { rlock(info); - } catch(Exception e) { + } catch (Exception e) { throw new Exception(e); } try { out.print(" " + key + ": "); - if(info.exists(key)) { + if (info.exists(key)) { out.print(info.get(key)); } else { out.print("not_set"); } - } catch(Exception e) { + } catch (Exception e) { runlock(info); throw new Exception(e); } @@ -232,13 +236,13 @@ public class DoCMDS implements Runnable { private void tfpnt(PrintStream out, NamedDB info, Object key) throws Exception { try { rlock(info); - } catch(Exception e) { + } catch (Exception e) { throw new Exception(e); } try { out.print(" " + key + ": "); out.print(info.exists(key)); - } catch(Exception e) { + } catch (Exception e) { runlock(info); throw new Exception(e); } @@ -264,7 +268,7 @@ public class DoCMDS implements Runnable { private void nickprint(PrintStream out, NamedDB info) throws Exception { try { rlock(info); - } catch(Exception e) { + } catch (Exception e) { throw new Exception(e); } try { @@ -280,7 +284,32 @@ public class DoCMDS implements Runnable { trypnt(out, info, P_OUTPORT); trypnt(out, info, P_OUTHOST); out.println(); - } catch(Exception e) { + } catch (Exception e) { + runlock(info); + throw new Exception(e); + } + + runlock(info); + } + + /** + * Dump properties information from the database + * + * @param out + * @param info + * @throws Exception + */ + private void propprint(PrintStream out, NamedDB info) throws Exception { + try { + rlock(info); + } catch (Exception e) { + throw new Exception(e); + } + try { + + trypnt(out, info, P_PROPERTIES); + out.println(); + } catch (Exception e) { runlock(info); throw new Exception(e); } @@ -297,16 +326,16 @@ public class DoCMDS implements Runnable { private void ttlpnt(PrintStream out, Object Arg) throws Exception { try { database.getReadLock(); - } catch(Exception e) { + } catch (Exception e) { throw new Exception(e); } try { - if(database.exists(Arg)) { + if (database.exists(Arg)) { out.print("DATA"); - nickprint(out, (NamedDB)database.get(Arg)); + nickprint(out, (NamedDB) database.get(Arg)); } - } catch(Exception e) { + } catch (Exception e) { database.releaseReadLock(); throw new Exception(e); } @@ -325,7 +354,7 @@ public class DoCMDS implements Runnable { boolean retval; try { rlock(Arg); - } catch(Exception e) { + } catch (Exception e) { throw new Exception(e); } @@ -333,7 +362,7 @@ public class DoCMDS implements Runnable { retval = (Arg.get(P_STARTING).equals(Boolean.TRUE) || Arg.get(P_STOPPING).equals(Boolean.TRUE) || Arg.get(P_RUNNING).equals(Boolean.TRUE)); - } catch(Exception e) { + } catch (Exception e) { runlock(); throw new Exception(e); } @@ -352,7 +381,7 @@ public class DoCMDS implements Runnable { try { Destination x = new Destination(data); return true; - } catch(Exception e) { + } catch (Exception e) { return false; } } @@ -369,50 +398,52 @@ public class DoCMDS implements Runnable { // Get input from the client BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream())); PrintStream out = new PrintStream(server.getOutputStream()); -quit: { -die: { + quit: + { + die: + { prikey = new ByteArrayOutputStream(); out.println("BOB " + BOBversion); out.println("OK"); - while((line = in.readLine()) != null) { + while ((line = in.readLine()) != null) { StringTokenizer token = new StringTokenizer(line, " "); // use a space as a delimiter String Command = ""; String Arg = ""; NamedDB info; - if(token.countTokens() != 0) { + if (token.countTokens() != 0) { Command = token.nextToken(); Command = Command.toLowerCase(); - if(token.countTokens() != 0) { + if (token.countTokens() != 0) { Arg = token.nextToken(); } else { Arg = ""; } // The rest of the tokens are considered junk, // and discarded without any warnings. - if(Command.equals(C_help)) { - for(int i = 0; !C_ALL[i][0].equals(" "); i++) { - if(C_ALL[i][0].equalsIgnoreCase(Arg)) { + if (Command.equals(C_help)) { + for (int i = 0; !C_ALL[i][0].equals(" "); i++) { + if (C_ALL[i][0].equalsIgnoreCase(Arg)) { out.println("OK " + C_ALL[i][1]); } } - } else if(Command.equals(C_getdest)) { - if(ns) { - if(dk) { + } else if (Command.equals(C_getdest)) { + if (ns) { + if (dk) { try { rlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { out.println("OK " + nickinfo.get(P_DEST)); - } catch(Exception e) { + } catch (Exception e) { try { runlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } break die; @@ -420,7 +451,7 @@ die: { try { runlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } @@ -432,31 +463,31 @@ die: { nns(out); } - } else if(Command.equals(C_list)) { + } else if (Command.equals(C_list)) { // Produce a formatted list of all nicknames database.getReadLock(); - for(int i = 0; i < + for (int i = 0; i < database.getcount(); i++) { try { - info = (NamedDB)database.getnext(i); + info = (NamedDB) database.getnext(i); out.print("DATA"); - } catch(Exception e) { + } catch (Exception e) { database.releaseReadLock(); break die; } try { info.getReadLock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { nickprint(out, info); - } catch(Exception e) { + } catch (Exception e) { try { info.releaseReadLock(); database.releaseReadLock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } break die; @@ -464,24 +495,24 @@ die: { try { info.releaseReadLock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } } try { database.releaseReadLock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } out.println("OK Listing done"); - } else if(Command.equals(C_quit)) { + } else if (Command.equals(C_quit)) { // End the command session break quit; - } else if(Command.equals(C_newkeys)) { - if(ns) { + } else if (Command.equals(C_newkeys)) { + if (ns) { try { - if(tunnelactive(nickinfo)) { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { try { @@ -490,17 +521,17 @@ die: { d = I2PClientFactory.createClient().createDestination(prikey); try { wlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } try { nickinfo.add(P_KEYS, prikey.toByteArray()); nickinfo.add(P_DEST, d.toBase64()); - } catch(Exception e) { + } catch (Exception e) { try { wunlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } break die; @@ -509,68 +540,68 @@ die: { dk = true; try { wunlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { rlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { out.println("OK " + nickinfo.get(P_DEST)); - } catch(Exception e) { + } catch (Exception e) { runlock(); break die; } try { runlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } - } catch(I2PException ipe) { + } catch (I2PException ipe) { BOB.error("Error generating keys" + ipe); out.println("ERROR generating keys"); } } - } catch(Exception e) { + } catch (Exception e) { break die; } } else { try { nns(out); - } catch(Exception ex) { + } catch (Exception ex) { break die; } } - } else if(Command.equals(C_getkeys)) { + } else if (Command.equals(C_getkeys)) { // Return public key - if(dk) { + if (dk) { prikey = new ByteArrayOutputStream(); try { rlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } try { - prikey.write(((byte[])nickinfo.get(P_KEYS))); - } catch(Exception ex) { + prikey.write(((byte[]) nickinfo.get(P_KEYS))); + } catch (Exception ex) { try { runlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } try { runlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } @@ -579,23 +610,23 @@ die: { out.println("ERROR no public key has been set"); } - } else if(Command.equals(C_quiet)) { - if(ns) { + } else if (Command.equals(C_quiet)) { + if (ns) { try { - if(tunnelactive(nickinfo)) { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { try { wlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { nickinfo.add(P_QUIET, new Boolean(Boolean.parseBoolean(Arg) == true)); - } catch(Exception ex) { + } catch (Exception ex) { try { wunlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; @@ -603,59 +634,59 @@ die: { try { wunlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } out.println("OK Quiet set"); } - } catch(Exception ex) { + } catch (Exception ex) { break die; } } else { try { nns(out); - } catch(Exception ex) { + } catch (Exception ex) { break die; } } - }else if(Command.equals(C_verify)) { - if(is64ok(Arg)) { + } else if (Command.equals(C_verify)) { + if (is64ok(Arg)) { out.println("OK"); } else { out.println("ERROR not in BASE64 format"); } - } else if(Command.equals(C_setkeys)) { + } else if (Command.equals(C_setkeys)) { // Set the NamedDB to a privatekey in BASE64 format - if(ns) { + if (ns) { try { - if(tunnelactive(nickinfo)) { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { try { prikey = new ByteArrayOutputStream(); prikey.write(net.i2p.data.Base64.decode(Arg)); d.fromBase64(Arg); - } catch(Exception ex) { + } catch (Exception ex) { Arg = ""; } - if((Arg.length() == 884) && is64ok(Arg)) { + if ((Arg.length() == 884) && is64ok(Arg)) { try { wlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { nickinfo.add(P_KEYS, prikey.toByteArray()); nickinfo.add(P_DEST, d.toBase64()); - } catch(Exception ex) { + } catch (Exception ex) { try { wunlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; @@ -663,22 +694,22 @@ die: { dk = true; try { wunlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { rlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { out.println("OK " + nickinfo.get(P_DEST)); - } catch(Exception e) { + } catch (Exception e) { try { runlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } break die; @@ -686,7 +717,7 @@ die: { try { runlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } } else { @@ -694,34 +725,34 @@ die: { } } - } catch(Exception ex) { + } catch (Exception ex) { break die; } } else { try { nns(out); - } catch(Exception ex) { + } catch (Exception ex) { break die; } } - } else if(Command.equals(C_setnick)) { + } else if (Command.equals(C_setnick)) { ns = dk = ip = op = false; try { database.getReadLock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { - nickinfo = (NamedDB)database.get(Arg); - if(!tunnelactive(nickinfo)) { + nickinfo = (NamedDB) database.get(Arg); + if (!tunnelactive(nickinfo)) { nickinfo = null; ns = true; } - } catch(Exception b) { + } catch (Exception b) { nickinfo = null; ns = true; @@ -729,15 +760,15 @@ die: { try { database.releaseReadLock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } // Clears and Sets the initial NamedDB structure to work with - if(ns) { + if (ns) { nickinfo = new NamedDB(); try { wlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } @@ -750,22 +781,23 @@ die: { nickinfo.add(P_QUIET, Boolean.FALSE); nickinfo.add(P_INHOST, "localhost"); nickinfo.add(P_OUTHOST, "localhost"); - Properties Q = new Properties(props); + Properties Q = new Properties(); + Lifted.copyProperties(this.props, Q); Q.setProperty("inbound.nickname", Arg); Q.setProperty("outbound.nickname", Arg); nickinfo.add(P_PROPERTIES, Q); - } catch(Exception e) { + } catch (Exception e) { try { wunlock(); break die; - } catch(Exception ee) { + } catch (Exception ee) { break die; } } try { wunlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } @@ -774,51 +806,51 @@ die: { out.println("ERROR tunnel is active"); } - } else if(Command.equals(C_option)) { - if(ns) { + } else if (Command.equals(C_option)) { + if (ns) { try { - if(tunnelactive(nickinfo)) { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { StringTokenizer otoken = new StringTokenizer(Arg, "="); // use an equal sign as a delimiter - if(otoken.countTokens() != 2) { + if (otoken.countTokens() != 2) { out.println("ERROR to many or no options."); } else { String pname = otoken.nextToken(); String pval = otoken.nextToken(); try { rlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } - Properties Q = (Properties)nickinfo.get(P_PROPERTIES); + Properties Q = (Properties) nickinfo.get(P_PROPERTIES); try { runlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } Q.setProperty(pname, pval); try { wlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { nickinfo.add(P_PROPERTIES, Q); - } catch(Exception ex) { + } catch (Exception ex) { try { wunlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } try { wunlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } @@ -826,7 +858,7 @@ die: { } } - } catch(Exception ex) { + } catch (Exception ex) { break die; } @@ -834,23 +866,23 @@ die: { nns(out); } - } else if(Command.equals(C_getnick)) { + } else if (Command.equals(C_getnick)) { // Get the NamedDB to work with... try { database.getReadLock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { - nickinfo = (NamedDB)database.get(Arg); + nickinfo = (NamedDB) database.get(Arg); ns = true; - } catch(RuntimeException b) { + } catch (RuntimeException b) { try { nns(out); - } catch(Exception ex) { + } catch (Exception ex) { try { database.releaseReadLock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; @@ -858,54 +890,54 @@ die: { } database.releaseReadLock(); - if(ns) { + if (ns) { try { rlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } try { dk = nickinfo.exists(P_KEYS); ip = nickinfo.exists(P_INPORT); op = nickinfo.exists(P_OUTPORT); - } catch(Exception ex) { + } catch (Exception ex) { try { runlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } try { runlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } // Finally say OK. out.println("OK Nickname set to " + Arg); } - } else if(Command.equals(C_inport)) { + } else if (Command.equals(C_inport)) { // Set the NamedDB inbound TO the router port // app --> BOB - if(ns) { + if (ns) { try { - if(tunnelactive(nickinfo)) { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { int prt; try { wlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { nickinfo.kill(P_INPORT); - } catch(Exception ex) { + } catch (Exception ex) { try { wunlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } @@ -913,13 +945,13 @@ die: { } try { prt = Integer.parseInt(Arg); - if(prt > 1 && prt < 65536) { + if (prt > 1 && prt < 65536) { try { nickinfo.add(P_INPORT, new Integer(prt)); - } catch(Exception ex) { + } catch (Exception ex) { try { wunlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } @@ -927,45 +959,45 @@ die: { } } - } catch(NumberFormatException nfe) { + } catch (NumberFormatException nfe) { out.println("ERROR not a number"); } try { wunlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { rlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { ip = nickinfo.exists(P_INPORT); - } catch(Exception ex) { + } catch (Exception ex) { try { runlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } try { runlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } - if(ip) { + if (ip) { out.println("OK inbound port set"); } else { out.println("ERROR port out of range"); } } - } catch(Exception ex) { + } catch (Exception ex) { break die; } @@ -973,196 +1005,196 @@ die: { nns(out); } - } else if(Command.equals(C_outport)) { + } else if (Command.equals(C_outport)) { // Set the NamedDB outbound FROM the router port // BOB --> app - if(ns) { + if (ns) { try { - if(tunnelactive(nickinfo)) { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { int prt; try { wlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { nickinfo.kill(P_OUTPORT); - } catch(Exception ex) { + } catch (Exception ex) { try { wunlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } try { prt = Integer.parseInt(Arg); - if(prt > 1 && prt < 65536) { + if (prt > 1 && prt < 65536) { try { nickinfo.add(P_OUTPORT, new Integer(prt)); - } catch(Exception ex) { + } catch (Exception ex) { try { wunlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } } - } catch(NumberFormatException nfe) { + } catch (NumberFormatException nfe) { out.println("ERROR not a number"); } try { wunlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { rlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { ip = nickinfo.exists(P_OUTPORT); - } catch(Exception ex) { + } catch (Exception ex) { try { runlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } try { runlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } - if(ip) { + if (ip) { out.println("OK outbound port set"); } else { out.println("ERROR port out of range"); } } - } catch(Exception ex) { + } catch (Exception ex) { break die; } } else { try { nns(out); - } catch(Exception ex) { + } catch (Exception ex) { break die; } } - } else if(Command.equals(C_inhost)) { - if(ns) { + } else if (Command.equals(C_inhost)) { + if (ns) { try { - if(tunnelactive(nickinfo)) { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { try { wlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { nickinfo.add(P_INHOST, Arg); - } catch(Exception ex) { + } catch (Exception ex) { try { wunlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } try { wunlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } out.println("OK inhost set"); } - } catch(Exception ex) { + } catch (Exception ex) { break die; } } else { try { nns(out); - } catch(Exception ex) { + } catch (Exception ex) { break die; } } - } else if(Command.equals(C_outhost)) { - if(ns) { + } else if (Command.equals(C_outhost)) { + if (ns) { try { - if(tunnelactive(nickinfo)) { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { try { wlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } try { nickinfo.add(P_OUTHOST, Arg); - } catch(Exception ex) { + } catch (Exception ex) { try { wunlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } try { wunlock(); - } catch(Exception ex) { + } catch (Exception ex) { break die; } out.println("OK outhost set"); } - } catch(Exception ex) { + } catch (Exception ex) { break die; } } else { try { nns(out); - } catch(Exception ex) { + } catch (Exception ex) { break die; } } - } else if(Command.equals(C_show)) { + } else if (Command.equals(C_show)) { // Get the current NamedDB properties - if(ns) { + if (ns) { out.print("OK"); try { rlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } try { nickprint(out, nickinfo); - } catch(Exception e) { + } catch (Exception e) { try { runlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } @@ -1172,23 +1204,60 @@ die: { try { runlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } } else { try { nns(out); - } catch(Exception e) { + } catch (Exception e) { break die; } } - } else if(Command.equals(C_start)) { - // Start the tunnel, if we have all the information - if(ns && dk && (ip || op)) { + } else if (Command.equals(C_show_props)) { + // Get the current options properties + if (ns) { + out.print("OK"); try { - if(tunnelactive(nickinfo)) { + rlock(); + } catch (Exception e) { + break die; + } + + try { + propprint(out, nickinfo); + } catch (Exception e) { + try { + runlock(); + } catch (Exception ee) { + break die; + } + + out.println(); // this will cause an IOE if IOE + break die; + } + + try { + runlock(); + } catch (Exception e) { + break die; + } + + } else { + try { + nns(out); + } catch (Exception e) { + break die; + } + } + + } else if (Command.equals(C_start)) { + // Start the tunnel, if we have all the information + if (ns && dk && (ip || op)) { + try { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { MUXlisten tunnel; @@ -1197,14 +1266,14 @@ die: { Thread t = new Thread(tunnel); t.start(); out.println("OK tunnel starting"); - } catch(I2PException e) { + } catch (I2PException e) { out.println("ERROR starting tunnel: " + e); - } catch(IOException e) { + } catch (IOException e) { out.println("ERROR starting tunnel: " + e); } } - } catch(Exception ex) { + } catch (Exception ex) { break die; } @@ -1212,26 +1281,26 @@ die: { out.println("ERROR tunnel settings incomplete"); } - } else if(Command.equals(C_stop)) { + } else if (Command.equals(C_stop)) { // Stop the tunnel, if it is running - if(ns) { + if (ns) { try { rlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } try { - if(nickinfo.get(P_RUNNING).equals(Boolean.TRUE) && nickinfo.get(P_STOPPING).equals(Boolean.FALSE) && nickinfo.get(P_STARTING).equals(Boolean.FALSE)) { + if (nickinfo.get(P_RUNNING).equals(Boolean.TRUE) && nickinfo.get(P_STOPPING).equals(Boolean.FALSE) && nickinfo.get(P_STARTING).equals(Boolean.FALSE)) { try { runlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } try { wlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } @@ -1239,7 +1308,7 @@ die: { try { wunlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } @@ -1247,16 +1316,16 @@ die: { } else { try { runlock(); - } catch(Exception e) { + } catch (Exception e) { break die; } out.println("ERROR tunnel is inactive"); } - } catch(Exception e) { + } catch (Exception e) { try { runlock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; @@ -1265,62 +1334,62 @@ die: { } else { try { nns(out); - } catch(Exception e) { + } catch (Exception e) { break die; } } - } else if(Command.equals(C_clear)) { + } else if (Command.equals(C_clear)) { // Clear use of the NamedDB if stopped - if(ns) { + if (ns) { try { - if(tunnelactive(nickinfo)) { + if (tunnelactive(nickinfo)) { out.println("ERROR tunnel is active"); } else { try { database.getWriteLock(); - } catch(Exception e) { + } catch (Exception e) { break die; } try { database.kill(nickinfo.get(P_NICKNAME)); - } catch(Exception e) { + } catch (Exception e) { try { database.releaseWriteLock(); - } catch(Exception ee) { + } catch (Exception ee) { break die; } break die; } try { database.releaseWriteLock(); - } catch(Exception e) { + } catch (Exception e) { break die; } dk = ns = ip = op = false; out.println("OK cleared"); } - } catch(Exception ex) { + } catch (Exception ex) { break die; } } else { try { nns(out); - } catch(Exception e) { + } catch (Exception e) { break die; } } - } else if(Command.equals(C_status)) { + } else if (Command.equals(C_status)) { try { - if(database.exists(Arg)) { + if (database.exists(Arg)) { // Show status of a NamedDB out.print("OK "); try { ttlpnt(out, Arg); - } catch(Exception e) { + } catch (Exception e) { out.println(); // this will cause an IOE if IOE break die; } @@ -1328,11 +1397,11 @@ die: { } else { try { nns(out); - } catch(Exception e) { + } catch (Exception e) { break die; } } - } catch(Exception e) { + } catch (Exception e) { break die; } @@ -1350,7 +1419,7 @@ die: { out.println("OK Bye!"); server.close(); - } catch(IOException ioe) { + } catch (IOException ioe) { BOB.warn("IOException on socket listen: " + ioe); ioe.printStackTrace(); } diff --git a/apps/BOB/src/net/i2p/BOB/Lifted.java b/apps/BOB/src/net/i2p/BOB/Lifted.java new file mode 100644 index 000000000..fbd23cba5 --- /dev/null +++ b/apps/BOB/src/net/i2p/BOB/Lifted.java @@ -0,0 +1,56 @@ +/** + * DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + * Version 2, December 2004 + * + * Copyright (C) sponge + * Planet Earth + * Everyone is permitted to copy and distribute verbatim or modified + * copies of this license document, and changing it is allowed as long + * as the name is changed. + * + * DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + * TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + * + * 0. You just DO WHAT THE FUCK YOU WANT TO. + * + * See... + * + * http://sam.zoy.org/wtfpl/ + * and + * http://en.wikipedia.org/wiki/WTFPL + * + * ...for any additional details and liscense questions. + */ +package net.i2p.BOB; + +import java.util.Enumeration; +import java.util.Properties; + +/** + * Sets of "friendly" utilities to make life easier. + * Any "Lifted" code will apear here, and credits given. + * It's better to "Lift" a small chunk of "free" code than add in piles of + * code we don't need, and don't want. + * + * @author sponge + */ +public class Lifted { + + /** + * Copy a set of properties from one Property to another. + * Lifted from Apache Derby code svn repository. + * Liscenced as follows: + * http://svn.apache.org/repos/asf/db/derby/code/trunk/LICENSE + * + * @param src_prop Source set of properties to copy from. + * @param dest_prop Dest Properties to copy into. + * + **/ + public static void copyProperties(Properties src_prop, Properties dest_prop) { + for (Enumeration propertyNames = src_prop.propertyNames(); + propertyNames.hasMoreElements();) { + Object key = propertyNames.nextElement(); + dest_prop.put(key, src_prop.get(key)); + } + } +} diff --git a/apps/BOB/src/net/i2p/BOB/MUXlisten.java b/apps/BOB/src/net/i2p/BOB/MUXlisten.java index 095481ac2..bd52e27fd 100644 --- a/apps/BOB/src/net/i2p/BOB/MUXlisten.java +++ b/apps/BOB/src/net/i2p/BOB/MUXlisten.java @@ -74,7 +74,10 @@ public class MUXlisten implements Runnable { this.info.getReadLock(); N = this.info.get("NICKNAME").toString(); prikey = new ByteArrayInputStream((byte[])info.get("KEYS")); - Properties Q = (Properties)info.get("PROPERTIES"); + // Make a new copy so that anything else won't muck with our database. + Properties R = (Properties)info.get("PROPERTIES"); + Properties Q = new Properties(); + Lifted.copyProperties(R, Q); this.database.releaseReadLock(); this.info.releaseReadLock(); From 161379f00483b94fe4596c0b5b6412b5591e7bea Mon Sep 17 00:00:00 2001 From: sponge Date: Tue, 30 Dec 2008 10:29:39 +0000 Subject: [PATCH 017/191] Removed debug line. --- apps/BOB/src/net/i2p/BOB/BOB.java | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/BOB/src/net/i2p/BOB/BOB.java b/apps/BOB/src/net/i2p/BOB/BOB.java index 0b7d17318..2c42a9834 100644 --- a/apps/BOB/src/net/i2p/BOB/BOB.java +++ b/apps/BOB/src/net/i2p/BOB/BOB.java @@ -218,7 +218,6 @@ public class BOB { } try { - info(props.toString()); info("BOB is now running."); ServerSocket listener = new ServerSocket(Integer.parseInt(props.getProperty(PROP_BOB_PORT)), 10, InetAddress.getByName(props.getProperty(PROP_BOB_HOST))); Socket server; From ba9108f937906d4afd862048bdfb790d1bfeccb8 Mon Sep 17 00:00:00 2001 From: sponge Date: Tue, 30 Dec 2008 13:20:54 +0000 Subject: [PATCH 018/191] bump revision to 0.0.3 --- apps/BOB/src/net/i2p/BOB/DoCMDS.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/BOB/src/net/i2p/BOB/DoCMDS.java b/apps/BOB/src/net/i2p/BOB/DoCMDS.java index 27442e35b..6033e303b 100644 --- a/apps/BOB/src/net/i2p/BOB/DoCMDS.java +++ b/apps/BOB/src/net/i2p/BOB/DoCMDS.java @@ -46,7 +46,7 @@ public class DoCMDS implements Runnable { // FIX ME // I need a better way to do versioning, but this will do for now. - public static final String BMAJ = "00", BMIN = "00", BREV = "02", BEXT = ""; + public static final String BMAJ = "00", BMIN = "00", BREV = "03", BEXT = ""; public static final String BOBversion = BMAJ + "." + BMIN + "." + BREV + BEXT; private Socket server; private Properties props; From b4d39860064d7c093d39edc5a3826fafbae0c87a Mon Sep 17 00:00:00 2001 From: sponge Date: Wed, 31 Dec 2008 07:53:30 +0000 Subject: [PATCH 019/191] router and core version bump --- core/java/src/net/i2p/CoreVersion.java | 2 +- router/java/src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/java/src/net/i2p/CoreVersion.java b/core/java/src/net/i2p/CoreVersion.java index d46c8a7fb..ac868bed5 100644 --- a/core/java/src/net/i2p/CoreVersion.java +++ b/core/java/src/net/i2p/CoreVersion.java @@ -15,7 +15,7 @@ package net.i2p; */ public class CoreVersion { public final static String ID = "$Revision: 1.72 $ $Date: 2008-08-24 12:00:00 $"; - public final static String VERSION = "0.6.5"; + public final static String VERSION = "0.6.6"; public static void main(String args[]) { System.out.println("I2P Core version: " + VERSION); diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 9ec540056..d415c7884 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = "0.6.5"; - public final static long BUILD = 5; + public final static long BUILD = 6; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From c0b616e51980053a14a684311627b0dc8df01a48 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 1 Jan 2009 12:45:33 +0000 Subject: [PATCH 020/191] revert core version, -7 --- core/java/src/net/i2p/CoreVersion.java | 2 +- router/java/src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/java/src/net/i2p/CoreVersion.java b/core/java/src/net/i2p/CoreVersion.java index ac868bed5..d46c8a7fb 100644 --- a/core/java/src/net/i2p/CoreVersion.java +++ b/core/java/src/net/i2p/CoreVersion.java @@ -15,7 +15,7 @@ package net.i2p; */ public class CoreVersion { public final static String ID = "$Revision: 1.72 $ $Date: 2008-08-24 12:00:00 $"; - public final static String VERSION = "0.6.6"; + public final static String VERSION = "0.6.5"; public static void main(String args[]) { System.out.println("I2P Core version: " + VERSION); diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index d415c7884..7f0c33214 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = "0.6.5"; - public final static long BUILD = 6; + public final static long BUILD = 7; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From a616a5f1c94d1e06b8498089d8beb65d35711c60 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 1 Jan 2009 13:13:04 +0000 Subject: [PATCH 021/191] prep for upcoming torrent updater --- .../src/net/i2p/router/web/UpdateHandler.java | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java index 56c291c43..be39da2fd 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java @@ -27,13 +27,13 @@ import net.i2p.util.Log; *

    */ public class UpdateHandler { - private static UpdateRunner _updateRunner; - private RouterContext _context; - private Log _log; - private DecimalFormat _pct = new DecimalFormat("00.0%"); + protected static UpdateRunner _updateRunner; + protected RouterContext _context; + protected Log _log; + protected DecimalFormat _pct = new DecimalFormat("00.0%"); - private static final String SIGNED_UPDATE_FILE = "i2pupdate.sud"; - private static final String PROP_UPDATE_IN_PROGRESS = "net.i2p.router.web.UpdateHandler.updateInProgress"; + protected static final String SIGNED_UPDATE_FILE = "i2pupdate.sud"; + protected static final String PROP_UPDATE_IN_PROGRESS = "net.i2p.router.web.UpdateHandler.updateInProgress"; public UpdateHandler() { this(ContextHelper.getContext(null)); @@ -93,9 +93,8 @@ public class UpdateHandler { } public class UpdateRunner implements Runnable, EepGet.StatusListener { - private boolean _isRunning; - private String _status; - private long _startedOn; + protected boolean _isRunning; + protected String _status; public UpdateRunner() { _isRunning = false; _status = "Updating"; @@ -108,8 +107,7 @@ public class UpdateHandler { System.setProperty(PROP_UPDATE_IN_PROGRESS, "false"); _isRunning = false; } - private void update() { - _startedOn = -1; + protected void update() { _status = "Updating"; String updateURL = selectUpdateURL(); if (_log.shouldLog(Log.DEBUG)) @@ -130,7 +128,6 @@ public class UpdateHandler { else get = new EepGet(_context, 1, SIGNED_UPDATE_FILE, updateURL, false); get.addStatusListener(UpdateRunner.this); - _startedOn = _context.clock().now(); get.fetch(); } catch (Throwable t) { _context.logManager().getLog(UpdateHandler.class).error("Error updating", t); From 44446d76e43e27d529ea2fa90b2ec72893be8def Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 1 Jan 2009 13:54:42 +0000 Subject: [PATCH 022/191] convert db to concurrent --- .../kademlia/TransientDataStore.java | 70 ++++++------------- 1 file changed, 22 insertions(+), 48 deletions(-) diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java index 492b164e7..d21141468 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java @@ -9,6 +9,7 @@ package net.i2p.router.networkdb.kademlia; */ import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -25,49 +26,38 @@ import net.i2p.util.Log; class TransientDataStore implements DataStore { private Log _log; - private Map _data; // hash --> DataStructure + private Map _data; protected RouterContext _context; public TransientDataStore(RouterContext ctx) { _context = ctx; _log = ctx.logManager().getLog(TransientDataStore.class); - _data = new HashMap(1024); + _data = new ConcurrentHashMap(1024); if (_log.shouldLog(Log.INFO)) _log.info("Data Store initialized"); } public void restart() { - synchronized (_data) { - _data.clear(); - } + _data.clear(); } public Set getKeys() { - synchronized (_data) { - return new HashSet(_data.keySet()); - } + return new HashSet(_data.keySet()); } public DataStructure get(Hash key) { - synchronized (_data) { - return (DataStructure)_data.get(key); - } + return _data.get(key); } public boolean isKnown(Hash key) { - synchronized (_data) { - return _data.containsKey(key); - } + return _data.containsKey(key); } public int countLeaseSets() { int count = 0; - synchronized (_data) { - for (Iterator iter = _data.values().iterator(); iter.hasNext();) { - DataStructure data = (DataStructure)iter.next(); - if (data instanceof LeaseSet) - count++; - } + for (DataStructure d : _data.values()) { + if (d instanceof LeaseSet) + count++; } return count; } @@ -81,10 +71,8 @@ class TransientDataStore implements DataStore { if (data == null) return; if (_log.shouldLog(Log.DEBUG)) _log.debug("Storing key " + key); - Object old = null; - synchronized (_data) { - old = _data.put(key, data); - } + DataStructure old = null; + old = _data.put(key, data); if (data instanceof RouterInfo) { _context.profileManager().heardAbout(key); RouterInfo ri = (RouterInfo)data; @@ -95,17 +83,13 @@ class TransientDataStore implements DataStore { _log.info("Almost clobbered an old router! " + key + ": [old published on " + new Date(ori.getPublished()) + " new on " + new Date(ri.getPublished()) + "]"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Number of router options for " + key + ": " + ri.getOptions().size() + " (old one had: " + ori.getOptions().size() + ")", new Exception("Updated routerInfo")); - synchronized (_data) { - _data.put(key, old); - } + _data.put(key, old); } else if (ri.getPublished() > _context.clock().now() + MAX_FUTURE_PUBLISH_DATE) { if (_log.shouldLog(Log.INFO)) _log.info("Hmm, someone tried to give us something with the publication date really far in the future (" + new Date(ri.getPublished()) + "), dropping it"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Number of router options for " + key + ": " + ri.getOptions().size() + " (old one had: " + ori.getOptions().size() + ")", new Exception("Updated routerInfo")); - synchronized (_data) { - _data.put(key, old); - } + _data.put(key, old); } else { if (_log.shouldLog(Log.INFO)) _log.info("Updated the old router for " + key + ": [old published on " + new Date(ori.getPublished()) + " new on " + new Date(ri.getPublished()) + "]"); @@ -125,15 +109,11 @@ class TransientDataStore implements DataStore { if (ls.getEarliestLeaseDate() < ols.getEarliestLeaseDate()) { if (_log.shouldLog(Log.INFO)) _log.info("Almost clobbered an old leaseSet! " + key + ": [old published on " + new Date(ols.getEarliestLeaseDate()) + " new on " + new Date(ls.getEarliestLeaseDate()) + "]"); - synchronized (_data) { - _data.put(key, old); - } + _data.put(key, old); } else if (ls.getEarliestLeaseDate() > _context.clock().now() + MAX_FUTURE_EXPIRATION_DATE) { if (_log.shouldLog(Log.INFO)) _log.info("Hmm, someone tried to give us something with the expiration date really far in the future (" + new Date(ls.getEarliestLeaseDate()) + "), dropping it"); - synchronized (_data) { - _data.put(key, old); - } + _data.put(key, old); } } } @@ -150,13 +130,9 @@ class TransientDataStore implements DataStore { public String toString() { StringBuffer buf = new StringBuffer(); buf.append("Transient DataStore: ").append(_data.size()).append("\nKeys: "); - Map data = new HashMap(); - synchronized (_data) { - data.putAll(_data); - } - for (Iterator iter = data.keySet().iterator(); iter.hasNext();) { - Hash key = (Hash)iter.next(); - DataStructure dp = (DataStructure)data.get(key); + for (Map.Entry e : _data.entrySet()) { + Hash key = e.getKey(); + DataStructure dp = e.getValue(); buf.append("\n\t*Key: ").append(key.toString()).append("\n\tContent: ").append(dp.toString()); } buf.append("\n"); @@ -168,10 +144,8 @@ class TransientDataStore implements DataStore { } public DataStructure remove(Hash key) { - synchronized (_data) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Removing key " + key.toBase64()); - return (DataStructure)_data.remove(key); - } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Removing key " + key.toBase64()); + return _data.remove(key); } } From ef998349cc0f0b30f110e07571b384fbb62d00f2 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 1 Jan 2009 13:58:00 +0000 Subject: [PATCH 023/191] require router.memoryUsed stat --- core/java/src/net/i2p/stat/StatManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/src/net/i2p/stat/StatManager.java b/core/java/src/net/i2p/stat/StatManager.java index 528421048..4c5c69c79 100644 --- a/core/java/src/net/i2p/stat/StatManager.java +++ b/core/java/src/net/i2p/stat/StatManager.java @@ -45,7 +45,7 @@ public class StatManager { public static final String DEFAULT_STAT_REQUIRED = "bw.recvRate,bw.sendBps,bw.sendRate,client.sendAckTime,clock.skew,crypto.elGamal.encrypt," + "jobQueue.jobLag,netDb.successTime,router.fastPeers," + - "prng.bufferFillTime,prng.bufferWaitTime," + + "prng.bufferFillTime,prng.bufferWaitTime,router.memoryUsed," + "transport.receiveMessageSize,transport.sendMessageSize,transport.sendProcessingTime," + "tunnel.acceptLoad,tunnel.buildRequestTime,tunnel.rejectOverloaded,tunnel.rejectTimeout" + "tunnel.buildClientExpire,tunnel.buildClientReject,tunnel.buildClientSuccess," + From 908c542b407c9321a292b3975d43ca6a46146137 Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 2 Jan 2009 18:07:16 +0000 Subject: [PATCH 024/191] move buttons --- apps/routerconsole/jsp/config.jsp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/routerconsole/jsp/config.jsp b/apps/routerconsole/jsp/config.jsp index 0d9573fe6..1c3d81518 100644 --- a/apps/routerconsole/jsp/config.jsp +++ b/apps/routerconsole/jsp/config.jsp @@ -103,6 +103,8 @@ substantially. When in doubt, leave the hostname and port number blank.

    Note: changing any of these settings will terminate all of your connections and effectively restart your router. +

    +

    Reachability Help:

    @@ -171,7 +173,6 @@


    --> -
    From d61af12867f740f6aba4d255acb50424dd8e8861 Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 2 Jan 2009 20:09:20 +0000 Subject: [PATCH 025/191] clean up and fix the possibly broken browser launcher config --- .../i2p/router/web/ConfigServiceHandler.java | 91 ++++--------------- 1 file changed, 16 insertions(+), 75 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java index a72ac9286..bd3bf7a5e 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java @@ -3,14 +3,13 @@ package net.i2p.router.web; import java.io.File; import java.io.FileWriter; import java.io.IOException; -import java.util.Iterator; -import java.util.Properties; -import java.util.TreeMap; +import java.util.List; import net.i2p.apps.systray.SysTray; import net.i2p.apps.systray.UrlLauncher; import net.i2p.data.DataHelper; import net.i2p.router.Router; +import net.i2p.router.startup.ClientAppConfig; import org.tanukisoftware.wrapper.WrapperManager; @@ -143,80 +142,22 @@ public class ConfigServiceHandler extends FormHandler { } } - private final static String NL = System.getProperty("line.separator"); private void browseOnStartup(boolean shouldLaunchBrowser) { - File f = new File("clients.config"); - Properties p = new Properties(); - try { - DataHelper.loadProps(p, f); - - int i = 0; - int launchIndex = -1; - while (true) { - String className = p.getProperty("clientApp." + i + ".main"); - if (className == null) break; - if (UrlLauncher.class.getName().equals(className)) { - launchIndex = i; - break; - } - i++; + List clients = ClientAppConfig.getClientApps(_context); + boolean found = false; + for (int cur = 0; cur < clients.size(); cur++) { + ClientAppConfig ca = (ClientAppConfig) clients.get(cur); + if (UrlLauncher.class.getName().equals(ca.className)) { + ca.disabled = !shouldLaunchBrowser; + found = true; + break; } - - if ((launchIndex >= 0) && shouldLaunchBrowser) - return; - if ((launchIndex < 0) && !shouldLaunchBrowser) - return; - - if (shouldLaunchBrowser) { - p.setProperty("clientApp." + i + ".main", UrlLauncher.class.getName()); - p.setProperty("clientApp." + i + ".name", "BrowserLauncher"); - p.setProperty("clientApp." + i + ".args", "http://localhost:7657/index.jsp"); - p.setProperty("clientApp." + i + ".delay", "5"); - } else { - p.remove("clientApp." + launchIndex + ".main"); - p.remove("clientApp." + launchIndex + ".name"); - p.remove("clientApp." + launchIndex + ".args"); - p.remove("clientApp." + launchIndex + ".onBoot"); - p.remove("clientApp." + launchIndex + ".delay"); - - i = launchIndex + 1; - while (true) { - String main = p.getProperty("clientApp." + i + ".main"); - String name = p.getProperty("clientApp." + i + ".name"); - String args = p.getProperty("clientApp." + i + ".args"); - String boot = p.getProperty("clientApp." + i + ".onBoot"); - String delay= p.getProperty("clientApp." + i + ".delay"); - - if (main == null) break; - - p.setProperty("clientApp." + (i-1) + ".main", main); - p.setProperty("clientApp." + (i-1) + ".name", name); - p.setProperty("clientApp." + (i-1) + ".args", args); - if (boot != null) - p.setProperty("clientApp." + (i-1) + ".onBoot", boot); - if (delay != null) - p.setProperty("clientApp." + (i-1) + ".delay", delay); - - p.remove("clientApp." + i + ".main"); - p.remove("clientApp." + i + ".name"); - p.remove("clientApp." + i + ".args"); - p.remove("clientApp." + i + ".onBoot"); - p.remove("clientApp." + i + ".delay"); - - i++; - } - } - - TreeMap sorted = new TreeMap(p); - FileWriter out = new FileWriter(f); - for (Iterator iter = sorted.keySet().iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - String val = (String)sorted.get(name); - out.write(name + "=" + val + NL); - } - out.close(); - } catch (IOException ioe) { - addFormError("Error updating the client config"); } + // releases <= 0.6.5 deleted the entry completely + if (shouldLaunchBrowser && !found) { + ClientAppConfig ca = new ClientAppConfig(UrlLauncher.class.getName(), "consoleBrowser", "http://localhost:7657", 5, false); + clients.add(ca); + } + ClientAppConfig.writeClientAppConfig(_context, clients); } } From 53ce3c480238effb439ce56e26c0a379df8357f6 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 3 Jan 2009 00:05:03 +0000 Subject: [PATCH 026/191] sort torrents with a locale-based sort --- .../java/src/org/klomp/snark/web/I2PSnarkServlet.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 c5bb93b21..e18c0febd 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -5,6 +5,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; +import java.text.Collator; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -378,7 +379,8 @@ public class I2PSnarkServlet extends HttpServlet { private List getSortedSnarks(HttpServletRequest req) { Set files = _manager.listTorrentFiles(); - TreeSet fileNames = new TreeSet(files); // sorts it alphabetically + TreeSet fileNames = new TreeSet(Collator.getInstance()); // sorts it alphabetically + fileNames.addAll(files); ArrayList rv = new ArrayList(fileNames.size()); for (Iterator iter = fileNames.iterator(); iter.hasNext(); ) { String name = (String)iter.next(); From debf92fd9b5f30562d76e6ce61522bd3510e2835 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 3 Jan 2009 00:49:33 +0000 Subject: [PATCH 027/191] history for prop., -8 --- history.txt | 21 +++++++++++++++++++ .../src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index 94c5f231f..cafd58404 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,24 @@ +2009-01-03 zzz + * config.jsp: Move the buttons up + * configservice.jsp: Clean up and fix the broken (?) + browser launch configuration + * i2psnark: + - Try again to remove the i2psnarkurl files on shutdown + - Sort torrents with a locale-based sort + * NetDb: + - Expire routers with introducers after 90m. + This should improve reachability to firewalled routers + by keeping introducer info current. + - Expire routers with no addresses after 90m. + - Convert to java concurrent + * Stats: Add router.memoryUsed, graph by default + * Summary bar: Remove spurious UDP warning on startup + * UpdateHandler: Make extensible for upcoming + torrent updater + +2008-12-15 zzz + * Remove apps/ bogobot jdom pants q rome stasher syndie + 2008-12-14 zzz * Contexts: Add int getProperty(String prop, int default) * I2PAppThread: Constructor fix diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 7f0c33214..55eaad0f3 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = "0.6.5"; - public final static long BUILD = 7; + public final static long BUILD = 8; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From 5c1864ed5e0cfa62fcaa3cdc6a596b0311e52a2a Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 5 Jan 2009 15:06:29 +0000 Subject: [PATCH 028/191] addressbook: Prevent Base32 hostnames --- apps/addressbook/java/src/addressbook/AddressBook.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/addressbook/java/src/addressbook/AddressBook.java b/apps/addressbook/java/src/addressbook/AddressBook.java index ca1e1916e..d8632bdee 100644 --- a/apps/addressbook/java/src/addressbook/AddressBook.java +++ b/apps/addressbook/java/src/addressbook/AddressBook.java @@ -179,6 +179,8 @@ public class AddressBook { // IDN - basic check, not complete validation (host.indexOf("--") < 0 || host.startsWith("xn--") || host.indexOf(".xn--") > 0) && host.replaceAll("[a-z0-9.-]", "").length() == 0 && + // Base32 spoofing (52chars.i2p) + (! (host.length() == 56 && host.substring(0,52).replaceAll("[a-z2-7]", "").length() == 0)) && // some reserved names that may be used for local configuration someday (! host.equals("proxy.i2p")) && (! host.equals("router.i2p")) && From efc604a25cb59835462aecf52f2a89ce2e27769e Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 5 Jan 2009 15:06:56 +0000 Subject: [PATCH 029/191] Remove readme_xx.html from updater --- build.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/build.xml b/build.xml index cc19c5671..9bff4c5c9 100644 --- a/build.xml +++ b/build.xml @@ -354,11 +354,6 @@ - - - - -
    From e2e4516a8f5b84c313e475c3c72da88bc2a5698b Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 5 Jan 2009 15:11:00 +0000 Subject: [PATCH 030/191] Fix display of outbound backup count --- .../java/src/net/i2p/router/web/ConfigTunnelsHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java index fe17164d3..98141771b 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java @@ -151,7 +151,7 @@ public class ConfigTunnelsHelper { buf.append("\n"); buf.append("\n"); boolean found = false; - for (int i = 10; i <= 60; i += 10) { - buf.append("\n"); + buf.append(val).append(" seconds\n"); } buf.append("\n"); return buf.toString(); From bc54908a22b0f794df86533f4029b6e35db865ce Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 10 Jan 2009 22:34:07 +0000 Subject: [PATCH 044/191] cleanups using getProperty(String, int) --- .../src/net/i2p/router/LoadTestManager.java | 17 +-- .../net/i2p/router/RouterThrottleImpl.java | 15 +-- .../router/peermanager/ProfileOrganizer.java | 34 +----- .../transport/FIFOBandwidthRefiller.java | 106 +++--------------- .../i2p/router/tunnel/pool/BuildExecutor.java | 13 +-- 5 files changed, 25 insertions(+), 160 deletions(-) diff --git a/router/java/src/net/i2p/router/LoadTestManager.java b/router/java/src/net/i2p/router/LoadTestManager.java index 55adb6138..d3e5c46f5 100644 --- a/router/java/src/net/i2p/router/LoadTestManager.java +++ b/router/java/src/net/i2p/router/LoadTestManager.java @@ -117,12 +117,7 @@ public class LoadTestManager { private int getConcurrency() { if (!isEnabled(_context)) return 0; - int rv = CONCURRENT_PEERS; - try { - rv = Integer.parseInt(_context.getProperty("router.loadTestConcurrency", CONCURRENT_PEERS+"")); - } catch (NumberFormatException nfe) { - rv = CONCURRENT_PEERS; - } + int rv = _context.getProperty("router.loadTestConcurrency", CONCURRENT_PEERS); if (rv < 0) rv = 0; if (rv > 50) @@ -131,15 +126,7 @@ public class LoadTestManager { } private int getPeerMessages() { - String msgsPerPeer = _context.getProperty("router.loadTestMessagesPerPeer"); - int rv = CONCURRENT_MESSAGES; - if (msgsPerPeer != null) { - try { - rv = Integer.parseInt(msgsPerPeer); - } catch (NumberFormatException nfe) { - rv = CONCURRENT_MESSAGES; - } - } + int rv = _context.getProperty("router.loadTestMessagesPerPeer", CONCURRENT_MESSAGES); if (rv < 1) rv = 1; if (rv > 50) diff --git a/router/java/src/net/i2p/router/RouterThrottleImpl.java b/router/java/src/net/i2p/router/RouterThrottleImpl.java index 2c3f2114e..fc38b1695 100644 --- a/router/java/src/net/i2p/router/RouterThrottleImpl.java +++ b/router/java/src/net/i2p/router/RouterThrottleImpl.java @@ -190,14 +190,7 @@ class RouterThrottleImpl implements RouterThrottle { } } - int max = DEFAULT_MAX_TUNNELS; - String maxTunnels = _context.getProperty(PROP_MAX_TUNNELS); - if (maxTunnels != null) { - try { - max = Integer.parseInt(maxTunnels); - } catch (NumberFormatException nfe) { - } - } + int max = _context.getProperty(PROP_MAX_TUNNELS, DEFAULT_MAX_TUNNELS); if (numTunnels >= max) { if (_log.shouldLog(Log.WARN)) _log.warn("Refusing tunnel request since we are already participating in " @@ -387,11 +380,7 @@ class RouterThrottleImpl implements RouterThrottle { /** dont ever probabalistically throttle tunnels if we have less than this many */ private int getMinThrottleTunnels() { - try { - return Integer.parseInt(_context.getProperty("router.minThrottleTunnels", "1000")); - } catch (NumberFormatException nfe) { - return 1000; - } + return _context.getProperty("router.minThrottleTunnels", 1000); } private double getTunnelGrowthFactor() { diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java index e759441d9..b090d6cc7 100644 --- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java +++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java @@ -1101,22 +1101,7 @@ public class ProfileOrganizer { * @return minimum number of peers to be placed in the 'fast' group */ protected int getMinimumFastPeers() { - String val = _context.getProperty(PROP_MINIMUM_FAST_PEERS, ""+DEFAULT_MINIMUM_FAST_PEERS); - if (val != null) { - try { - int rv = Integer.parseInt(val); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("router context said " + PROP_MINIMUM_FAST_PEERS + '=' + val); - return rv; - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Minimum fast peers improperly set in the router environment [" + val + "]", nfe); - } - } - - if (_log.shouldLog(Log.DEBUG)) - _log.debug("no config for " + PROP_MINIMUM_FAST_PEERS + ", using " + DEFAULT_MINIMUM_FAST_PEERS); - return DEFAULT_MINIMUM_FAST_PEERS; + return _context.getProperty(PROP_MINIMUM_FAST_PEERS, DEFAULT_MINIMUM_FAST_PEERS); } @@ -1130,22 +1115,7 @@ public class ProfileOrganizer { * @return minimum number of peers to be placed in the 'fast' group */ protected int getMinimumHighCapacityPeers() { - String val = _context.getProperty(PROP_MINIMUM_HIGH_CAPACITY_PEERS, ""+DEFAULT_MINIMUM_HIGH_CAPACITY_PEERS); - if (val != null) { - try { - int rv = Integer.parseInt(val); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("router context said " + PROP_MINIMUM_HIGH_CAPACITY_PEERS + '=' + val); - return rv; - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Minimum high capacity peers improperly set in the router environment [" + val + "]", nfe); - } - } - - if (_log.shouldLog(Log.DEBUG)) - _log.debug("no config for " + PROP_MINIMUM_HIGH_CAPACITY_PEERS + ", using " + DEFAULT_MINIMUM_HIGH_CAPACITY_PEERS); - return DEFAULT_MINIMUM_HIGH_CAPACITY_PEERS; + return _context.getProperty(PROP_MINIMUM_HIGH_CAPACITY_PEERS, DEFAULT_MINIMUM_HIGH_CAPACITY_PEERS); } private final static DecimalFormat _fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK)); diff --git a/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java b/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java index 0210f8da5..d4bdfea85 100644 --- a/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java +++ b/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java @@ -154,55 +154,30 @@ public class FIFOBandwidthRefiller implements Runnable { } private void updateInboundRate() { - String inBwStr = _context.getProperty(PROP_INBOUND_BANDWIDTH); - if ( (inBwStr != null) && - (inBwStr.trim().length() > 0) && - (!(inBwStr.equals(String.valueOf(_inboundKBytesPerSecond)))) ) { + int in = _context.getProperty(PROP_INBOUND_BANDWIDTH, DEFAULT_INBOUND_BANDWIDTH); + if (in != _inboundKBytesPerSecond) { // bandwidth was specified *and* changed - try { - int in = Integer.parseInt(inBwStr); if ( (in <= 0) || (in > MIN_INBOUND_BANDWIDTH) ) _inboundKBytesPerSecond = in; else _inboundKBytesPerSecond = MIN_INBOUND_BANDWIDTH; if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating inbound rate to " + _inboundKBytesPerSecond); - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Invalid inbound bandwidth limit [" + inBwStr - + "], keeping as " + _inboundKBytesPerSecond); - } - } else { - if ( (inBwStr == null) && (_log.shouldLog(Log.DEBUG)) ) - _log.debug("Inbound bandwidth limits not specified in the config via " + PROP_INBOUND_BANDWIDTH); } if (_inboundKBytesPerSecond <= 0) _inboundKBytesPerSecond = DEFAULT_INBOUND_BANDWIDTH; } private void updateOutboundRate() { - String outBwStr = _context.getProperty(PROP_OUTBOUND_BANDWIDTH); - - if ( (outBwStr != null) && - (outBwStr.trim().length() > 0) && - (!(outBwStr.equals(String.valueOf(_outboundKBytesPerSecond)))) ) { + int out = _context.getProperty(PROP_OUTBOUND_BANDWIDTH, DEFAULT_OUTBOUND_BANDWIDTH); + if (out != _outboundKBytesPerSecond) { // bandwidth was specified *and* changed - try { - int out = Integer.parseInt(outBwStr); if ( (out <= 0) || (out >= MIN_OUTBOUND_BANDWIDTH) ) _outboundKBytesPerSecond = out; else _outboundKBytesPerSecond = MIN_OUTBOUND_BANDWIDTH; if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating outbound rate to " + _outboundKBytesPerSecond); - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Invalid outbound bandwidth limit [" + outBwStr - + "], keeping as " + _outboundKBytesPerSecond); - } - } else { - if ( (outBwStr == null) && (_log.shouldLog(Log.DEBUG)) ) - _log.debug("Outbound bandwidth limits not specified in the config via " + PROP_OUTBOUND_BANDWIDTH); } if (_outboundKBytesPerSecond <= 0) @@ -210,27 +185,15 @@ public class FIFOBandwidthRefiller implements Runnable { } private void updateInboundBurstRate() { - String inBwStr = _context.getProperty(PROP_INBOUND_BURST_BANDWIDTH); - if ( (inBwStr != null) && - (inBwStr.trim().length() > 0) && - (!(inBwStr.equals(String.valueOf(_inboundBurstKBytesPerSecond)))) ) { + int in = _context.getProperty(PROP_INBOUND_BURST_BANDWIDTH, DEFAULT_INBOUND_BURST_BANDWIDTH); + if (in != _inboundBurstKBytesPerSecond) { // bandwidth was specified *and* changed - try { - int in = Integer.parseInt(inBwStr); if ( (in <= 0) || (in >= _inboundKBytesPerSecond) ) _inboundBurstKBytesPerSecond = in; else _inboundBurstKBytesPerSecond = _inboundKBytesPerSecond; if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating inbound burst rate to " + _inboundBurstKBytesPerSecond); - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Invalid inbound bandwidth burst limit [" + inBwStr - + "], keeping as " + _inboundBurstKBytesPerSecond); - } - } else { - if ( (inBwStr == null) && (_log.shouldLog(Log.DEBUG)) ) - _log.debug("Inbound bandwidth burst limits not specified in the config via " + PROP_INBOUND_BURST_BANDWIDTH); } if (_inboundBurstKBytesPerSecond <= 0) @@ -239,28 +202,15 @@ public class FIFOBandwidthRefiller implements Runnable { } private void updateOutboundBurstRate() { - String outBwStr = _context.getProperty(PROP_OUTBOUND_BURST_BANDWIDTH); - - if ( (outBwStr != null) && - (outBwStr.trim().length() > 0) && - (!(outBwStr.equals(String.valueOf(_outboundBurstKBytesPerSecond)))) ) { + int out = _context.getProperty(PROP_OUTBOUND_BURST_BANDWIDTH, DEFAULT_OUTBOUND_BURST_BANDWIDTH); + if (out != _outboundBurstKBytesPerSecond) { // bandwidth was specified *and* changed - try { - int out = Integer.parseInt(outBwStr); if ( (out <= 0) || (out >= _outboundKBytesPerSecond) ) _outboundBurstKBytesPerSecond = out; else _outboundBurstKBytesPerSecond = _outboundKBytesPerSecond; if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating outbound burst rate to " + _outboundBurstKBytesPerSecond); - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Invalid outbound bandwidth burst limit [" + outBwStr - + "], keeping as " + _outboundBurstKBytesPerSecond); - } - } else { - if ( (outBwStr == null) && (_log.shouldLog(Log.DEBUG)) ) - _log.debug("Outbound bandwidth burst limits not specified in the config via " + PROP_OUTBOUND_BURST_BANDWIDTH); } if (_outboundBurstKBytesPerSecond <= 0) @@ -269,13 +219,10 @@ public class FIFOBandwidthRefiller implements Runnable { } private void updateInboundPeak() { - String inBwStr = _context.getProperty(PROP_INBOUND_BANDWIDTH_PEAK); - if ( (inBwStr != null) && - (inBwStr.trim().length() > 0) && - (!(inBwStr.equals(String.valueOf(_limiter.getInboundBurstBytes())))) ) { + int in = _context.getProperty(PROP_INBOUND_BANDWIDTH_PEAK, + DEFAULT_BURST_SECONDS * _inboundBurstKBytesPerSecond); + if (in != _limiter.getInboundBurstBytes()) { // peak bw was specified *and* changed - try { - int in = Integer.parseInt(inBwStr); if (in >= MIN_INBOUND_BANDWIDTH_PEAK) { if (in < _inboundBurstKBytesPerSecond) _limiter.setInboundBurstBytes(_inboundBurstKBytesPerSecond * 1024); @@ -287,27 +234,13 @@ public class FIFOBandwidthRefiller implements Runnable { else _limiter.setInboundBurstBytes(MIN_INBOUND_BANDWIDTH_PEAK * 1024); } - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Invalid inbound bandwidth burst limit [" + inBwStr - + "]"); - _limiter.setInboundBurstBytes(DEFAULT_BURST_SECONDS * _inboundBurstKBytesPerSecond * 1024); - } - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Inbound bandwidth burst limits not specified in the config via " - + PROP_INBOUND_BANDWIDTH_PEAK); - _limiter.setInboundBurstBytes(DEFAULT_BURST_SECONDS * _inboundBurstKBytesPerSecond * 1024); } } private void updateOutboundPeak() { - String inBwStr = _context.getProperty(PROP_OUTBOUND_BANDWIDTH_PEAK); - if ( (inBwStr != null) && - (inBwStr.trim().length() > 0) && - (!(inBwStr.equals(String.valueOf(_limiter.getOutboundBurstBytes())))) ) { + int in = _context.getProperty(PROP_OUTBOUND_BANDWIDTH_PEAK, + DEFAULT_BURST_SECONDS * _outboundBurstKBytesPerSecond); + if (in != _limiter.getOutboundBurstBytes()) { // peak bw was specified *and* changed - try { - int in = Integer.parseInt(inBwStr); if (in >= MIN_OUTBOUND_BANDWIDTH_PEAK) { if (in < _outboundBurstKBytesPerSecond) _limiter.setOutboundBurstBytes(_outboundBurstKBytesPerSecond * 1024); @@ -319,17 +252,6 @@ public class FIFOBandwidthRefiller implements Runnable { else _limiter.setOutboundBurstBytes(MIN_OUTBOUND_BANDWIDTH_PEAK * 1024); } - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Invalid outbound bandwidth burst limit [" + inBwStr - + "]"); - _limiter.setOutboundBurstBytes(DEFAULT_BURST_SECONDS * _outboundBurstKBytesPerSecond * 1024); - } - } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Outbound bandwidth burst limits not specified in the config via " - + PROP_OUTBOUND_BANDWIDTH_PEAK); - _limiter.setOutboundBurstBytes(DEFAULT_BURST_SECONDS * _outboundBurstKBytesPerSecond * 1024); } } diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java index e1f040b8b..390fe888d 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java @@ -39,10 +39,10 @@ class BuildExecutor implements Runnable { _context.statManager().createRateStat("tunnel.concurrentBuildsLagged", "How many builds are going at once when we reject further builds, due to job lag (period is lag)", "Tunnels", new long[] { 60*1000, 5*60*1000, 60*60*1000 }); _context.statManager().createRateStat("tunnel.buildExploratoryExpire", "How often an exploratory tunnel times out during creation", "Tunnels", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("tunnel.buildClientExpire", "How often a client tunnel times out during creation", "Tunnels", new long[] { 60*1000, 10*60*1000 }); - _context.statManager().createRateStat("tunnel.buildExploratorySuccess", "How often an exploratory tunnel is fully built", "Tunnels", new long[] { 60*1000, 10*60*1000 }); - _context.statManager().createRateStat("tunnel.buildClientSuccess", "How often a client tunnel is fully built", "Tunnels", new long[] { 60*1000, 10*60*1000 }); - _context.statManager().createRateStat("tunnel.buildExploratoryReject", "How often an exploratory tunnel is rejected", "Tunnels", new long[] { 60*1000, 10*60*1000 }); - _context.statManager().createRateStat("tunnel.buildClientReject", "How often a client tunnel is rejected", "Tunnels", new long[] { 60*1000, 10*60*1000 }); + _context.statManager().createRateStat("tunnel.buildExploratorySuccess", "Response time for success", "Tunnels", new long[] { 60*1000, 10*60*1000 }); + _context.statManager().createRateStat("tunnel.buildClientSuccess", "Response time for success", "Tunnels", new long[] { 60*1000, 10*60*1000 }); + _context.statManager().createRateStat("tunnel.buildExploratoryReject", "Response time for rejection", "Tunnels", new long[] { 60*1000, 10*60*1000 }); + _context.statManager().createRateStat("tunnel.buildClientReject", "Response time for rejection", "Tunnels", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("tunnel.buildRequestTime", "How long it takes to build a tunnel request", "Tunnels", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("tunnel.buildRequestZeroHopTime", "How long it takes to build a zero hop tunnel", "Tunnels", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("tunnel.pendingRemaining", "How many inbound requests are pending after a pass (period is how long the pass takes)?", "Tunnels", new long[] { 60*1000, 10*60*1000 }); @@ -71,10 +71,7 @@ class BuildExecutor implements Runnable { int allowed = maxKBps / 6; // Max. 1 concurrent build per 6 KB/s outbound if (allowed < 2) allowed = 2; // Never choke below 2 builds (but congestion may) if (allowed > 10) allowed = 10; // Never go beyond 10, that is uncharted territory (old limit was 5) - - String prop = _context.getProperty("router.tunnelConcurrentBuilds"); - if (prop != null) - try { allowed = Integer.valueOf(prop).intValue(); } catch (NumberFormatException nfe) {} + allowed = _context.getProperty("router.tunnelConcurrentBuilds", allowed); List expired = null; int concurrent = 0; From e3abea1ad26d2b9cc076c9887af5a689dd62d692 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 11 Jan 2009 15:25:23 +0000 Subject: [PATCH 045/191] add netdb links on tunnels.jsp --- .../net/i2p/router/tunnel/pool/TunnelPoolManager.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java index 4416582df..1a3e0d1b6 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java @@ -472,7 +472,7 @@ public class TunnelPoolManager implements TunnelManagerFacade { else out.write("n/a"); if (cfg.getReceiveFrom() != null) - out.write("" + cfg.getReceiveFrom().toBase64().substring(0,4) +""); + out.write("" + netDbLink(cfg.getReceiveFrom()) +""); else out.write(" "); if (cfg.getSendTunnel() != null) @@ -480,7 +480,7 @@ public class TunnelPoolManager implements TunnelManagerFacade { else out.write(" "); if (cfg.getSendTo() != null) - out.write("" + cfg.getSendTo().toBase64().substring(0,4) +""); + out.write("" + netDbLink(cfg.getSendTo()) +""); else out.write(" "); long timeLeft = cfg.getExpiration()-_context.clock().now(); @@ -549,7 +549,7 @@ public class TunnelPoolManager implements TunnelManagerFacade { if (_context.routerHash().equals(peer)) out.write("" + (id == null ? "" : "" + id) + ""); else - out.write("" + peer.toBase64().substring(0,4) + (id == null ? "" : ":" + id) + cap + ""); + out.write("" + netDbLink(peer) + (id == null ? "" : ":" + id) + cap + ""); } out.write("\n"); @@ -601,4 +601,9 @@ public class TunnelPoolManager implements TunnelManagerFacade { return "[unkn]"; } } + + private static String netDbLink(Hash peer) { + String h = peer.toBase64().substring(0, 4); + return "" + h + ""; + } } From 85615b972b086cd11e3b00726b33c781f1495c0d Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 12 Jan 2009 05:21:16 +0000 Subject: [PATCH 046/191] noobhelp --- installer/resources/dnfh-header.ht | 79 ++++++++++++++++-------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/installer/resources/dnfh-header.ht b/installer/resources/dnfh-header.ht index 2f8548448..ff6195ca9 100644 --- a/installer/resources/dnfh-header.ht +++ b/installer/resources/dnfh-header.ht @@ -4,41 +4,44 @@ Cache-control: no-cache Connection: close Proxy-Connection: close - -Eepsite unknown - - - - - -
    -The eepsite was not found in your router's addressbook. -Check the link or find a BASE64 address. -If you have the BASE64 address, paste it into your userhosts.txt using -SusiDNS, -use a BASE64 address helper, or use a jump service link below. -

    Could not find the following destination:

    + +Eepsite unknown + + + + + +
    +The eepsite was not found in your router's addressbook. +Check the link or find a BASE64 address. +If you have the BASE64 address, paste it into your userhosts.txt using +SusiDNS, +use a BASE64 address helper, or use a jump service link below. +Seeing this page often? See the FAQ +for help in adding some subscriptions +to your addressbook. +

    Could not find the following destination:

    From 05a63531429cd2d549fee6920c7dc48c050b8391 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 12 Jan 2009 14:31:43 +0000 Subject: [PATCH 047/191] .b32.i2p --- apps/i2ptunnel/jsp/index.jsp | 2 +- core/java/src/net/i2p/client/naming/HostsTxtNamingService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index cc68096ad..9fc2d2a96 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -196,7 +196,7 @@ <% if ("httpserver".equals(indexBean.getInternalType(curServer)) && indexBean.getTunnelStatus(curServer) == IndexBean.RUNNING) { %> - Preview + Preview <% } else { %>No Preview diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java index 4cfb07d40..0f1ccab6f 100644 --- a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java +++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java @@ -72,7 +72,7 @@ public class HostsTxtNamingService extends NamingService { } // Try Base32 decoding - if (hostname.length() == BASE32_HASH_LENGTH + 4 && hostname.endsWith(".i2p")) { + if (hostname.length() == BASE32_HASH_LENGTH + 8 && hostname.endsWith(".b32.i2p")) { d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH)); if (d != null) { putCache(hostname, d); From 70b99cf4f9b882ba7e2cbaa8ec992fbed8024f0c Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 12 Jan 2009 14:51:38 +0000 Subject: [PATCH 048/191] prevent possible latency-measuring attack --- .../src/net/i2p/router/tunnel/InboundMessageDistributor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java index 65066e133..c5c46c365 100644 --- a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java +++ b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java @@ -78,6 +78,7 @@ public class InboundMessageDistributor implements GarlicMessageReceiver.CloveRec _log.info("distributing inbound tunnel message into our inNetMessagePool: " + msg); _context.inNetMessagePool().add(msg, null, null); } +/****** latency measuring attack? } else if (_context.routerHash().equals(target)) { // the want to send it to a tunnel, except we are also that tunnel's gateway // dispatch it directly @@ -89,6 +90,7 @@ public class InboundMessageDistributor implements GarlicMessageReceiver.CloveRec gw.setMessageExpiration(_context.clock().now()+10*1000); gw.setUniqueId(_context.random().nextLong(I2NPMessage.MAX_ID_VALUE)); _context.tunnelDispatcher().dispatch(gw); +******/ } else { // ok, they want us to send it remotely, but that'd bust our anonymity, // so we send it out a tunnel first From 957c80977471154b6abc790199e23cfc21abb333 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 12 Jan 2009 16:28:36 +0000 Subject: [PATCH 049/191] drop more syndie files --- installer/resources/blogMeta.snm | 7 ------- installer/resources/blogPost.snd | Bin 8647 -> 0 bytes 2 files changed, 7 deletions(-) delete mode 100644 installer/resources/blogMeta.snm delete mode 100644 installer/resources/blogPost.snd diff --git a/installer/resources/blogMeta.snm b/installer/resources/blogMeta.snm deleted file mode 100644 index 1345eab10..000000000 --- a/installer/resources/blogMeta.snm +++ /dev/null @@ -1,7 +0,0 @@ -Owner:U1oHd4XghnvqZzFaxx2Z8ogH9bfkJ4MCMUZKIu5lGV~0098TLaqB~pOc~GyAPtP55ckS54KX1uJN3pttaawwt61edntulHpatOCwrw5lpAytJcpZhJaahs64NhdnNeFCindHbXFxYU7BiRt7iyHswMlGjup~03uy7xp-JdWlNjw= -Posters: -Name:jrandom -Edition:0 -ContactURL:jrandom@i2p.net -Description:jrandom's ranting -Signature:PstiGeiWOV8VKARVNvk4NRe-EOAwS10yGHMkXb~FUS7GBMVHxaGeDA== diff --git a/installer/resources/blogPost.snd b/installer/resources/blogPost.snd deleted file mode 100644 index e0baf0626064a2cc062fd0213374b89f04a82a56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8647 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 Date: Tue, 13 Jan 2009 19:27:14 +0000 Subject: [PATCH 050/191] * HTTPClient: Fix per-tunnel settings for i2cp.gzip and i2ptunnel.httpclient.send* (thx tino) --- .../src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index a7c76e2f3..a3d6f75e4 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -470,7 +470,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable if (_log.shouldLog(Log.INFO)) _log.info(getPrefix(requestId) + "Setting host = " + host); } else if (lowercaseLine.startsWith("user-agent: ") && - !Boolean.valueOf(getTunnel().getContext().getProperty(PROP_USER_AGENT)).booleanValue()) { + !Boolean.valueOf(getTunnel().getClientOptions().getProperty(PROP_USER_AGENT)).booleanValue()) { line = null; continue; } else if (lowercaseLine.startsWith("accept")) { @@ -479,13 +479,13 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable line = null; continue; } else if (lowercaseLine.startsWith("referer: ") && - !Boolean.valueOf(getTunnel().getContext().getProperty(PROP_REFERER)).booleanValue()) { + !Boolean.valueOf(getTunnel().getClientOptions().getProperty(PROP_REFERER)).booleanValue()) { // Shouldn't we be more specific, like accepting in-site referers ? //line = "Referer: i2p"; line = null; continue; // completely strip the line } else if (lowercaseLine.startsWith("via: ") && - !Boolean.valueOf(getTunnel().getContext().getProperty(PROP_VIA)).booleanValue()) { + !Boolean.valueOf(getTunnel().getClientOptions().getProperty(PROP_VIA)).booleanValue()) { //line = "Via: i2p"; line = null; continue; // completely strip the line @@ -498,7 +498,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable if (line.length() == 0) { - String ok = getTunnel().getContext().getProperty("i2ptunnel.gzip"); + String ok = getTunnel().getClientOptions().getProperty("i2ptunnel.gzip"); boolean gzip = DEFAULT_GZIP; if (ok != null) gzip = Boolean.valueOf(ok).booleanValue(); @@ -509,7 +509,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable newRequest.append("Accept-Encoding: \r\n"); newRequest.append("X-Accept-Encoding: x-i2p-gzip;q=1.0, identity;q=0.5, deflate;q=0, gzip;q=0, *;q=0\r\n"); } - if (!Boolean.valueOf(getTunnel().getContext().getProperty(PROP_USER_AGENT)).booleanValue()) + if (!Boolean.valueOf(getTunnel().getClientOptions().getProperty(PROP_USER_AGENT)).booleanValue()) newRequest.append("User-Agent: MYOB/6.66 (AN/ON)\r\n"); newRequest.append("Connection: close\r\n\r\n"); break; From bdcb625e6da80618ed1df2d2b27c5b82e9ce327f Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 13 Jan 2009 19:28:09 +0000 Subject: [PATCH 051/191] fix rare NPE --- apps/streaming/java/src/net/i2p/client/streaming/Packet.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Packet.java b/apps/streaming/java/src/net/i2p/client/streaming/Packet.java index 61a7de96d..7a7a0a6c9 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/Packet.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/Packet.java @@ -530,6 +530,8 @@ public class Packet { public boolean verifySignature(I2PAppContext ctx, Destination from, byte buffer[]) { if (!isFlagSet(FLAG_SIGNATURE_INCLUDED)) return false; if (_optionSignature == null) return false; + // prevent receiveNewSyn() ... !active ... sendReset() ... verifySignature ... NPE + if (from == null) return false; int size = writtenSize(); From 366da1b37c54c601601b9feb7036254f20a1c9d2 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 13 Jan 2009 19:32:10 +0000 Subject: [PATCH 052/191] add b32 config for mosfet --- .../java/src/net/i2p/client/naming/HostsTxtNamingService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java index 0f1ccab6f..9fa227f81 100644 --- a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java +++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java @@ -39,6 +39,7 @@ public class HostsTxtNamingService extends NamingService { * given file for hostname=destKey values when resolving names */ public final static String PROP_HOSTS_FILE = "i2p.hostsfilelist"; + public final static String PROP_B32 = "i2p.naming.hostsTxt.useB32"; /** default hosts.txt filename */ public final static String DEFAULT_HOSTS_FILE = @@ -72,7 +73,8 @@ public class HostsTxtNamingService extends NamingService { } // Try Base32 decoding - if (hostname.length() == BASE32_HASH_LENGTH + 8 && hostname.endsWith(".b32.i2p")) { + if (hostname.length() == BASE32_HASH_LENGTH + 8 && hostname.endsWith(".b32.i2p") && + Boolean.valueOf(_context.getProperty(PROP_B32, "true")).booleanValue()) { d = LookupDest.lookupBase32Hash(_context, hostname.substring(0, BASE32_HASH_LENGTH)); if (d != null) { putCache(hostname, d); From 1c76d240e03eb322afc2600c6aa8d787de7a1aa3 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 13 Jan 2009 19:52:45 +0000 Subject: [PATCH 053/191] * i2psnark: - Fix double completion message - Add crstrack --- apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java | 1 + apps/i2psnark/java/src/org/klomp/snark/Storage.java | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index dc10c3b1a..7e5cd962f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -693,6 +693,7 @@ public class SnarkManager implements Snark.CompleteListener { , "welterde", "http://BGKmlDOoH3RzFbPRfRpZV2FjpVj8~3moFftw5-dZfDf2070TOe8Tf2~DAVeaM6ZRLdmFEt~9wyFL8YMLMoLoiwGEH6IGW6rc45tstN68KsBDWZqkTohV1q9XFgK9JnCwE~Oi89xLBHsLMTHOabowWM6dkC8nI6QqJC2JODqLPIRfOVrDdkjLwtCrsckzLybNdFmgfoqF05UITDyczPsFVaHtpF1sRggOVmdvCM66otyonlzNcJbn59PA-R808vUrCPMGU~O9Wys0i-NoqtIbtWfOKnjCRFMNw5ex4n9m5Sxm9e20UkpKG6qzEuvKZWi8vTLe1NW~CBrj~vG7I3Ok4wybUFflBFOaBabxYJLlx4xTE1zJIVxlsekmAjckB4v-cQwulFeikR4LxPQ6mCQknW2HZ4JQIq6hL9AMabxjOlYnzh7kjOfRGkck8YgeozcyTvcDUcUsOuSTk06L4kdrv8h2Cozjbloi5zl6KTbj5ZTciKCxi73Pn9grICn-HQqEAAAA.i2p/a=http://tracker.welterde.i2p/stats?mode=top5" // , "mastertracker", "http://VzXD~stRKbL3MOmeTn1iaCQ0CFyTmuFHiKYyo0Rd~dFPZFCYH-22rT8JD7i-C2xzYFa4jT5U2aqHzHI-Jre4HL3Ri5hFtZrLk2ax3ji7Qfb6qPnuYkuiF2E2UDmKUOppI8d9Ye7tjdhQVCy0izn55tBaB-U7UWdcvSK2i85sauyw3G0Gfads1Rvy5-CAe2paqyYATcDmGjpUNLoxbfv9KH1KmwRTNH6k1v4PyWYYnhbT39WfKMbBjSxVQRdi19cyJrULSWhjxaQfJHeWx5Z8Ev4bSPByBeQBFl2~4vqy0S5RypINsRSa3MZdbiAAyn5tr5slWR6QdoqY3qBQgBJFZppy-3iWkFqqKgSxCPundF8gdDLC5ddizl~KYcYKl42y9SGFHIukH-TZs8~em0~iahzsqWVRks3zRG~tlBcX2U3M2~OJs~C33-NKhyfZT7-XFBREvb8Szmd~p66jDxrwOnKaku-G6DyoQipJqIz4VHmY9-y5T8RrUcJcM-5lVoMpAAAA.i2p/announce.php=http://tracker.mastertracker.i2p/" // , "Galen", "http://5jpwQMI5FT303YwKa5Rd38PYSX04pbIKgTaKQsWbqoWjIfoancFdWCShXHLI5G5ofOb0Xu11vl2VEMyPsg1jUFYSVnu4-VfMe3y4TKTR6DTpetWrnmEK6m2UXh91J5DZJAKlgmO7UdsFlBkQfR2rY853-DfbJtQIFl91tbsmjcA5CGQi4VxMFyIkBzv-pCsuLQiZqOwWasTlnzey8GcDAPG1LDcvfflGV~6F5no9mnuisZPteZKlrv~~TDoXTj74QjByWc4EOYlwqK8sbU9aOvz~s31XzErbPTfwiawiaZ0RUI-IDrKgyvmj0neuFTWgjRGVTH8bz7cBZIc3viy6ioD-eMQOrXaQL0TCWZUelRwHRvgdPiQrxdYQs7ixkajeHzxi-Pq0EMm5Vbh3j3Q9kfUFW3JjFDA-MLB4g6XnjCbM5J1rC0oOBDCIEfhQkszru5cyLjHiZ5yeA0VThgu~c7xKHybv~OMXION7V8pBKOgET7ZgAkw1xgYe3Kkyq5syAAAA.i2p/tr/announce.php=http://galen.i2p/tr/" + , "crstrack", "http://b4G9sCdtfvccMAXh~SaZrPqVQNyGQbhbYMbw6supq2XGzbjU4NcOmjFI0vxQ8w1L05twmkOvg5QERcX6Mi8NQrWnR0stLExu2LucUXg1aYjnggxIR8TIOGygZVIMV3STKH4UQXD--wz0BUrqaLxPhrm2Eh9Hwc8TdB6Na4ShQUq5Xm8D4elzNUVdpM~RtChEyJWuQvoGAHY3ppX-EJJLkiSr1t77neS4Lc-KofMVmgI9a2tSSpNAagBiNI6Ak9L1T0F9uxeDfEG9bBSQPNMOSUbAoEcNxtt7xOW~cNOAyMyGydwPMnrQ5kIYPY8Pd3XudEko970vE0D6gO19yoBMJpKx6Dh50DGgybLQ9CpRaynh2zPULTHxm8rneOGRcQo8D3mE7FQ92m54~SvfjXjD2TwAVGI~ae~n9HDxt8uxOecAAvjjJ3TD4XM63Q9TmB38RmGNzNLDBQMEmJFpqQU8YeuhnS54IVdUoVQFqui5SfDeLXlSkh4vYoMU66pvBfWbAAAA.i2p/tracker/announce.php=http://crstrack.i2p/tracker/" }; /** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */ diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java index a8f023295..69e5a198f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java @@ -696,9 +696,6 @@ public class Storage listener.setWantedPieces(this); _util.debug("WARNING: Not really done, missing " + needed + " pieces", Snark.WARNING); - } else { - if (listener != null) - listener.storageCompleted(this); } } From 0275c5e13b183768fd3d50133022755afacf68a5 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 13 Jan 2009 19:53:35 +0000 Subject: [PATCH 054/191] crstrack --- hosts.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/hosts.txt b/hosts.txt index b32734a71..95ca28af6 100644 --- a/hosts.txt +++ b/hosts.txt @@ -312,3 +312,4 @@ galen.i2p=5jpwQMI5FT303YwKa5Rd38PYSX04pbIKgTaKQsWbqoWjIfoancFdWCShXHLI5G5ofOb0Xu tracker.mastertracker.i2p=VzXD~stRKbL3MOmeTn1iaCQ0CFyTmuFHiKYyo0Rd~dFPZFCYH-22rT8JD7i-C2xzYFa4jT5U2aqHzHI-Jre4HL3Ri5hFtZrLk2ax3ji7Qfb6qPnuYkuiF2E2UDmKUOppI8d9Ye7tjdhQVCy0izn55tBaB-U7UWdcvSK2i85sauyw3G0Gfads1Rvy5-CAe2paqyYATcDmGjpUNLoxbfv9KH1KmwRTNH6k1v4PyWYYnhbT39WfKMbBjSxVQRdi19cyJrULSWhjxaQfJHeWx5Z8Ev4bSPByBeQBFl2~4vqy0S5RypINsRSa3MZdbiAAyn5tr5slWR6QdoqY3qBQgBJFZppy-3iWkFqqKgSxCPundF8gdDLC5ddizl~KYcYKl42y9SGFHIukH-TZs8~em0~iahzsqWVRks3zRG~tlBcX2U3M2~OJs~C33-NKhyfZT7-XFBREvb8Szmd~p66jDxrwOnKaku-G6DyoQipJqIz4VHmY9-y5T8RrUcJcM-5lVoMpAAAA codevoid.i2p=tV-4GJjgYIoCDTTJ91nfDbhSnT8B2o3v-TUfHtiAAjJJdroCAEDbmJWFPUQJEEispvrjNe~fP7VAYkk9fAhSrmdBLtEGB3NUESdiZEPsDtKJBdxijPGb1erZF2Z6eYHoK-t5g7MWWTsgLz~4xn211Jpfa-T4pqL2tcjsa7ixsaMpHF8NXFrITdyxSJRPz8OnHYgDR~ULFyzroi255MpiSUBzGcUZEiQSFLHLhjT5D5tP~gfJirFnfgOHvzWBK9L7y91qY~gYvM2eDcxMxq4Ac1gw0JeahkzAk3j6Spco3LHW3bJvELopf1QmLFu3nfPaegH1Hejt9AhXEH~FV-~M9F1BePipcIYlm7nKyre3aVPLYDZSCvkUx~8nnD3HEpMijD8fdfqSFPU7aZQe19a7rZJUbX~a4M3rBDO-C4uAid6Uznb1tLu2XR1GVVITGHaLwmumImXjlU~1nEnluBQB6iBQPZ9xJccArlYgWSooR9gpyN93PwTPsPe5cPkxCFuxAAAA echelon.i2p=w6zK9m4fqSfvJck9EGIR1wRIbWsEQ2DkjZ-VI57ESFqLqbTIA1cD5nOfSSbpELqPyhjifdrNiBNAsSdyil3C0a2B7CGtwUcTS2dCG0tKf2nAbvpsbcCK17nI4Xbu5KqZU0y3hJ~l7rcJqQBR0nfV5cU30ZDrpQV6VL875cihGlnmwLFq6qSzNcEb88Nw6wFG~FIgB2PJ6A3jJyuTnLrdiMvwqgD6nSyeOylOgBCsNxXh8-drrhASjladfNrwjlGRCZTiQ~H92HIyOwiabDiG3TUugMaFWs87yuXnZ~ni9jgjoAMFo8xV8Od2BiRgCxkZoMU07FhgUjew9qtXNa04wkexf3gx77nVPhqE0GHqCuwHwmBVf92RdYEys76u~akaOMq5UhayDpCBCaHiYLkKDNqmh47tfMCwxf6z8VIcR4zv25QfJDIWPs~RA~9U7m4raytiAs5PvYZBn4B3SqOL8XdkL9sDT54sQXbsYCJr3olu6ieMtNWlmos0uohYXNUyAAAA +crstrack.i2p=b4G9sCdtfvccMAXh~SaZrPqVQNyGQbhbYMbw6supq2XGzbjU4NcOmjFI0vxQ8w1L05twmkOvg5QERcX6Mi8NQrWnR0stLExu2LucUXg1aYjnggxIR8TIOGygZVIMV3STKH4UQXD--wz0BUrqaLxPhrm2Eh9Hwc8TdB6Na4ShQUq5Xm8D4elzNUVdpM~RtChEyJWuQvoGAHY3ppX-EJJLkiSr1t77neS4Lc-KofMVmgI9a2tSSpNAagBiNI6Ak9L1T0F9uxeDfEG9bBSQPNMOSUbAoEcNxtt7xOW~cNOAyMyGydwPMnrQ5kIYPY8Pd3XudEko970vE0D6gO19yoBMJpKx6Dh50DGgybLQ9CpRaynh2zPULTHxm8rneOGRcQo8D3mE7FQ92m54~SvfjXjD2TwAVGI~ae~n9HDxt8uxOecAAvjjJ3TD4XM63Q9TmB38RmGNzNLDBQMEmJFpqQU8YeuhnS54IVdUoVQFqui5SfDeLXlSkh4vYoMU66pvBfWbAAAA From 3e7e5d6113f464280d864f64b6ee39a66b656381 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 13 Jan 2009 19:54:07 +0000 Subject: [PATCH 055/191] dont build sam tests by default --- apps/sam/java/build.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/sam/java/build.xml b/apps/sam/java/build.xml index 5f48bff98..bb692f2db 100644 --- a/apps/sam/java/build.xml +++ b/apps/sam/java/build.xml @@ -25,7 +25,14 @@ + + + @@ -38,6 +45,9 @@ + + + From 104cf8346e20e1d95f582649b515f08fb1947d88 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 13 Jan 2009 21:17:01 +0000 Subject: [PATCH 056/191] add .de thx echelon --- initialNews.xml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/initialNews.xml b/initialNews.xml index 34da38eb3..ffc8c414f 100644 --- a/initialNews.xml +++ b/initialNews.xml @@ -18,12 +18,35 @@ If you can, open up port 8887 on your firewall, then enable inbound TC
  • Once you have a "shared clients" destination listed on the left, please check out our -FAQ. +FAQ.
  • Point your IRC client to localhost:6668 and say hi to us on #i2p.
  • + + +

    Gratulation zur erfolgreichen Installation von I2P!

    +
      +
    • +Willkommen bei I2P! +Bitte noch etwas Geduld während I2P startet und weitere I2P Router findet. +
    • +
    • +Passe bitte In der Wartezeit deine Einstellungen zur Bandbreite auf der +Einstellungsseite an. +
    • +
    • +Bitte öffne sobald möglich den Port 8887 in deiner Firewall, aktiviere danach den eingehenden TCP Verkehr auf der Einstellungsseite. +
    • +
    • +Sobald auf der linken Seite eine "shared clients" Verbindung aufgelistet ist besuche bitte unsere FAQ. +
    • +
    • +Verbinde deinen IRC Klienten mit dem Server auf localhost:6668 und sage Hallo zu uns im Kanal #i2p. +
    • +
    + From 0ea532c72ed3e511951a58c8ecf52855f90722ab Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 14 Jan 2009 13:50:44 +0000 Subject: [PATCH 057/191] reduce initial RTT to 8s --- .../java/src/net/i2p/client/streaming/ConnectionOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionOptions.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionOptions.java index 6ad79ad15..4363e3f49 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionOptions.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionOptions.java @@ -56,7 +56,7 @@ public class ConnectionOptions extends I2PSocketOptionsImpl { private static final int TREND_COUNT = 3; static final int INITIAL_WINDOW_SIZE = 6; static final int DEFAULT_MAX_SENDS = 8; - public static final int DEFAULT_INITIAL_RTT = 10*1000; + public static final int DEFAULT_INITIAL_RTT = 8*1000; static final int MIN_WINDOW_SIZE = 1; /** From 011ded2ee42c660cdfb33cc5c2db42dc89517570 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 14 Jan 2009 17:06:52 +0000 Subject: [PATCH 058/191] -10 --- history.txt | 21 +++++++++++++++++++ .../src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index 92a044dc4..01c33f85c 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,24 @@ +2009-01-14 zzz + * config.jsp: Fix burst seconds display + * HTTPClient: Fix per-tunnel settings for i2cp.gzip and + i2ptunnel.httpclient.send* (thx tino) + * i2psnark: + - Fix double completion message + - Add crstrack + * initialNews.xml: Add .de (thx echelon) + * Message: Always distribute an inbound msg back out + a tunnel to foil a possible latency-measuring attack + (welterde) + * Naming: + - Change base32 names to *.b32.i2p + - Add i2p.naming.hostsTxt.useB32 config + * profiles.jsp: Remove 1m column + * SAM: Don't build tests by default + * Streaming: + - Prevent a rare NPE + - Reduce initial RTT to 8s (was 10s) + * tunnels.jsp: Add netdb links + 2009-01-08 zzz * addressbook: Prevent Base32 hostnames * build.xml: Remove readme_xx.html from updater diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index f495b1af9..485c48984 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = "0.6.5"; - public final static long BUILD = 9; + public final static long BUILD = 10; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From 416b0e4540b4f51cdf2d2f5bfab100cb81ddc750 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 17 Jan 2009 17:28:37 +0000 Subject: [PATCH 059/191] Prevent two NTCP Pumpers --- .../net/i2p/router/transport/CommSystemFacadeImpl.java | 7 ++++--- .../src/net/i2p/router/transport/ntcp/EventPumper.java | 7 +++++++ .../src/net/i2p/router/transport/ntcp/NTCPTransport.java | 8 ++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java index d20c97846..c94178004 100644 --- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java +++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java @@ -285,9 +285,10 @@ public class CommSystemFacadeImpl extends CommSystemFacade { _log.warn("Halting NTCP to change address"); t.stopListening(); newAddr.setOptions(newProps); - // Give NTCP Pumper time to stop so we don't end up with two... - // Need better way - try { Thread.sleep(5*1000); } catch (InterruptedException ie) {} + // Wait for NTCP Pumper to stop so we don't end up with two... + while (t.isAlive()) { + try { Thread.sleep(5*1000); } catch (InterruptedException ie) {} + } t.restartListening(newAddr); _log.warn("Changed NTCP Address and started up, address is now " + newAddr); return; diff --git a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java index d27a95172..9c75f5328 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java +++ b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java @@ -85,6 +85,13 @@ public class EventPumper implements Runnable { _selector.wakeup(); } + /** + * Selector can take quite a while to close after calling stopPumping() + */ + public boolean isAlive() { + return _alive || (_selector != null && _selector.isOpen()); + } + public void register(ServerSocketChannel chan) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Registering server socket channel"); synchronized (_wantsRegister) { _wantsRegister.add(chan); } diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java index 67b9296a9..4b5573917 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java @@ -432,6 +432,10 @@ public class NTCPTransport extends TransportImpl { return bindAddress(); } + public boolean isAlive() { + return _pumper.isAlive(); + } + private RouterAddress bindAddress() { if (_myAddress != null) { try { @@ -538,6 +542,10 @@ public class NTCPTransport extends TransportImpl { } } + /** + * This doesn't (completely) block, caller should check isAlive() + * before calling startListening() or restartListening() + */ public void stopListening() { if (_log.shouldLog(Log.DEBUG)) _log.debug("Stopping ntcp transport"); _pumper.stopPumping(); From 807f0665b1967c6264154b50b45895858f3c88e7 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 17 Jan 2009 17:31:00 +0000 Subject: [PATCH 060/191] tweak --- apps/routerconsole/jsp/config.jsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/routerconsole/jsp/config.jsp b/apps/routerconsole/jsp/config.jsp index 1c3d81518..132bd5315 100644 --- a/apps/routerconsole/jsp/config.jsp +++ b/apps/routerconsole/jsp/config.jsp @@ -75,7 +75,7 @@ with "SSU introductions" - peers who will relay a request from someone you don't know to your router for your router so that you can make an outbound connection to them. I2P will use these introductions automatically if it detects that the port is not forwarded (as shown by - the Status: OK (NAT) line), or you can manually require them here. + the Status: Firewalled line), or you can manually require them here. Users behind symmetric NATs, such as OpenBSD's pf, are not currently supported.


    From 72fd42ef9b5eff2772599b65ba5766173fc7f539 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 17 Jan 2009 17:35:14 +0000 Subject: [PATCH 061/191] -11 --- history.txt | 3 +++ router/java/src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index 01c33f85c..413e4a408 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,6 @@ +2009-01-17 zzz + * NTCP: Prevent two NTCP Pumpers + 2009-01-14 zzz * config.jsp: Fix burst seconds display * HTTPClient: Fix per-tunnel settings for i2cp.gzip and diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 485c48984..bbf270287 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = "0.6.5"; - public final static long BUILD = 10; + public final static long BUILD = 11; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From 8d891b99d17889ef2fea5bd51619c934acd70e86 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 20 Jan 2009 17:12:24 +0000 Subject: [PATCH 062/191] * Router: Add a keyring for decrypting leases * Routerconsole: Add configkeyring.jsp --- .../i2p/router/web/ConfigKeyringHandler.java | 55 ++++++++++ .../i2p/router/web/ConfigKeyringHelper.java | 36 ++++++ apps/routerconsole/jsp/configkeyring.jsp | 58 ++++++++++ apps/routerconsole/jsp/confignav.jsp | 2 + core/java/src/net/i2p/I2PAppContext.java | 22 ++++ core/java/src/net/i2p/util/KeyRing.java | 20 ++++ .../src/net/i2p/router/PersistentKeyRing.java | 103 ++++++++++++++++++ .../src/net/i2p/router/RouterContext.java | 18 +++ 8 files changed, 314 insertions(+) create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java create mode 100644 apps/routerconsole/jsp/configkeyring.jsp create mode 100644 core/java/src/net/i2p/util/KeyRing.java create mode 100644 router/java/src/net/i2p/router/PersistentKeyRing.java diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java new file mode 100644 index 000000000..09f0905bf --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java @@ -0,0 +1,55 @@ +package net.i2p.router.web; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; + +/** + * Support additions via B64 Destkey, B64 Desthash, or blahblah.i2p + */ +public class ConfigKeyringHandler extends FormHandler { + private String _peer; + private String _key; + + protected void processForm() { + if ("Add key".equals(_action)) { + if (_peer == null || _key == null) { + addFormError("You must enter a destination and a key"); + return; + } + Hash h = new Hash(); + try { + h.fromBase64(_peer); + } catch (DataFormatException dfe) {} + if (h.getData() == null) { + try { + Destination d = new Destination(); + d.fromBase64(_peer); + h = d.calculateHash(); + } catch (DataFormatException dfe) {} + } + if (h.getData() == null) { + Destination d = _context.namingService().lookup(_peer); + if (d != null) + h = d.calculateHash(); + } + SessionKey sk = new SessionKey(); + try { + sk.fromBase64(_key); + } catch (DataFormatException dfe) {} + if (h.getData() != null && sk.getData() != null) { + _context.keyRing().put(h, sk); + addFormNotice("Key for " + h.toBase64() + " added to keyring"); + } else { + addFormError("Invalid destination or key"); + } + } else { + addFormError("Unsupported"); + } + } + + public void setPeer(String peer) { _peer = peer; } + public void setKey(String peer) { _key = peer; } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java new file mode 100644 index 000000000..48bc15068 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java @@ -0,0 +1,36 @@ +package net.i2p.router.web; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; + +import net.i2p.router.RouterContext; + +public class ConfigKeyringHelper { + private RouterContext _context; + /** + * Configure this bean to query a particular router context + * + * @param contextId begging few characters of the routerHash, or null to pick + * the first one we come across. + */ + public void setContextId(String contextId) { + try { + _context = ContextHelper.getContext(contextId); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + public ConfigKeyringHelper() {} + + public String getSummary() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(4*1024); + try { + _context.keyRing().renderStatusHTML(new OutputStreamWriter(baos)); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + return new String(baos.toByteArray()); + } +} diff --git a/apps/routerconsole/jsp/configkeyring.jsp b/apps/routerconsole/jsp/configkeyring.jsp new file mode 100644 index 000000000..7dd8bf178 --- /dev/null +++ b/apps/routerconsole/jsp/configkeyring.jsp @@ -0,0 +1,58 @@ +<%@page contentType="text/html"%> +<%@page pageEncoding="UTF-8"%> + + + +I2P Router Console - config keyring + + + +<%@include file="nav.jsp" %> +<%@include file="summary.jsp" %> + +
    + <%@include file="confignav.jsp" %> + + + + " /> + + + + + + + " /> + +

    +

    Keyring

    + The router keyring is used to decrypt encrypted leaseSets. + The keyring may contain keys for local or remote encrypted destinations. +

    +

    + +
    + +
    + <% String prev = System.getProperty("net.i2p.router.web.ConfigKeyringHandler.nonce"); + if (prev != null) System.setProperty("net.i2p.router.web.ConfigKeyringHandler.noncePrev", prev); + System.setProperty("net.i2p.router.web.ConfigKeyringHandler.nonce", new java.util.Random().nextLong()+""); %> + " /> +

    Manual Keyring Addition

    + Enter keys for encrypted remote destinations here. + Keys for local destinations must be entered on the I2PTunnel page. +

    + +
    Dest. name, hash, or full key: + +
    Session Key: + +
    +
    +

    + + +
    + + + diff --git a/apps/routerconsole/jsp/confignav.jsp b/apps/routerconsole/jsp/confignav.jsp index b6a5ce6df..851ab79b5 100644 --- a/apps/routerconsole/jsp/confignav.jsp +++ b/apps/routerconsole/jsp/confignav.jsp @@ -10,6 +10,8 @@ %>Clients | <% } else { %>Clients | <% } if (request.getRequestURI().indexOf("configpeer.jsp") != -1) { %>Peers | <% } else { %>Peers | <% } + if (request.getRequestURI().indexOf("configkeyring.jsp") != -1) { + %>Keyring | <% } else { %>Keyring | <% } if (request.getRequestURI().indexOf("configlogging.jsp") != -1) { %>Logging | <% } else { %>Logging | <% } if (request.getRequestURI().indexOf("configstats.jsp") != -1) { diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java index 3e514ea18..6b3b0fd5b 100644 --- a/core/java/src/net/i2p/I2PAppContext.java +++ b/core/java/src/net/i2p/I2PAppContext.java @@ -24,6 +24,7 @@ import net.i2p.data.RoutingKeyGenerator; import net.i2p.stat.StatManager; import net.i2p.util.Clock; import net.i2p.util.FortunaRandomSource; +import net.i2p.util.KeyRing; import net.i2p.util.LogManager; import net.i2p.util.PooledRandomSource; import net.i2p.util.RandomSource; @@ -75,6 +76,7 @@ public class I2PAppContext { private RoutingKeyGenerator _routingKeyGenerator; private RandomSource _random; private KeyGenerator _keyGenerator; + protected KeyRing _keyRing; // overridden in RouterContext private volatile boolean _statManagerInitialized; private volatile boolean _sessionKeyManagerInitialized; private volatile boolean _namingServiceInitialized; @@ -91,6 +93,7 @@ public class I2PAppContext { private volatile boolean _routingKeyGeneratorInitialized; private volatile boolean _randomInitialized; private volatile boolean _keyGeneratorInitialized; + protected volatile boolean _keyRingInitialized; // used in RouterContext /** @@ -141,12 +144,14 @@ public class I2PAppContext { _elGamalEngine = null; _elGamalAESEngine = null; _logManager = null; + _keyRing = null; _statManagerInitialized = false; _sessionKeyManagerInitialized = false; _namingServiceInitialized = false; _elGamalEngineInitialized = false; _elGamalAESEngineInitialized = false; _logManagerInitialized = false; + _keyRingInitialized = false; } /** @@ -512,6 +517,23 @@ public class I2PAppContext { } } + /** + * Basic hash map + */ + public KeyRing keyRing() { + if (!_keyRingInitialized) + initializeKeyRing(); + return _keyRing; + } + + protected void initializeKeyRing() { + synchronized (this) { + if (_keyRing == null) + _keyRing = new KeyRing(); + _keyRingInitialized = true; + } + } + /** * [insert snarky comment here] * diff --git a/core/java/src/net/i2p/util/KeyRing.java b/core/java/src/net/i2p/util/KeyRing.java new file mode 100644 index 000000000..6bbfb38de --- /dev/null +++ b/core/java/src/net/i2p/util/KeyRing.java @@ -0,0 +1,20 @@ +package net.i2p.util; + +import java.io.IOException; +import java.io.Writer; + +import java.util.concurrent.ConcurrentHashMap; + +import net.i2p.data.Hash; +import net.i2p.data.SessionKey; + +/** + * simple + */ +public class KeyRing extends ConcurrentHashMap { + public KeyRing() { + super(0); + } + + public void renderStatusHTML(Writer out) throws IOException {} +} diff --git a/router/java/src/net/i2p/router/PersistentKeyRing.java b/router/java/src/net/i2p/router/PersistentKeyRing.java new file mode 100644 index 000000000..d02275ea2 --- /dev/null +++ b/router/java/src/net/i2p/router/PersistentKeyRing.java @@ -0,0 +1,103 @@ +package net.i2p.router; + +import java.io.IOException; +import java.io.Writer; + +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +import net.i2p.data.SessionKey; +import net.i2p.router.TunnelPoolSettings; +import net.i2p.util.KeyRing; + +/** + * ConcurrentHashMap with backing in the router.config file. + * router.keyring.key.{base64 hash, with = replaced with $}={base64 session key} + * Caution - not all HashMap methods are overridden. + */ +public class PersistentKeyRing extends KeyRing { + private RouterContext _ctx; + private static final String PROP_PFX = "router.keyring.key."; + + public PersistentKeyRing(RouterContext ctx) { + super(); + _ctx = ctx; + addFromProperties(); + } + + public SessionKey put(Hash h, SessionKey sk) { + SessionKey old = super.put(h, sk); + if (!sk.equals(old)) { + _ctx.router().setConfigSetting(PROP_PFX + h.toBase64().replace("=", "$"), + sk.toBase64()); + _ctx.router().saveConfig(); + } + return old; + } + + public SessionKey remove(Hash h) { + _ctx.router().removeConfigSetting(PROP_PFX + h.toBase64().replace("=", "$")); + _ctx.router().saveConfig(); + return super.remove(h); + } + + private void addFromProperties() { + for (Iterator iter = _ctx.getPropertyNames().iterator(); iter.hasNext(); ) { + String prop = (String) iter.next(); + if (!prop.startsWith(PROP_PFX)) + continue; + String key = _ctx.getProperty(prop); + if (key == null || key.length() != 44) + continue; + String hb = prop.substring(PROP_PFX.length()); + hb.replace("$", "="); + Hash dest = new Hash(); + SessionKey sk = new SessionKey(); + try { + dest.fromBase64(hb); + sk.fromBase64(key); + super.put(dest, sk); + } catch (DataFormatException dfe) { continue; } + } + } + + public void renderStatusHTML(Writer out) throws IOException { + StringBuffer buf = new StringBuffer(1024); + buf.append("\n"); + for (Entry e : entrySet()) { + buf.append("\n
    Destination HashName or Dest.Session Key
    "); + Hash h = e.getKey(); + buf.append(h.toBase64().substring(0, 6)).append("..."); + buf.append(""); + LeaseSet ls = _ctx.netDb().lookupLeaseSetLocally(h); + if (ls != null) { + Destination dest = ls.getDestination(); + if (_ctx.clientManager().isLocal(dest)) { + TunnelPoolSettings in = _ctx.tunnelManager().getInboundSettings(h); + if (in != null && in.getDestinationNickname() != null) + buf.append(in.getDestinationNickname()); + else + buf.append(dest.toBase64().substring(0, 6)).append("..."); + } else { + String host = _ctx.namingService().reverseLookup(dest); + if (host != null) + buf.append(host); + else + buf.append(dest.toBase64().substring(0, 6)).append("..."); + } + } + buf.append(""); + SessionKey sk = e.getValue(); + buf.append(sk.toBase64()); + } + buf.append("\n
    \n"); + out.write(buf.toString()); + out.flush(); + } +} diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index 087cde918..517a5ba35 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -26,6 +26,7 @@ import net.i2p.router.transport.VMCommSystem; import net.i2p.router.tunnel.TunnelDispatcher; import net.i2p.router.tunnel.pool.TunnelPoolManager; import net.i2p.util.Clock; +import net.i2p.util.KeyRing; /** * Build off the core I2P context to provide a root for a router instance to @@ -366,4 +367,21 @@ public class RouterContext extends I2PAppContext { } } + /** override to support storage in router.config */ + @Override + public KeyRing keyRing() { + if (!_keyRingInitialized) + initializeKeyRing(); + return _keyRing; + } + + @Override + protected void initializeKeyRing() { + synchronized (this) { + if (_keyRing == null) + _keyRing = new PersistentKeyRing(this); + _keyRingInitialized = true; + } + } + } From 0e2a4227ef653e4dbad4bb1daae4bb1f647abd28 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 20 Jan 2009 17:16:24 +0000 Subject: [PATCH 063/191] * LeaseSet: Add encrypt/decrypt methods --- core/java/src/net/i2p/data/LeaseSet.java | 172 ++++++++++++++++++++++- 1 file changed, 170 insertions(+), 2 deletions(-) diff --git a/core/java/src/net/i2p/data/LeaseSet.java b/core/java/src/net/i2p/data/LeaseSet.java index 7dd74a9d7..8a05dd956 100644 --- a/core/java/src/net/i2p/data/LeaseSet.java +++ b/core/java/src/net/i2p/data/LeaseSet.java @@ -9,6 +9,7 @@ package net.i2p.data; * */ +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -17,13 +18,34 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import net.i2p.I2PAppContext; import net.i2p.crypto.DSAEngine; import net.i2p.util.Clock; import net.i2p.util.Log; +import net.i2p.util.RandomSource; /** * Defines the set of leases a destination currently has. * + * Support encryption and decryption with a supplied key. + * Only the gateways and tunnel IDs in the individual + * leases are encrypted. + * + * Encrypted leases are not indicated as such. + * The only way to tell a lease is encrypted is to + * determine that the listed gateways do not exist. + * Routers wishing to decrypt a leaseset must have the + * desthash and key in their keyring. + * This is required for the local router as well, since + * the encryption is done on the client side of I2CP, the + * router must decrypt it back again for local usage + * (but not for transmission to the floodfills) + * + * Decrypted leases are only available through the getLease() + * method, so that storage and network transmission via + * writeBytes() will output the original encrypted + * leases and the original leaseset signature. + * * @author jrandom */ public class LeaseSet extends DataStructureImpl { @@ -40,6 +62,9 @@ public class LeaseSet extends DataStructureImpl { // Store these since isCurrent() and getEarliestLeaseDate() are called frequently private long _firstExpiration; private long _lastExpiration; + private List _decryptedLeases; + private boolean _decrypted; + private boolean _checked; /** This seems like plenty */ private final static int MAX_LEASES = 6; @@ -55,6 +80,8 @@ public class LeaseSet extends DataStructureImpl { _receivedAsPublished = false; _firstExpiration = Long.MAX_VALUE; _lastExpiration = 0; + _decrypted = false; + _checked = false; } public Destination getDestination() { @@ -104,11 +131,17 @@ public class LeaseSet extends DataStructureImpl { } public int getLeaseCount() { - return _leases.size(); + if (isEncrypted()) + return _leases.size() - 1; + else + return _leases.size(); } public Lease getLease(int index) { - return (Lease) _leases.get(index); + if (isEncrypted()) + return (Lease) _decryptedLeases.get(index); + else + return (Lease) _leases.get(index); } public Signature getSignature() { @@ -335,4 +368,139 @@ public class LeaseSet extends DataStructureImpl { buf.append("]"); return buf.toString(); } + + private static final int DATA_LEN = Hash.HASH_LENGTH + 4; + private static final int IV_LEN = 16; + + /** + * Encrypt the gateway and tunnel ID of each lease, leaving the expire dates unchanged. + * This adds an extra dummy lease, because AES data must be padded to 16 bytes. + * The fact that it is encrypted is not stored anywhere. + * Must be called after all the leases are in place, but before sign(). + */ + public void encrypt(SessionKey key) { + if (_log.shouldLog(Log.WARN)) + _log.warn("encrypting lease: " + _destination.calculateHash()); + try { + encryp(key); + } catch (DataFormatException dfe) { + _log.error("Error encrypting lease: " + _destination.calculateHash()); + } catch (IOException ioe) { + _log.error("Error encrypting lease: " + _destination.calculateHash()); + } + } + + /** + * - Put the {Gateway Hash, TunnelID} pairs for all the leases in a buffer + * - Pad with random data to a multiple of 16 bytes + * - Use the first part of the dest's public key as an IV + * - Encrypt + * - Pad with random data to a multiple of 36 bytes + * - Add an extra lease + * - Replace the Hash and TunnelID in each Lease + */ + private void encryp(SessionKey key) throws DataFormatException, IOException { + int size = _leases.size(); + if (size < 1 || size > MAX_LEASES-1) + throw new IllegalArgumentException("Bad number of leases for encryption"); + int datalen = ((DATA_LEN * size / 16) + 1) * 16; + ByteArrayOutputStream baos = new ByteArrayOutputStream(datalen); + for (int i = 0; i < size; i++) { + ((Lease)_leases.get(i)).getGateway().writeBytes(baos); + ((Lease)_leases.get(i)).getTunnelId().writeBytes(baos); + } + // pad out to multiple of 16 with random data before encryption + int padlen = datalen - (DATA_LEN * size); + byte[] pad = new byte[padlen]; + RandomSource.getInstance().nextBytes(pad); + baos.write(pad); + byte[] iv = new byte[IV_LEN]; + System.arraycopy(_destination.getPublicKey().getData(), 0, iv, 0, IV_LEN); + byte[] enc = new byte[DATA_LEN * (size + 1)]; + I2PAppContext.getGlobalContext().aes().encrypt(baos.toByteArray(), 0, enc, 0, key, iv, datalen); + // pad out to multiple of 36 with random data after encryption + // (even for 4 leases, where 36*4 is a multiple of 16, we add another, just to be consistent) + padlen = enc.length - datalen; + pad = new byte[padlen]; + RandomSource.getInstance().nextBytes(pad); + System.arraycopy(pad, 0, enc, datalen, padlen); + // add the padded lease... + Lease padLease = new Lease(); + padLease.setEndDate(((Lease)_leases.get(0)).getEndDate()); + _leases.add(padLease); + // ...and replace all the gateways and tunnel ids + ByteArrayInputStream bais = new ByteArrayInputStream(enc); + for (int i = 0; i < size+1; i++) { + Hash h = new Hash(); + h.readBytes(bais); + ((Lease)_leases.get(i)).setGateway(h); + TunnelId t = new TunnelId(); + t.readBytes(bais); + ((Lease)_leases.get(i)).setTunnelId(t); + } + } + + /** + * Decrypt the leases, except for the last one which is partially padding. + * Store the new decrypted leases in a backing store, + * and keep the original leases so that verify() still works and the + * encrypted leaseset can be sent on to others (via writeBytes()) + */ + private void decrypt(SessionKey key) throws DataFormatException, IOException { + if (_log.shouldLog(Log.WARN)) + _log.warn("decrypting lease: " + _destination.calculateHash()); + int size = _leases.size(); + if (size < 2) + throw new DataFormatException("Bad number of leases for decryption"); + int datalen = DATA_LEN * size; + ByteArrayOutputStream baos = new ByteArrayOutputStream(datalen); + for (int i = 0; i < size; i++) { + ((Lease)_leases.get(i)).getGateway().writeBytes(baos); + ((Lease)_leases.get(i)).getTunnelId().writeBytes(baos); + } + byte[] iv = new byte[IV_LEN]; + System.arraycopy(_destination.getPublicKey().getData(), 0, iv, 0, IV_LEN); + int enclen = ((DATA_LEN * (size - 1) / 16) + 1) * 16; + byte[] enc = new byte[enclen]; + System.arraycopy(baos.toByteArray(), 0, enc, 0, enclen); + byte[] dec = new byte[enclen]; + I2PAppContext.getGlobalContext().aes().decrypt(enc, 0, dec, 0, key, iv, enclen); + ByteArrayInputStream bais = new ByteArrayInputStream(dec); + _decryptedLeases = new ArrayList(size - 1); + for (int i = 0; i < size-1; i++) { + Lease l = new Lease(); + Hash h = new Hash(); + h.readBytes(bais); + l.setGateway(h); + TunnelId t = new TunnelId(); + t.readBytes(bais); + l.setTunnelId(t); + l.setEndDate(((Lease)_leases.get(i)).getEndDate()); + _decryptedLeases.add(l); + } + } + + /** + * @return true if it was encrypted, and we decrypted it successfully. + * Decrypts on first call. + */ + private synchronized boolean isEncrypted() { + if (_decrypted) + return true; + if (_checked || _destination == null) + return false; + SessionKey key = I2PAppContext.getGlobalContext().keyRing().get(_destination.calculateHash()); + if (key != null) { + try { + decrypt(key); + _decrypted = true; + } catch (DataFormatException dfe) { + _log.error("Error decrypting lease: " + _destination.calculateHash() + dfe); + } catch (IOException ioe) { + _log.error("Error decrypting lease: " + _destination.calculateHash() + ioe); + } + } + _checked = true; + return _decrypted; + } } From ab92206b773351f9d6b278d4fa2fee368a5f3e3e Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 20 Jan 2009 17:20:37 +0000 Subject: [PATCH 064/191] * Streaming: TCB control block sharing also tweak ResendPacketEvent to prepare for PacketQueue sending timeout to I2CP --- .../net/i2p/client/streaming/Connection.java | 57 +++++--- .../client/streaming/ConnectionManager.java | 6 + .../streaming/ConnectionPacketHandler.java | 4 + .../net/i2p/client/streaming/TCBShare.java | 137 ++++++++++++++++++ 4 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java index 73e7253ac..85872e9c5 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java @@ -45,6 +45,7 @@ public class Connection { private long _congestionWindowEnd; private long _highestAckedThrough; private boolean _isInbound; + private boolean _updatedShareOpts; /** Packet ID (Long) to PacketLocal for sent but unacked packets */ private Map _outboundPackets; private PacketQueue _outboundQueue; @@ -120,6 +121,7 @@ public class Connection { _activeResends = 0; _resetSentOn = -1; _isInbound = false; + _updatedShareOpts = false; _connectionEvent = new ConEvent(); _context.statManager().createRateStat("stream.con.windowSizeAtCongestion", "How large was our send window when we send a dup?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); _context.statManager().createRateStat("stream.chokeSizeBegin", "How many messages were outstanding when we started to choke?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); @@ -586,6 +588,8 @@ public class Connection { if (_remotePeerSet) throw new RuntimeException("Remote peer already set [" + _remotePeer + ", " + peer + "]"); _remotePeerSet = true; _remotePeer = peer; + // now that we know who the other end is, get the rtt etc. from the cache + _connectionManager.updateOptsFromShare(this); } private boolean _sendStreamIdSet = false; @@ -709,7 +713,13 @@ public class Connection { } public long getCloseReceivedOn() { return _closeReceivedOn; } public void setCloseReceivedOn(long when) { _closeReceivedOn = when; } - + + public void updateShareOpts() { + if (_closeSentOn > 0 && !_updatedShareOpts) { + _connectionManager.updateShareOpts(this); + _updatedShareOpts = true; + } + } public void incrementUnackedPacketsReceived() { _unackedPacketsReceived++; } public int getUnackedPacketsReceived() { return _unackedPacketsReceived; } /** how many packets have we sent but not yet received an ACK for? @@ -998,7 +1008,7 @@ public class Connection { /** * Coordinate the resends of a given packet */ - private class ResendPacketEvent implements SimpleTimer.TimedEvent { + public class ResendPacketEvent implements SimpleTimer.TimedEvent { private PacketLocal _packet; private long _nextSendTime; public ResendPacketEvent(PacketLocal packet, long sendTime) { @@ -1104,26 +1114,6 @@ public class Connection { _context.sessionKeyManager().failTags(_remotePeer.getPublicKey()); } - if (numSends - 1 <= _options.getMaxResends()) { - if (_log.shouldLog(Log.INFO)) - _log.info("Resend packet " + _packet + " time " + numSends + - " activeResends: " + _activeResends + - " (wsize " - + newWindowSize + " lifetime " - + (_context.clock().now() - _packet.getCreatedOn()) + "ms)"); - _outboundQueue.enqueue(_packet); - _lastSendTime = _context.clock().now(); - } - - // acked during resending (... or somethin') - if ( (_packet.getAckTime() > 0) && (_packet.getNumSends() > 1) ) { - _activeResends--; - synchronized (_outboundPackets) { - _outboundPackets.notifyAll(); - } - return true; - } - if (numSends - 1 > _options.getMaxResends()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Too many resends"); @@ -1137,11 +1127,32 @@ public class Connection { long timeout = rto << (numSends-1); if ( (timeout > MAX_RESEND_DELAY) || (timeout <= 0) ) timeout = MAX_RESEND_DELAY; + // set this before enqueue() as it passes it on to the router + _nextSendTime = timeout + _context.clock().now(); + + if (_log.shouldLog(Log.INFO)) + _log.info("Resend packet " + _packet + " time " + numSends + + " activeResends: " + _activeResends + + " (wsize " + + newWindowSize + " lifetime " + + (_context.clock().now() - _packet.getCreatedOn()) + "ms)"); + _outboundQueue.enqueue(_packet); + _lastSendTime = _context.clock().now(); + if (_log.shouldLog(Log.DEBUG)) _log.debug("Scheduling resend in " + timeout + "ms for " + _packet); RetransmissionTimer.getInstance().addEvent(ResendPacketEvent.this, timeout); - _nextSendTime = timeout + _context.clock().now(); } + + // acked during resending (... or somethin') + if ( (_packet.getAckTime() > 0) && (_packet.getNumSends() > 1) ) { + _activeResends--; + synchronized (_outboundPackets) { + _outboundPackets.notifyAll(); + } + return true; + } + return true; } else { //if (_log.shouldLog(Log.DEBUG)) diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java index da2b1ab12..7826ba2a8 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionManager.java @@ -30,6 +30,7 @@ public class ConnectionManager { private PacketQueue _outboundQueue; private SchedulerChooser _schedulerChooser; private ConnectionPacketHandler _conPacketHandler; + private TCBShare _tcbShare; /** Inbound stream ID (Long) to Connection map */ private Map _connectionByInboundId; /** Ping ID (Long) to PingRequest */ @@ -52,6 +53,7 @@ public class ConnectionManager { _connectionHandler = new ConnectionHandler(context, this); _schedulerChooser = new SchedulerChooser(context); _conPacketHandler = new ConnectionPacketHandler(context); + _tcbShare = new TCBShare(context); _session = session; session.setSessionListener(_messageHandler); _outboundQueue = new PacketQueue(context, session, this); @@ -127,6 +129,7 @@ public class ConnectionManager { */ public Connection receiveConnection(Packet synPacket) { Connection con = new Connection(_context, this, _schedulerChooser, _outboundQueue, _conPacketHandler, new ConnectionOptions(_defaultOptions)); + _tcbShare.updateOptsFromShare(con); con.setInbound(); long receiveId = _context.random().nextLong(Packet.MAX_STREAM_ID-1)+1; boolean reject = false; @@ -277,6 +280,8 @@ public class ConnectionManager { public ConnectionHandler getConnectionHandler() { return _connectionHandler; } public I2PSession getSession() { return _session; } public PacketQueue getPacketQueue() { return _outboundQueue; } + public void updateOptsFromShare(Connection con) { _tcbShare.updateOptsFromShare(con); } + public void updateShareOpts(Connection con) { _tcbShare.updateShareOpts(con); } /** * Something b0rked hard, so kill all of our connections without mercy. @@ -292,6 +297,7 @@ public class ConnectionManager { _connectionByInboundId.clear(); _connectionLock.notifyAll(); } + _tcbShare.stop(); } /** diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java index 6a062d4a6..7c445f038 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java @@ -213,6 +213,10 @@ public class ConnectionPacketHandler { packet.releasePayload(); } + // update the TCB Cache now that we've processed the acks and updated our rtt etc. + if (isNew && packet.isFlagSet(Packet.FLAG_CLOSE) && packet.isFlagSet(Packet.FLAG_SIGNATURE_INCLUDED)) + con.updateShareOpts(); + //if (choke) // con.fastRetransmit(); } diff --git a/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java b/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java new file mode 100644 index 000000000..1562f948e --- /dev/null +++ b/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java @@ -0,0 +1,137 @@ +package net.i2p.client.streaming; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import net.i2p.I2PAppContext; +import net.i2p.data.Destination; +import net.i2p.util.Log; +import net.i2p.util.SimpleTimer; + +/** + * Share important TCP Control Block parameters across Connections + * to the same remote peer. + * This is intended for "temporal" sharing at connection open/close time, + * not "ensemble" sharing during a connection. Ref. RFC 2140. + * + * There is a TCB share per ConnectionManager (i.e. per local Destination) + * so that there is no information leakage to other Destinations on the + * same router. + * + */ +public class TCBShare { + private I2PAppContext _context; + private Log _log; + private Map _cache; + private CleanEvent _cleaner; + + private static final long EXPIRE_TIME = 30*60*1000; + private static final long CLEAN_TIME = 10*60*1000; + private static final double RTT_DAMPENING = 0.75; + private static final double WDW_DAMPENING = 0.75; + private static final int MAX_RTT = ((int) Connection.MAX_RESEND_DELAY) / 2; + private static final int MAX_WINDOW_SIZE = Connection.MAX_WINDOW_SIZE / 4; + + public TCBShare(I2PAppContext ctx) { + _context = ctx; + _log = ctx.logManager().getLog(TCBShare.class); + _cache = new ConcurrentHashMap(4); + _cleaner = new CleanEvent(); + SimpleTimer.getInstance().addEvent(_cleaner, CLEAN_TIME); + } + + public void stop() { + SimpleTimer.getInstance().removeEvent(_cleaner); + } + + public void updateOptsFromShare(Connection con) { + Destination dest = con.getRemotePeer(); + if (dest == null) + return; + ConnectionOptions opts = con.getOptions(); + if (opts == null) + return; + Entry e = _cache.get(dest); + if (e == null || e.isExpired()) + return; + if (_log.shouldLog(Log.DEBUG)) + _log.debug("From cache: " + + con.getSession().getMyDestination().calculateHash().toBase64().substring(0, 4) + + '-' + + dest.calculateHash().toBase64().substring(0, 4) + + " RTT: " + e.getRTT() + " wdw: " + e.getWindowSize()); + opts.setRTT(e.getRTT()); + opts.setWindowSize(e.getWindowSize()); + } + + public void updateShareOpts(Connection con) { + Destination dest = con.getRemotePeer(); + if (dest == null) + return; + if (con.getAckedPackets() <= 0) + return; + ConnectionOptions opts = con.getOptions(); + if (opts == null) + return; + int old = -1; + int oldw = -1; + Entry e = _cache.get(dest); + if (e == null || e.isExpired()) { + e = new Entry(opts.getRTT(), opts.getWindowSize()); + _cache.put(dest, e); + } else { + old = e.getRTT(); + oldw = e.getWindowSize(); + e.setRTT(opts.getRTT()); + e.setWindowSize(opts.getWindowSize()); + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("To cache: " + + con.getSession().getMyDestination().calculateHash().toBase64().substring(0, 4) + + '-' + + dest.calculateHash().toBase64().substring(0, 4) + + " old: " + old + " con: " + opts.getRTT() + " new: " + e.getRTT() + + " oldw: " + oldw + " conw: " + opts.getWindowSize() + " neww: " + e.getWindowSize()); + } + + private class Entry { + int _rtt; + int _wdw; + long _updated; + + public Entry(int ms, int wdw) { + _rtt = ms; + _wdw = wdw; + _updated = _context.clock().now(); + } + public int getRTT() { return _rtt; } + public void setRTT(int ms) { + _rtt = (int)(RTT_DAMPENING*_rtt + (1-RTT_DAMPENING)*ms); + if (_rtt > MAX_RTT) + _rtt = MAX_RTT; + _updated = _context.clock().now(); + } + public int getWindowSize() { return _wdw; } + public void setWindowSize(int wdw) { + _wdw = (int)(0.5 + WDW_DAMPENING*_wdw + (1-WDW_DAMPENING)*wdw); + if (_wdw > MAX_WINDOW_SIZE) + _wdw = MAX_WINDOW_SIZE; + _updated = _context.clock().now(); + } + public boolean isExpired() { + return _updated < _context.clock().now() - EXPIRE_TIME; + } + } + + private class CleanEvent implements SimpleTimer.TimedEvent { + public CleanEvent() {} + public void timeReached() { + for (Iterator iter = _cache.keySet().iterator(); iter.hasNext(); ) { + if (_cache.get(iter.next()).isExpired()) + iter.remove(); + } + SimpleTimer.getInstance().addEvent(CleanEvent.this, CLEAN_TIME); + } + } +} From 6be54942ec9a07ca59de6ee4d223268710189a6e Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 20 Jan 2009 17:22:56 +0000 Subject: [PATCH 065/191] * Streaming, I2CP, Client Message sending: Pass message timeout through new I2CP message SendMessageExpiresMessage, so that the router uses the same expiration as the streaming lib. Should help reliability. * I2CP: Implement new I2CP message ReconfigureSessionMessage. Will be used for tunnel reduction. --- .../net/i2p/client/streaming/PacketQueue.java | 11 +- .../net/i2p/client/I2CPMessageProducer.java | 11 +- core/java/src/net/i2p/client/I2PSession.java | 1 + .../src/net/i2p/client/I2PSessionImpl.java | 8 +- .../src/net/i2p/client/I2PSessionImpl2.java | 30 +++-- .../client/RequestLeaseSetMessageHandler.java | 14 ++- .../net/i2p/data/i2cp/I2CPMessageHandler.java | 4 +- .../data/i2cp/ReconfigureSessionMessage.java | 103 +++++++++++++++ .../data/i2cp/SendMessageExpiresMessage.java | 117 ++++++++++++++++++ .../src/net/i2p/router/ClientMessage.java | 10 ++ .../router/client/ClientConnectionRunner.java | 6 +- .../net/i2p/router/client/ClientManager.java | 3 +- .../client/ClientMessageEventListener.java | 16 +++ .../OutboundClientMessageOneShotJob.java | 48 ++++--- 14 files changed, 343 insertions(+), 39 deletions(-) create mode 100644 core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java create mode 100644 core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java diff --git a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java index 2d22226d3..a56e7753d 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java @@ -82,7 +82,16 @@ class PacketQueue { // this should not block! begin = _context.clock().now(); - sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent); + long expires = 0; + Connection.ResendPacketEvent rpe = (Connection.ResendPacketEvent) packet.getResendEvent(); + if (rpe != null) + // we want the router to expire it a little before we do, + // so if we retransmit it will use a new tunnel/lease combo + expires = rpe.getNextSendTime() - 500; + if (expires > 0) + sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires); + else + sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent); end = _context.clock().now(); if ( (end-begin > 1000) && (_log.shouldLog(Log.WARN)) ) diff --git a/core/java/src/net/i2p/client/I2CPMessageProducer.java b/core/java/src/net/i2p/client/I2CPMessageProducer.java index 9af1fbd19..5b45ee7a3 100644 --- a/core/java/src/net/i2p/client/I2CPMessageProducer.java +++ b/core/java/src/net/i2p/client/I2CPMessageProducer.java @@ -9,6 +9,7 @@ package net.i2p.client; * */ +import java.util.Date; import java.util.Set; import net.i2p.I2PAppContext; @@ -28,6 +29,7 @@ import net.i2p.data.i2cp.DestroySessionMessage; import net.i2p.data.i2cp.MessageId; import net.i2p.data.i2cp.ReportAbuseMessage; import net.i2p.data.i2cp.SendMessageMessage; +import net.i2p.data.i2cp.SendMessageExpiresMessage; import net.i2p.data.i2cp.SessionConfig; import net.i2p.util.Log; @@ -91,8 +93,13 @@ class I2CPMessageProducer { * */ public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, SessionTag tag, - SessionKey key, Set tags, SessionKey newKey) throws I2PSessionException { - SendMessageMessage msg = new SendMessageMessage(); + SessionKey key, Set tags, SessionKey newKey, long expires) throws I2PSessionException { + SendMessageMessage msg; + if (expires > 0) { + msg = new SendMessageExpiresMessage(); + ((SendMessageExpiresMessage)msg).setExpiration(new Date(expires)); + } else + msg = new SendMessageMessage(); msg.setDestination(dest); msg.setSessionId(session.getSessionId()); msg.setNonce(nonce); diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java index 627d1775a..d8c64f222 100644 --- a/core/java/src/net/i2p/client/I2PSession.java +++ b/core/java/src/net/i2p/client/I2PSession.java @@ -70,6 +70,7 @@ public interface I2PSession { */ public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException; public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent) throws I2PSessionException; + public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expire) throws I2PSessionException; /** Receive a message that the router has notified the client about, returning * the payload. diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index 78f4ba763..a57957107 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -550,10 +550,10 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa * Pass off the error to the listener */ void propogateError(String msg, Throwable error) { - if (_log.shouldLog(Log.WARN)) - _log.warn(getPrefix() + "Error occurred: " + msg + " - " + error.getMessage()); - if (_log.shouldLog(Log.WARN)) - _log.warn(getPrefix() + " cause", error); + if (_log.shouldLog(Log.ERROR)) + _log.error(getPrefix() + "Error occurred: " + msg + " - " + error.getMessage()); + if (_log.shouldLog(Log.ERROR)) + _log.error(getPrefix() + " cause", error); if (_sessionListener != null) _sessionListener.errorOccurred(this, msg, error); } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java index 81c6ef22f..6a90952a5 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl2.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java @@ -107,15 +107,19 @@ class I2PSessionImpl2 extends I2PSessionImpl { return sendMessage(dest, payload, 0, payload.length); } public boolean sendMessage(Destination dest, byte[] payload, int offset, int size) throws I2PSessionException { - return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64)); + return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64), 0); } @Override public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException { - return sendMessage(dest, payload, 0, payload.length, keyUsed, tagsSent); + return sendMessage(dest, payload, 0, payload.length, keyUsed, tagsSent, 0); } public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent) throws I2PSessionException { + return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0); + } + public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expires) + throws I2PSessionException { if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message"); if (isClosed()) throw new I2PSessionException("Already closed"); @@ -142,7 +146,7 @@ class I2PSessionImpl2 extends I2PSessionImpl { } _context.statManager().addRateData("i2cp.tx.msgCompressed", compressed, 0); _context.statManager().addRateData("i2cp.tx.msgExpanded", size, 0); - return sendBestEffort(dest, payload, keyUsed, tagsSent); + return sendBestEffort(dest, payload, keyUsed, tagsSent, expires); } /** @@ -168,7 +172,7 @@ class I2PSessionImpl2 extends I2PSessionImpl { private static final int NUM_TAGS = 50; - private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent) + private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent, long expires) throws I2PSessionException { SessionKey key = null; SessionKey newKey = null; @@ -176,6 +180,7 @@ class I2PSessionImpl2 extends I2PSessionImpl { Set sentTags = null; int oldTags = 0; long begin = _context.clock().now(); + /*********** if (I2CPMessageProducer.END_TO_END_CRYPTO) { if (_log.shouldLog(Log.DEBUG)) _log.debug("begin sendBestEffort"); key = _context.sessionKeyManager().getCurrentKey(dest.getPublicKey()); @@ -220,6 +225,7 @@ class I2PSessionImpl2 extends I2PSessionImpl { } else { // not using end to end crypto, so don't ever bundle any tags } + **********/ if (_log.shouldLog(Log.DEBUG)) _log.debug("before creating nonce"); @@ -233,14 +239,14 @@ class I2PSessionImpl2 extends I2PSessionImpl { if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Setting key = " + key); if (keyUsed != null) { - if (I2CPMessageProducer.END_TO_END_CRYPTO) { - if (newKey != null) - keyUsed.setData(newKey.getData()); - else - keyUsed.setData(key.getData()); - } else { + //if (I2CPMessageProducer.END_TO_END_CRYPTO) { + // if (newKey != null) + // keyUsed.setData(newKey.getData()); + // else + // keyUsed.setData(key.getData()); + //} else { keyUsed.setData(SessionKey.INVALID_KEY.getData()); - } + //} } if (tagsSent != null) { if (sentTags != null) { @@ -261,7 +267,7 @@ class I2PSessionImpl2 extends I2PSessionImpl { + state.getNonce() + " for best effort " + " sync took " + (inSendingSync-beforeSendingSync) + " add took " + (afterSendingSync-inSendingSync)); - _producer.sendMessage(this, dest, nonce, payload, tag, key, sentTags, newKey); + _producer.sendMessage(this, dest, nonce, payload, tag, key, sentTags, newKey, expires); // since this is 'best effort', all we're waiting for is a status update // saying that the router received it - in theory, that should come back diff --git a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java index 7d6d816c1..6163771e3 100644 --- a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java +++ b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java @@ -21,6 +21,7 @@ import net.i2p.data.Lease; import net.i2p.data.LeaseSet; import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; +import net.i2p.data.SessionKey; import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPublicKey; import net.i2p.data.i2cp.I2CPMessage; @@ -78,6 +79,17 @@ class RequestLeaseSetMessageHandler extends HandlerImpl { leaseSet.setEncryptionKey(li.getPublicKey()); leaseSet.setSigningKey(li.getSigningPublicKey()); + String sk = session.getOptions().getProperty("i2cp.sessionKey"); + if (sk != null) { + SessionKey key = new SessionKey(); + try { + key.fromBase64(sk); + leaseSet.encrypt(key); + _context.keyRing().put(session.getMyDestination().calculateHash(), key); + } catch (DataFormatException dfe) { + _log.error("Bad session key: " + sk); + } + } try { leaseSet.sign(session.getPrivateKey()); session.getProducer().createLeaseSet(session, leaseSet, li.getSigningPrivateKey(), li.getPrivateKey()); @@ -137,4 +149,4 @@ class RequestLeaseSetMessageHandler extends HandlerImpl { && DataHelper.eq(_signingPrivKey, li.getSigningPrivateKey()); } } -} \ No newline at end of file +} diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java index 128c312dc..15045028a 100644 --- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java @@ -18,7 +18,7 @@ import net.i2p.data.DataHelper; import net.i2p.util.Log; /** - * Handle messages from the server for the client + * Handle messages from the server for the client or vice versa * */ public class I2CPMessageHandler { @@ -75,6 +75,8 @@ public class I2CPMessageHandler { return new RequestLeaseSetMessage(); case SendMessageMessage.MESSAGE_TYPE: return new SendMessageMessage(); + case SendMessageExpiresMessage.MESSAGE_TYPE: + return new SendMessageExpiresMessage(); case SessionStatusMessage.MESSAGE_TYPE: return new SessionStatusMessage(); case GetDateMessage.MESSAGE_TYPE: diff --git a/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java b/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java new file mode 100644 index 000000000..7165f6d32 --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/ReconfigureSessionMessage.java @@ -0,0 +1,103 @@ +package net.i2p.data.i2cp; + +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +/** + * Defines the message a client sends to a router when + * updating the config on an existing session. + * + * @author zzz + */ +public class ReconfigureSessionMessage extends I2CPMessageImpl { + private final static Log _log = new Log(ReconfigureSessionMessage.class); + public final static int MESSAGE_TYPE = 2; + private SessionId _sessionId; + private SessionConfig _sessionConfig; + + public ReconfigureSessionMessage() { + _sessionId = null; + _sessionConfig = null; + } + + public SessionId getSessionId() { + return _sessionId; + } + + public void setSessionId(SessionId id) { + _sessionId = id; + } + + public SessionConfig getSessionConfig() { + return _sessionConfig; + } + + public void setSessionConfig(SessionConfig config) { + _sessionConfig = config; + } + + @Override + protected void doReadMessage(InputStream in, int size) throws I2CPMessageException, IOException { + try { + _sessionId = new SessionId(); + _sessionId.readBytes(in); + _sessionConfig = new SessionConfig(); + _sessionConfig.readBytes(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + @Override + protected byte[] doWriteMessage() throws I2CPMessageException, IOException { + if (_sessionId == null || _sessionConfig == null) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + ByteArrayOutputStream os = new ByteArrayOutputStream(64); + try { + _sessionId.writeBytes(os); + _sessionConfig.writeBytes(os); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing out the message data", dfe); + } + return os.toByteArray(); + } + + public int getType() { + return MESSAGE_TYPE; + } + + @Override + public boolean equals(Object object) { + if ((object != null) && (object instanceof ReconfigureSessionMessage)) { + ReconfigureSessionMessage msg = (ReconfigureSessionMessage) object; + return DataHelper.eq(getSessionId(), msg.getSessionId()) + && DataHelper.eq(getSessionConfig(), msg.getSessionConfig()); + } + + return false; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[ReconfigureSessionMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("\n\tSessionConfig: ").append(getSessionConfig()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java b/core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java new file mode 100644 index 000000000..d15c1979c --- /dev/null +++ b/core/java/src/net/i2p/data/i2cp/SendMessageExpiresMessage.java @@ -0,0 +1,117 @@ +package net.i2p.data.i2cp; + +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; + +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Payload; +import net.i2p.util.Log; + +/** + * Same as SendMessageMessage, but with an expiration to be passed to the router + * + * @author zzz + */ +public class SendMessageExpiresMessage extends SendMessageMessage { + private final static Log _log = new Log(SendMessageExpiresMessage.class); + public final static int MESSAGE_TYPE = 36; + private SessionId _sessionId; + private Destination _destination; + private Payload _payload; + private Date _expiration; + + public SendMessageExpiresMessage() { + super(); + setExpiration(null); + } + + public Date getExpiration() { + return _expiration; + } + + public void setExpiration(Date d) { + _expiration = d; + } + + /** + * Read the body into the data structures + * + * @throws IOException + */ + @Override + public void readMessage(InputStream in, int length, int type) throws I2CPMessageException, IOException { + super.readMessage(in, length, type); + + try { + _expiration = DataHelper.readDate(in); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Unable to load the message data", dfe); + } + } + + /** + * Write out the full message to the stream, including the 4 byte size and 1 + * byte type header. Override the parent so we can be more mem efficient + * + * @throws IOException + */ + @Override + public void writeMessage(OutputStream out) throws I2CPMessageException, IOException { + if ((getSessionId() == null) || (getDestination() == null) || (getPayload() == null) || (getNonce() <= 0) || (_expiration == null)) + throw new I2CPMessageException("Unable to write out the message as there is not enough data"); + int len = 2 + getDestination().size() + getPayload().getSize() + 4 + 4 + DataHelper.DATE_LENGTH; + + try { + DataHelper.writeLong(out, 4, len); + DataHelper.writeLong(out, 1, getType()); + getSessionId().writeBytes(out); + getDestination().writeBytes(out); + getPayload().writeBytes(out); + DataHelper.writeLong(out, 4, getNonce()); + DataHelper.writeDate(out, _expiration); + } catch (DataFormatException dfe) { + throw new I2CPMessageException("Error writing the msg", dfe); + } + } + + public int getType() { + return MESSAGE_TYPE; + } + + @Override + public boolean equals(Object object) { + if ((object != null) && (object instanceof SendMessageExpiresMessage)) { + SendMessageExpiresMessage msg = (SendMessageExpiresMessage) object; + return super.equals(object) + && DataHelper.eq(getExpiration(), msg.getExpiration()); + } + + return false; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append("[SendMessageMessage: "); + buf.append("\n\tSessionId: ").append(getSessionId()); + buf.append("\n\tNonce: ").append(getNonce()); + buf.append("\n\tDestination: ").append(getDestination()); + buf.append("\n\tExpiration: ").append(getExpiration()); + buf.append("\n\tPayload: ").append(getPayload()); + buf.append("]"); + return buf.toString(); + } +} diff --git a/router/java/src/net/i2p/router/ClientMessage.java b/router/java/src/net/i2p/router/ClientMessage.java index 005f69a2d..ec7820d69 100644 --- a/router/java/src/net/i2p/router/ClientMessage.java +++ b/router/java/src/net/i2p/router/ClientMessage.java @@ -27,6 +27,7 @@ public class ClientMessage { private SessionConfig _senderConfig; private Hash _destinationHash; private MessageId _messageId; + private long _expiration; public ClientMessage() { setPayload(null); @@ -36,6 +37,7 @@ public class ClientMessage { setSenderConfig(null); setDestinationHash(null); setMessageId(null); + setExpiration(0); } /** @@ -91,4 +93,12 @@ public class ClientMessage { */ public SessionConfig getSenderConfig() { return _senderConfig; } public void setSenderConfig(SessionConfig config) { _senderConfig = config; } + + /** + * Expiration requested by the client that sent the message. This will only be available + * for locally originated messages. + * + */ + public long getExpiration() { return _expiration; } + public void setExpiration(long e) { _expiration = e; } } diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java index 544badcad..133ad142c 100644 --- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java +++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java @@ -29,6 +29,7 @@ import net.i2p.data.i2cp.I2CPMessageReader; import net.i2p.data.i2cp.MessageId; import net.i2p.data.i2cp.MessageStatusMessage; import net.i2p.data.i2cp.SendMessageMessage; +import net.i2p.data.i2cp.SendMessageExpiresMessage; import net.i2p.data.i2cp.SessionConfig; import net.i2p.data.i2cp.SessionId; import net.i2p.router.Job; @@ -270,6 +271,9 @@ public class ClientConnectionRunner { Destination dest = message.getDestination(); MessageId id = new MessageId(); id.setMessageId(getNextMessageId()); + long expiration = 0; + if (message instanceof SendMessageExpiresMessage) + expiration = ((SendMessageExpiresMessage) message).getExpiration().getTime(); long beforeLock = _context.clock().now(); long inLock = 0; synchronized (_acceptedPending) { @@ -291,7 +295,7 @@ public class ClientConnectionRunner { // the following blocks as described above SessionConfig cfg = _config; if (cfg != null) - _manager.distributeMessage(cfg.getDestination(), dest, payload, id); + _manager.distributeMessage(cfg.getDestination(), dest, payload, id, expiration); long timeToDistribute = _context.clock().now() - beforeDistribute; if (_log.shouldLog(Log.DEBUG)) _log.warn("Time to distribute in the manager to " diff --git a/router/java/src/net/i2p/router/client/ClientManager.java b/router/java/src/net/i2p/router/client/ClientManager.java index d9838ef7b..18c9c7742 100644 --- a/router/java/src/net/i2p/router/client/ClientManager.java +++ b/router/java/src/net/i2p/router/client/ClientManager.java @@ -140,7 +140,7 @@ public class ClientManager { } } - void distributeMessage(Destination fromDest, Destination toDest, Payload payload, MessageId msgId) { + void distributeMessage(Destination fromDest, Destination toDest, Payload payload, MessageId msgId, long expiration) { // check if there is a runner for it ClientConnectionRunner runner = getRunner(toDest); if (runner != null) { @@ -168,6 +168,7 @@ public class ClientManager { msg.setSenderConfig(runner.getConfig()); msg.setFromDestination(runner.getConfig().getDestination()); msg.setMessageId(msgId); + msg.setExpiration(expiration); _ctx.clientMessagePool().add(msg, true); } } diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java index 033e28f2f..d36d26401 100644 --- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -21,7 +21,9 @@ import net.i2p.data.i2cp.MessageId; import net.i2p.data.i2cp.MessagePayloadMessage; import net.i2p.data.i2cp.ReceiveMessageBeginMessage; import net.i2p.data.i2cp.ReceiveMessageEndMessage; +import net.i2p.data.i2cp.ReconfigureSessionMessage; import net.i2p.data.i2cp.SendMessageMessage; +import net.i2p.data.i2cp.SendMessageExpiresMessage; import net.i2p.data.i2cp.SessionId; import net.i2p.data.i2cp.SessionStatusMessage; import net.i2p.data.i2cp.SetDateMessage; @@ -67,6 +69,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi case SendMessageMessage.MESSAGE_TYPE: handleSendMessage(reader, (SendMessageMessage)message); break; + case SendMessageExpiresMessage.MESSAGE_TYPE: + handleSendMessage(reader, (SendMessageExpiresMessage)message); + break; case ReceiveMessageBeginMessage.MESSAGE_TYPE: handleReceiveBegin(reader, (ReceiveMessageBeginMessage)message); break; @@ -237,6 +242,17 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi _context.jobQueue().addJob(new LookupDestJob(_context, _runner, message.getHash())); } + /** + * Message's Session ID ignored. This doesn't support removing previously set options. + * Nor do we bother with message.getSessionConfig().verifySignature() ... should we? + * + */ + private void handleReconfigureSession(I2CPMessageReader reader, ReconfigureSessionMessage message) { + if (_log.shouldLog(Log.INFO)) + _log.info("Updating options - session " + _runner.getSessionId()); + _runner.getConfig().getOptions().putAll(message.getSessionConfig().getOptions()); + } + // this *should* be mod 65536, but UnsignedInteger is still b0rked. FIXME private final static int MAX_SESSION_ID = 32767; diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java index ccef8192a..e7c369b1e 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java @@ -69,6 +69,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { */ public final static String OVERALL_TIMEOUT_MS_PARAM = "clientMessageTimeout"; private final static long OVERALL_TIMEOUT_MS_DEFAULT = 60*1000; + private final static long OVERALL_TIMEOUT_MS_MIN = 5*1000; /** priority of messages, that might get honored some day... */ private final static int SEND_PRIORITY = 500; @@ -125,23 +126,34 @@ public class OutboundClientMessageOneShotJob extends JobImpl { _to = msg.getDestination(); _toString = _to.calculateHash().toBase64().substring(0,4); _leaseSetLookupBegin = -1; - - String param = msg.getSenderConfig().getOptions().getProperty(OVERALL_TIMEOUT_MS_PARAM); - if (param == null) - param = ctx.router().getConfigSetting(OVERALL_TIMEOUT_MS_PARAM); - if (param != null) { - try { - timeoutMs = Long.parseLong(param); - } catch (NumberFormatException nfe) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Invalid client message timeout specified [" + param - + "], defaulting to " + OVERALL_TIMEOUT_MS_DEFAULT, nfe); - timeoutMs = OVERALL_TIMEOUT_MS_DEFAULT; - } - } - _start = getContext().clock().now(); - _overallExpiration = timeoutMs + _start; + + // use expiration requested by client if available, otherwise session config, + // otherwise router config, otherwise default + _overallExpiration = msg.getExpiration(); + if (_overallExpiration > 0) { + _overallExpiration = Math.max(_overallExpiration, _start + OVERALL_TIMEOUT_MS_MIN); + _overallExpiration = Math.min(_overallExpiration, _start + OVERALL_TIMEOUT_MS_DEFAULT); + if (_log.shouldLog(Log.WARN)) + _log.warn("Message Expiration (ms): " + (_overallExpiration - _start)); + } else { + String param = msg.getSenderConfig().getOptions().getProperty(OVERALL_TIMEOUT_MS_PARAM); + if (param == null) + param = ctx.router().getConfigSetting(OVERALL_TIMEOUT_MS_PARAM); + if (param != null) { + try { + timeoutMs = Long.parseLong(param); + } catch (NumberFormatException nfe) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Invalid client message timeout specified [" + param + + "], defaulting to " + OVERALL_TIMEOUT_MS_DEFAULT, nfe); + timeoutMs = OVERALL_TIMEOUT_MS_DEFAULT; + } + } + _overallExpiration = timeoutMs + _start; + if (_log.shouldLog(Log.WARN)) + _log.warn("Default Expiration (ms): " + timeoutMs); + } _finished = false; } @@ -445,6 +457,10 @@ public class OutboundClientMessageOneShotJob extends JobImpl { } boolean wantACK = true; int existingTags = GarlicMessageBuilder.estimateAvailableTags(getContext(), _leaseSet.getEncryptionKey()); + // what's the point of 5% random? possible improvements or replacements: + // - wantACK if we changed their inbound lease + // - wantACK if we changed our outbound tunnel (requires moving selectOutboundTunnel() before this) + // - wantACK if we haven't in last 1m (requires a new static cache probably) if ( (existingTags > 30) && (getContext().random().nextInt(100) >= 5) ) wantACK = false; From c620420a6f86537913657102141fee764a48cc38 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 20 Jan 2009 17:24:28 +0000 Subject: [PATCH 066/191] * I2PTunnel Edit Pages: - Change default length to 2+0 - Cleanup helper code - Stub out the following new options (C=client, S=server): + Access list (S) + Certificate type (S) + Encrypted LeaseSet (S) + New dest on idle restart (C) + Tunnel closure on idle (C) + Tunnel reduction on idle (C,S) --- .../src/net/i2p/i2ptunnel/web/EditBean.java | 188 ++++++++---------- apps/i2ptunnel/jsp/editClient.jsp | 64 +++++- apps/i2ptunnel/jsp/editServer.jsp | 134 ++++++++++++- 3 files changed, 276 insertions(+), 110 deletions(-) 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 90e5f7e20..06a46b701 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -8,9 +8,12 @@ package net.i2p.i2ptunnel.web; * */ +import java.util.Arrays; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.StringTokenizer; import net.i2p.i2ptunnel.TunnelController; @@ -82,119 +85,100 @@ public class EditBean extends IndexBean { } public boolean shouldDelay(int tunnel) { - TunnelController tun = getController(tunnel); - if (tun != null) { - Properties opts = getOptions(tun); - if (opts != null) { - String delay = opts.getProperty("i2p.streaming.connectDelay"); - if ( (delay == null) || ("0".equals(delay)) ) - return false; - else - return true; - } else { - return false; - } - } else { - return false; - } + return getProperty(tunnel, "i2p.streaming.connectDelay", 0) > 0; } public boolean isInteractive(int tunnel) { - TunnelController tun = getController(tunnel); - if (tun != null) { - Properties opts = getOptions(tun); - if (opts != null) { - String wsiz = opts.getProperty("i2p.streaming.maxWindowSize"); - if ( (wsiz == null) || (!"1".equals(wsiz)) ) - return false; - else - return true; - } else { - return false; - } - } else { - return false; - } + return getProperty(tunnel, "i2p.streaming.maxWindowSize", 128) == 12; } public int getTunnelDepth(int tunnel, int defaultLength) { - TunnelController tun = getController(tunnel); - if (tun != null) { - Properties opts = getOptions(tun); - if (opts != null) { - String len = opts.getProperty("inbound.length"); - if (len == null) return defaultLength; - try { - return Integer.parseInt(len); - } catch (NumberFormatException nfe) { - return defaultLength; - } - } else { - return defaultLength; - } - } else { - return defaultLength; - } + return getProperty(tunnel, "inbound.length", defaultLength); } public int getTunnelQuantity(int tunnel, int defaultQuantity) { - TunnelController tun = getController(tunnel); - if (tun != null) { - Properties opts = getOptions(tun); - if (opts != null) { - String len = opts.getProperty("inbound.quantity"); - if (len == null) return defaultQuantity; - try { - return Integer.parseInt(len); - } catch (NumberFormatException nfe) { - return defaultQuantity; - } - } else { - return defaultQuantity; - } - } else { - return defaultQuantity; - } + return getProperty(tunnel, "inbound.quantity", defaultQuantity); } public int getTunnelBackupQuantity(int tunnel, int defaultBackupQuantity) { - TunnelController tun = getController(tunnel); - if (tun != null) { - Properties opts = getOptions(tun); - if (opts != null) { - String len = opts.getProperty("inbound.backupQuantity"); - if (len == null) return defaultBackupQuantity; - try { - return Integer.parseInt(len); - } catch (NumberFormatException nfe) { - return defaultBackupQuantity; - } - } else { - return defaultBackupQuantity; - } - } else { - return defaultBackupQuantity; - } + return getProperty(tunnel, "inbound.backupQuantity", defaultBackupQuantity); } public int getTunnelVariance(int tunnel, int defaultVariance) { + return getProperty(tunnel, "inbound.lengthVariance", defaultVariance); + } + + public boolean getReduce(int tunnel) { + return false; + } + + public int getReduceCount(int tunnel) { + return getProperty(tunnel, "inbound.reduceQuantity", 1); + } + + public int getReduceTime(int tunnel) { + return getProperty(tunnel, "reduceIdleTime", 20); + } + + public int getCert(int tunnel) { + return 0; + } + + public int getEffort(int tunnel) { + return 23; + } + + public String getSigner(int tunnel) { + return ""; + } + + public boolean getEncrypt(int tunnel) { + return false; + } + + public String getEncryptKey(int tunnel) { + return getProperty(tunnel, "encryptKey", ""); + } + + public boolean getAccess(int tunnel) { + return false; + } + + public String getAccessList(int tunnel) { + return getProperty(tunnel, "accessList", ""); + } + + public boolean getClose(int tunnel) { + return false; + } + + public boolean getNewDest(int tunnel) { + return false; + } + + private int getProperty(int tunnel, String prop, int def) { TunnelController tun = getController(tunnel); if (tun != null) { Properties opts = getOptions(tun); if (opts != null) { - String len = opts.getProperty("inbound.lengthVariance"); - if (len == null) return defaultVariance; + String s = opts.getProperty(prop); + if (s == null) return def; try { - return Integer.parseInt(len); - } catch (NumberFormatException nfe) { - return defaultVariance; - } - } else { - return defaultVariance; + return Integer.parseInt(s); + } catch (NumberFormatException nfe) {} } - } else { - return defaultVariance; } + return def; + } + + private String getProperty(int tunnel, String prop, String def) { + TunnelController tun = getController(tunnel); + if (tun != null) { + Properties opts = getOptions(tun); + if (opts != null) + return opts.getProperty(prop, def); + } + return def; } public String getI2CPHost(int tunnel) { @@ -213,6 +197,14 @@ public class EditBean extends IndexBean { return "7654"; } + private static final String noShowProps[] = { + "inbound.length", "outbound.length", "inbound.lengthVariance", "outbound.lengthVariance", + "inbound.backupQuantity", "outbound.backupQuantity", "inbound.quantity", "outbound.quantity", + "inbound.nickname", "outbound.nickname", "i2p.streaming.connectDelay", "i2p.streaming.maxWindowSize" + }; + private static final Set noShowSet = new HashSet(noShowProps.length); + static { noShowSet.addAll(Arrays.asList(noShowProps)); } + public String getCustomOptions(int tunnel) { TunnelController tun = getController(tunnel); if (tun != null) { @@ -222,19 +214,9 @@ public class EditBean extends IndexBean { int i = 0; for (Iterator iter = opts.keySet().iterator(); iter.hasNext(); ) { String key = (String)iter.next(); + if (noShowSet.contains(key)) + continue; String val = opts.getProperty(key); - if ("inbound.length".equals(key)) continue; - if ("outbound.length".equals(key)) continue; - if ("inbound.lengthVariance".equals(key)) continue; - if ("outbound.lengthVariance".equals(key)) continue; - if ("inbound.backupQuantity".equals(key)) continue; - if ("outbound.backupQuantity".equals(key)) continue; - if ("inbound.quantity".equals(key)) continue; - if ("outbound.quantity".equals(key)) continue; - if ("inbound.nickname".equals(key)) continue; - if ("outbound.nickname".equals(key)) continue; - if ("i2p.streaming.connectDelay".equals(key)) continue; - if ("i2p.streaming.maxWindowSize".equals(key)) continue; if (i != 0) buf.append(' '); buf.append(key).append('=').append(val); i++; diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index 97f8adbe5..2f06e1779 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -207,10 +207,10 @@
    + +
    +
    +
    + +
    + +
    +
    + + class="tickbox" /> +
    +
    + + class="tickbox" /> +
    +
    + + +
    + +
    +
    +
    + +
    + +
    +
    + + class="tickbox" /> +
    +
    + + +
    +
    + + +

    @@ -284,8 +340,10 @@
    diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index 0cdf5e0e9..5b7ad9105 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -177,12 +177,12 @@ Variance: class="tickbox" /> +
    +
    + + + (Users will require this key) +
    + +
    +
    +
    + +
    + +
    +
    + + class="tickbox" /> +
    +
    + + + (Restrict to these clients only) +
    + +
    +
    +
    + +
    + +
    +
    + + class="tickbox" /> +
    +
    + + +
    +
    + + +
    + +
    +
    +
    + +
    + +
    +
    +
    + + class="tickbox" /> + +
    +
    + + class="tickbox" /> + +
    +
    +
    + + +
    +
    +
    + + class="tickbox" /> + +
    +
    + + class="tickbox" /> + + +
    +
    +
    + + + (Tunnel must be stopped first) +
    + +
    +
    +
    +
    From 10e2c3832d8bfddf6b2c7d66df949b0f3571006c Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 22 Jan 2009 04:02:41 +0000 Subject: [PATCH 067/191] Move SummaryHelper.getTransferred() to DataHelper, rename to formatSize(), use on tunnels.jsp --- .../src/net/i2p/router/web/SummaryHelper.java | 58 +++++-------------- core/java/src/net/i2p/data/DataHelper.java | 26 ++++++++- .../router/tunnel/pool/TunnelPoolManager.java | 5 +- 3 files changed, 42 insertions(+), 47 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java index 2b445a004..ad8e7135d 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java @@ -244,7 +244,7 @@ public class SummaryHelper { */ public String getInboundSecondKBps() { if (_context == null) - return "0.0"; + return "0"; double kbps = _context.bandwidthLimiter().getReceiveBps()/1024d; DecimalFormat fmt = new DecimalFormat("##0.00"); return fmt.format(kbps); @@ -256,7 +256,7 @@ public class SummaryHelper { */ public String getOutboundSecondKBps() { if (_context == null) - return "0.0"; + return "0"; double kbps = _context.bandwidthLimiter().getSendBps()/1024d; DecimalFormat fmt = new DecimalFormat("##0.00"); return fmt.format(kbps); @@ -269,10 +269,10 @@ public class SummaryHelper { */ public String getInboundFiveMinuteKBps() { if (_context == null) - return "0.0"; + return "0"; RateStat receiveRate = _context.statManager().getRate("bw.recvRate"); - if (receiveRate == null) return "0.0"; + if (receiveRate == null) return "0"; Rate rate = receiveRate.getRate(5*60*1000); double kbps = rate.getAverageValue()/1024; DecimalFormat fmt = new DecimalFormat("##0.00"); @@ -286,10 +286,10 @@ public class SummaryHelper { */ public String getOutboundFiveMinuteKBps() { if (_context == null) - return "0.0"; + return "0"; RateStat receiveRate = _context.statManager().getRate("bw.sendRate"); - if (receiveRate == null) return "0.0"; + if (receiveRate == null) return "0"; Rate rate = receiveRate.getRate(5*60*1000); double kbps = rate.getAverageValue()/1024; DecimalFormat fmt = new DecimalFormat("##0.00"); @@ -303,10 +303,10 @@ public class SummaryHelper { */ public String getInboundLifetimeKBps() { if (_context == null) - return "0.0"; + return "0"; RateStat receiveRate = _context.statManager().getRate("bw.recvRate"); - if (receiveRate == null) return "0.0"; + if (receiveRate == null) return "0"; double kbps = receiveRate.getLifetimeAverageValue()/1024; DecimalFormat fmt = new DecimalFormat("##0.00"); return fmt.format(kbps); @@ -319,10 +319,10 @@ public class SummaryHelper { */ public String getOutboundLifetimeKBps() { if (_context == null) - return "0.0"; + return "0"; RateStat sendRate = _context.statManager().getRate("bw.sendRate"); - if (sendRate == null) return "0.0"; + if (sendRate == null) return "0"; double kbps = sendRate.getLifetimeAverageValue()/1024; DecimalFormat fmt = new DecimalFormat("##0.00"); return fmt.format(kbps); @@ -335,11 +335,11 @@ public class SummaryHelper { */ public String getInboundTransferred() { if (_context == null) - return "0.0"; + return "0"; long received = _context.bandwidthLimiter().getTotalAllocatedInboundBytes(); - return getTransferred(received); + return DataHelper.formatSize(received) + 'B'; } /** @@ -349,40 +349,10 @@ public class SummaryHelper { */ public String getOutboundTransferred() { if (_context == null) - return "0.0"; + return "0"; long sent = _context.bandwidthLimiter().getTotalAllocatedOutboundBytes(); - return getTransferred(sent); - } - - private static String getTransferred(long bytes) { - double val = bytes; - int scale = 0; - if (bytes > 1024*1024*1024) { - // gigs transferred - scale = 3; - val /= (double)(1024*1024*1024); - } else if (bytes > 1024*1024) { - // megs transferred - scale = 2; - val /= (double)(1024*1024); - } else if (bytes > 1024) { - // kbytes transferred - scale = 1; - val /= (double)1024; - } else { - scale = 0; - } - - DecimalFormat fmt = new DecimalFormat("##0.00"); - - String str = fmt.format(val); - switch (scale) { - case 1: return str + "KB"; - case 2: return str + "MB"; - case 3: return str + "GB"; - default: return bytes + "bytes"; - } + return DataHelper.formatSize(sent) + 'B'; } /** diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java index 835e6a0dd..4a074f17c 100644 --- a/core/java/src/net/i2p/data/DataHelper.java +++ b/core/java/src/net/i2p/data/DataHelper.java @@ -25,6 +25,7 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.math.BigInteger; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -235,7 +236,7 @@ public class DataHelper { int split = line.indexOf('='); if (split <= 0) continue; String key = line.substring(0, split); - String val = line.substring(split+1); + String val = line.substring(split+1); //.trim() ?????????????? // Unescape line breaks after loading. // Remember: "\" needs escaping both for regex and string. val = val.replaceAll("\\\\r","\r"); @@ -842,6 +843,29 @@ public class DataHelper { } } + /** + * Caller should append 'B' or 'b' as appropriate + */ + public static String formatSize(long bytes) { + double val = bytes; + int scale = 0; + while (val >= 1024) { + scale++; + val /= 1024; + } + + DecimalFormat fmt = new DecimalFormat("##0.00"); + + String str = fmt.format(val); + switch (scale) { + case 1: return str + "K"; + case 2: return str + "M"; + case 3: return str + "G"; + case 4: return str + "T"; + default: return bytes + ""; + } + } + /** * Strip out any HTML (simply removing any less than / greater than symbols) */ diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java index 1a3e0d1b6..43120d0b0 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java @@ -507,7 +507,7 @@ public class TunnelPoolManager implements TunnelManagerFacade { } out.write("\n"); out.write("Inactive participating tunnels: " + inactive + "
    \n"); - out.write("Lifetime bandwidth usage: " + processed + "KB
    \n"); + out.write("Lifetime bandwidth usage: " + DataHelper.formatSize(processed*1024) + "B
    \n"); } class TunnelComparator implements Comparator { @@ -577,7 +577,8 @@ public class TunnelPoolManager implements TunnelManagerFacade { } if (live <= 0) out.write("No tunnels, waiting for the grace period to end
    \n"); - out.write("Lifetime bandwidth usage: " + processedIn + "KB in, " + processedOut + "KB out
    "); + out.write("Lifetime bandwidth usage: " + DataHelper.formatSize(processedIn*1024) + "B in, " + + DataHelper.formatSize(processedOut*1024) + "B out
    "); } private String getCapacity(Hash peer) { From 173e8a04347e60a3ef4510d9396791779fe67c90 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 22 Jan 2009 16:00:41 +0000 Subject: [PATCH 068/191] console css tweaks --- apps/routerconsole/jsp/default.css | 7 ++++--- apps/routerconsole/jsp/nav.jsp | 1 - apps/routerconsole/jsp/summary.jsp | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/routerconsole/jsp/default.css b/apps/routerconsole/jsp/default.css index b5a63a1ad..527a7c9be 100644 --- a/apps/routerconsole/jsp/default.css +++ b/apps/routerconsole/jsp/default.css @@ -22,11 +22,12 @@ div.logo { top: 1em; margin: 0em; padding: .5em; - text-align: left; + text-align: center; } div.toolbar { - font-weight: bold + margin: 0em 0em 2em 0em; + font-weight: bold; } div.routersummary { @@ -70,7 +71,7 @@ div.news { margin: 0em 1em 1em 224px; padding: .5em 1em; background-color: #ffffc0; - border: medium solid #ffffd0; + border: medium solid #ffffa0; text-align: left; color: inherit; } diff --git a/apps/routerconsole/jsp/nav.jsp b/apps/routerconsole/jsp/nav.jsp index a0c6076f8..22bb8ec24 100644 --- a/apps/routerconsole/jsp/nav.jsp +++ b/apps/routerconsole/jsp/nav.jsp @@ -12,7 +12,6 @@
    <% if (new File("docs/toolbar.html").exists()) { %> diff --git a/apps/routerconsole/jsp/summary.jsp b/apps/routerconsole/jsp/summary.jsp index 6668683e6..687ccf15a 100644 --- a/apps/routerconsole/jsp/summary.jsp +++ b/apps/routerconsole/jsp/summary.jsp @@ -12,6 +12,9 @@ " />
    +
    Configuration  Help
    +
    + General
    Ident: (, never reveal it to anyone" href="netdb.jsp?r=.">view)
    Version:
    @@ -97,6 +100,5 @@ Tunnel lag:
    Handle backlog:

    -
    From 70f07e5bc7da366fcf6a666ffbcf359cdcc8de99 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 22 Jan 2009 16:03:12 +0000 Subject: [PATCH 069/191] sv encoding take 3 --- readme_sv.html | 132 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 46 deletions(-) diff --git a/readme_sv.html b/readme_sv.html index a742f1fdf..22d842400 100644 --- a/readme_sv.html +++ b/readme_sv.html @@ -1,61 +1,101 @@ -

    English | Deutsch | Nederlands | Svenska

    -

    Om du just har startat I2P kommer de "Aktiva: #/#" börja öka inom några få minuter och du kommer se en destination kallad "delade klienter" på den vänstra listan (om inte se nedan). När de syns kan du:

    +

    English +| Deutsch | Nederlands | Svenska

    +

    Om du just har startat I2P kommer de "Aktiva: #/#" börja öka inom +några få minuter och du kommer se en destination kallad "delade +klienter" på den vänstra listan (om inte se +nedan). När de syns kan du:

      -
    • surfa pÃ¥ "eepsidor" - inom I2P finns det anonyma sajter - - ställ in din webbläsare till att använda HTTP proxy vid localhost port 4444, surfa sen till en eepsida - +
    • surfa på "eepsidor" - inom I2P finns det anonyma sajter - + ställ in din webbläsare till att använda HTTP proxy vid +localhost port 4444, surfa sen till en eepsida - - Det finns mÃ¥nga fler eepsidor - följ bara länkarna frÃ¥n dom du ser, - spara dina favoriter och besök dom ofta!
    • -
    • surfa pÃ¥ nätet - det finns för närvarande en "utproxy" i I2P som är ansluten - till din egen HTTP proxt pÃ¥ port 4444 - ställ helt enkelt in din webläsares proxy till - att använda den och gÃ¥ till vilken vanlig URL osm helst - dina fröfrÃ¥gningar kommer skickas - genom I2P nätverket.
    • -
    • överföra filer - det finns en integrerad adaption av - Snark BitTorrent + Det finns många fler eepsidor - följ bara länkarna från dom du ser, + spara dina favoriter och besök dom ofta!
    • +
    • surfa på nätet - det finns för närvarande en "utproxy" i I2P +som är ansluten + till din egen HTTP proxt på port 4444 - ställ helt enkelt in din +webläsares proxy till + att använda den och gå till vilken vanlig URL som helst - dina +fröfrågningar kommer skickas + genom I2P nätverket.
    • +
    • överföra filer - det finns en integrerad adaption av + Snark BitTorrent klienten.
    • -
    • maila anonymt - postman har skapat ett emailsystem som är fungerar med vanliga email-klienter - (POP3 / SMTP),som lÃ¥ter dig skicka email inom I2P sÃ¥ väl som till och frÃ¥n det vanliga Internet! - skaffa dig ett konto hos hq.postman.i2p. +
    • maila anonymt - postman har skapat ett emailsystem som är +fungerar med vanliga email-klienter + (POP3 / SMTP),som låter dig skicka email inom I2P så väl som till +och från det vanliga Internet! + skaffa dig ett konto hos hq.postman.i2p. Vi skickar med susimail, - som är en webb-baserad anonymt inriktad pop3/smtp-klient, inställd till att ansluta till postmans email-tjänst.
    • -
    • chatta anonymt - starta din IRC-klient och anslut till servern vid - localhost port 6668. Den pekar mot en av tvÃ¥ anonyma IRC servrar, - men varken du eller dom vet var den andra är.
    • -
    • bloga anonymt - kika pÃ¥ Syndie
    • + som är en webb-baserad anonymt inriktad pop3/smtp-klient, inställd +till att ansluta till postmans email-tjänst. +
    • chatta anonymt - starta din IRC-klient och anslut till +servern vid + localhost port 6668. Den pekar mot en av två anonyma IRC +servrar, + men varken du eller dom vet var den andra är.
    • +
    • bloga anonymt - kika på Syndie
    • och mycket mer

    Vill du ha en egen eepsida?

    -

    Vi har skickat med mjukvara som låter dig driva en egen eepsida - en -Jetty instans lyssnar på -http://localhost:7658/. Lägg helt enkelt dina filer i -eepsite/docroot/ mappen (eller standard JSP/Servlet .war -filer i eepsite/webapps, eller standard CGI-script i eepsite/cgi-bin) -så kommer de synas. När du startat en eepsite tunnel som pekar på Jetty-server, så kommer sajten möjlig att nå för alla andra. -Mer detaljerade instruktionr för att skapa en eepsite finns på -din tillfälliga eepsite. +

    Vi har skickat med mjukvara som låter dig driva en egen eepsida - en +Jetty instans lyssnar på +http://localhost:7658/. Lägg helt +enkelt dina filer i +eepsite/docroot/ mappen (eller standard JSP/Servlet +.war +filer i eepsite/webapps, eller standard CGI-script i +eepsite/cgi-bin) +så kommer de synas. När du startat en eepsite +tunnel som pekar på Jetty-server, så kommer sajten möjlig att nå för +alla andra. +Mer detaljerade instruktionr för att skapa en eepsite finns på +din tillfälliga eepsite.

    Problem

    -

    Ha tålamod - I2P kan ta tid att starta första gången, medan den söker efter noder att ansluta till. -Om, efter 30 minuter, "Aktiva: anslutna/anslutna nyligen" statistiken visar mindre än 10 anslutna -noder, bör du öppna port 8887 i din brandvägg. -Om du inte lyckas besöka några eepsidor alls (inte ens www.i2p2.i2p), -försäkra dig om att din webbläsare är inställd till att avända en proxy, localhost på port 4444. -Du kanska också vill kika på information på -I2Ps webbsida, fråga frågor på -I2P diskussions forumet, eller kika förbi #i2p eller -#i2p-chat på IRC vid irc.freenode.net, irc.postman.i2p eller irc.freshcoffee.i2p (de är alla sammankopplade).

    +

    Ha tålamod - I2P kan ta tid att starta första gången, medan den söker +efter noder att ansluta till. +Om, efter 30 minuter, "Aktiva: anslutna/anslutna nyligen" statistiken +visar mindre än 10 anslutna +noder, bör du öppna port 8887 i din brandvägg. +Om du inte lyckas besöka några eepsidor alls (inte ens www.i2p2.i2p), +försäkra dig om att din webbläsare är inställd till att avända en proxy, +localhost på port 4444. +Du kanska också vill kika på information på +I2Ps webbsida, fråga frågor på +I2P diskussions forumet, eller kika +förbi #i2p eller +#i2p-chat på IRC vid irc.freenode.net, irc.postman.i2p +eller irc.freshcoffee.i2p (de är alla sammankopplade).

    -

    Du kan förändra denhär sidan genom att ändra i filen "docs/readme_sv.html"

    +

    Du kan förändra denhär sidan genom att ändra i filen +"docs/readme_sv.html"

    \ No newline at end of file From a4468219c753aba5c6e8728de419b801ec639a6f Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 22 Jan 2009 17:24:38 +0000 Subject: [PATCH 070/191] sv take 4 --- readme_sv.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/readme_sv.html b/readme_sv.html index 22d842400..e368fbc53 100644 --- a/readme_sv.html +++ b/readme_sv.html @@ -13,7 +13,7 @@ localhost port 4444
    , surfa sen till en eepsida -
  • inproxy.tino.i2p och perv.i2p: för -statistic över aktiva eepsidor
  • +statistik över aktiva eepsidor
  • forum.i2p: en anonym och säker anslutning till forum.i2p2.de
  • @@ -36,13 +36,13 @@ som är ansluten webläsares proxy till att använda den och gå till vilken vanlig URL som helst - dina fröfrågningar kommer skickas - genom I2P nätverket. + genom I2P ntverket.
  • överföra filer - det finns en integrerad adaption av +href="i2psnark/">översätting av Snark BitTorrent klienten.
  • -
  • maila anonymt - postman har skapat ett emailsystem som är +
  • maila anonymt - postman har skapat ett emailsystem som fungerar med vanliga email-klienter (POP3 / SMTP),som låter dig skicka email inom I2P så väl som till och från det vanliga Internet! @@ -56,7 +56,7 @@ servern vid localhost port 6668. Den pekar mot en av två anonyma IRC servrar, men varken du eller dom vet var den andra är.
  • -
  • bloga anonymt - kika på blogga anonymt - kika på Syndie
  • och mycket mer
  • @@ -72,9 +72,9 @@ enkelt dina filer i filer i eepsite/webapps, eller standard CGI-script i eepsite/cgi-bin) så kommer de synas. När du startat en eepsite -tunnel som pekar på Jetty-server, så kommer sajten möjlig att nå för +tunnel som pekar på Jetty-server, så kommer sajten vara möjlig att nå för alla andra. -Mer detaljerade instruktionr för att skapa en eepsite finns på +Mer detaljerade instruktioner för att skapa en eepsite finns på din tillfälliga eepsite.

    @@ -89,11 +89,11 @@ Om du inte lyckas besöka några eepsidor alls (inte ens www.i2p2.i2p), försäkra dig om att din webbläsare är inställd till att avända en proxy, localhost på port 4444. -Du kanska också vill kika på information på +Du kanske också vill kika på information på I2Ps webbsida, fråga frågor på I2P diskussions forumet, eller kika förbi #i2p eller -#i2p-chat på IRC vid irc.freenode.net, irc.postman.i2p eller irc.freshcoffee.i2p (de är alla sammankopplade).

    From 28cfd8cffee89a5c7e8c1e0a283205cf9e64e637 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 22 Jan 2009 17:36:03 +0000 Subject: [PATCH 071/191] sv back in the updater --- build.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.xml b/build.xml index 9bff4c5c9..f9a015a6e 100644 --- a/build.xml +++ b/build.xml @@ -354,6 +354,9 @@ + + + From e105ca92f2fae0c5857d43ac9284f386813fcaa8 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 22 Jan 2009 18:25:30 +0000 Subject: [PATCH 072/191] Bundle a reply when we switch tunnels, to detect failure sooner --- .../message/OutboundClientMessageOneShotJob.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java index e7c369b1e..0515d5c34 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java @@ -61,6 +61,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { private long _leaseSetLookupBegin; private TunnelInfo _outTunnel; private TunnelInfo _inTunnel; + private boolean _wantACK; /** * final timeout (in milliseconds) that the outbound message will fail in. @@ -279,6 +280,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { long lookupTime = getContext().clock().now() - _leaseSetLookupBegin; getContext().statManager().addRateData("client.leaseSetFoundRemoteTime", lookupTime, lookupTime); } + _wantACK = false; boolean ok = getNextLease(); if (ok) { send(); @@ -412,6 +414,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { } if (_log.shouldLog(Log.INFO)) _log.info("Added to cache - lease for " + _toString); + _wantACK = true; return true; } @@ -455,14 +458,14 @@ public class OutboundClientMessageOneShotJob extends JobImpl { dieFatal(); return; } - boolean wantACK = true; + int existingTags = GarlicMessageBuilder.estimateAvailableTags(getContext(), _leaseSet.getEncryptionKey()); + _outTunnel = selectOutboundTunnel(_to); // what's the point of 5% random? possible improvements or replacements: - // - wantACK if we changed their inbound lease - // - wantACK if we changed our outbound tunnel (requires moving selectOutboundTunnel() before this) + // - wantACK if we changed their inbound lease (getNextLease() sets _wantACK) + // - wantACK if we changed our outbound tunnel (selectOutboundTunnel() sets _wantACK) // - wantACK if we haven't in last 1m (requires a new static cache probably) - if ( (existingTags > 30) && (getContext().random().nextInt(100) >= 5) ) - wantACK = false; + boolean wantACK = _wantACK || existingTags <= 30 || getContext().random().nextInt(100) < 5; PublicKey key = _leaseSet.getEncryptionKey(); SessionKey sessKey = new SessionKey(); @@ -519,7 +522,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl { + _lease.getTunnelId() + " on " + _lease.getGateway().toBase64()); - _outTunnel = selectOutboundTunnel(_to); if (_outTunnel != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug(getJobId() + ": Sending tunnel message out " + _outTunnel.getSendTunnelId(0) + " to " @@ -734,6 +736,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { _log.warn("Switching back to tunnel " + tunnel + " for " + _toString); _backloggedTunnelCache.remove(hashPair()); _tunnelCache.put(hashPair(), tunnel); + _wantACK = true; return tunnel; } // else still backlogged } else // no longer valid @@ -756,6 +759,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { tunnel = selectOutboundTunnel(); if (tunnel != null) _tunnelCache.put(hashPair(), tunnel); + _wantACK = true; } return tunnel; } From 9885779cab9028eddd8e025fbce97ff6a0dad76f Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 23 Jan 2009 01:22:14 +0000 Subject: [PATCH 073/191] Add socks to gui, prevent NPE on socks 4 request, general cleanup --- .../java/src/net/i2p/i2ptunnel/I2PTunnel.java | 7 +- .../net/i2p/i2ptunnel/TunnelController.java | 15 ++- .../i2ptunnel/socks/SOCKSServerFactory.java | 5 +- .../src/net/i2p/i2ptunnel/web/EditBean.java | 4 +- .../src/net/i2p/i2ptunnel/web/IndexBean.java | 94 ++++++------------- apps/i2ptunnel/jsp/edit.jsp | 4 +- apps/i2ptunnel/jsp/editClient.jsp | 6 +- apps/i2ptunnel/jsp/index.jsp | 3 + 8 files changed, 59 insertions(+), 79 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java index 560475e42..3b279a679 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java @@ -662,7 +662,7 @@ public class I2PTunnel implements Logging, EventDispatcher { * "openSOCKSTunnelResult" = "ok" or "error" after the client tunnel has * started. * - * @param args {portNumber} + * @param args {portNumber [, sharedClient]} * @param l logger to receive events and output */ public void runSOCKSTunnel(String args[], Logging l) { @@ -677,6 +677,11 @@ public class I2PTunnel implements Logging, EventDispatcher { return; } + boolean isShared = false; + if (args.length > 1) + isShared = "true".equalsIgnoreCase(args[1].trim()); + + ownDest = !isShared; I2PTunnelTask task; task = new I2PSOCKSTunnel(port, l, ownDest, (EventDispatcher) this, this); addtask(task); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 955d3abd1..f8592fcd3 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -135,8 +135,10 @@ public class TunnelController implements Logging { } if ("httpclient".equals(type)) { startHttpClient(); - }else if("ircclient".equals(type)) { + } else if("ircclient".equals(type)) { startIrcClient(); + } else if("sockstunnel".equals(type)) { + startSocksClient(); } else if ("client".equals(type)) { startClient(); } else if ("server".equals(type)) { @@ -176,6 +178,17 @@ public class TunnelController implements Logging { _running = true; } + private void startSocksClient() { + setI2CPOptions(); + setSessionOptions(); + setListenOn(); + String listenPort = getListenPort(); + String sharedClient = getSharedClient(); + _tunnel.runSOCKSTunnel(new String[] { listenPort, sharedClient }, this); + acquire(); + _running = true; + } + /** * Note the fact that we are using some sessions, so that they dont get * closed by some other tunnels diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java index 357149652..b9b04c57a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java @@ -39,8 +39,7 @@ public class SOCKSServerFactory { serv = new SOCKS5Server(s); break; default: - _log.debug("SOCKS protocol version not supported (" + Integer.toHexString(socksVer) + ")"); - return null; + throw new SOCKSException("SOCKS protocol version not supported (" + Integer.toHexString(socksVer) + ")"); } } catch (IOException e) { _log.debug("error reading SOCKS protocol version"); @@ -49,4 +48,4 @@ public class SOCKSServerFactory { return serv; } -} \ No newline at end of file +} 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 06a46b701..07730718a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -31,9 +31,7 @@ public class EditBean extends IndexBean { if (controllers.size() > tunnel) { TunnelController cur = (TunnelController)controllers.get(tunnel); if (cur == null) return false; - return ( ("client".equals(cur.getType())) || - ("httpclient".equals(cur.getType()))|| - ("ircclient".equals(cur.getType()))); + return isClient(cur.getType()); } else { return false; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 8c361195a..1500150e3 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -209,10 +209,7 @@ public class IndexBean { } // Only modify other shared tunnels // if the current tunnel is shared, and of supported type - if ("true".equalsIgnoreCase(cur.getSharedClient()) && - ("ircclient".equals(cur.getType()) || - "httpclient".equals(cur.getType()) || - "client".equals(cur.getType()))) { + if ("true".equalsIgnoreCase(cur.getSharedClient()) && isClient(cur.getType())) { // all clients use the same I2CP session, and as such, use the same I2CP options List controllers = _group.getControllers(); @@ -224,11 +221,7 @@ public class IndexBean { // Only modify this non-current tunnel // if it belongs to a shared destination, and is of supported type - if ("true".equalsIgnoreCase(c.getSharedClient()) && - ("httpclient".equals(c.getType()) || - "ircclient".equals(c.getType()) || - "client".equals(c.getType()))) { - + if ("true".equalsIgnoreCase(c.getSharedClient()) && isClient(c.getType())) { Properties cOpt = c.getConfig(""); if (_tunnelQuantity != null) { cOpt.setProperty("option.inbound.quantity", _tunnelQuantity); @@ -326,9 +319,14 @@ public class IndexBean { public boolean isClient(int tunnelNum) { TunnelController cur = getController(tunnelNum); if (cur == null) return false; - return ( ("client".equals(cur.getType())) || - ("httpclient".equals(cur.getType())) || - ("ircclient".equals(cur.getType()))); + return isClient(cur.getType()); + } + + public static boolean isClient(String type) { + return ( ("client".equals(type)) || + ("httpclient".equals(type)) || + ("sockstunnel".equals(type)) || + ("ircclient".equals(type))); } public String getTunnelName(int tunnel) { @@ -361,6 +359,7 @@ public class IndexBean { else if ("ircclient".equals(internalType)) return "IRC client"; else if ("server".equals(internalType)) return "Standard server"; else if ("httpserver".equals(internalType)) return "HTTP server"; + else if ("sockstunnel".equals(internalType)) return "SOCKS proxy"; else return internalType; } @@ -579,77 +578,40 @@ public class IndexBean { Properties config = new Properties(); updateConfigGeneric(config); - if ("httpclient".equals(_type)) { + if (isClient(_type)) { + // generic client stuff 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); - - config.setProperty("option.inbound.nickname", CLIENT_NICKNAME); - config.setProperty("option.outbound.nickname", CLIENT_NICKNAME); + config.setProperty("option.inbound.nickname", CLIENT_NICKNAME); + config.setProperty("option.outbound.nickname", CLIENT_NICKNAME); if (_name != null && !_sharedClient) { config.setProperty("option.inbound.nickname", _name); config.setProperty("option.outbound.nickname", _name); } - config.setProperty("sharedClient", _sharedClient + ""); - }else if ("ircclient".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 { + // generic server stuff + if (_targetHost != null) + config.setProperty("targetHost", _targetHost); + if (_targetPort != null) + config.setProperty("targetPort", _targetPort); + if (_privKeyFile != null) + config.setProperty("privKeyFile", _privKeyFile); + } - config.setProperty("option.inbound.nickname", CLIENT_NICKNAME); - config.setProperty("option.outbound.nickname", CLIENT_NICKNAME); - if (_name != null && !_sharedClient) { - config.setProperty("option.inbound.nickname", _name); - config.setProperty("option.outbound.nickname", _name); - } - - config.setProperty("sharedClient", _sharedClient + ""); - } 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 ("httpclient".equals(_type)) { + if (_proxyList != null) + config.setProperty("proxyList", _proxyList); + } else if ("ircclient".equals(_type) || "client".equals(_type)) { if (_targetDestination != null) config.setProperty("targetDestination", _targetDestination); - - config.setProperty("option.inbound.nickname", CLIENT_NICKNAME); - config.setProperty("option.outbound.nickname", CLIENT_NICKNAME); - if (_name != null && !_sharedClient) { - config.setProperty("option.inbound.nickname", _name); - config.setProperty("option.outbound.nickname", _name); - } - config.setProperty("sharedClient", _sharedClient + ""); - } 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 if ("httpserver".equals(_type)) { - if (_targetHost != null) - config.setProperty("targetHost", _targetHost); - if (_targetPort != null) - config.setProperty("targetPort", _targetPort); - if (_privKeyFile != null) - config.setProperty("privKeyFile", _privKeyFile); if (_spoofedHost != null) config.setProperty("spoofedHost", _spoofedHost); - } else { - return null; } return config; diff --git a/apps/i2ptunnel/jsp/edit.jsp b/apps/i2ptunnel/jsp/edit.jsp index 931629fb1..67fdf016c 100644 --- a/apps/i2ptunnel/jsp/edit.jsp +++ b/apps/i2ptunnel/jsp/edit.jsp @@ -14,7 +14,7 @@ String tun = request.getParameter("tunnel"); } else { String type = request.getParameter("type"); int curTunnel = -1; - if ("client".equals(type) || "httpclient".equals(type) || "ircclient".equals(type)) { + if (EditBean.isClient(type)) { %><% } else if ("server".equals(type) || "httpserver".equals(type)) { %><% @@ -22,4 +22,4 @@ String tun = request.getParameter("tunnel"); %>Invalid tunnel type<% } } -%> \ No newline at end of file +%> diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index 2f06e1779..f7ee2294c 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -116,14 +116,14 @@
    - <% if ("httpclient".equals(editBean.getInternalType(curTunnel))) { + <% if ("httpclient".equals(tunnelType)) { %>
    - <% } else { + <% } else if ("client".equals(tunnelType) || "ircclient".equals(tunnelType)) { %>
    + <% } %>
    @@ -140,6 +142,7 @@ +
    From c02711ccad799cb26a4a14b7db5308ee7a17f340 Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 23 Jan 2009 02:23:13 +0000 Subject: [PATCH 074/191] Fix socks so it uses existing tunnels rather than building a new one for every request. Now works with or without 'shared clients' enabled. --- .../java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java | 4 ++-- .../java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java index 9b216e13a..627cd0616 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java @@ -46,11 +46,11 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase { try { SOCKSServer serv = SOCKSServerFactory.createSOCKSServer(s); Socket clientSock = serv.getClientSocket(); - I2PSocket destSock = serv.getDestinationI2PSocket(); + I2PSocket destSock = serv.getDestinationI2PSocket(this); new I2PTunnelRunner(clientSock, destSock, sockLock, null, mySockets); } catch (SOCKSException e) { _log.error("Error from SOCKS connection: " + e.getMessage()); closeSocket(s); } } -} \ No newline at end of file +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java index caf4d1ce3..94602c0c0 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java @@ -59,7 +59,7 @@ public abstract class SOCKSServer { * * @return an I2PSocket connected with the destination */ - public I2PSocket getDestinationI2PSocket() throws SOCKSException { + public I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException { setupServer(); if (connHostName == null) { @@ -79,8 +79,11 @@ public abstract class SOCKSServer { try { if (connHostName.toLowerCase().endsWith(".i2p")) { _log.debug("connecting to " + connHostName + "..."); - I2PSocketManager sm = I2PSocketManagerFactory.createManager(); - destSock = sm.connect(I2PTunnel.destFromName(connHostName), null); + // Let's not due a new Dest for every request, huh? + //I2PSocketManager sm = I2PSocketManagerFactory.createManager(); + //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null); + // TODO get the streaming lib options in there + destSock = t.createI2PSocket(I2PTunnel.destFromName(connHostName)); confirmConnection(); _log.debug("connection confirmed - exchanging data..."); } else { From f7170aa00a52ba7dea0ea881901c0b8f69eae507 Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 23 Jan 2009 16:02:53 +0000 Subject: [PATCH 075/191] Move getDestinationI2PSocket from SocksServer to Socks5Server so we can do better error handling --- .../net/i2p/i2ptunnel/socks/SOCKS5Server.java | 64 ++++++++++++++++++- .../net/i2p/i2ptunnel/socks/SOCKSServer.java | 56 +--------------- 2 files changed, 64 insertions(+), 56 deletions(-) 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 5efc51d9f..fea3285a6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java @@ -12,7 +12,12 @@ import java.io.DataOutputStream; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; +import java.net.SocketException; +import net.i2p.I2PException; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.data.DataFormatException; +import net.i2p.i2ptunnel.I2PTunnel; import net.i2p.util.HexDump; import net.i2p.util.Log; @@ -126,6 +131,7 @@ public class SOCKS5Server extends SOCKSServer { throw new SOCKSException("UDP ASSOCIATE command not supported"); default: _log.debug("unknown command in request (" + Integer.toHexString(command) + ")"); + sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("Invalid command in request"); } @@ -166,12 +172,14 @@ public class SOCKS5Server extends SOCKSServer { throw new SOCKSException("IPV6 addresses not supported"); default: _log.debug("unknown address type in request (" + Integer.toHexString(command) + ")"); + sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("Invalid addresses type in request"); } connPort = in.readUnsignedShort(); if (connPort == 0) { _log.debug("trying to connect to TCP port 0? Dropping!"); + sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("Invalid port number in request"); } } @@ -248,6 +256,60 @@ public class SOCKS5Server extends SOCKSServer { out.write(reply); } + /** + * Get an I2PSocket that can be used to send/receive 8-bit clean data + * to/from the destination of the SOCKS connection. + * + * @return an I2PSocket connected with the destination + */ + public I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException { + setupServer(); + + if (connHostName == null) { + _log.error("BUG: destination host name has not been initialized!"); + throw new SOCKSException("BUG! See the logs!"); + } + if (connPort == 0) { + _log.error("BUG: destination port has not been initialized!"); + throw new SOCKSException("BUG! See the logs!"); + } + + // FIXME: here we should read our config file, select an + // outproxy, and instantiate the proper socket class that + // handles the outproxy itself (SOCKS4a, SOCKS5, HTTP CONNECT...). + I2PSocket destSock; + + try { + if (connHostName.toLowerCase().endsWith(".i2p")) { + _log.debug("connecting to " + connHostName + "..."); + // Let's not due a new Dest for every request, huh? + //I2PSocketManager sm = I2PSocketManagerFactory.createManager(); + //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null); + // TODO get the streaming lib options in there + destSock = t.createI2PSocket(I2PTunnel.destFromName(connHostName)); + confirmConnection(); + _log.debug("connection confirmed - exchanging data..."); + } else { + // if (connPort == 80) ... + _log.error("We don't support outproxies (yet)"); + throw new SOCKSException("Ouproxies not supported (yet)"); + } + } catch (DataFormatException e) { + throw new SOCKSException("Error in destination format"); + } catch (SocketException e) { + throw new SOCKSException("Error connecting (" + + e.getMessage() + ")"); + } catch (IOException e) { + throw new SOCKSException("Error connecting (" + + e.getMessage() + ")"); + } catch (I2PException e) { + throw new SOCKSException("Error connecting (" + + e.getMessage() + ")"); + } + + return destSock; + } + /* * Some namespaces to enclose SOCKS protocol codes */ @@ -279,4 +341,4 @@ public class SOCKS5Server extends SOCKSServer { private static final int COMMAND_NOT_SUPPORTED = 0x07; private static final int ADDRESS_TYPE_NOT_SUPPORTED = 0x08; } -} \ No newline at end of file +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java index 94602c0c0..06c3fab55 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java @@ -6,15 +6,9 @@ */ package net.i2p.i2ptunnel.socks; -import java.io.IOException; import java.net.Socket; -import java.net.SocketException; -import net.i2p.I2PException; import net.i2p.client.streaming.I2PSocket; -import net.i2p.client.streaming.I2PSocketManager; -import net.i2p.client.streaming.I2PSocketManagerFactory; -import net.i2p.data.DataFormatException; import net.i2p.i2ptunnel.I2PTunnel; import net.i2p.util.Log; @@ -30,10 +24,6 @@ public abstract class SOCKSServer { protected String connHostName = null; protected int connPort = 0; - I2PSocket destSocket = null; - - Object FIXME = new Object(); - /** * Perform server initialization (expecially regarding protected * variables). @@ -59,50 +49,6 @@ public abstract class SOCKSServer { * * @return an I2PSocket connected with the destination */ - public I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException { - setupServer(); + public abstract I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException; - if (connHostName == null) { - _log.error("BUG: destination host name has not been initialized!"); - throw new SOCKSException("BUG! See the logs!"); - } - if (connPort == 0) { - _log.error("BUG: destination port has not been initialized!"); - throw new SOCKSException("BUG! See the logs!"); - } - - // FIXME: here we should read our config file, select an - // outproxy, and instantiate the proper socket class that - // handles the outproxy itself (SOCKS4a, SOCKS5, HTTP CONNECT...). - I2PSocket destSock; - - try { - if (connHostName.toLowerCase().endsWith(".i2p")) { - _log.debug("connecting to " + connHostName + "..."); - // Let's not due a new Dest for every request, huh? - //I2PSocketManager sm = I2PSocketManagerFactory.createManager(); - //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null); - // TODO get the streaming lib options in there - destSock = t.createI2PSocket(I2PTunnel.destFromName(connHostName)); - confirmConnection(); - _log.debug("connection confirmed - exchanging data..."); - } else { - _log.error("We don't support outproxies (yet)"); - throw new SOCKSException("Ouproxies not supported (yet)"); - } - } catch (DataFormatException e) { - throw new SOCKSException("Error in destination format"); - } catch (SocketException e) { - throw new SOCKSException("Error connecting (" - + e.getMessage() + ")"); - } catch (IOException e) { - throw new SOCKSException("Error connecting (" - + e.getMessage() + ")"); - } catch (I2PException e) { - throw new SOCKSException("Error connecting (" - + e.getMessage() + ")"); - } - - return destSock; - } } From e5d76a5a77bab200f4eb4ef9b7ccd412610e851e Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 23 Jan 2009 19:17:27 +0000 Subject: [PATCH 076/191] beginnings of outproxy configuration and routing --- .../i2p/i2ptunnel/socks/I2PSOCKSTunnel.java | 48 +++++++++++++- .../net/i2p/i2ptunnel/socks/SOCKS5Server.java | 62 ++++++++++++++++--- 2 files changed, 101 insertions(+), 9 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java index 627cd0616..be398f770 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java @@ -7,6 +7,12 @@ package net.i2p.i2ptunnel.socks; import java.net.Socket; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; import net.i2p.client.streaming.I2PSocket; import net.i2p.data.Destination; @@ -20,7 +26,7 @@ import net.i2p.util.Log; public class I2PSOCKSTunnel extends I2PTunnelClientBase { private static final Log _log = new Log(I2PSOCKSTunnel.class); - + private HashMap> proxies = null; // port# + "" or "default" -> hostname list protected Destination outProxyDest = null; //public I2PSOCKSTunnel(int localPort, Logging l, boolean ownDest) { @@ -36,7 +42,7 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase { } setName(getLocalPort() + " -> SOCKSTunnel"); - + parseOptions(); startRunning(); notifyEvent("openSOCKSTunnelResult", "ok"); @@ -53,4 +59,42 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase { closeSocket(s); } } + + private static final String PROP_PROXY = "i2ptunnel.socks.proxy."; + private void parseOptions() { + Properties opts = getTunnel().getClientOptions(); + proxies = new HashMap(0); + for (Map.Entry e : opts.entrySet()) { + String prop = (String)e.getKey(); + if ((!prop.startsWith(PROP_PROXY)) || prop.length() <= PROP_PROXY.length()) + continue; + String port = prop.substring(PROP_PROXY.length()); + List proxyList = new ArrayList(1); + StringTokenizer tok = new StringTokenizer((String)e.getValue(), ", \t"); + while (tok.hasMoreTokens()) { + String proxy = tok.nextToken().trim(); + if (proxy.endsWith(".i2p")) + proxyList.add(proxy); + else + _log.error("Non-i2p SOCKS outproxy: " + proxy); + } + proxies.put(port, proxyList); + } + } + + public HashMap> getProxyMap() { + return proxies; + } + + public List getProxies(int port) { + List rv = proxies.get(port + ""); + if (rv == null) + rv = getDefaultProxies(); + return rv; + } + + public List getDefaultProxies() { + return proxies.get("default"); + } + } 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 fea3285a6..252d4e1aa 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java @@ -13,7 +13,9 @@ import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; +import java.util.List; +import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.streaming.I2PSocket; import net.i2p.data.DataFormatException; @@ -33,7 +35,6 @@ public class SOCKS5Server extends SOCKSServer { private static final int SOCKS_VERSION_5 = 0x05; private Socket clientSock = null; - private boolean setupCompleted = false; /** @@ -274,6 +275,13 @@ public class SOCKS5Server extends SOCKSServer { throw new SOCKSException("BUG! See the logs!"); } + DataOutputStream out; // for errors + try { + out = new DataOutputStream(clientSock.getOutputStream()); + } catch (IOException e) { + throw new SOCKSException("Connection error (" + e.getMessage() + ")"); + } + // FIXME: here we should read our config file, select an // outproxy, and instantiate the proper socket class that // handles the outproxy itself (SOCKS4a, SOCKS5, HTTP CONNECT...). @@ -285,24 +293,64 @@ public class SOCKS5Server extends SOCKSServer { // Let's not due a new Dest for every request, huh? //I2PSocketManager sm = I2PSocketManagerFactory.createManager(); //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null); - // TODO get the streaming lib options in there destSock = t.createI2PSocket(I2PTunnel.destFromName(connHostName)); - confirmConnection(); - _log.debug("connection confirmed - exchanging data..."); + } else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) { + String err = "No localhost accesses allowed through the Socks Proxy"; + _log.error(err); + try { + sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + } catch (IOException ioe) {} + throw new SOCKSException(err); + } else if (connPort == 80) { + // rewrite GET line to include hostname??? or add Host: line??? + // or forward to local eepProxy (but that's a Socket not an I2PSocket) + // use eepProxy configured outproxies? + String err = "No handler for HTTP outproxy implemented"; + _log.error(err); + try { + sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + } catch (IOException ioe) {} + throw new SOCKSException(err); } else { - // if (connPort == 80) ... - _log.error("We don't support outproxies (yet)"); - throw new SOCKSException("Ouproxies not supported (yet)"); + List proxies = t.getProxies(connPort); + if (proxies == null || proxies.size() <= 0) { + String err = "No outproxy configured for port " + connPort + " and no default configured either"; + _log.error(err); + try { + sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + } catch (IOException ioe) {} + throw new SOCKSException(err); + } + int p = I2PAppContext.getGlobalContext().random().nextInt(proxies.size()); + String proxy = proxies.get(p); + _log.debug("connecting to port " + connPort + " proxy " + proxy + " for " + connHostName + "..."); + // this isn't going to work, these need to be socks outproxies so we need + // to do a socks session to them? + destSock = t.createI2PSocket(I2PTunnel.destFromName(proxy)); } + confirmConnection(); + _log.debug("connection confirmed - exchanging data..."); } catch (DataFormatException e) { + try { + sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + } catch (IOException ioe) {} throw new SOCKSException("Error in destination format"); } catch (SocketException e) { + try { + sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + } catch (IOException ioe) {} throw new SOCKSException("Error connecting (" + e.getMessage() + ")"); } catch (IOException e) { + try { + sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + } catch (IOException ioe) {} throw new SOCKSException("Error connecting (" + e.getMessage() + ")"); } catch (I2PException e) { + try { + sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + } catch (IOException ioe) {} throw new SOCKSException("Error connecting (" + e.getMessage() + ")"); } From 9a089b7da0a2da6f72390d8c977fdcb90a6c28b4 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 24 Jan 2009 17:20:51 +0000 Subject: [PATCH 077/191] * Build files: - Don't bundle unneeded XML parser xercesImpl.jar for Jetty (1MB) - Don't include unneeded stuff in Copy, Delete, Exec.jar (300KB) --- apps/jetty/build.xml | 1 - apps/routerconsole/jsp/help.jsp | 5 ++--- build.xml | 13 +++++-------- installer/resources/wrapper.config | 9 ++++----- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/apps/jetty/build.xml b/apps/jetty/build.xml index aaa978354..643dd79fd 100644 --- a/apps/jetty/build.xml +++ b/apps/jetty/build.xml @@ -78,7 +78,6 @@ - diff --git a/apps/routerconsole/jsp/help.jsp b/apps/routerconsole/jsp/help.jsp index 8580f6e65..d53f93a88 100644 --- a/apps/routerconsole/jsp/help.jsp +++ b/apps/routerconsole/jsp/help.jsp @@ -34,9 +34,8 @@ licenses and dependencies. This webpage is being served as part of the I2P rout client application, which is built off a trimmed down Jetty instance (trimmed down, as in, we do not include the demo apps or other add-ons, and we simplify configuration), allowing you to deploy standard JSP/Servlet web applications into your router. Jetty in turn makes use of -Apache's javax.servlet (javax.servlet.jar) implementation, as well as their xerces-j XML parser (xerces.jar). -Their XML parser requires the Sun XML APIs (JAXP) which is included in binary form (xml-apis.jar) as required -by their binary code license. This product includes software developed by the Apache Software Foundation +Apache's javax.servlet (javax.servlet.jar) implementation. +This product includes software developed by the Apache Software Foundation (http://www.apache.org/).

    Another application you can see on this webpage is I2PTunnel diff --git a/build.xml b/build.xml index 9bff4c5c9..a07c41053 100644 --- a/build.xml +++ b/build.xml @@ -60,7 +60,6 @@ - @@ -87,7 +86,7 @@ - + @@ -219,7 +218,6 @@ - @@ -368,17 +366,16 @@ - - + - + - + @@ -449,7 +446,7 @@ - + diff --git a/installer/resources/wrapper.config b/installer/resources/wrapper.config index 2550f4e3d..177bc1c0b 100644 --- a/installer/resources/wrapper.config +++ b/installer/resources/wrapper.config @@ -47,14 +47,13 @@ wrapper.java.classpath.12=lib/jasper-runtime.jar wrapper.java.classpath.13=lib/commons-logging.jar wrapper.java.classpath.14=lib/commons-el.jar wrapper.java.classpath.15=lib/ant.jar -wrapper.java.classpath.16=lib/xercesImpl.jar # java service wrapper, BSD -wrapper.java.classpath.17=lib/wrapper.jar +wrapper.java.classpath.16=lib/wrapper.jar # systray, LGPL -wrapper.java.classpath.18=lib/systray.jar -wrapper.java.classpath.19=lib/systray4j.jar +wrapper.java.classpath.17=lib/systray.jar +wrapper.java.classpath.18=lib/systray4j.jar # BOB -wrapper.java.classpath.20=lib/BOB.jar +wrapper.java.classpath.19=lib/BOB.jar # Java Library Path (location of Wrapper.DLL or libwrapper.so) wrapper.java.library.path.1=. From d8298c63abe61456f6d6653c5c81442871b6a295 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 24 Jan 2009 17:27:06 +0000 Subject: [PATCH 078/191] http error message --- .../i2p/i2ptunnel/socks/SOCKSServerFactory.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java index b9b04c57a..67a52d688 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java @@ -7,6 +7,7 @@ package net.i2p.i2ptunnel.socks; import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; @@ -18,6 +19,15 @@ import net.i2p.util.Log; public class SOCKSServerFactory { private final static Log _log = new Log(SOCKSServerFactory.class); + private final static String ERR_REQUEST_DENIED = + "HTTP/1.1 403 Access Denied\r\n" + + "Content-Type: text/html; charset=iso-8859-1\r\n" + + "Cache-control: no-cache\r\n" + + "\r\n" + + "

    I2P SOCKS PROXY ERROR: REQUEST DENIED

    " + + "Your browser is misconfigured. This is a SOCKS proxy, not a HTTP proxy" + + ""; + /** * Create a new SOCKS server, using the provided socket (that must * be connected to a client) to select the proper SOCKS protocol @@ -38,6 +48,13 @@ public class SOCKSServerFactory { // SOCKS version 5 serv = new SOCKS5Server(s); break; + case 'C': + case 'G': + case 'H': + case 'P': + DataOutputStream out = new DataOutputStream(s.getOutputStream()); + out.write(ERR_REQUEST_DENIED.getBytes()); + throw new SOCKSException("HTTP request to socks"); default: throw new SOCKSException("SOCKS protocol version not supported (" + Integer.toHexString(socksVer) + ")"); } From ae0bcc492da8f0a2abf8be92d4e0413d8955629a Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 24 Jan 2009 20:07:41 +0000 Subject: [PATCH 079/191] * netdb.jsp: Don't show stats by default * RebuildRouterInfoJob: Don't run it * PublishLocalRouterInfoJob: - Delay for 5m at startup - Run every 20m (was 7.5m) --- .../src/net/i2p/router/web/NetDbHelper.java | 6 ++- apps/routerconsole/jsp/netdb.jsp | 1 + .../net/i2p/router/NetworkDatabaseFacade.java | 1 + .../networkdb/PublishLocalRouterInfoJob.java | 6 +-- .../KademliaNetworkDatabaseFacade.java | 48 +++++++++++++------ .../router/startup/RebuildRouterInfoJob.java | 7 +++ .../startup/StartAcceptingClientsJob.java | 3 +- 7 files changed, 52 insertions(+), 20 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java index 41f0ad90c..114579a27 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java @@ -11,6 +11,7 @@ public class NetDbHelper { private RouterContext _context; private Writer _out; private String _routerPrefix; + private boolean _full = false; /** * Configure this bean to query a particular router context @@ -30,6 +31,7 @@ public class NetDbHelper { public void setWriter(Writer writer) { _out = writer; } public void setRouter(String r) { _routerPrefix = r; } + public void setFull(String f) { _full = "1".equals(f); }; public String getNetDbSummary() { try { @@ -37,14 +39,14 @@ public class NetDbHelper { if (_routerPrefix != null) _context.netDb().renderRouterInfoHTML(_out, _routerPrefix); else - _context.netDb().renderStatusHTML(_out); + _context.netDb().renderStatusHTML(_out, _full); return ""; } else { ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024); if (_routerPrefix != null) _context.netDb().renderRouterInfoHTML(new OutputStreamWriter(baos), _routerPrefix); else - _context.netDb().renderStatusHTML(new OutputStreamWriter(baos)); + _context.netDb().renderStatusHTML(new OutputStreamWriter(baos), _full); return new String(baos.toByteArray()); } } catch (IOException ioe) { diff --git a/apps/routerconsole/jsp/netdb.jsp b/apps/routerconsole/jsp/netdb.jsp index 89c2bdec2..08a1377d3 100644 --- a/apps/routerconsole/jsp/netdb.jsp +++ b/apps/routerconsole/jsp/netdb.jsp @@ -14,6 +14,7 @@ " /> + " /> " /> diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java index ed4fe1555..e4a5ce08b 100644 --- a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java @@ -62,4 +62,5 @@ public abstract class NetworkDatabaseFacade implements Service { public int getKnownRouters() { return 0; } public int getKnownLeaseSets() { return 0; } public void renderRouterInfoHTML(Writer out, String s) throws IOException {} + public void renderStatusHTML(Writer out, boolean b) throws IOException {} } diff --git a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java index 96dbadd6c..8fa729d63 100644 --- a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java +++ b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java @@ -20,12 +20,12 @@ import net.i2p.router.RouterContext; import net.i2p.util.Log; /** - * Publish the local router's RouterInfo every 5 to 10 minutes + * Publish the local router's RouterInfo periodically * */ public class PublishLocalRouterInfoJob extends JobImpl { private Log _log; - final static long PUBLISH_DELAY = 5*60*1000; // every 5 to 10 minutes (since we randomize) + final static long PUBLISH_DELAY = 20*60*1000; public PublishLocalRouterInfoJob(RouterContext ctx) { super(ctx); @@ -67,6 +67,6 @@ public class PublishLocalRouterInfoJob extends JobImpl { } catch (DataFormatException dfe) { _log.error("Error signing the updated local router info!", dfe); } - requeue(PUBLISH_DELAY + getContext().random().nextInt((int)PUBLISH_DELAY)); + requeue((PUBLISH_DELAY/2) + getContext().random().nextInt((int)PUBLISH_DELAY)); } } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java index c74499f95..23ef78d86 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java @@ -125,6 +125,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { private final static long ROUTER_INFO_EXPIRATION_SHORT = 90*60*1000l; private final static long EXPLORE_JOB_DELAY = 10*60*1000l; + private final static long PUBLISH_JOB_DELAY = 5*60*1000l; public KademliaNetworkDatabaseFacade(RouterContext context) { _context = context; @@ -326,7 +327,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } // periodically update and resign the router's 'published date', which basically // serves as a version - _context.jobQueue().addJob(new PublishLocalRouterInfoJob(_context)); + Job plrij = new PublishLocalRouterInfoJob(_context); + plrij.getTiming().setStartAfter(_context.clock().now() + PUBLISH_JOB_DELAY); + _context.jobQueue().addJob(plrij); try { publish(ri); } catch (IllegalArgumentException iae) { @@ -970,7 +973,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { StringBuffer buf = new StringBuffer(4*1024); buf.append("

    Network Database RouterInfo Lookup

    \n"); if (".".equals(routerPrefix)) { - renderRouterInfo(buf, _context.router().getRouterInfo(), true); + renderRouterInfo(buf, _context.router().getRouterInfo(), true, true); } else { boolean notFound = true; Set routers = getRouters(); @@ -978,7 +981,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { RouterInfo ri = (RouterInfo)iter.next(); Hash key = ri.getIdentity().getHash(); if (key.toBase64().startsWith(routerPrefix)) { - renderRouterInfo(buf, ri, false); + renderRouterInfo(buf, ri, false, true); notFound = false; } } @@ -990,7 +993,14 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } public void renderStatusHTML(Writer out) throws IOException { - StringBuffer buf = new StringBuffer(getKnownRouters() * 2048); + renderStatusHTML(out, true); + } + + public void renderStatusHTML(Writer out, boolean full) throws IOException { + int size = getKnownRouters() * 512; + if (full) + size *= 4; + StringBuffer buf = new StringBuffer(size); buf.append("

    Network Database Contents

    \n"); if (!_initialized) { buf.append("Not initialized\n"); @@ -1044,10 +1054,15 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } Hash us = _context.routerHash(); - out.write("

    Routers

    \n"); + out.write("

    Routers (view without"); + else + out.write("?f=1#routers\" >view with"); + out.write(" stats)

    \n"); RouterInfo ourInfo = _context.router().getRouterInfo(); - renderRouterInfo(buf, ourInfo, true); + renderRouterInfo(buf, ourInfo, true, true); out.write(buf.toString()); buf.setLength(0); @@ -1061,7 +1076,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { Hash key = ri.getIdentity().getHash(); boolean isUs = key.equals(us); if (!isUs) { - renderRouterInfo(buf, ri, false); + renderRouterInfo(buf, ri, false, full); out.write(buf.toString()); buf.setLength(0); String coreVersion = ri.getOption("coreVersion"); @@ -1102,7 +1117,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { out.flush(); } - private void renderRouterInfo(StringBuffer buf, RouterInfo info, boolean isUs) { + private void renderRouterInfo(StringBuffer buf, RouterInfo info, boolean isUs, boolean full) { String hash = info.getIdentity().getHash().toBase64(); buf.append(""); if (isUs) { @@ -1129,13 +1144,18 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } } buf.append("

    \n"); - buf.append("Stats:
    \n"); - for (Iterator iter = info.getOptions().keySet().iterator(); iter.hasNext(); ) { - String key = (String)iter.next(); - String val = info.getOption(key); - buf.append(DataHelper.stripHTML(key)).append(" = ").append(DataHelper.stripHTML(val)).append("
    \n"); + if (full) { + buf.append("Stats:
    \n"); + for (Iterator iter = info.getOptions().keySet().iterator(); iter.hasNext(); ) { + String key = (String)iter.next(); + String val = info.getOption(key); + buf.append(DataHelper.stripHTML(key)).append(" = ").append(DataHelper.stripHTML(val)).append("
    \n"); + } + buf.append("
    \n"); + } else { + buf.append("
    Full entry\n"); } - buf.append("
    \n"); + buf.append("
    \n"); } } diff --git a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java index a43fc6311..967bc7a79 100644 --- a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java +++ b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java @@ -28,6 +28,13 @@ import net.i2p.router.RouterContext; import net.i2p.util.Log; /** + * This used be called from StartAcceptingClientsJob but is now disabled. + * It is still called once from LoadRouterInfoJob (but not run as a Job). + * + * The following comments appear to be incorrect... + * it rebuilds if the router.info file does not exist. + * There is no check for a router.info.rebuild file. + * * If the file router.info.rebuild exists, rebuild the router info and republish. * This is useful for dhcp or other situations where the router addresses change - * simply create the router.info.rebuild file after modifying router.config and within diff --git a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java index 7ad54299f..727d06ac6 100644 --- a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java +++ b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java @@ -28,7 +28,8 @@ public class StartAcceptingClientsJob extends JobImpl { getContext().clientManager().startup(); getContext().jobQueue().addJob(new ReadConfigJob(getContext())); - getContext().jobQueue().addJob(new RebuildRouterInfoJob(getContext())); + // pointless + //getContext().jobQueue().addJob(new RebuildRouterInfoJob(getContext())); getContext().jobQueue().allowParallelOperation(); } } From 6ed17c1a5f5e8569d874f6534696d10cc238e951 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 24 Jan 2009 23:42:31 +0000 Subject: [PATCH 080/191] prevent null spoofhost --- apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java | 4 ++-- apps/i2ptunnel/jsp/editServer.jsp | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) 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 07730718a..627024b67 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -39,7 +39,7 @@ public class EditBean extends IndexBean { public String getTargetHost(int tunnel) { TunnelController tun = getController(tunnel); - if (tun != null) + if (tun != null && tun.getTargetHost() != null) return tun.getTargetHost(); else return "127.0.0.1"; @@ -53,7 +53,7 @@ public class EditBean extends IndexBean { } public String getSpoofedHost(int tunnel) { TunnelController tun = getController(tunnel); - if (tun != null) + if (tun != null && tun.getSpoofedHost() != null) return tun.getSpoofedHost(); else return ""; diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index 5b7ad9105..e6cc0497d 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -110,7 +110,8 @@ - + + (leave blank for outproxies) <% } %>
    From baebd1fdd2701daf1f5fa51e5655b7f9b05e34e4 Mon Sep 17 00:00:00 2001 From: complication Date: Sat, 24 Jan 2009 23:57:39 +0000 Subject: [PATCH 081/191] * Update versions, package release --- core/java/src/net/i2p/CoreVersion.java | 2 +- history.txt | 5 +++ initialNews.xml | 4 +- installer/install.xml | 2 +- news.xml | 39 +++++++++---------- .../src/net/i2p/router/RouterVersion.java | 4 +- 6 files changed, 30 insertions(+), 26 deletions(-) diff --git a/core/java/src/net/i2p/CoreVersion.java b/core/java/src/net/i2p/CoreVersion.java index d46c8a7fb..c24542b00 100644 --- a/core/java/src/net/i2p/CoreVersion.java +++ b/core/java/src/net/i2p/CoreVersion.java @@ -15,7 +15,7 @@ package net.i2p; */ public class CoreVersion { public final static String ID = "$Revision: 1.72 $ $Date: 2008-08-24 12:00:00 $"; - public final static String VERSION = "0.6.5"; + public final static String VERSION = "0.7"; public static void main(String args[]) { System.out.println("I2P Core version: " + VERSION); diff --git a/history.txt b/history.txt index 413e4a408..347c1cdde 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,8 @@ +* 2008-01-24 0.7 released + +2009-01-24 Complication + * Update versions, package release + 2009-01-17 zzz * NTCP: Prevent two NTCP Pumpers diff --git a/initialNews.xml b/initialNews.xml index ffc8c414f..6a9c4b4ca 100644 --- a/initialNews.xml +++ b/initialNews.xml @@ -1,5 +1,5 @@ - - +

    Congratulations on getting I2P installed!

      diff --git a/installer/install.xml b/installer/install.xml index c8a0787a5..8c7ff1bf4 100644 --- a/installer/install.xml +++ b/installer/install.xml @@ -4,7 +4,7 @@ i2p - 0.6.5 + 0.7 diff --git a/news.xml b/news.xml index 465654622..46e84388f 100644 --- a/news.xml +++ b/news.xml @@ -1,5 +1,5 @@ - - +

      • -2008-12-01: 0.6.5 Released +2009-01-24: 0.7 Released

      -The 0.6.5 release introduces new components, -drops some old ones (like the old TCP transport) -and has been optimized to perform better. +The 0.7 release adds stability and flexibility to I2PSnark, +which can hopefully be used to distribute I2P updates in future.

      -The BOB (Basic Open Bridge) protocol is introduced, -for use by client applications which cannot import -I2CP libraries directly. This deprecates the old -SAM protocol which was previously used in such cases. -For now however, BOB is not started automatically yet -on new installations, and SAM remains active on old installations. +The I2P router gets fixes and optimizations to various +transport-level and streaming issues, network exploration, +NetDB performance and the UDP introducer system. +Among other features, the new release offers +better connection limiting, higher tolerance to "out of memory" exceptions +in helper applications, and an experimental new address system +using Base32 hashes of destination keys (".b32.i2p" URLs).

      -Improved code should be better at preventing congestion -by probabalistically dropping participating traffic, -and likewise behave better when congestion occurs. -The floodfill NetDB should operate more reliably, -the streaming library should choose better message sizes, -offer a socket timeout function, and work proceeds -on the "hidden" mode of operation for I2P routers. +Both the BOB and SAM protocols are improved upon, +more old components dropped, Router Console features added +and a possible latency measurement attack mitigated. +From this release onwards, block lists for misbehaving peers +are activated by default.

      -From this release onward, I2P requires Java 1.5 or higher. +It seems worthwhile to remind that already since +the last release, I2P requires Java 1.5 or higher. If you are uncertain about your Java version, you can verify by opening a terminal window or command prompt, and entering the command "java -version". diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index bbf270287..bb0056122 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -16,8 +16,8 @@ import net.i2p.CoreVersion; */ public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; - public final static String VERSION = "0.6.5"; - public final static long BUILD = 11; + public final static String VERSION = "0.7"; + public final static long BUILD = 0; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From 4682bb41471524cc337d79ea63092a91bdf0c93c Mon Sep 17 00:00:00 2001 From: complication Date: Sun, 25 Jan 2009 00:47:57 +0000 Subject: [PATCH 082/191] * Removing duplicate end tag from news.xml --- news.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/news.xml b/news.xml index 46e84388f..63adee7fa 100644 --- a/news.xml +++ b/news.xml @@ -36,6 +36,4 @@ If you are uncertain about your Java version, you can verify by opening a terminal window or command prompt, and entering the command "java -version". If you have an older Java installed, please update it first!

      - -

      From 6235b49300f83264d692a80320d6945587b658f4 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 25 Jan 2009 01:01:48 +0000 Subject: [PATCH 083/191] cleanup of lease stuff --- .../kademlia/PersistentDataStore.java | 116 ------------------ 1 file changed, 116 deletions(-) diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java index 77d2427cd..5d0d219db 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java @@ -65,10 +65,6 @@ class PersistentDataStore extends TransientDataStore { return super.remove(key); } - public DataStructure removeLease(Hash key) { - return super.removeLease(key); - } - public void put(Hash key, DataStructure data) { if ( (data == null) || (key == null) ) return; super.put(key, data); @@ -77,26 +73,6 @@ class PersistentDataStore extends TransientDataStore { _writer.queue(key, data); } -/* - * We don't store leasesets here anymore, use the TransientDataStore count - * - public int countLeaseSets() { - File dbDir = null; - try { - dbDir = getDbDir(); - } catch (IOException ioe) { - return 0; - } - if (dbDir == null) - return 0; - File leaseSetFiles[] = dbDir.listFiles(LeaseSetFilter.getInstance()); - if (leaseSetFiles == null) - return 0; - else - return leaseSetFiles.length; - } -*/ - private void accept(LeaseSet ls) { super.put(ls.getDestination().calculateHash(), ls); } @@ -249,18 +225,6 @@ class PersistentDataStore extends TransientDataStore { int routerCount = 0; try { File dbDir = getDbDir(); -/**** - if (getContext().router().getUptime() < 10*60*1000) { - File leaseSetFiles[] = dbDir.listFiles(LeaseSetFilter.getInstance()); - if (leaseSetFiles != null) { - for (int i = 0; i < leaseSetFiles.length; i++) { - Hash key = getLeaseSetHash(leaseSetFiles[i].getName()); - if ( (key != null) && (!isKnown(key)) ) - PersistentDataStore.this._context.jobQueue().addJob(new ReadLeaseJob(leaseSetFiles[i], key)); - } - } - } -****/ File routerInfoFiles[] = dbDir.listFiles(RouterInfoFilter.getInstance()); if (routerInfoFiles != null) { routerCount += routerInfoFiles.length; @@ -283,63 +247,6 @@ class PersistentDataStore extends TransientDataStore { } } -/**** - private class ReadLeaseJob extends JobImpl { - private File _leaseFile; - private Hash _key; - public ReadLeaseJob(File leaseFile, Hash key) { - super(PersistentDataStore.this._context); - _leaseFile = leaseFile; - _key = key; - } - public String getName() { return "Read LeaseSet"; } - private boolean shouldRead() { - DataStructure data = get(_key); - if (data == null) return true; - if (data instanceof LeaseSet) { - long knownDate = ((LeaseSet)data).getEarliestLeaseDate(); - long fileDate = _leaseFile.lastModified(); - if (fileDate > knownDate) - return true; - else - return false; - } else { - // wtf - return true; - } - } - public void runJob() { - if (!shouldRead()) return; - try { - FileInputStream fis = null; - boolean corrupt = false; - try { - fis = new FileInputStream(_leaseFile); - LeaseSet ls = new LeaseSet(); - ls.readBytes(fis); - try { - _facade.store(ls.getDestination().calculateHash(), ls); - } catch (IllegalArgumentException iae) { - _log.info("Refused locally loaded leaseSet - deleting"); - corrupt = true; - } - } catch (DataFormatException dfe) { - _log.warn("Error reading the leaseSet from " + _leaseFile.getAbsolutePath(), dfe); - corrupt = true; - } catch (FileNotFoundException fnfe) { - _log.debug("Deleted prior to read.. a race during expiration / load"); - corrupt = false; - } finally { - if (fis != null) try { fis.close(); } catch (IOException ioe) {} - } - if (corrupt) _leaseFile.delete(); - } catch (IOException ioe) { - _log.warn("Error reading the leaseSet from " + _leaseFile.getAbsolutePath(), ioe); - } - } - } -****/ - private class ReadRouterJob extends JobImpl { private File _routerFile; private Hash _key; @@ -464,31 +371,8 @@ class PersistentDataStore extends TransientDataStore { _log.info("Removed router info at " + f.getAbsolutePath()); return; } -/*** - String lsName = getLeaseSetName(key); - File f = new File(dir, lsName); - if (f.exists()) { - boolean removed = f.delete(); - if (!removed) - _log.warn("Unable to remove lease set at " + f.getAbsolutePath()); - else - _log.info("Removed lease set at " + f.getAbsolutePath()); - return; - } -***/ } -/*** - private final static class LeaseSetFilter implements FilenameFilter { - private static final FilenameFilter _instance = new LeaseSetFilter(); - public static final FilenameFilter getInstance() { return _instance; } - public boolean accept(File dir, String name) { - if (name == null) return false; - name = name.toUpperCase(); - return (name.startsWith(LEASESET_PREFIX.toUpperCase()) && name.endsWith(LEASESET_SUFFIX.toUpperCase())); - } - } -***/ private final static class RouterInfoFilter implements FilenameFilter { private static final FilenameFilter _instance = new RouterInfoFilter(); public static final FilenameFilter getInstance() { return _instance; } From 82180592f90a94c7f588b2e41c2f3d1ab5073f7f Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 25 Jan 2009 01:18:52 +0000 Subject: [PATCH 084/191] -1 --- history.txt | 45 ++++++++++++++++++- .../src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/history.txt b/history.txt index 347c1cdde..42fa010c1 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,47 @@ -* 2008-01-24 0.7 released +2009-01-25 zzz + * Build files: + - Don't bundle unneeded XML parser xercesImpl.jar (1MB) + - Don't include unneeded stuff in Copy, Delete, Exec.jar (300KB) + * I2CP: + Implement new I2CP message ReconfigureSessionMessage. + Will be used for tunnel reduction. + * I2PTunnel Edit Pages: + - Change default length to 2+0 + - Cleanup helper code + - Prevent null spoofhost + - Stub out the following new options (C=client, S=server): + + Access list (S) + + Certificate type (S) + + Encrypted LeaseSet (S) + + New dest on idle restart (C) + + Tunnel closure on idle (C) + + Tunnel reduction on idle (C,S) + * I2PTunnel Socks: + - Add support for SOCKS to GUI + - Don't NPE on SOCKS 4, just close + - Don't have SOCKS build a new dest for every request + - Beginnings of SOCKS configuration by port + - HTML error msg for attempted HTTP access + * LeaseSet: Add encrypt/decrypt methods + * netdb.jsp: Don't show stats by default + * OCMOSJ: Bundle a reply when we switch tunnel or lease, + to detect failure sooner + * PublishLocalRouterInfoJob: + - Delay for 5m at startup + - Run every 20m (was 7.5m) + * RebuildRouterInfoJob: Don't run it + * Router: Add a keyring for decrypting leases + * Routerconsole: Add configkeyring.jsp + * SummaryHelper.getTransferred() move to DataHelper, + rename to formatSize(), use on tunnels.jsp + * Streaming, I2CP, Client Message sending: + Pass message timeout through new I2CP message + SendMessageExpiresMessage, so that the router + uses the same expiration as the streaming lib. + Should help reliability. + * Streaming: TCB control block sharing + +* 2009-01-24 0.7 released 2009-01-24 Complication * Update versions, package release diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index bb0056122..3b6c5a0a0 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = "0.7"; - public final static long BUILD = 0; + public final static long BUILD = 1; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From 37f9d3afe1020e3a5def931ff83e40974e3d55f4 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 29 Jan 2009 02:12:02 +0000 Subject: [PATCH 085/191] * Remove source from susimail.war, susidns.war, i2ptunnel.war (85KB) --- apps/i2ptunnel/java/build.xml | 2 +- apps/susidns/src/build.xml | 2 -- apps/susimail/build.xml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/i2ptunnel/java/build.xml b/apps/i2ptunnel/java/build.xml index 635cad2e4..564f6fc4b 100644 --- a/apps/i2ptunnel/java/build.xml +++ b/apps/i2ptunnel/java/build.xml @@ -42,7 +42,7 @@ + basedir="../jsp/" excludes="web.xml, **/*.java, *.jsp"> diff --git a/apps/susidns/src/build.xml b/apps/susidns/src/build.xml index f31340954..d3f5f1662 100644 --- a/apps/susidns/src/build.xml +++ b/apps/susidns/src/build.xml @@ -63,12 +63,10 @@ - - diff --git a/apps/susimail/build.xml b/apps/susimail/build.xml index 2bdc4f164..abf2a88cf 100644 --- a/apps/susimail/build.xml +++ b/apps/susimail/build.xml @@ -19,7 +19,7 @@ + basedir="src/" excludes="WEB-INF/web.xml LICENSE src/**/*"> From 9d9d4093bc9c2ad40d187d1d9f5f9e7c924b3460 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 29 Jan 2009 02:13:00 +0000 Subject: [PATCH 086/191] simple readme for the source pkg --- README.txt | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 README.txt diff --git a/README.txt b/README.txt new file mode 100644 index 000000000..49a62ee5b --- /dev/null +++ b/README.txt @@ -0,0 +1,31 @@ +Prerequisites to build from source: + Java SDK (preferably Sun) 1.5.0 or higher (1.6 recommended) + Apache Ant 1.7.0 or higher + +To build: + ant pkg + Run 'ant' with no arguments to see other build options. + See http://www.i2p2.de/download.html for installation instructions. + +Documentation: + http://www.i2p2.de/ + API: run 'ant javadoc' then start at build/javadoc/index.html + +Latest release: + http://www.i2p2.de/download.html + +To get development branch from source control: + http://www.i2p2.de/newdevelopers.html + +FAQ: + http://www.i2p2.de/faq.html + +Need help? + IRC irc.freenode.net #i2p + http://forum.i2p2.de/ + +Licenses: + http://www.i2p2.de/licenses.html + Also http://localhost:7657/help.jsp + Also see licenses for the individual bundled apps in apps/* + From 69e6393442aad7242c1dbd1f1dcf1ad60065781c Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 29 Jan 2009 02:16:18 +0000 Subject: [PATCH 087/191] * Routerconsole: - Move common methods to new HelperBase class - Make reseed link a button --- .../i2p/router/web/ConfigAdvancedHelper.java | 17 +---------- .../i2p/router/web/ConfigClientsHelper.java | 17 +---------- .../i2p/router/web/ConfigKeyringHelper.java | 17 +---------- .../i2p/router/web/ConfigLoggingHelper.java | 17 +---------- .../net/i2p/router/web/ConfigNetHelper.java | 17 +---------- .../net/i2p/router/web/ConfigPeerHelper.java | 18 +----------- .../net/i2p/router/web/ConfigStatsHelper.java | 3 +- .../i2p/router/web/ConfigTunnelsHelper.java | 17 +---------- .../i2p/router/web/ConfigUpdateHandler.java | 2 +- .../i2p/router/web/ConfigUpdateHelper.java | 17 +---------- .../src/net/i2p/router/web/ContentHelper.java | 16 +--------- .../src/net/i2p/router/web/GraphHelper.java | 18 +----------- .../src/net/i2p/router/web/HelperBase.java | 29 +++++++++++++++++++ .../net/i2p/router/web/JobQueueHelper.java | 20 +------------ .../src/net/i2p/router/web/LogsHelper.java | 17 +---------- .../src/net/i2p/router/web/NavHelper.java | 16 +--------- .../src/net/i2p/router/web/NetDbHelper.java | 19 +----------- .../src/net/i2p/router/web/NoticeHelper.java | 19 ++---------- .../net/i2p/router/web/OldConsoleHelper.java | 22 +------------- .../src/net/i2p/router/web/PeerHelper.java | 18 +----------- .../net/i2p/router/web/ProfilesHelper.java | 17 +---------- .../src/net/i2p/router/web/StatHelper.java | 8 ++--- .../src/net/i2p/router/web/SummaryHelper.java | 17 +---------- .../src/net/i2p/router/web/TunnelHelper.java | 20 +------------ apps/routerconsole/jsp/graphs.jsp | 2 +- apps/routerconsole/jsp/peers.jsp | 2 +- apps/routerconsole/jsp/summary.jsp | 12 ++++---- 27 files changed, 63 insertions(+), 351 deletions(-) create mode 100644 apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigAdvancedHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigAdvancedHelper.java index c90194860..3ab6354a6 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigAdvancedHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigAdvancedHelper.java @@ -6,22 +6,7 @@ import java.util.TreeSet; import net.i2p.router.RouterContext; -public class ConfigAdvancedHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class ConfigAdvancedHelper extends HelperBase { public ConfigAdvancedHelper() {} public String getSettings() { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index d6441736a..2bee43533 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -9,22 +9,7 @@ import java.util.TreeSet; import net.i2p.router.RouterContext; import net.i2p.router.startup.ClientAppConfig; -public class ConfigClientsHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class ConfigClientsHelper extends HelperBase { public ConfigClientsHelper() {} public String getForm1() { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java index 48bc15068..85c8ee423 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHelper.java @@ -6,22 +6,7 @@ import java.io.OutputStreamWriter; import net.i2p.router.RouterContext; -public class ConfigKeyringHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class ConfigKeyringHelper extends HelperBase { public ConfigKeyringHelper() {} public String getSummary() { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java index 07acb0849..635d2e544 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java @@ -6,22 +6,7 @@ import java.util.TreeSet; import net.i2p.router.RouterContext; -public class ConfigLoggingHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class ConfigLoggingHelper extends HelperBase { public ConfigLoggingHelper() {} public String getLogFilePattern() { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java index d355e9d61..9beeb33cf 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java @@ -10,22 +10,7 @@ import net.i2p.router.transport.udp.UDPAddress; import net.i2p.router.transport.udp.UDPTransport; import net.i2p.time.Timestamper; -public class ConfigNetHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class ConfigNetHelper extends HelperBase { public ConfigNetHelper() {} /** copied from various private components */ diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHelper.java index 63fc1f5e5..662a078b8 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHelper.java @@ -4,25 +4,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; -import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; -public class ConfigPeerHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class ConfigPeerHelper extends HelperBase { public ConfigPeerHelper() {} public String getBlocklistSummary() { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHelper.java index 925fce79a..3af4ffafb 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigStatsHelper.java @@ -15,8 +15,7 @@ import net.i2p.stat.RateStat; import net.i2p.stat.StatManager; import net.i2p.util.Log; -public class ConfigStatsHelper { - private RouterContext _context; +public class ConfigStatsHelper extends HelperBase { private Log _log; private String _filter; private Set _filters; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java index 98141771b..e21f9d9ce 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java @@ -8,22 +8,7 @@ import net.i2p.data.Destination; import net.i2p.router.RouterContext; import net.i2p.router.TunnelPoolSettings; -public class ConfigTunnelsHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class ConfigTunnelsHelper extends HelperBase { public ConfigTunnelsHelper() {} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java index 68e2ec5b9..818b748a7 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java @@ -51,7 +51,7 @@ public class ConfigUpdateHandler extends FormHandler { if ( (_updatePolicy == null) || (!_updatePolicy.equals("notify")) ) addFormNotice("Update available, attempting to download now"); else - addFormNotice("Update available, click link on left to download"); + addFormNotice("Update available, click button on left to download"); } else addFormNotice("No update available"); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java index 0ecaca4f0..d0d243799 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java @@ -4,22 +4,7 @@ import net.i2p.crypto.TrustedUpdate; import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; -public class ConfigUpdateHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class ConfigUpdateHelper extends HelperBase { public ConfigUpdateHelper() {} public boolean updateAvailable() { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java index edacfaa41..ce29250b9 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ContentHelper.java @@ -6,25 +6,11 @@ import java.util.Locale; import net.i2p.router.RouterContext; import net.i2p.util.FileUtil; -public class ContentHelper { +public class ContentHelper extends HelperBase { private String _page; private int _maxLines; private boolean _startAtBeginning; private String _lang; - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } public ContentHelper() {} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java index 658caa1a3..16ce7337d 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java @@ -11,27 +11,12 @@ import net.i2p.data.DataHelper; import net.i2p.router.RouterContext; import net.i2p.stat.Rate; -public class GraphHelper { - private RouterContext _context; - private Writer _out; +public class GraphHelper extends HelperBase { private int _periodCount; private boolean _showEvents; private int _width; private int _height; private int _refreshDelaySeconds; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } public GraphHelper() { _periodCount = 60; // SummaryListener.PERIODS; @@ -41,7 +26,6 @@ public class GraphHelper { _refreshDelaySeconds = 60; } - public void setOut(Writer out) { _out = out; } public void setPeriodCount(String str) { try { _periodCount = Integer.parseInt(str); } catch (NumberFormatException nfe) {} } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java b/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java new file mode 100644 index 000000000..db5aa9ba2 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java @@ -0,0 +1,29 @@ +package net.i2p.router.web; + +import java.io.Writer; + +import net.i2p.router.RouterContext; + +/** + * Base helper + */ +public abstract class HelperBase { + protected RouterContext _context; + protected Writer _out; + + /** + * Configure this bean to query a particular router context + * + * @param contextId begging few characters of the routerHash, or null to pick + * the first one we come across. + */ + public void setContextId(String contextId) { + try { + _context = ContextHelper.getContext(contextId); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + public void setWriter(Writer out) { _out = out; } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/JobQueueHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/JobQueueHelper.java index a56cce19a..cf8ed2352 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/JobQueueHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/JobQueueHelper.java @@ -7,27 +7,9 @@ import java.io.Writer; import net.i2p.router.RouterContext; -public class JobQueueHelper { - private RouterContext _context; - private Writer _out; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class JobQueueHelper extends HelperBase { public JobQueueHelper() {} - public void setWriter(Writer writer) { _out = writer; } - public String getJobQueueSummary() { try { if (_out != null) { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java index 67d2fc38c..e1fce8f3e 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java @@ -5,22 +5,7 @@ import java.util.List; import net.i2p.router.RouterContext; import net.i2p.util.FileUtil; -public class LogsHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class LogsHelper extends HelperBase { public LogsHelper() {} public String getLogs() { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java index a4b2125e3..2d50379f3 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NavHelper.java @@ -6,22 +6,8 @@ import java.util.Map; import net.i2p.router.RouterContext; -public class NavHelper { +public class NavHelper extends HelperBase { private static Map _apps = new HashMap(); - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } public NavHelper() {} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java index 114579a27..a3280ac44 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java @@ -7,29 +7,12 @@ import java.io.Writer; import net.i2p.router.RouterContext; -public class NetDbHelper { - private RouterContext _context; - private Writer _out; +public class NetDbHelper extends HelperBase { private String _routerPrefix; private boolean _full = false; - - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } public NetDbHelper() {} - public void setWriter(Writer writer) { _out = writer; } public void setRouter(String r) { _routerPrefix = r; } public void setFull(String f) { _full = "1".equals(f); }; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NoticeHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NoticeHelper.java index cd656e9e9..d5ce2b0d9 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NoticeHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NoticeHelper.java @@ -7,22 +7,7 @@ import net.i2p.router.RouterContext; * Simple helper to query the appropriate router for data necessary to render * any emergency notices */ -public class NoticeHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class NoticeHelper extends HelperBase { public String getSystemNotice() { if (true) return ""; // moved to the left hand nav if (_context.router().gracefulShutdownInProgress()) { @@ -35,4 +20,4 @@ public class NoticeHelper { return ""; } } -} \ No newline at end of file +} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java index 556367e27..6237183ab 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/OldConsoleHelper.java @@ -8,29 +8,9 @@ import java.io.Writer; import net.i2p.router.RouterContext; import net.i2p.router.admin.StatsGenerator; -public class OldConsoleHelper { - private RouterContext _context; - private Writer _out; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class OldConsoleHelper extends HelperBase { public OldConsoleHelper() {} - public void setWriter(Writer writer) { - _out = writer; - } - public String getConsole() { try { if (_out != null) { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PeerHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/PeerHelper.java index e5561fe1f..2504067ac 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PeerHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PeerHelper.java @@ -5,28 +5,12 @@ import java.io.Writer; import net.i2p.router.RouterContext; -public class PeerHelper { - private RouterContext _context; - private Writer _out; +public class PeerHelper extends HelperBase { private int _sortFlags; private String _urlBase; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } public PeerHelper() {} - public void setOut(Writer out) { _out = out; } public void setSort(String flags) { if (flags != null) { try { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java index 4db1010a5..702a63e50 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ProfilesHelper.java @@ -6,22 +6,7 @@ import java.io.OutputStreamWriter; import net.i2p.router.RouterContext; -public class ProfilesHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class ProfilesHelper extends HelperBase { public ProfilesHelper() {} public String getProfileSummary() { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/StatHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/StatHelper.java index 8b67d2622..ce6fefd5d 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/StatHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/StatHelper.java @@ -11,12 +11,10 @@ import net.i2p.router.RouterContext; * uuuugly. dump the peer profile data if given a peer. * */ -public class StatHelper { +public class StatHelper extends HelperBase { private String _peer; - private Writer _writer; public void setPeer(String peer) { _peer = peer; } - public void setWriter(Writer writer) { _writer = writer; } public String getProfile() { RouterContext ctx = (RouterContext)net.i2p.router.RouterContext.listContexts().get(0); @@ -25,10 +23,10 @@ public class StatHelper { Hash peer = (Hash)iter.next(); if (peer.toBase64().startsWith(_peer)) { try { - WriterOutputStream wos = new WriterOutputStream(_writer); + WriterOutputStream wos = new WriterOutputStream(_out); ctx.profileOrganizer().exportProfile(peer, wos); wos.flush(); - _writer.flush(); + _out.flush(); return ""; } catch (Exception e) { e.printStackTrace(); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java index ad8e7135d..2e56e858b 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java @@ -25,22 +25,7 @@ import net.i2p.stat.RateStat; * Simple helper to query the appropriate router for data necessary to render * the summary sections on the router console. */ -public class SummaryHelper { - private RouterContext _context; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class SummaryHelper extends HelperBase { /** * Retrieve the shortened 4 character ident for the router located within * the current JVM at the given context. diff --git a/apps/routerconsole/java/src/net/i2p/router/web/TunnelHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/TunnelHelper.java index 4d4eba76b..3cd8a96e3 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/TunnelHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/TunnelHelper.java @@ -7,27 +7,9 @@ import java.io.Writer; import net.i2p.router.RouterContext; -public class TunnelHelper { - private RouterContext _context; - private Writer _out; - /** - * Configure this bean to query a particular router context - * - * @param contextId begging few characters of the routerHash, or null to pick - * the first one we come across. - */ - public void setContextId(String contextId) { - try { - _context = ContextHelper.getContext(contextId); - } catch (Throwable t) { - t.printStackTrace(); - } - } - +public class TunnelHelper extends HelperBase { public TunnelHelper() {} - public void setWriter(Writer writer) { _out = writer; } - public String getTunnelSummary() { try { if (_out != null) { diff --git a/apps/routerconsole/jsp/graphs.jsp b/apps/routerconsole/jsp/graphs.jsp index 422bf19d6..06807f397 100644 --- a/apps/routerconsole/jsp/graphs.jsp +++ b/apps/routerconsole/jsp/graphs.jsp @@ -14,7 +14,7 @@ " /> - +
    diff --git a/apps/routerconsole/jsp/peers.jsp b/apps/routerconsole/jsp/peers.jsp index a537af634..d3b941a34 100644 --- a/apps/routerconsole/jsp/peers.jsp +++ b/apps/routerconsole/jsp/peers.jsp @@ -13,7 +13,7 @@
    " /> - + " /> diff --git a/apps/routerconsole/jsp/summary.jsp b/apps/routerconsole/jsp/summary.jsp index 6668683e6..48f3b4fef 100644 --- a/apps/routerconsole/jsp/summary.jsp +++ b/apps/routerconsole/jsp/summary.jsp @@ -12,6 +12,9 @@ " />
    +
    Configuration  Help
    +
    + General
    Ident: (, never reveal it to anyone" href="netdb.jsp?r=.">view)
    Version:
    @@ -60,11 +63,9 @@ if (prev != null) System.setProperty("net.i2p.router.web.ReseedHandler.noncePrev", prev); System.setProperty("net.i2p.router.web.ReseedHandler.nonce", nonce+""); String uri = request.getRequestURI(); - if (uri.indexOf('?') > 0) - uri = uri + "&reseedNonce=" + nonce; - else - uri = uri + "?reseedNonce=" + nonce; - out.print(" reseed
    "); + out.print("

    \n"); + out.print("\n"); + out.print("

    \n"); } } // If a new reseed ain't running, and the last reseed had errors, show error message @@ -97,6 +98,5 @@ Tunnel lag:
    Handle backlog:

    -
    From 4aa9c7fdcf06a2aeadd7d2742dc6f7639e15cfa3 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 29 Jan 2009 21:13:24 +0000 Subject: [PATCH 088/191] * NTCP: Use a java.util.concurrent execution queue instead of SimpleTimer for afterSend() to reduce lock contention --- .../transport/ntcp/NTCPSendFinisher.java | 89 +++++++++++++++++++ .../router/transport/ntcp/NTCPTransport.java | 29 ++---- 2 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java new file mode 100644 index 000000000..8d19c6249 --- /dev/null +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPSendFinisher.java @@ -0,0 +1,89 @@ +package net.i2p.router.transport.ntcp; + +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadFactory; + +import net.i2p.I2PAppContext; +import net.i2p.router.OutNetMessage; +import net.i2p.util.Log; + +/** + * Previously, NTCP was using SimpleTimer with a delay of 0, which + * was a real abuse. + * + * Here we use the non-scheduled, lockless ThreadPoolExecutor with + * a fixed pool size and an unbounded queue. + * + * The old implementation was having problems with lock contention; + * this should work a lot better - and not clog up the SimpleTimer queue. + * + * @author zzz + */ +public class NTCPSendFinisher { + private static final int THREADS = 4; + private I2PAppContext _context; + private NTCPTransport _transport; + private Log _log; + private int _count; + private ThreadPoolExecutor _executor; + + public NTCPSendFinisher(I2PAppContext context, NTCPTransport transport) { + _context = context; + _log = _context.logManager().getLog(NTCPSendFinisher.class); + _transport = transport; + } + + public void start() { + _count = 0; + _executor = new CustomThreadPoolExecutor(); + } + + public void stop() { + _executor.shutdownNow(); + } + + public void add(OutNetMessage msg) { + _executor.execute(new RunnableEvent(msg)); + } + + // not really needed for now but in case we want to add some hooks like afterExecute() + private class CustomThreadPoolExecutor extends ThreadPoolExecutor { + public CustomThreadPoolExecutor() { + // use unbounded queue, so maximumPoolSize and keepAliveTime have no effect + super(THREADS, THREADS, 1000, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), new CustomThreadFactory()); + } + } + + private class CustomThreadFactory implements ThreadFactory { + public Thread newThread(Runnable r) { + Thread rv = Executors.defaultThreadFactory().newThread(r); + rv.setName("NTCPSendFinisher " + (++_count) + '/' + THREADS); + rv.setDaemon(true); + return rv; + } + } + + /** + * Call afterSend() for the message + */ + private class RunnableEvent implements Runnable { + private OutNetMessage _msg; + + public RunnableEvent(OutNetMessage msg) { + _msg = msg; + } + + public void run() { + try { + _transport.afterSend(_msg, true, false, _msg.getSendTime()); + } catch (Throwable t) { + _log.log(Log.CRIT, " wtf, afterSend borked", t); + } + } + } +} + diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java index 4b5573917..c23245bae 100644 --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java @@ -27,7 +27,6 @@ import net.i2p.router.transport.Transport; import net.i2p.router.transport.TransportBid; import net.i2p.router.transport.TransportImpl; import net.i2p.util.Log; -import net.i2p.util.SimpleTimer; /** * @@ -50,7 +49,7 @@ public class NTCPTransport extends TransportImpl { private List _establishing; private List _sent; - private SendFinisher _finisher; + private NTCPSendFinisher _finisher; public NTCPTransport(RouterContext ctx) { super(ctx); @@ -124,7 +123,7 @@ public class NTCPTransport extends TransportImpl { _conByIdent = new HashMap(64); _sent = new ArrayList(4); - _finisher = new SendFinisher(); + _finisher = new NTCPSendFinisher(ctx, this); _pumper = new EventPumper(ctx, this); _reader = new Reader(ctx); @@ -310,27 +309,8 @@ public class NTCPTransport extends TransportImpl { return countActivePeers() < getMaxConnections() * 4 / 5; } + /** queue up afterSend call, which can take some time w/ jobs, etc */ void sendComplete(OutNetMessage msg) { _finisher.add(msg); } - /** async afterSend call, which can take some time w/ jobs, etc */ - private class SendFinisher implements SimpleTimer.TimedEvent { - public void add(OutNetMessage msg) { - synchronized (_sent) { _sent.add(msg); } - SimpleTimer.getInstance().addEvent(SendFinisher.this, 0); - } - public void timeReached() { - int pending = 0; - OutNetMessage msg = null; - synchronized (_sent) { - pending = _sent.size()-1; - if (pending >= 0) - msg = (OutNetMessage)_sent.remove(0); - } - if (msg != null) - afterSend(msg, true, false, msg.getSendTime()); - if (pending > 0) - SimpleTimer.getInstance().addEvent(SendFinisher.this, 0); - } - } private boolean isEstablished(RouterIdentity peer) { return isEstablished(peer.calculateHash()); @@ -412,6 +392,7 @@ public class NTCPTransport extends TransportImpl { public RouterAddress startListening() { if (_log.shouldLog(Log.DEBUG)) _log.debug("Starting ntcp transport listening"); + _finisher.start(); _pumper.startPumping(); _reader.startReading(NUM_CONCURRENT_READERS); @@ -423,6 +404,7 @@ public class NTCPTransport extends TransportImpl { public RouterAddress restartListening(RouterAddress addr) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Restarting ntcp transport listening"); + _finisher.start(); _pumper.startPumping(); _reader.startReading(NUM_CONCURRENT_READERS); @@ -551,6 +533,7 @@ public class NTCPTransport extends TransportImpl { _pumper.stopPumping(); _writer.stopWriting(); _reader.stopReading(); + _finisher.stop(); Map cons = null; synchronized (_conLock) { cons = new HashMap(_conByIdent); From d75e1deae7b0c9e399fde417305623dfec7cdf9e Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 30 Jan 2009 21:25:18 +0000 Subject: [PATCH 089/191] Fix readLong() bug where it wasnt throwing an exception on EOF --- core/java/src/net/i2p/data/DataHelper.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java index 4a074f17c..53e32a347 100644 --- a/core/java/src/net/i2p/data/DataHelper.java +++ b/core/java/src/net/i2p/data/DataHelper.java @@ -344,8 +344,9 @@ public class DataHelper { long rv = 0; for (int i = 0; i < numBytes; i++) { - long cur = rawStream.read() & 0xFF; + long cur = rawStream.read(); if (cur == -1) throw new DataFormatException("Not enough bytes for the field"); + cur &= 0xFF; // we loop until we find a nonzero byte (or we reach the end) if (cur != 0) { // ok, data found, now iterate through it to fill the rv @@ -355,9 +356,10 @@ public class DataHelper { cur = cur << shiftAmount; rv += cur; if (j + 1 < remaining) { - cur = rawStream.read() & 0xFF; + cur = rawStream.read(); if (cur == -1) throw new DataFormatException("Not enough bytes for the field"); + cur &= 0xFF; } } break; From 7365ca849f6c0756dda56b21f13c6620ed5b083a Mon Sep 17 00:00:00 2001 From: dream Date: Fri, 30 Jan 2009 22:32:52 +0000 Subject: [PATCH 090/191] preliminary debian package support This sets i2p up as a functional Debian source package. dpkg-buildpackage will build i2p using ant preppkg (tarball takes too long and not helpful). It creates a binary .deb archive of the i2p installation, which when installed goes into /var/lib/i2p as the non-root user i2p, and adds an /etc/init.d script to start it up. Some problems not yet solved: 1) under Debian the conf should go into /etc/i2p, but since it doesn't things like the eepsite index file get overwritten if you reinstall. should check for those somehow and not replace them, or ask the user. 2) under Debian they like it if you split the generated data from the static code, so i2p should go into /usr/lib/i2p maybe, but its netDB and any other cache files into /var/cache/i2p that's important not just for organization, but also /var is often on a filesystem optimized for churn. For now just put it in /var/lib 3) i2p is supposedly architecture independant, but it does choose a native jbigi library on postinstall, so does that really count as architecture independant? --- debian/changelog | 4 +++ debian/control | 24 ++++++++++++++++++ debian/copyright | 8 ++++++ debian/rules | 20 +++++++++++++++ debian/scripts/init | 54 +++++++++++++++++++++++++++++++++++++++++ debian/scripts/postinst | 9 +++++++ debian/scripts/postrm | 2 ++ debian/scripts/prerm | 2 ++ 8 files changed, 123 insertions(+) create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/rules create mode 100755 debian/scripts/init create mode 100755 debian/scripts/postinst create mode 100755 debian/scripts/postrm create mode 100755 debian/scripts/prerm diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 000000000..bb17d98ae --- /dev/null +++ b/debian/changelog @@ -0,0 +1,4 @@ +i2p (0.7-0) testing; urgency=low + * just setting this debian thing up + um... + -- dream Wed, 01 Jan 2009 17:14:57 +0000 diff --git a/debian/control b/debian/control new file mode 100644 index 000000000..5c9d9f112 --- /dev/null +++ b/debian/control @@ -0,0 +1,24 @@ +Source: i2p +Maintainer: jrandom +Section: net +Priority: optional +Homepage: http://dev.i2p2.de +Build-Depends: java-sdk, ant +Recommends: libgmp3c2 +Version: 0.7-0 +Tags: implemented-in::java, interface::daemon, network::client, network::server, role::program, security::cryptography + +Package: i2p +Architecture: all +Section: net +Priority: optional +Depends: java-runtime +Recommends: libgmp3c2 +Description: load-balanced unspoofable packet switching network + I2P is an anonymizing network, offering a simple layer that identity-sensitive + applications can use to securely communicate. All data is wrapped with several + layers of encryption, and the network is both distributed and dynamic, with no + trusted parties. +Homepage: http://www.i2p2.de +Version: 0.7-0 +Tags: implemented-in::java, interface::daemon, network::client, network::server, role::program, security::cryptography diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 000000000..0b434e175 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,8 @@ +everything is released according to the +terms of the I2P license policy. For the I2P SDK, +that means everything contained within this +module is released into the public domain unless +otherwise marked. Alternate licenses that may be +used include BSD (used by thecrypto's DSA, ElGamal, +and SHA256 implementations), Cryptix (used by cryptix's +AES implementation), and MIT. diff --git a/debian/rules b/debian/rules new file mode 100755 index 000000000..6f8aa2c9b --- /dev/null +++ b/debian/rules @@ -0,0 +1,20 @@ +#!/usr/bin/make -f + +build: + ant preppkg && \ + (cd pkg-temp; chmod +x postinstall.sh) && \ + mkdir -p debian/tmp/var/lib && \ + mkdir -p debian/tmp/etc/init.d && \ + cp -a debian/scripts/init debian/tmp/etc/init.d/i2p && \ + cp -a pkg-temp debian/tmp/var/lib/i2p && \ + touch debian/build +binary: build + mkdir -p debian/tmp/DEBIAN && \ + dpkg-gencontrol && \ + cp -a debian/scripts/postinst debian/scripts/postrm debian/scripts/prerm debian/tmp/DEBIAN && \ + dpkg-deb -b debian/tmp .. +clean: + rm -f debian/build + ant clean + rm -Rf pkg-temp + @exit 0 diff --git a/debian/scripts/init b/debian/scripts/init new file mode 100755 index 000000000..5d04ac8fe --- /dev/null +++ b/debian/scripts/init @@ -0,0 +1,54 @@ +#! /bin/sh + +### BEGIN INIT INFO +# Provides: i2p +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: +# Default-Stop: 1 2 3 4 5 +# Short-Description: I2P anonymizing mixnet +### END INIT INFO + +set -e + +. /lib/lsb/init-functions + +function I2P { + su i2p -c "/var/lib/i2p/i2prouter $1" +} + +case "$1" in + start) + log_daemon_msg "Starting I2P" "i2p" + if I2P start; then + log_end_msg 0 + else + log_end_msg 1 + fi + ;; + stop) + log_daemon_msg "Stopping I2P" "i2p" + if I2P stop; then + log_end_msg 0 + else + log_end_msg 1 + fi + ;; + restart) + log_daemon_msg "Restarting I2P" "i2p" + if I2P restart; then + log_end_msg 0 + else + log_end_msg 1 + fi + ;; + status) + I2P status + ;; + + *) + log_action_msg "Usage: /etc/init.d/i2p {start|stop|restart|status}" + exit 1 +esac + +exit 0 diff --git a/debian/scripts/postinst b/debian/scripts/postinst new file mode 100755 index 000000000..a1f03b83a --- /dev/null +++ b/debian/scripts/postinst @@ -0,0 +1,9 @@ +#!/bin/sh +TOP=/var/lib/i2p +useradd -b $TOP -r i2p 2>/dev/null +chown i2p $TOP -R + +update-rc.d + +cd $TOP +exec su i2p -c ./postinstall.sh diff --git a/debian/scripts/postrm b/debian/scripts/postrm new file mode 100755 index 000000000..633f3a9f4 --- /dev/null +++ b/debian/scripts/postrm @@ -0,0 +1,2 @@ +#!/bin/sh +exec userdel i2p diff --git a/debian/scripts/prerm b/debian/scripts/prerm new file mode 100755 index 000000000..cd32c3754 --- /dev/null +++ b/debian/scripts/prerm @@ -0,0 +1,2 @@ +#!/bin/sh +exec /etc/init.d/i2p stop From f7f93fda0c791a078116dfa895c9376ae2cc8aea Mon Sep 17 00:00:00 2001 From: sponge Date: Sat, 31 Jan 2009 05:18:12 +0000 Subject: [PATCH 091/191] Discarded int fix. --- apps/BOB/src/net/i2p/BOB/BOB.java | 1 + history.txt | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/apps/BOB/src/net/i2p/BOB/BOB.java b/apps/BOB/src/net/i2p/BOB/BOB.java index 2c42a9834..0229932a1 100644 --- a/apps/BOB/src/net/i2p/BOB/BOB.java +++ b/apps/BOB/src/net/i2p/BOB/BOB.java @@ -217,6 +217,7 @@ public class BOB { } } + i = 0; try { info("BOB is now running."); ServerSocket listener = new ServerSocket(Integer.parseInt(props.getProperty(PROP_BOB_PORT)), 10, InetAddress.getByName(props.getProperty(PROP_BOB_HOST))); diff --git a/history.txt b/history.txt index 42fa010c1..a1472cb27 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,7 @@ +2009-01-31 sponge + * One line BOB discarded interger fix + (not that it mattered at this point) + 2009-01-25 zzz * Build files: - Don't bundle unneeded XML parser xercesImpl.jar (1MB) From a5ab6f576d98d5199af4ff4e26ac568f51eca649 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 31 Jan 2009 14:22:07 +0000 Subject: [PATCH 092/191] * SimpleScheduler: New replacement for SimpleTimer when events will not be rescheduled or cancelled, to reduce SimpleTimer lock contention --- .../src/org/klomp/snark/I2PSnarkUtil.java | 3 +- .../org/klomp/snark/PeerConnectionOut.java | 3 +- .../i2p/i2ptunnel/I2PTunnelClientBase.java | 3 +- .../net/i2p/client/streaming/Connection.java | 9 +- .../client/streaming/ConnectionHandler.java | 3 +- .../streaming/ConnectionPacketHandler.java | 3 +- .../src/net/i2p/apps/systray/SysTray.java | 4 +- .../src/net/i2p/client/I2PSessionImpl.java | 3 +- .../crypto/TransientSessionKeyManager.java | 4 +- core/java/src/net/i2p/util/ByteCache.java | 3 +- .../src/net/i2p/util/SimpleScheduler.java | 164 ++++++++++++++++++ router/java/src/net/i2p/router/Router.java | 7 +- .../router/client/ClientConnectionRunner.java | 3 +- .../i2p/router/peermanager/PeerManager.java | 5 +- .../transport/udp/EstablishmentManager.java | 9 +- .../router/transport/udp/PeerTestManager.java | 9 +- .../i2p/router/transport/udp/UDPReceiver.java | 3 +- .../router/transport/udp/UDPTransport.java | 3 +- 18 files changed, 207 insertions(+), 34 deletions(-) create mode 100644 core/java/src/net/i2p/util/SimpleScheduler.java diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index f014580ec..26ed5860f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -24,6 +24,7 @@ import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.util.EepGet; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -183,7 +184,7 @@ public class I2PSnarkUtil { synchronized (_shitlist) { _shitlist.add(dest); } - SimpleTimer.getInstance().addEvent(new Unshitlist(dest), 10*60*1000); + SimpleScheduler.getInstance().addEvent(new Unshitlist(dest), 10*60*1000); throw new IOException("Unable to reach the peer " + peer + ": " + ie.getMessage()); } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java index 8fed9577a..1a53c342f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java @@ -28,6 +28,7 @@ import java.util.List; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; class PeerConnectionOut implements Runnable @@ -215,7 +216,7 @@ class PeerConnectionOut implements Runnable private void addMessage(Message m) { if (m.type == Message.PIECE) - SimpleTimer.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT); + SimpleScheduler.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT); synchronized(sendQueue) { sendQueue.add(m); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java index d6e5bf9f9..38311eaf1 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java @@ -27,6 +27,7 @@ import net.i2p.data.Destination; import net.i2p.util.EventDispatcher; import net.i2p.util.I2PThread; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runnable { @@ -401,7 +402,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna } if (_maxWaitTime > 0) - SimpleTimer.getInstance().addEvent(new CloseEvent(s), _maxWaitTime); + SimpleScheduler.getInstance().addEvent(new CloseEvent(s), _maxWaitTime); synchronized (_waitingSockets) { _waitingSockets.add(s); diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java index 85872e9c5..ee93d20ee 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java @@ -12,6 +12,7 @@ import net.i2p.client.I2PSession; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -246,7 +247,7 @@ public class Connection { void sendReset() { if (_disconnectScheduledOn < 0) { _disconnectScheduledOn = _context.clock().now(); - SimpleTimer.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT); + SimpleScheduler.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT); } long now = _context.clock().now(); if (_resetSentOn + 10*1000 > now) return; // don't send resets too fast @@ -460,7 +461,7 @@ public class Connection { void resetReceived() { if (_disconnectScheduledOn < 0) { _disconnectScheduledOn = _context.clock().now(); - SimpleTimer.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT); + SimpleScheduler.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT); } _resetReceived = true; MessageOutputStream mos = _outputStream; @@ -509,7 +510,7 @@ public class Connection { if (removeFromConMgr) { if (_disconnectScheduledOn < 0) { _disconnectScheduledOn = _context.clock().now(); - SimpleTimer.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT); + SimpleScheduler.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT); } } _connected = false; @@ -708,7 +709,7 @@ public class Connection { _closeSentOn = when; if (_disconnectScheduledOn < 0) { _disconnectScheduledOn = _context.clock().now(); - SimpleTimer.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT); + SimpleScheduler.getInstance().addEvent(new DisconnectEvent(), DISCONNECT_TIMEOUT); } } public long getCloseReceivedOn() { return _closeReceivedOn; } diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java index 7d1d4827f..a123708e4 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java @@ -5,6 +5,7 @@ import java.util.List; import net.i2p.I2PAppContext; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -54,7 +55,7 @@ class ConnectionHandler { } if (_log.shouldLog(Log.DEBUG)) _log.debug("Receive new SYN: " + packet + ": timeout in " + _acceptTimeout); - RetransmissionTimer.getInstance().addEvent(new TimeoutSyn(packet), _acceptTimeout); + SimpleScheduler.getInstance().addEvent(new TimeoutSyn(packet), _acceptTimeout); synchronized (_synQueue) { _synQueue.add(packet); _synQueue.notifyAll(); diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java index 7c445f038..f7b245cb8 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionPacketHandler.java @@ -7,6 +7,7 @@ import net.i2p.I2PException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -168,7 +169,7 @@ public class ConnectionPacketHandler { // take note of congestion if (_log.shouldLog(Log.WARN)) _log.warn("congestion.. dup " + packet); - RetransmissionTimer.getInstance().addEvent(new AckDup(con), con.getOptions().getSendAckDelay()); + SimpleScheduler.getInstance().addEvent(new AckDup(con), con.getOptions().getSendAckDelay()); //con.setNextSendTime(_context.clock().now() + con.getOptions().getSendAckDelay()); //fastAck = true; } else { diff --git a/apps/systray/java/src/net/i2p/apps/systray/SysTray.java b/apps/systray/java/src/net/i2p/apps/systray/SysTray.java index 380c5b172..4a635fd08 100644 --- a/apps/systray/java/src/net/i2p/apps/systray/SysTray.java +++ b/apps/systray/java/src/net/i2p/apps/systray/SysTray.java @@ -11,6 +11,7 @@ package net.i2p.apps.systray; import java.awt.Frame; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; import snoozesoft.systray4j.SysTrayMenu; import snoozesoft.systray4j.SysTrayMenuEvent; @@ -60,14 +61,13 @@ public class SysTray implements SysTrayMenuListener { private SysTray() { _sysTrayMenuIcon.addSysTrayMenuListener(this); createSysTrayMenu(); - SimpleTimer.getInstance().addEvent(new RefreshDisplayEvent(), REFRESH_DISPLAY_FREQUENCY); + SimpleScheduler.getInstance().addPeriodicEvent(new RefreshDisplayEvent(), REFRESH_DISPLAY_FREQUENCY); } private static final long REFRESH_DISPLAY_FREQUENCY = 30*1000; private class RefreshDisplayEvent implements SimpleTimer.TimedEvent { public void timeReached() { refreshDisplay(); - SimpleTimer.getInstance().addEvent(RefreshDisplayEvent.this, REFRESH_DISPLAY_FREQUENCY); } } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index a57957107..d4ff7360a 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -40,6 +40,7 @@ import net.i2p.data.i2cp.MessagePayloadMessage; import net.i2p.data.i2cp.SessionId; import net.i2p.util.I2PThread; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -369,7 +370,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Notified availability for session " + _sessionId + ", message " + id); } - SimpleTimer.getInstance().addEvent(new VerifyUsage(mid), 30*1000); + SimpleScheduler.getInstance().addEvent(new VerifyUsage(mid), 30*1000); } private class VerifyUsage implements SimpleTimer.TimedEvent { private Long _msgId; diff --git a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java index 1b160f8dd..0d71677a9 100644 --- a/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java +++ b/core/java/src/net/i2p/crypto/TransientSessionKeyManager.java @@ -24,6 +24,7 @@ import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -70,7 +71,7 @@ class TransientSessionKeyManager extends SessionKeyManager { _inboundTagSets = new HashMap(1024); context.statManager().createRateStat("crypto.sessionTagsExpired", "How many tags/sessions are expired?", "Encryption", new long[] { 10*60*1000, 60*60*1000, 3*60*60*1000 }); context.statManager().createRateStat("crypto.sessionTagsRemaining", "How many tags/sessions are remaining after a cleanup?", "Encryption", new long[] { 10*60*1000, 60*60*1000, 3*60*60*1000 }); - SimpleTimer.getInstance().addEvent(new CleanupEvent(), 60*1000); + SimpleScheduler.getInstance().addPeriodicEvent(new CleanupEvent(), 60*1000); } private TransientSessionKeyManager() { this(null); } @@ -80,7 +81,6 @@ class TransientSessionKeyManager extends SessionKeyManager { int expired = aggressiveExpire(); long expireTime = _context.clock().now() - beforeExpire; _context.statManager().addRateData("crypto.sessionTagsExpired", expired, expireTime); - SimpleTimer.getInstance().addEvent(CleanupEvent.this, 60*1000); } } diff --git a/core/java/src/net/i2p/util/ByteCache.java b/core/java/src/net/i2p/util/ByteCache.java index aadc721aa..4bd3da6ef 100644 --- a/core/java/src/net/i2p/util/ByteCache.java +++ b/core/java/src/net/i2p/util/ByteCache.java @@ -55,7 +55,7 @@ public final class ByteCache { _maxCached = maxCachedEntries; _entrySize = entrySize; _lastOverflow = -1; - SimpleTimer.getInstance().addEvent(new Cleanup(), CLEANUP_FREQUENCY); + SimpleScheduler.getInstance().addPeriodicEvent(new Cleanup(), CLEANUP_FREQUENCY); _log = I2PAppContext.getGlobalContext().logManager().getLog(ByteCache.class); } @@ -120,7 +120,6 @@ public final class ByteCache { _log.debug("Removing " + toRemove + " cached entries of size " + _entrySize); } } - SimpleTimer.getInstance().addEvent(Cleanup.this, CLEANUP_FREQUENCY); } } } diff --git a/core/java/src/net/i2p/util/SimpleScheduler.java b/core/java/src/net/i2p/util/SimpleScheduler.java new file mode 100644 index 000000000..91415102c --- /dev/null +++ b/core/java/src/net/i2p/util/SimpleScheduler.java @@ -0,0 +1,164 @@ +package net.i2p.util; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadFactory; + +import net.i2p.I2PAppContext; + +/** + * Simple event scheduler - toss an event on the queue and it gets fired at the + * appropriate time. The method that is fired however should NOT block (otherwise + * they b0rk the timer). + * + * This is like SimpleScheduler but addEvent() for an existing event adds a second + * job. Events cannot be cancelled or rescheduled. + * + * For events that cannot or will not be cancelled or rescheduled - + * for example, a call such as: + * SimpleTimer.getInstance().addEvent(new FooEvent(bar), timeoutMs); + * use SimpleScheduler instead to reduce lock contention in SimpleTimer... + * + * For periodic events, use addPeriodicEvent(). Unlike SimpleTimer, + * uncaught Exceptions will not prevent subsequent executions. + * + * @author zzz + */ +public class SimpleScheduler { + private static final SimpleScheduler _instance = new SimpleScheduler(); + public static SimpleScheduler getInstance() { return _instance; } + private static final int THREADS = 4; + private I2PAppContext _context; + private Log _log; + private ScheduledThreadPoolExecutor _executor; + private String _name; + private int _count; + + protected SimpleScheduler() { this("SimpleScheduler"); } + protected SimpleScheduler(String name) { + _context = I2PAppContext.getGlobalContext(); + _log = _context.logManager().getLog(SimpleScheduler.class); + _name = name; + _count = 0; + _executor = new ScheduledThreadPoolExecutor(THREADS, new CustomThreadFactory()); + } + + /** + * Removes the SimpleScheduler. + */ + public void stop() { + _executor.shutdownNow(); + } + + /** + * Queue up the given event to be fired no sooner than timeoutMs from now. + * + * @param event + * @param timeoutMs + */ + public void addEvent(SimpleTimer.TimedEvent event, long timeoutMs) { + if (event == null) + throw new IllegalArgumentException("addEvent null"); + RunnableEvent re = new RunnableEvent(event, timeoutMs); + re.schedule(); + } + + public void addPeriodicEvent(SimpleTimer.TimedEvent event, long timeoutMs) { + addPeriodicEvent(event, timeoutMs, timeoutMs); + } + + /** + * Queue up the given event to be fired after initialDelay and every + * timeoutMs thereafter. The TimedEvent must not do its own rescheduling. + * As all Exceptions are caught in run(), these will not prevent + * subsequent executions (unlike SimpleTimer, where the TimedEvent does + * its own rescheduling) + * + * @param event + * @param initialDelay (ms) + * @param timeoutMs + */ + public void addPeriodicEvent(SimpleTimer.TimedEvent event, long initialDelay, long timeoutMs) { + if (event == null) + throw new IllegalArgumentException("addEvent null"); + RunnableEvent re = new PeriodicRunnableEvent(event, initialDelay, timeoutMs); + re.schedule(); + } + + private class CustomThreadFactory implements ThreadFactory { + public Thread newThread(Runnable r) { + Thread rv = Executors.defaultThreadFactory().newThread(r); + rv.setName(_name + ' ' + (++_count) + '/' + THREADS); + rv.setDaemon(true); + return rv; + } + } + + /** + * Same as SimpleTimer.TimedEvent but use run() instead of timeReached(), and remembers the time + */ + private class RunnableEvent implements Runnable { + protected SimpleTimer.TimedEvent _timedEvent; + protected long _scheduled; + + public RunnableEvent(SimpleTimer.TimedEvent t, long timeoutMs) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Creating w/ delay " + timeoutMs + " : " + t); + _timedEvent = t; + _scheduled = timeoutMs + System.currentTimeMillis(); + } + public void schedule() { + _executor.schedule(this, _scheduled - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + public void run() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Running: " + _timedEvent); + long before = System.currentTimeMillis(); + if (_log.shouldLog(Log.WARN) && before < _scheduled - 100) + _log.warn(_name + " wtf, early execution " + (_scheduled - before) + ": " + _timedEvent); + else if (_log.shouldLog(Log.WARN) && before > _scheduled + 1000) + _log.warn(" wtf, late execution " + (before - _scheduled) + ": " + _timedEvent + debug()); + try { + _timedEvent.timeReached(); + } catch (Throwable t) { + _log.log(Log.CRIT, _name + " wtf, event borked: " + _timedEvent, t); + } + long time = System.currentTimeMillis() - before; + if (time > 1000 && _log.shouldLog(Log.WARN)) + _log.warn(_name + " wtf, event execution took " + time + ": " + _timedEvent); + long completed = _executor.getCompletedTaskCount(); + if (_log.shouldLog(Log.INFO) && completed % 250 == 0) + _log.info(debug()); + } + } + + /** Run every timeoutMs. TimedEvent must not do its own reschedule via addEvent() */ + private class PeriodicRunnableEvent extends RunnableEvent { + private long _timeoutMs; + private long _initialDelay; + public PeriodicRunnableEvent(SimpleTimer.TimedEvent t, long initialDelay, long timeoutMs) { + super(t, timeoutMs); + _initialDelay = initialDelay; + _timeoutMs = timeoutMs; + _scheduled = initialDelay + System.currentTimeMillis(); + } + public void schedule() { + _executor.scheduleWithFixedDelay(this, _initialDelay, _timeoutMs, TimeUnit.MILLISECONDS); + } + public void run() { + super.run(); + _scheduled = _timeoutMs + System.currentTimeMillis(); + } + } + + private String debug() { + return + " Pool: " + _name + + " Active: " + _executor.getActiveCount() + '/' + _executor.getPoolSize() + + " Completed: " + _executor.getCompletedTaskCount() + + " Queued: " + _executor.getQueue().size(); + } +} + diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index f7342413a..033678924 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -43,6 +43,7 @@ import net.i2p.stat.StatManager; import net.i2p.util.FileUtil; import net.i2p.util.I2PThread; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -257,7 +258,7 @@ public class Router { _context.inNetMessagePool().startup(); startupQueue(); //_context.jobQueue().addJob(new CoalesceStatsJob(_context)); - SimpleTimer.getInstance().addEvent(new CoalesceStatsEvent(_context), 0); + SimpleScheduler.getInstance().addPeriodicEvent(new CoalesceStatsEvent(_context), 20*1000); _context.jobQueue().addJob(new UpdateRoutingKeyModifierJob(_context)); warmupCrypto(); _sessionKeyPersistenceHelper.startup(); @@ -346,7 +347,7 @@ public class Router { if (blockingRebuild) r.timeReached(); else - SimpleTimer.getInstance().addEvent(r, 0); + SimpleScheduler.getInstance().addEvent(r, 0); } catch (DataFormatException dfe) { _log.log(Log.CRIT, "Internal error - unable to sign our own address?!", dfe); } @@ -1261,8 +1262,6 @@ class CoalesceStatsEvent implements SimpleTimer.TimedEvent { getContext().statManager().addRateData("bw.sendBps", (long)KBps, 60*1000); } } - - SimpleTimer.getInstance().addEvent(this, 20*1000); } } diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java index 133ad142c..189568ead 100644 --- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java +++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java @@ -38,6 +38,7 @@ import net.i2p.router.RouterContext; import net.i2p.util.I2PThread; import net.i2p.util.Log; import net.i2p.util.RandomSource; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -419,7 +420,7 @@ public class ClientConnectionRunner { // theirs is newer } else { // ours is newer, so wait a few secs and retry - SimpleTimer.getInstance().addEvent(new Rerequest(set, expirationTime, onCreateJob, onFailedJob), 3*1000); + SimpleScheduler.getInstance().addEvent(new Rerequest(set, expirationTime, onCreateJob, onFailedJob), 3*1000); } // fire onCreated? return; // already requesting diff --git a/router/java/src/net/i2p/router/peermanager/PeerManager.java b/router/java/src/net/i2p/router/peermanager/PeerManager.java index b2b16a00d..1c265ee67 100644 --- a/router/java/src/net/i2p/router/peermanager/PeerManager.java +++ b/router/java/src/net/i2p/router/peermanager/PeerManager.java @@ -24,6 +24,7 @@ import net.i2p.router.PeerSelectionCriteria; import net.i2p.router.RouterContext; import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -50,7 +51,7 @@ class PeerManager { _peersByCapability[i] = new ArrayList(64); loadProfiles(); ////_context.jobQueue().addJob(new EvaluateProfilesJob(_context)); - SimpleTimer.getInstance().addEvent(new Reorg(), 0); + SimpleScheduler.getInstance().addPeriodicEvent(new Reorg(), 0, 30*1000); //_context.jobQueue().addJob(new PersistProfilesJob(_context, this)); } @@ -60,8 +61,6 @@ class PeerManager { _organizer.reorganize(true); } catch (Throwable t) { _log.log(Log.CRIT, "Error evaluating profiles", t); - } finally { - SimpleTimer.getInstance().addEvent(Reorg.this, 30*1000); } } } diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java index 6ab159408..896fe1ce4 100644 --- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java +++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java @@ -22,6 +22,7 @@ import net.i2p.router.Router; import net.i2p.router.RouterContext; import net.i2p.util.I2PThread; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -184,7 +185,7 @@ public class EstablishmentManager { msg.getTarget().getIdentity(), new SessionKey(addr.getIntroKey()), addr); _outboundStates.put(to, state); - SimpleTimer.getInstance().addEvent(new Expire(to, state), 10*1000); + SimpleScheduler.getInstance().addEvent(new Expire(to, state), 10*1000); } } if (state != null) { @@ -394,7 +395,7 @@ public class EstablishmentManager { msg.getTarget().getIdentity(), new SessionKey(addr.getIntroKey()), addr); _outboundStates.put(to, qstate); - SimpleTimer.getInstance().addEvent(new Expire(to, qstate), 10*1000); + SimpleScheduler.getInstance().addEvent(new Expire(to, qstate), 10*1000); for (int i = 0; i < queued.size(); i++) { OutNetMessage m = (OutNetMessage)queued.get(i); @@ -477,7 +478,7 @@ public class EstablishmentManager { dsm.setMessageExpiration(_context.clock().now()+10*1000); dsm.setMessageId(_context.random().nextLong(I2NPMessage.MAX_ID_VALUE)); _transport.send(dsm, peer); - SimpleTimer.getInstance().addEvent(new PublishToNewInbound(peer), 0); + SimpleScheduler.getInstance().addEvent(new PublishToNewInbound(peer), 0); } private class PublishToNewInbound implements SimpleTimer.TimedEvent { private PeerState _peer; @@ -629,7 +630,7 @@ public class EstablishmentManager { } } } - SimpleTimer.getInstance().addEvent(new FailIntroduction(state, nonce), INTRO_ATTEMPT_TIMEOUT); + SimpleScheduler.getInstance().addEvent(new FailIntroduction(state, nonce), INTRO_ATTEMPT_TIMEOUT); state.setIntroNonce(nonce); _context.statManager().addRateData("udp.sendIntroRelayRequest", 1, 0); UDPPacket requests[] = _builder.buildRelayRequest(_transport, state, _transport.getIntroKey()); diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java index 7aa3c2fa1..35c5511be 100644 --- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java +++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java @@ -15,6 +15,7 @@ import net.i2p.data.SessionKey; import net.i2p.router.CommSystemFacade; import net.i2p.router.RouterContext; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -79,7 +80,7 @@ class PeerTestManager { sendTestToBob(); - SimpleTimer.getInstance().addEvent(new ContinueTest(), RESEND_TIMEOUT); + SimpleScheduler.getInstance().addEvent(new ContinueTest(), RESEND_TIMEOUT); } private class ContinueTest implements SimpleTimer.TimedEvent { @@ -103,7 +104,7 @@ class PeerTestManager { // second message from Charlie yet sendTestToCharlie(); } - SimpleTimer.getInstance().addEvent(ContinueTest.this, RESEND_TIMEOUT); + SimpleScheduler.getInstance().addEvent(ContinueTest.this, RESEND_TIMEOUT); } } } @@ -430,7 +431,7 @@ class PeerTestManager { synchronized (_activeTests) { _activeTests.put(new Long(nonce), state); } - SimpleTimer.getInstance().addEvent(new RemoveTest(nonce), MAX_CHARLIE_LIFETIME); + SimpleScheduler.getInstance().addEvent(new RemoveTest(nonce), MAX_CHARLIE_LIFETIME); } UDPPacket packet = _packetBuilder.buildPeerTestToBob(bobIP, from.getPort(), aliceIP, alicePort, aliceIntroKey, nonce, state.getBobCipherKey(), state.getBobMACKey()); @@ -511,7 +512,7 @@ class PeerTestManager { synchronized (_activeTests) { _activeTests.put(new Long(nonce), state); } - SimpleTimer.getInstance().addEvent(new RemoveTest(nonce), MAX_CHARLIE_LIFETIME); + SimpleScheduler.getInstance().addEvent(new RemoveTest(nonce), MAX_CHARLIE_LIFETIME); } UDPPacket packet = _packetBuilder.buildPeerTestToCharlie(aliceIP, from.getPort(), aliceIntroKey, nonce, diff --git a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java index 10876a0e7..3535484c9 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPReceiver.java @@ -9,6 +9,7 @@ import net.i2p.router.RouterContext; import net.i2p.router.transport.FIFOBandwidthLimiter; import net.i2p.util.I2PThread; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -115,7 +116,7 @@ public class UDPReceiver { long delay = ARTIFICIAL_DELAY_BASE + _context.random().nextInt(ARTIFICIAL_DELAY); if (_log.shouldLog(Log.INFO)) _log.info("Delay packet " + packet + " for " + delay); - SimpleTimer.getInstance().addEvent(new ArtificiallyDelayedReceive(packet), delay); + SimpleScheduler.getInstance().addEvent(new ArtificiallyDelayedReceive(packet), delay); return -1; } diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java index 03714d7ef..e5185defa 100644 --- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java +++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java @@ -33,6 +33,7 @@ import net.i2p.router.transport.Transport; import net.i2p.router.transport.TransportBid; import net.i2p.router.transport.TransportImpl; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; /** @@ -631,7 +632,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority } if (added) { _context.statManager().addRateData("udp.dropPeerDroplist", droplistSize, 0); - SimpleTimer.getInstance().addEvent(new RemoveDropList(remote), DROPLIST_PERIOD); + SimpleScheduler.getInstance().addEvent(new RemoveDropList(remote), DROPLIST_PERIOD); } } markUnreachable(peerHash); From 951f0828848c38a2ab0fab62d15966533abdd87f Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 31 Jan 2009 14:23:33 +0000 Subject: [PATCH 093/191] * i2psnark: Increase tunnels and pipeline to 3 --- apps/i2psnark/java/src/org/klomp/snark/PeerState.java | 2 +- apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java | 2 +- apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java index 1b4feee75..054b58262 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java @@ -60,7 +60,7 @@ class PeerState // If we have te resend outstanding requests (true after we got choked). private boolean resend = false; - private final static int MAX_PIPELINE = 2; // this is for outbound requests + private final static int MAX_PIPELINE = 3; // this is for outbound requests private final static int MAX_PIPELINE_BYTES = 128*1024; // this is for inbound requests public final static int PARTSIZE = 32*1024; // Snark was 16K, i2p-bt uses 64KB private final static int MAX_PARTSIZE = 64*1024; // Don't let anybody request more than this diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 7e5cd962f..7b62ace84 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -141,7 +141,7 @@ public class SnarkManager implements Snark.CompleteListener { if (!_config.containsKey(PROP_I2CP_PORT)) _config.setProperty(PROP_I2CP_PORT, "7654"); if (!_config.containsKey(PROP_I2CP_OPTS)) - _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0"); + _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0 inbound.quantity=3 outbound.quantity=3"); if (!_config.containsKey(PROP_EEP_HOST)) _config.setProperty(PROP_EEP_HOST, "localhost"); if (!_config.containsKey(PROP_EEP_PORT)) 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 c7ee93d7d..c791ad2fb 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -580,7 +580,7 @@ public class I2PSnarkServlet extends HttpServlet { else if ("AUZV".equals(ch) || "AkZV".equals(ch) || "A0ZV".equals(ch)) client = "Robert"; else - client = "Unknown"; + client = "Unknown (" + ch + ')'; out.write("" + client + "  " + peer.toString().substring(5, 9) + ""); if (showDebug) out.write(" inactive " + (peer.getInactiveTime() / 1000) + "s"); From 395baf0274c9b08500efb57bab75426f0379ef27 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 31 Jan 2009 14:27:45 +0000 Subject: [PATCH 094/191] * Convert some inner classes to static (findbugs) --- .../java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java | 8 ++++---- .../src/net/i2p/data/i2cp/RequestLeaseSetMessage.java | 4 ++-- router/java/src/net/i2p/router/Blocklist.java | 2 +- router/java/src/net/i2p/router/Shitlist.java | 2 +- .../src/net/i2p/router/tunnel/pool/TunnelPoolManager.java | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) 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 252d4e1aa..38c50f266 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java @@ -361,24 +361,24 @@ public class SOCKS5Server extends SOCKSServer { /* * Some namespaces to enclose SOCKS protocol codes */ - private class Method { + private static class Method { private static final int NO_AUTH_REQUIRED = 0x00; private static final int NO_ACCEPTABLE_METHODS = 0xff; } - private class AddressType { + private static class AddressType { private static final int IPV4 = 0x01; private static final int DOMAINNAME = 0x03; private static final int IPV6 = 0x04; } - private class Command { + private static class Command { private static final int CONNECT = 0x01; private static final int BIND = 0x02; private static final int UDP_ASSOCIATE = 0x03; } - private class Reply { + private static class Reply { private static final int SUCCEEDED = 0x00; private static final int GENERAL_SOCKS_SERVER_FAILURE = 0x01; private static final int CONNECTION_NOT_ALLOWED_BY_RULESET = 0x02; diff --git a/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java b/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java index 2cd630db6..b5fca013d 100644 --- a/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java +++ b/core/java/src/net/i2p/data/i2cp/RequestLeaseSetMessage.java @@ -156,7 +156,7 @@ public class RequestLeaseSetMessage extends I2CPMessageImpl { return buf.toString(); } - private class TunnelEndpoint { + private static class TunnelEndpoint { private Hash _router; private TunnelId _tunnelId; @@ -186,4 +186,4 @@ public class RequestLeaseSetMessage extends I2CPMessageImpl { _tunnelId = tunnelId; } } -} \ No newline at end of file +} diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java index 5f686c192..1c50eaa65 100644 --- a/router/java/src/net/i2p/router/Blocklist.java +++ b/router/java/src/net/i2p/router/Blocklist.java @@ -256,7 +256,7 @@ public class Blocklist { } } - private class Entry { + private static class Entry { String comment; byte ip1[]; byte ip2[]; diff --git a/router/java/src/net/i2p/router/Shitlist.java b/router/java/src/net/i2p/router/Shitlist.java index 7d86926cf..2005366c2 100644 --- a/router/java/src/net/i2p/router/Shitlist.java +++ b/router/java/src/net/i2p/router/Shitlist.java @@ -36,7 +36,7 @@ public class Shitlist { private RouterContext _context; private Map _entries; - private class Entry { + private static class Entry { /** when it should expire, per the i2p clock */ long expireOn; /** why they were shitlisted */ diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java index 43120d0b0..c6b1f5b9a 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java @@ -376,7 +376,7 @@ public class TunnelPoolManager implements TunnelManagerFacade { _context.jobQueue().addJob(new BootstrapPool(_context, _outboundExploratory)); } - private class BootstrapPool extends JobImpl { + private static class BootstrapPool extends JobImpl { private TunnelPool _pool; public BootstrapPool(RouterContext ctx, TunnelPool pool) { super(ctx); From 78d5080d788a53fc610e44785da7705dfc65c602 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 31 Jan 2009 15:36:24 +0000 Subject: [PATCH 095/191] * Tunnel Pool: - Remove tunnel from participating if can't contact next hop - Fail outbound build faster if can't contact first hop --- .../i2p/router/tunnel/pool/BuildExecutor.java | 1 + .../i2p/router/tunnel/pool/BuildHandler.java | 50 +++++++++++++++---- .../router/tunnel/pool/BuildRequestor.java | 31 ++++++++++++ 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java index 390fe888d..3a84f4810 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java @@ -46,6 +46,7 @@ class BuildExecutor implements Runnable { _context.statManager().createRateStat("tunnel.buildRequestTime", "How long it takes to build a tunnel request", "Tunnels", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("tunnel.buildRequestZeroHopTime", "How long it takes to build a zero hop tunnel", "Tunnels", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("tunnel.pendingRemaining", "How many inbound requests are pending after a pass (period is how long the pass takes)?", "Tunnels", new long[] { 60*1000, 10*60*1000 }); + _context.statManager().createRateStat("tunnel.buildFailFirstHop", "How often we fail to build a OB tunnel because we can't contact the first hop", "Tunnels", new long[] { 60*1000, 10*60*1000 }); // Get stat manager, get recognized bandwidth tiers StatManager statMgr = _context.statManager(); diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java index 3c2a9dd20..f6b4d3478 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java @@ -61,6 +61,7 @@ class BuildHandler { _context.statManager().createRateStat("tunnel.decryptRequestTime", "How long it takes to decrypt a new tunnel build request", "Tunnels", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("tunnel.rejectTimeout", "How often we reject a tunnel because we can't find the next hop", "Tunnels", new long[] { 60*1000, 10*60*1000 }); + _context.statManager().createRateStat("tunnel.rejectTimeout2", "How often we fail a tunnel because we can't contact the next hop", "Tunnels", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("tunnel.rejectOverloaded", "How long we had to wait before processing the request (when it was rejected)", "Tunnels", new long[] { 60*1000, 10*60*1000 }); _context.statManager().createRateStat("tunnel.acceptLoad", "Delay before processing the accepted request", "Tunnels", new long[] { 60*1000, 10*60*1000 }); @@ -413,7 +414,7 @@ class BuildHandler { } } - private class TimeoutReq extends JobImpl { + private static class TimeoutReq extends JobImpl { private BuildMessageState _state; private BuildRequestRecord _req; private Hash _nextPeer; @@ -425,10 +426,12 @@ class BuildHandler { } public String getName() { return "Timeout looking for next peer for tunnel join"; } public void runJob() { - getContext().statManager().addRateData("tunnel.rejectTimeout", 1, 1); - if (_log.shouldLog(Log.WARN)) - _log.warn("Request " + _state.msg.getUniqueId() - + " could no be satisfied, as the next peer could not be found: " + _nextPeer.toBase64()); + getContext().statManager().addRateData("tunnel.rejectTimeout", 1, 0); + // logging commented out so class can be static + //if (_log.shouldLog(Log.WARN)) + // _log.warn("Request " + _state.msg.getUniqueId() + // + " could no be satisfied, as the next peer could not be found: " + _nextPeer.toBase64()); + // ??? should we blame the peer here? getContext().profileManager().tunnelTimedOut(_nextPeer); getContext().messageHistory().tunnelRejected(_state.fromHash, new TunnelId(_req.readReceiveTunnelId()), _nextPeer, "rejected because we couldn't find " + _nextPeer.toBase64() + ": " + @@ -516,8 +519,9 @@ class BuildHandler { + " from " + (state.fromHash != null ? state.fromHash.toBase64() : state.from != null ? state.from.calculateHash().toBase64() : "tunnel")); + HopConfig cfg = null; if (response == 0) { - HopConfig cfg = new HopConfig(); + cfg = new HopConfig(); cfg.setCreation(_context.clock().now()); cfg.setExpiration(_context.clock().now() + 10*60*1000); cfg.setIVKey(req.readIVKey()); @@ -593,6 +597,8 @@ class BuildHandler { msg.setExpiration(state.msg.getMessageExpiration()); msg.setPriority(300); msg.setTarget(nextPeerInfo); + if (response == 0) + msg.setOnFailedSendJob(new TunnelBuildNextHopFailJob(_context, cfg)); _context.outNetMessagePool().add(msg); } else { // send it to the reply tunnel on the reply peer within a new TunnelBuildReplyMessage @@ -619,6 +625,8 @@ class BuildHandler { outMsg.setMessage(m); outMsg.setPriority(300); outMsg.setTarget(nextPeerInfo); + if (response == 0) + outMsg.setOnFailedSendJob(new TunnelBuildNextHopFailJob(_context, cfg)); _context.outNetMessagePool().add(outMsg); } } @@ -762,7 +770,7 @@ class BuildHandler { } /** normal inbound requests from other people */ - private class BuildMessageState { + private static class BuildMessageState { TunnelBuildMessage msg; RouterIdentity from; Hash fromHash; @@ -775,7 +783,7 @@ class BuildHandler { } } /** replies for outbound tunnels that we have created */ - private class BuildReplyMessageState { + private static class BuildReplyMessageState { TunnelBuildReplyMessage msg; long recvTime; public BuildReplyMessageState(I2NPMessage m) { @@ -784,7 +792,7 @@ class BuildHandler { } } /** replies for inbound tunnels we have created */ - private class BuildEndMessageState { + private static class BuildEndMessageState { TunnelBuildMessage msg; PooledTunnelCreatorConfig cfg; long recvTime; @@ -796,15 +804,35 @@ class BuildHandler { } // noop - private class TunnelBuildMessageHandlerJob extends JobImpl { + private static class TunnelBuildMessageHandlerJob extends JobImpl { private TunnelBuildMessageHandlerJob(RouterContext ctx) { super(ctx); } public void runJob() {} public String getName() { return "Receive tunnel build message"; } } // noop - private class TunnelBuildReplyMessageHandlerJob extends JobImpl { + private static class TunnelBuildReplyMessageHandlerJob extends JobImpl { private TunnelBuildReplyMessageHandlerJob(RouterContext ctx) { super(ctx); } public void runJob() {} public String getName() { return "Receive tunnel build reply message"; } } + + /** + * Remove the participating tunnel if we can't contact the next hop + * Not strictly necessary, as the entry doesn't use that much space, + * but it affects capacity calculations + */ + private static class TunnelBuildNextHopFailJob extends JobImpl { + HopConfig _cfg; + private TunnelBuildNextHopFailJob(RouterContext ctx, HopConfig cfg) { + super(ctx); + _cfg = cfg; + } + public String getName() { return "Timeout contacting next peer for tunnel join"; } + public void runJob() { + getContext().tunnelDispatcher().remove(_cfg); + getContext().statManager().addRateData("tunnel.rejectTimeout2", 1, 0); + // static, no _log + //_log.error("Cant contact next hop for " + _cfg); + } + } } diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java index a4917772f..21325be85 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java @@ -12,6 +12,7 @@ import net.i2p.data.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.I2NPMessage; import net.i2p.data.i2np.TunnelBuildMessage; +import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; import net.i2p.router.RouterContext; import net.i2p.router.TunnelInfo; @@ -136,6 +137,7 @@ class BuildRequestor { return; } outMsg.setTarget(peer); + outMsg.setOnFailedSendJob(new TunnelBuildFirstHopFailJob(ctx, pool, cfg, exec)); ctx.outNetMessagePool().add(outMsg); } if (log.shouldLog(Log.DEBUG)) @@ -213,4 +215,33 @@ class BuildRequestor { ctx.jobQueue().addJob(expireJob); // can it get much easier? } + + /** + * Do two important things if we can't get the build msg to the + * first hop on an outbound tunnel - + * - Call buildComplete() so we can get started on the next build + * without waiting for the full expire time + * - Blame the first hop in the profile + * Most likely to happen on an exploratory tunnel, obviously. + * Can't do this for inbound tunnels since the msg goes out an expl. tunnel. + */ + private static class TunnelBuildFirstHopFailJob extends JobImpl { + TunnelPool _pool; + PooledTunnelCreatorConfig _cfg; + BuildExecutor _exec; + private TunnelBuildFirstHopFailJob(RouterContext ctx, TunnelPool pool, PooledTunnelCreatorConfig cfg, BuildExecutor exec) { + super(ctx); + _cfg = cfg; + _exec = exec; + _pool = pool; + } + public String getName() { return "Timeout contacting first peer for OB tunnel"; } + public void runJob() { + _exec.buildComplete(_cfg, _pool); + getContext().profileManager().tunnelTimedOut(_cfg.getPeer(1)); + getContext().statManager().addRateData("tunnel.buildFailFirstHop", 1, 0); + // static, no _log + //System.err.println("Cant contact first hop for " + _cfg); + } + } } From 45a2159290fe47f393a5b930e5bdc37455fc64bd Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 1 Feb 2009 01:34:57 +0000 Subject: [PATCH 096/191] -2 --- history.txt | 22 +++++++++++++++++++ .../src/net/i2p/router/RouterVersion.java | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/history.txt b/history.txt index a1472cb27..f91c455ee 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,25 @@ +2009-02-01 zzz + * Convert some inner classes to static (findbugs) + * DataHelper.readLong(): Was returning -1 on EOF instead + of throwing exception + * i2psnark: Increase tunnels and pipeline to 3 + * NTCP: Use a java.util.concurrent execution queue instead of + SimpleTimer for afterSend() to reduce lock contention + * Remove source from susimail.war, susidns.war, i2ptunnel.war (85KB) + * Routerconsole: + - Move common methods to new HelperBase class + - Make reseed link a button + * SimpleScheduler: New replacement for SimpleTimer when events + will not be rescheduled or cancelled, to reduce SimpleTimer + lock contention + * Tunnel Pool: + - Remove tunnel from participating if can't contact next hop + - Fail outbound build faster if can't contact first hop + * Wrapper: Remove dup timeout + +2009-01-31 dream + * Debian files + 2009-01-31 sponge * One line BOB discarded interger fix (not that it mattered at this point) diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 3b6c5a0a0..a727710d6 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -16,8 +16,8 @@ import net.i2p.CoreVersion; */ public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; - public final static String VERSION = "0.7"; - public final static long BUILD = 1; + public final static String VERSION = CoreVersion.VERSION; + public final static long BUILD = 2; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From c48700216cce3a8492be966651884a782364bd5a Mon Sep 17 00:00:00 2001 From: sponge Date: Sun, 1 Feb 2009 07:51:38 +0000 Subject: [PATCH 097/191] SlackBuild! --- Slackware/README | 21 +++++++ Slackware/i2p-base/doinst.sh | 45 ++++++++++++++ Slackware/i2p-base/i2p-base.SlackBuild | 42 +++++++++++++ Slackware/i2p-base/rc.i2p_def | 27 ++++++++ Slackware/i2p-base/readme.txt | 10 +++ Slackware/i2p-base/slack-desc | 19 ++++++ Slackware/i2p-base/slack-required | 1 + Slackware/i2p/blocklist.txt | 11 ++++ Slackware/i2p/doinst.sh | 39 ++++++++++++ Slackware/i2p/i2p.SlackBuild | 85 ++++++++++++++++++++++++++ Slackware/i2p/readme.txt | 41 +++++++++++++ Slackware/i2p/slack-desc | 21 +++++++ Slackware/i2p/slack-required | 2 + history.txt | 3 + 14 files changed, 367 insertions(+) create mode 100644 Slackware/README create mode 100644 Slackware/i2p-base/doinst.sh create mode 100644 Slackware/i2p-base/i2p-base.SlackBuild create mode 100644 Slackware/i2p-base/rc.i2p_def create mode 100644 Slackware/i2p-base/readme.txt create mode 100644 Slackware/i2p-base/slack-desc create mode 100644 Slackware/i2p-base/slack-required create mode 100644 Slackware/i2p/blocklist.txt create mode 100644 Slackware/i2p/doinst.sh create mode 100755 Slackware/i2p/i2p.SlackBuild create mode 100644 Slackware/i2p/readme.txt create mode 100644 Slackware/i2p/slack-desc create mode 100644 Slackware/i2p/slack-required diff --git a/Slackware/README b/Slackware/README new file mode 100644 index 000000000..9228a0f5a --- /dev/null +++ b/Slackware/README @@ -0,0 +1,21 @@ +You will need atleast monotone > = 0.41 to get the most recent build source +and connect it to an already running i2p router. + +OR: + +You may download the actual source from +http://mirror.i2p2.de/i2psource_0.7.tar.bz2 for the "stable" releases. + + +You will need to follwing tools to build the i2p and i2p-base packages: + +bash >= 3.1.017 +jre >= 6u11 +jdk >= 6u11 + +apache-ant >= 1.7.1 +perl >= 5.10.0 +python >= 2.5.2 + +Reccomended: +monotone >= 0.41 (from pkgs.dr.ea.ms) diff --git a/Slackware/i2p-base/doinst.sh b/Slackware/i2p-base/doinst.sh new file mode 100644 index 000000000..087ee112c --- /dev/null +++ b/Slackware/i2p-base/doinst.sh @@ -0,0 +1,45 @@ +#!/bin/sh +touch /etc/rc.d/rc.local +touch /etc/rc.d/rc.local_shutdown + +I2PRCA=`grep -c /etc/rc.d/rc.local -e i2p` +I2PRCB=`grep -c /etc/rc.d/rc.local_shutdown -e i2p` + +echo + +if [ $I2PRCA -eq 0 ] ; then + echo "if [ -x /etc/rc.d/rc.i2p ] ; then" >> /etc/rc.d/rc.local + echo " sh /etc/rc.d/rc.i2p start" >> /etc/rc.d/rc.local + echo "fi" >> /etc/rc.d/rc.local + echo "/etc/rc.d/rc.local modified." +else + echo "/etc/rc.d/rc.local looks OK" +fi + +if [ $I2PRCB -eq 0 ] ; then + echo "if [ -x /etc/rc.d/rc.i2p ] ; then" >> /etc/rc.d/rc.local_shutdown + echo " sh /etc/rc.d/rc.i2p stop" >> /etc/rc.d/rc.local_shutdown + echo "fi" >> /etc/rc.d/rc.local_shutdown + echo "/etc/rc.d/rc.local_shutdown modified." +else + echo "/etc/rc.d/rc.local_shutdown looks OK" +fi + +if [ -f /etc/rc.d/rc.i2p ] ; then + if [ -x /etc/rc.d/rc.i2p ] ; then + chmod +x /etc/rc.d/rc.i2p.new + fi + echo + echo "It apears that you already have /etc/rc.d/rc.i2p" + echo "You may wish to replace it with /etc/rc.d/rc.i2p.new" + echo +else + mv /etc/rc.d/rc.i2p.new /etc/rc.d/rc.i2p + echo + echo "Installation finished. The i2p start/stop script has been" + echo "installed on /etc/rc.d directory. You should chmod +x" + echo '/etc/rc.d/rc.i2p to start it on boot.' + echo +fi + +exit diff --git a/Slackware/i2p-base/i2p-base.SlackBuild b/Slackware/i2p-base/i2p-base.SlackBuild new file mode 100644 index 000000000..d91d87263 --- /dev/null +++ b/Slackware/i2p-base/i2p-base.SlackBuild @@ -0,0 +1,42 @@ +#!/bin/sh +# Heavily based on the Slackware 12.1 SlackBuild +# Slackware build script for i2p + +# PLEASE READ THIS: +# Probably you will never have to update i2p packages with upgradepkg, +# just because i2p have an auto-update function. +# How to start i2p: +# After installpkg command, doinst.sh will execute a postinstallation script +# needed by i2p. After that you have to chmod +x /etc/rc.d/rc.i2p and start +# i2p service with /etc/rc.d/rc.i2p start. +# Now tell your browser to user this proxy: localhost on port 4444 and open +# this page: http://localhost:7657/index.jsp +# Here you can configure i2p, watch network status and navigate anonimously. +# It's suggested to subscribe to various dns host, like i2host.i2p +# For any additional information, visit i2host.i2p and forum.i2p + +CWD=$(pwd) +TMP=/tmp +PKG=/$TMP/package-base-i2p +rm -rf $PKG +mkdir -p $PKG +# put here installation dir, without first and last / +# es: usr/local +NAME=i2p-base +VERSION=0.0.1 +BUILD=1sim +ARCH=noarch +INSTALL_DIR=opt +cd $PKG +chown -R root:root . + +mkdir -p $PKG/etc/rc.d +mkdir -p $PKG/install +sed "s|directory|/$INSTALL_DIR/i2p/i2prouter|g" $CWD/rc.i2p_def > $PKG/etc/rc.d/rc.i2p.new +chmod 644 $PKG/etc/rc.d/rc.i2p.new +sed "s|directory|/$INSTALL_DIR/i2p/|g" $CWD/doinst.sh > $PKG/install/doinst.sh +cat $CWD/slack-desc > $PKG/install/slack-desc + +cd $PKG +requiredbuilder -v -y -s $CWD $PKG +makepkg -l y -c n $CWD/${NAME}-$VERSION-$ARCH-$BUILD.tgz diff --git a/Slackware/i2p-base/rc.i2p_def b/Slackware/i2p-base/rc.i2p_def new file mode 100644 index 000000000..268968042 --- /dev/null +++ b/Slackware/i2p-base/rc.i2p_def @@ -0,0 +1,27 @@ +#!/bin/sh +# Start/stop i2p service. + +i2p_start() { + /bin/su - -c "( export PATH=\"$PATH:/usr/lib/java/bin:/usr/lib/java/jre/bin\"; directory start )" +} + +i2p_stop() { + /bin/su - -c "( export PATH=\"$PATH:/usr/lib/java/bin:/usr/lib/java/jre/bin\"; directory stop )" +} + +case "$1" in +'start') + i2p_start + ;; +'stop') + i2p_stop + ;; +'restart') + i2p_stop + i2p_start + ;; +*) + echo "usage $0 start|stop|restart" + ;; +esac + diff --git a/Slackware/i2p-base/readme.txt b/Slackware/i2p-base/readme.txt new file mode 100644 index 000000000..6575387f7 --- /dev/null +++ b/Slackware/i2p-base/readme.txt @@ -0,0 +1,10 @@ +An rc file called rc.i2p has been placed into the /etc/rc.d directory. +If you want to change installation dir, change the variable INSTALL_DIR +on base-i2p.SlackBuild and rebuild the package. You also will need to do the +same for the i2p package. + +The install script will insert everything needed into /etc/rc.d/rc.local and +into /etc/rc.d/rc.local_shutdown automatically. + +If you want to start I2P at boot you have to chmod +x /etc/rc.d/rc.i2p + diff --git a/Slackware/i2p-base/slack-desc b/Slackware/i2p-base/slack-desc new file mode 100644 index 000000000..4e94753a9 --- /dev/null +++ b/Slackware/i2p-base/slack-desc @@ -0,0 +1,19 @@ +# HOW TO EDIT THIS FILE: +# The "handy ruler" below makes it easier to edit a package description. Line +# up the first '|' above the ':' following the base package name, and the '|' on +# the right side marks the last column you can put a character in. You must make +# exactly 11 lines for the formatting to be correct. It's also customary to +# leave one space after the ':'. + + |-----handy-ruler------------------------------------------------------| +base-i2p: base-i2p (I2P anonymizing network base config files) +base-i2p: +base-i2p: I2P is an anonymizing network, offering a simple layer that +base-i2p: identity-sensitive applications can use to securely communicate. All +base-i2p: data is wrapped with several layers of encryption, and the network is +base-i2p: both distributed and dynamic, with no trusted parties. +base-i2p: Many applications are available that interface with I2P, including +base-i2p: mail, peer-peer file sharing, IRC chat, and others. +base-i2p: +base-i2p: This package provides the startup files. +base-i2p: diff --git a/Slackware/i2p-base/slack-required b/Slackware/i2p-base/slack-required new file mode 100644 index 000000000..7a7220e75 --- /dev/null +++ b/Slackware/i2p-base/slack-required @@ -0,0 +1 @@ +bash >= 3.1.017 diff --git a/Slackware/i2p/blocklist.txt b/Slackware/i2p/blocklist.txt new file mode 100644 index 000000000..1ac6548e9 --- /dev/null +++ b/Slackware/i2p/blocklist.txt @@ -0,0 +1,11 @@ +# To enable set router.blocklist.enable=true on configadvanced.jsp and restart. +# Add additional entries as desired, sorting not required. +# Warning - a large list will increase memory usage. +# Please do not block too broadly, it will segment and harm the network. +# Also you must update this list yourself, it is not overwritten by the update process. +# For example, http://www.bluetack.co.uk/config/splist.zip is very broad and includes Tor users, it is not recommended. +# A more reasonable list: http://www.bluetack.co.uk/config/level1.zip +# +Fucktard Floodfill Flooder:159.226.40.7 +Friend of the Fucktard Floodfill Flooder:159.226.40.3 +Fucktard Class O Unreachable:Do51O6vNvAYQRCK-~REhrblGHHOKGwH4RkpGp75nnNs= diff --git a/Slackware/i2p/doinst.sh b/Slackware/i2p/doinst.sh new file mode 100644 index 000000000..7493c7010 --- /dev/null +++ b/Slackware/i2p/doinst.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +INST_DIR=directory + +( cd install + +echo +for i in *.config ; { + if [ -f $INST_DIR/$i ] ; then + echo "Please check $i, as there is a new version." + cp $i $INST_DIR/$i.new + else + cp $i $INST_DIR/$i + fi +} + +) +echo +echo "FINISHING I2P INSTALLATION. PLEASE WAIT." + +cd $INST_DIR +sh postinstall.sh || ( + echo "ERROR: failed execution of postinstall.sh. Please" + echo "cd into i2p installation directory and run " + echo "postinstall.sh manually with ./postinstall.sh" + echo "It is also reccomended to set router.blocklist.enable=true " + echo "in the router.config file." + exit 1 +) + +sleep 10 + +sh i2prouter stop || exit 1 + +echo +echo "Installation finished." +echo + +exit diff --git a/Slackware/i2p/i2p.SlackBuild b/Slackware/i2p/i2p.SlackBuild new file mode 100755 index 000000000..ed7641768 --- /dev/null +++ b/Slackware/i2p/i2p.SlackBuild @@ -0,0 +1,85 @@ +#!/bin/sh +# Heavily based on the Slackware 12.1 SlackBuild +# Slackware build script for i2p + +# PLEASE READ THIS: +# Probably you will never have to update i2p packages with upgradepkg, +# just because i2p have an auto-update function. +# How to start i2p: +# After installpkg command, doinst.sh will execute a postinstallation script +# needed by i2p. After that you have to chmod +x /etc/rc.d/rc.i2p and start +# i2p service with /etc/rc.d/rc.i2p start. +# Now tell your browser to user this proxy: localhost on port 4444 and open +# this page: http://localhost:7657/index.jsp +# Here you can configure i2p, watch network status and navigate anonimously. +# It's suggested to subscribe to various dns host, like i2host.i2p +# For any additional information, visit i2host.i2p and forum.i2p + +BUILD=1sim + +# put here installation dir, without first and last / +# es: usr/local +INSTALL_DIR=opt +NAME=i2p +ARCH=noarch + + +# +# This mess is here due to the totally moronic way i2p does versioning. +# We correct it here. +# +ROUTER=$(echo -ne "-")$(cat ../../router/java/src/net/i2p/router/RouterVersion.java | grep -e "public final static long BUILD" | cut -f2 -d"=" | cut -f1 -d";" | sed -re "s/ //g") +if [ "$ROUTER" == "-" ] ; then + ROUTER="-0" +fi + +# +# That was the easy one, now for the tough one. +# + +CORE=$(cat ../../core/java/src/net/i2p/CoreVersion.java | grep -e "public final static String VERSION" | cut -f2 -d'"' | sed -re "s/ //g") +CORE1=$(echo -n $CORE.x.x | sed -re "s/(.*)\.(.*)\.(.*)\.(.*)/\1/") +CORE2=$(echo -n $CORE.x | sed -re "s/(.*)\.(.*)\.(.*)\.(.*)/\1/") + +if [ "$CORE.x.x" == "$CORE1" ] ; then + CORE=$(echo -ne $CORE".0.0") +fi +if [ "$CORE.x" == "$CORE2" ] ; then + CORE=$(echo -ne $CORE".0") +fi + +VERSION=$(echo $CORE$ROUTER) +# +# Whew! +# OK, let's build i2p +# + +CWD=$(pwd) +TMP=/tmp + +PKG=$TMP/package-i2p +rm -rf $PKG +mkdir -p $PKG + +cd $CWD/../../ + +ant distclean +ant dist + + +tar xjvf i2p.tar.bz2 -C $TMP + +cd $TMP/i2p +chown -R root:root . + +mkdir -p $PKG/$INSTALL_DIR/ +cp -a ../i2p $PKG/$INSTALL_DIR/ + +mkdir -p $PKG/install +mv $PKG/$INSTALL_DIR/i2p/blocklist.txt $PKG/$INSTALL_DIR/i2p/blocklist.txt.new +mv $PKG/$INSTALL_DIR/i2p/*.config $PKG/install +sed "s|directory|/$INSTALL_DIR/i2p/|g" $CWD/doinst.sh > $PKG/install/doinst.sh +cat $CWD/slack-desc > $PKG/install/slack-desc +cd $PKG +requiredbuilder -v -y -s $CWD $PKG +makepkg -l y -c n $CWD/${NAME}-$VERSION-$ARCH-$BUILD.tgz diff --git a/Slackware/i2p/readme.txt b/Slackware/i2p/readme.txt new file mode 100644 index 000000000..7ad3f1bec --- /dev/null +++ b/Slackware/i2p/readme.txt @@ -0,0 +1,41 @@ +Building: +The i2p package will be installed in /opt/i2p + +If you want to change installation dir, change the variable INSTALL_DIR +on i2p.SlackBuild and rebuild the package. You will also need to do the same +in the base-i2p package. + +Installation and Upgrade: +Probably you will never have to update i2p packages with upgradepkg, +just because I2P has an auto-update function. However using upgradepkg +lowers the demand on the I2P network as a whole, and is by far faster. + +After installpkg command, doinst.sh will execute a postinstallation script +needed by I2P. Be sure to also install the base-i2p package. + +Optional: + +chmod +x /etc/rc.d/rc.i2p only if you want it to start on boot and stop on +shutdown. + +How to start I2P: + +Start I2P service with- +sh /etc/rc.d/rc.i2p start + +Now tell your browser to user this proxy: localhost on port 4444 and open +this page: http://localhost:7657/index.jsp +Here you can configure I2P, watch network status and navigate anonimously. +It's suggested to subscribe to various addressbook hosts, see the faqs on +http://www.i2p2.i2p/ or http://www.i2p2.de/ + +To stop I2P: + /etc/rc.d/rc.i2p stop + + +For any additional information: + +Within I2P- http://www.i2p2.i2p/, http://forum.i2p/, http://zzz.i2p + +Internet (not reccomended!) - http://www.i2p2.de/, http://forum.i2p2.de/ + diff --git a/Slackware/i2p/slack-desc b/Slackware/i2p/slack-desc new file mode 100644 index 000000000..7915f5cef --- /dev/null +++ b/Slackware/i2p/slack-desc @@ -0,0 +1,21 @@ +# HOW TO EDIT THIS FILE: +# The "handy ruler" below makes it easier to edit a package description. Line +# up the first '|' above the ':' following the base package name, and the '|' on +# the right side marks the last column you can put a character in. You must make +# exactly 11 lines for the formatting to be correct. It's also customary to +# leave one space after the ':'. + + |-----handy-ruler----------------------------------------------------------| +i2p: i2p (an anonymizing network) +i2p: +i2p: I2P is an anonymizing network, offering a simple layer that +i2p: identity-sensitive applications can use to securely communicate. All +i2p: data is wrapped with several layers of encryption, and the network is +i2p: both distributed and dynamic, with no trusted parties. +i2p: Many applications are available that interface with I2P, including +i2p: mail, peer-peer file sharing, IRC chat, and others. +i2p: +i2p: For more information, see: http://www.i2p2.de/ +i2p: +i2p: This package requires i2p-base. +i2p: diff --git a/Slackware/i2p/slack-required b/Slackware/i2p/slack-required new file mode 100644 index 000000000..3dcf36221 --- /dev/null +++ b/Slackware/i2p/slack-required @@ -0,0 +1,2 @@ +glibc >= 2.7-i486-17 | glibc-solibs >= 2.7-i486-17 +perl >= 5.10.0-i486-1 diff --git a/history.txt b/history.txt index f91c455ee..91c0abf15 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,6 @@ +2009-02-01 sponge + * Slackbuild files... if we can have them for Debian, why not :-) + 2009-02-01 zzz * Convert some inner classes to static (findbugs) * DataHelper.readLong(): Was returning -1 on EOF instead From f70be2965113ca9fb00d876def8a51a9de3b09c4 Mon Sep 17 00:00:00 2001 From: sponge Date: Sun, 1 Feb 2009 11:04:42 +0000 Subject: [PATCH 098/191] small change so that the version number makes more sense --- Slackware/i2p/i2p.SlackBuild | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Slackware/i2p/i2p.SlackBuild b/Slackware/i2p/i2p.SlackBuild index ed7641768..bc077b53c 100755 --- a/Slackware/i2p/i2p.SlackBuild +++ b/Slackware/i2p/i2p.SlackBuild @@ -28,9 +28,9 @@ ARCH=noarch # This mess is here due to the totally moronic way i2p does versioning. # We correct it here. # -ROUTER=$(echo -ne "-")$(cat ../../router/java/src/net/i2p/router/RouterVersion.java | grep -e "public final static long BUILD" | cut -f2 -d"=" | cut -f1 -d";" | sed -re "s/ //g") -if [ "$ROUTER" == "-" ] ; then - ROUTER="-0" +ROUTER=$(echo -ne "_")$(cat ../../router/java/src/net/i2p/router/RouterVersion.java | grep -e "public final static long BUILD" | cut -f2 -d"=" | cut -f1 -d";" | sed -re "s/ //g") +if [ "$ROUTER" == "_" ] ; then + ROUTER="_0" fi # From b6b14913685c4a5cf39fb3fd33aa1231c1af7a9a Mon Sep 17 00:00:00 2001 From: sponge Date: Mon, 2 Feb 2009 01:22:31 +0000 Subject: [PATCH 099/191] Final Slackbuild cleanups, ant slackpkg target added. --- Slackware/README | 21 +++++++++++++++------ Slackware/i2p-base/build.xml | 8 ++++++++ Slackware/i2p/blocklist.txt | 11 ----------- Slackware/i2p/build.xml | 8 ++++++++ Slackware/i2p/doinst.sh | 34 +++++++++++++++++++++++++++++++--- Slackware/i2p/i2p.SlackBuild | 5 ++++- Slackware/i2p/readme.txt | 16 +++++++++++----- Slackware/i2p/slack-desc | 4 +--- build.xml | 6 ++++++ history.txt | 4 ++++ 10 files changed, 88 insertions(+), 29 deletions(-) create mode 100644 Slackware/i2p-base/build.xml delete mode 100644 Slackware/i2p/blocklist.txt create mode 100644 Slackware/i2p/build.xml diff --git a/Slackware/README b/Slackware/README index 9228a0f5a..494ffa420 100644 --- a/Slackware/README +++ b/Slackware/README @@ -1,21 +1,30 @@ -You will need atleast monotone > = 0.41 to get the most recent build source +ou will need atleast monotone > = 0.41 to get the most recent build source and connect it to an already running i2p router. OR: -You may download the actual source from -http://mirror.i2p2.de/i2psource_0.7.tar.bz2 for the "stable" releases. - +You may download the actual "stable" source from +http://code.google.com/p/i2p/downloads/list You will need to follwing tools to build the i2p and i2p-base packages: bash >= 3.1.017 +requiredbuilder >= 0.16.3 ( http://www.stabellini.net/requiredbuilder.html ) jre >= 6u11 jdk >= 6u11 - apache-ant >= 1.7.1 perl >= 5.10.0 python >= 2.5.2 Reccomended: -monotone >= 0.41 (from pkgs.dr.ea.ms) +monotone >= 0.41 ( http://pkgs.dr.ea.ms ) + +See also: + +i2p/readme.txt + +AND + +i2p-base/readme.txt + +for information and handy tips. diff --git a/Slackware/i2p-base/build.xml b/Slackware/i2p-base/build.xml new file mode 100644 index 000000000..f8def337e --- /dev/null +++ b/Slackware/i2p-base/build.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Slackware/i2p/blocklist.txt b/Slackware/i2p/blocklist.txt deleted file mode 100644 index 1ac6548e9..000000000 --- a/Slackware/i2p/blocklist.txt +++ /dev/null @@ -1,11 +0,0 @@ -# To enable set router.blocklist.enable=true on configadvanced.jsp and restart. -# Add additional entries as desired, sorting not required. -# Warning - a large list will increase memory usage. -# Please do not block too broadly, it will segment and harm the network. -# Also you must update this list yourself, it is not overwritten by the update process. -# For example, http://www.bluetack.co.uk/config/splist.zip is very broad and includes Tor users, it is not recommended. -# A more reasonable list: http://www.bluetack.co.uk/config/level1.zip -# -Fucktard Floodfill Flooder:159.226.40.7 -Friend of the Fucktard Floodfill Flooder:159.226.40.3 -Fucktard Class O Unreachable:Do51O6vNvAYQRCK-~REhrblGHHOKGwH4RkpGp75nnNs= diff --git a/Slackware/i2p/build.xml b/Slackware/i2p/build.xml new file mode 100644 index 000000000..0683bdeb0 --- /dev/null +++ b/Slackware/i2p/build.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Slackware/i2p/doinst.sh b/Slackware/i2p/doinst.sh index 7493c7010..91dd09721 100644 --- a/Slackware/i2p/doinst.sh +++ b/Slackware/i2p/doinst.sh @@ -7,7 +7,7 @@ INST_DIR=directory echo for i in *.config ; { if [ -f $INST_DIR/$i ] ; then - echo "Please check $i, as there is a new version." + echo "Please check ${INST_DIR}${i}, as there is a new version." cp $i $INST_DIR/$i.new else cp $i $INST_DIR/$i @@ -15,6 +15,36 @@ for i in *.config ; { } ) + +( cd $INST_DIR + if [ -f blocklist.txt ] ; then + echo "Please check ${INST_DIR}blocklist.txt, as there is a new version." + else + mv blocklist.txt.new blocklist.txt + fi +) + +( cd $INST_DIR/eepsite + if [ -f jetty.xml ] ; then + rm jetty.xml.new + else + mv jetty.xml.new jetty.xml + fi +) + +( cd $INST_DIR/eepsite/docroot + if [ -f index.html ] ; then + rm index.html.new + else + mv index.html.new index.html + fi + if [ -f favicon.ico ] ; then + rm favicon.ico.new + else + mv favicon.ico.new favicon.ico + fi +) + echo echo "FINISHING I2P INSTALLATION. PLEASE WAIT." @@ -23,8 +53,6 @@ sh postinstall.sh || ( echo "ERROR: failed execution of postinstall.sh. Please" echo "cd into i2p installation directory and run " echo "postinstall.sh manually with ./postinstall.sh" - echo "It is also reccomended to set router.blocklist.enable=true " - echo "in the router.config file." exit 1 ) diff --git a/Slackware/i2p/i2p.SlackBuild b/Slackware/i2p/i2p.SlackBuild index bc077b53c..421033356 100755 --- a/Slackware/i2p/i2p.SlackBuild +++ b/Slackware/i2p/i2p.SlackBuild @@ -76,8 +76,11 @@ mkdir -p $PKG/$INSTALL_DIR/ cp -a ../i2p $PKG/$INSTALL_DIR/ mkdir -p $PKG/install -mv $PKG/$INSTALL_DIR/i2p/blocklist.txt $PKG/$INSTALL_DIR/i2p/blocklist.txt.new mv $PKG/$INSTALL_DIR/i2p/*.config $PKG/install +mv $PKG/$INSTALL_DIR/i2p/blocklist.txt $PKG/$INSTALL_DIR/i2p/blocklist.txt.new +mv $PKG/$INSTALL_DIR/i2p/eepsite/jetty.xml $PKG/$INSTALL_DIR/i2p/eepsite/jetty.xml.new +mv $PKG/$INSTALL_DIR/i2p/eepsite/docroot/index.html $PKG/$INSTALL_DIR/i2p/eepsite/docroot/index.html.new +mv $PKG/$INSTALL_DIR/i2p/eepsite/docroot/favicon.ico $PKG/$INSTALL_DIR/i2p/eepsite/docroot/favicon.ico.new sed "s|directory|/$INSTALL_DIR/i2p/|g" $CWD/doinst.sh > $PKG/install/doinst.sh cat $CWD/slack-desc > $PKG/install/slack-desc cd $PKG diff --git a/Slackware/i2p/readme.txt b/Slackware/i2p/readme.txt index 7ad3f1bec..2c6d4fa50 100644 --- a/Slackware/i2p/readme.txt +++ b/Slackware/i2p/readme.txt @@ -6,9 +6,11 @@ on i2p.SlackBuild and rebuild the package. You will also need to do the same in the base-i2p package. Installation and Upgrade: -Probably you will never have to update i2p packages with upgradepkg, -just because I2P has an auto-update function. However using upgradepkg -lowers the demand on the I2P network as a whole, and is by far faster. +Probably you will never have to update i2p packages. However if you do, +be sure to installpkg first, then removepkg or custom config files can +be lost with upgradepkg. I2P has an auto-update function. However using +installpkg then removepkg lowers the demand on the I2P network as a +whole, and is by far faster. After installpkg command, doinst.sh will execute a postinstallation script needed by I2P. Be sure to also install the base-i2p package. @@ -26,8 +28,12 @@ sh /etc/rc.d/rc.i2p start Now tell your browser to user this proxy: localhost on port 4444 and open this page: http://localhost:7657/index.jsp Here you can configure I2P, watch network status and navigate anonimously. -It's suggested to subscribe to various addressbook hosts, see the faqs on -http://www.i2p2.i2p/ or http://www.i2p2.de/ +It's suggested to subscribe to various addressbook hosts so that you can +get to the many available eepsites and other service on I2P. These are not +set up by default for security reasons. + +Please see the faqs on http://www.i2p2.i2p/ or http://www.i2p2.de/ on how +to subscribe to the various addressbook services. To stop I2P: /etc/rc.d/rc.i2p stop diff --git a/Slackware/i2p/slack-desc b/Slackware/i2p/slack-desc index 7915f5cef..281e5e894 100644 --- a/Slackware/i2p/slack-desc +++ b/Slackware/i2p/slack-desc @@ -14,8 +14,6 @@ i2p: data is wrapped with several layers of encryption, and the network is i2p: both distributed and dynamic, with no trusted parties. i2p: Many applications are available that interface with I2P, including i2p: mail, peer-peer file sharing, IRC chat, and others. -i2p: +i2p: WARNING: To upgrade installpkg FIRST _THEN_ removepkg. i2p: For more information, see: http://www.i2p2.de/ i2p: -i2p: This package requires i2p-base. -i2p: diff --git a/build.xml b/build.xml index 9b69a13f3..b4a374697 100644 --- a/build.xml +++ b/build.xml @@ -15,6 +15,7 @@ + @@ -437,6 +438,11 @@ + + + + + diff --git a/history.txt b/history.txt index 91c0abf15..f94b1369a 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,7 @@ +2009-02-02 sponge + * Final? cleanups to Slackbuilds. + * ant target for Slackbuilds. + 2009-02-01 sponge * Slackbuild files... if we can have them for Debian, why not :-) From 6f948df089fe30312596748e172a0058a0dfc86a Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 2 Feb 2009 13:59:50 +0000 Subject: [PATCH 100/191] remove dup --- installer/resources/wrapper.config | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/installer/resources/wrapper.config b/installer/resources/wrapper.config index 177bc1c0b..4d09ba12c 100644 --- a/installer/resources/wrapper.config +++ b/installer/resources/wrapper.config @@ -136,14 +136,13 @@ wrapper.on_exit.4=RESTART wrapper.on_exit.5=RESTART # the router may take a few seconds to save state, etc -wrapper.jvm_exit.timeout=10 +wrapper.jvm_exit.timeout=30 # give the OS 60s to clear all the old sockets / etc before restarting wrapper.restart.delay=60 wrapper.ping.interval=600 wrapper.ping.timeout=605 -wrapper.jvm_exit.timeout=30 # use the wrapper's internal timer thread. otherwise this would # force a restart of the router during daylight savings time as well From 1ee2b5e89958f542b92403de9957281c62a4bcdf Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 2 Feb 2009 14:00:51 +0000 Subject: [PATCH 101/191] one more static --- .../java/src/net/i2p/router/networkdb/kademlia/SearchJob.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java index 6c2369320..868c6b108 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java @@ -760,7 +760,7 @@ class SearchJob extends JobImpl { } } - private class Search { + private static class Search { private Job _onFind; private Job _onFail; private long _expiration; From 8d7340500ffbcfce652f2eee0d9bf438299304e7 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 2 Feb 2009 14:03:17 +0000 Subject: [PATCH 102/191] * I2CP: Implement optional reduce tunnels on idle - not hooked in to i2ptunnel GUI yet - still needs tweaks --- .../src/net/i2p/i2ptunnel/web/EditBean.java | 4 + apps/i2ptunnel/jsp/editClient.jsp | 4 +- .../i2p/router/web/ConfigTunnelsHelper.java | 15 ++- .../net/i2p/client/I2CPMessageProducer.java | 31 +++++ .../src/net/i2p/client/I2PSessionImpl.java | 35 ++++++ .../src/net/i2p/client/I2PSessionImpl2.java | 1 + .../src/net/i2p/client/SessionIdleTimer.java | 106 ++++++++++++++++++ .../net/i2p/data/i2cp/I2CPMessageHandler.java | 2 + .../client/ClientMessageEventListener.java | 53 ++++++--- 9 files changed, 228 insertions(+), 23 deletions(-) create mode 100644 core/java/src/net/i2p/client/SessionIdleTimer.java 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 627024b67..e8bda1a29 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -150,6 +150,10 @@ public class EditBean extends IndexBean { return false; } + public int getCloseTime(int tunnel) { + return getProperty(tunnel, "closeIdleTime", 30); + } + public boolean getNewDest(int tunnel) { return false; } diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index f7ee2294c..f5e1fac7f 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -289,9 +289,9 @@
    - +
    diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java index e21f9d9ce..8b8b2fb16 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigTunnelsHelper.java @@ -49,7 +49,9 @@ public class ConfigTunnelsHelper extends HelperBase { private static final int WARN_LENGTH = 4; private static final int MAX_LENGTH = 4; - private static final int MAX_QUANTITY = 3; + private static final int WARN_QUANTITY = 5; + private static final int MAX_QUANTITY = 6; + private static final int MAX_BACKUP_QUANTITY = 3; private static final int MAX_VARIANCE = 2; private static final int MIN_NEG_VARIANCE = -1; private void renderForm(StringBuffer buf, int index, String prefix, String name, TunnelPoolSettings in, TunnelPoolSettings out) { @@ -64,6 +66,9 @@ public class ConfigTunnelsHelper extends HelperBase { if (in.getLength() + Math.abs(in.getLengthVariance()) >= WARN_LENGTH || out.getLength() + Math.abs(out.getLengthVariance()) >= WARN_LENGTH) buf.append("PERFORMANCE WARNING - Settings include very long tunnels"); + if (in.getQuantity() + in.getBackupQuantity() >= WARN_QUANTITY || + out.getQuantity() + out.getBackupQuantity() >= WARN_QUANTITY) + buf.append("PERFORMANCE WARNING - Settings include high tunnel quantities"); buf.append("InboundOutbound\n"); @@ -130,15 +135,15 @@ public class ConfigTunnelsHelper extends HelperBase { buf.append("Backup quantity\n"); buf.append("\n"); buf.append("\n"); buf.append("\n"); diff --git a/core/java/src/net/i2p/client/I2CPMessageProducer.java b/core/java/src/net/i2p/client/I2CPMessageProducer.java index 5b45ee7a3..b897d22d0 100644 --- a/core/java/src/net/i2p/client/I2CPMessageProducer.java +++ b/core/java/src/net/i2p/client/I2CPMessageProducer.java @@ -10,6 +10,7 @@ package net.i2p.client; */ import java.util.Date; +import java.util.Properties; import java.util.Set; import net.i2p.I2PAppContext; @@ -27,6 +28,7 @@ import net.i2p.data.i2cp.CreateLeaseSetMessage; import net.i2p.data.i2cp.CreateSessionMessage; import net.i2p.data.i2cp.DestroySessionMessage; import net.i2p.data.i2cp.MessageId; +import net.i2p.data.i2cp.ReconfigureSessionMessage; import net.i2p.data.i2cp.ReportAbuseMessage; import net.i2p.data.i2cp.SendMessageMessage; import net.i2p.data.i2cp.SendMessageExpiresMessage; @@ -188,4 +190,33 @@ class I2CPMessageProducer { msg.setSessionId(session.getSessionId()); session.sendMessage(msg); } + + /** + * Update number of tunnels + * + * @param tunnels 0 for original configured number + */ + public void updateTunnels(I2PSessionImpl session, int tunnels) throws I2PSessionException { + ReconfigureSessionMessage msg = new ReconfigureSessionMessage(); + SessionConfig cfg = new SessionConfig(session.getMyDestination()); + Properties props = session.getOptions(); + if (tunnels > 0) { + Properties newprops = new Properties(); + newprops.putAll(props); + props = newprops; + props.setProperty("inbound.quantity", "" + tunnels); + props.setProperty("outbound.quantity", "" + tunnels); + props.setProperty("inbound.backupQuantity", "0"); + props.setProperty("outbound.backupQuantity", "0"); + } + cfg.setOptions(props); + try { + cfg.signSessionConfig(session.getPrivateKey()); + } catch (DataFormatException dfe) { + throw new I2PSessionException("Unable to sign the session config", dfe); + } + msg.setSessionConfig(cfg); + msg.setSessionId(session.getSessionId()); + session.sendMessage(msg); + } } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index d4ff7360a..2c8582a4f 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -110,6 +110,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa */ protected AvailabilityNotifier _availabilityNotifier; + private long _lastActivity; + private boolean _isReduced; + void dateUpdated() { _dateReceived = true; synchronized (_dateReceivedLock) { @@ -290,6 +293,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa _log.info(getPrefix() + "Lease set created with inbound tunnels after " + (connected - startConnect) + "ms - ready to participate in the network!"); + startIdleMonitor(); } catch (UnknownHostException uhe) { _closed = true; throw new I2PSessionException(getPrefix() + "Invalid session configuration", uhe); @@ -316,6 +320,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa _log.error("Receive message " + msgId + " had no matches, remaining=" + remaining); return null; } + updateActivity(); return msg.getPayload().getUnencryptedData(); } @@ -668,4 +673,34 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa public Destination lookupDest(Hash h) throws I2PSessionException { return null; } + + protected void updateActivity() { + _lastActivity = _context.clock().now(); + if (_isReduced) { + _isReduced = false; + try { + _producer.updateTunnels(this, 0); + } catch (I2PSessionException ise) { + _log.error(getPrefix() + "bork restore from reduced"); + } + } + } + + public long lastActivity() { + return _lastActivity; + } + + public void setReduced() { + _isReduced = true; + } + + private void startIdleMonitor() { + _isReduced = false; + boolean reduce = Boolean.valueOf(_options.getProperty("i2cp.reduceOnIdle")).booleanValue(); + boolean close = Boolean.valueOf(_options.getProperty("i2cp.closeOnIdle")).booleanValue(); + if (reduce || close) { + updateActivity(); + SimpleScheduler.getInstance().addEvent(new SessionIdleTimer(_context, this, reduce, close), SessionIdleTimer.MINIMUM_TIME); + } + } } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java index 6a90952a5..56ef88974 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl2.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java @@ -122,6 +122,7 @@ class I2PSessionImpl2 extends I2PSessionImpl { throws I2PSessionException { if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message"); if (isClosed()) throw new I2PSessionException("Already closed"); + updateActivity(); // Sadly there is no way to send something completely uncompressed in a backward-compatible way, // so we have to still send it in a gzip format, which adds 23 bytes (2.4% for a 960-byte msg) diff --git a/core/java/src/net/i2p/client/SessionIdleTimer.java b/core/java/src/net/i2p/client/SessionIdleTimer.java new file mode 100644 index 000000000..1babd9551 --- /dev/null +++ b/core/java/src/net/i2p/client/SessionIdleTimer.java @@ -0,0 +1,106 @@ +package net.i2p.client; + +/* + * free (adj.): unencumbered; not under the control of others + * + */ + +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; +import net.i2p.util.SimpleTimer; + +/** + * Reduce tunnels or shutdown the session on idle if so configured + * + * @author zzz + */ +public class SessionIdleTimer implements SimpleTimer.TimedEvent { + public static final long MINIMUM_TIME = 5*60*1000; + private static final long DEFAULT_REDUCE_TIME = 20*60*1000; + private static final long DEFAULT_CLOSE_TIME = 30*60*1000; + private final static Log _log = new Log(SessionIdleTimer.class); + private I2PAppContext _context; + private I2PSessionImpl _session; + private boolean _reduceEnabled; + private int _reduceQuantity; + private long _reduceTime; + private boolean _shutdownEnabled; + private long _shutdownTime; + private long _minimumTime; + + /** + * reduce, shutdown, or both must be true + */ + public SessionIdleTimer(I2PAppContext context, I2PSessionImpl session, boolean reduce, boolean shutdown) { + _context = context; + _session = session; + _reduceEnabled = reduce; + _shutdownEnabled = shutdown; + if (! (reduce || shutdown)) + throw new IllegalArgumentException("At least one must be enabled"); + Properties props = session.getOptions(); + _minimumTime = Long.MAX_VALUE; + if (reduce) { + _reduceQuantity = 1; + String p = props.getProperty("i2cp.reduceQuantity"); + if (p != null) { + try { + _reduceQuantity = Math.max(Integer.parseInt(p), 1); + // also check vs. configured quantities? + } catch (NumberFormatException nfe) {} + } + _reduceTime = DEFAULT_REDUCE_TIME; + p = props.getProperty("i2cp.reduceTime"); + if (p != null) { + try { + _reduceTime = Math.max(Long.parseLong(p), MINIMUM_TIME); + } catch (NumberFormatException nfe) {} + } + _minimumTime = _reduceTime; + } + if (shutdown) { + _shutdownTime = DEFAULT_CLOSE_TIME; + String p = props.getProperty("i2cp.closeTime"); + if (p != null) { + try { + _shutdownTime = Math.max(Long.parseLong(p), MINIMUM_TIME); + } catch (NumberFormatException nfe) {} + } + _minimumTime = Math.min(_minimumTime, _shutdownTime); + if (reduce && _shutdownTime <= _reduceTime) + reduce = false; + } + } + + public void timeReached() { + if (_session.isClosed()) + return; + long now = _context.clock().now(); + long lastActivity = _session.lastActivity(); + if (_log.shouldLog(Log.INFO)) + _log.info("Fire idle timer, last activity: " + DataHelper.formatDuration(now - lastActivity) + " ago "); + if (_shutdownEnabled && now - lastActivity >= _shutdownTime) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Closing on idle " + _session); + _session.destroySession(); + } else if (_reduceEnabled && now - lastActivity >= _reduceTime) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Reducing quantity on idle " + _session); + try { + _session.getProducer().updateTunnels(_session, _reduceQuantity); + } catch (I2PSessionException ise) { + _log.error("bork idle reduction " + ise); + } + _session.setReduced(); + if (_shutdownEnabled) + SimpleScheduler.getInstance().addEvent(this, _shutdownTime - (now - lastActivity)); + // else sessionimpl must reschedule?? + } else { + SimpleScheduler.getInstance().addEvent(this, _minimumTime - (now - lastActivity)); + } + } +} diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java index 15045028a..294059bdb 100644 --- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java @@ -69,6 +69,8 @@ public class I2CPMessageHandler { return new ReceiveMessageBeginMessage(); case ReceiveMessageEndMessage.MESSAGE_TYPE: return new ReceiveMessageEndMessage(); + case ReconfigureSessionMessage.MESSAGE_TYPE: + return new ReconfigureSessionMessage(); case ReportAbuseMessage.MESSAGE_TYPE: return new ReportAbuseMessage(); case RequestLeaseSetMessage.MESSAGE_TYPE: diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java index d36d26401..0dcc81870 100644 --- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -8,6 +8,8 @@ package net.i2p.router.client; * */ +import java.util.Properties; + import net.i2p.data.Payload; import net.i2p.data.i2cp.CreateLeaseSetMessage; import net.i2p.data.i2cp.CreateSessionMessage; @@ -27,6 +29,7 @@ import net.i2p.data.i2cp.SendMessageExpiresMessage; import net.i2p.data.i2cp.SessionId; import net.i2p.data.i2cp.SessionStatusMessage; import net.i2p.data.i2cp.SetDateMessage; +import net.i2p.router.ClientTunnelSettings; import net.i2p.router.RouterContext; import net.i2p.util.Log; import net.i2p.util.RandomSource; @@ -87,6 +90,9 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi case DestLookupMessage.MESSAGE_TYPE: handleDestLookup(reader, (DestLookupMessage)message); break; + case ReconfigureSessionMessage.MESSAGE_TYPE: + handleReconfigureSession(reader, (ReconfigureSessionMessage)message); + break; default: if (_log.shouldLog(Log.ERROR)) _log.error("Unhandled I2CP type received: " + message.getType()); @@ -138,24 +144,13 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi return; } - SessionStatusMessage msg = new SessionStatusMessage(); SessionId sessionId = new SessionId(); sessionId.setSessionId(getNextSessionId()); _runner.setSessionId(sessionId); - msg.setSessionId(sessionId); - msg.setStatus(SessionStatusMessage.STATUS_CREATED); - try { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("before sending sessionStatusMessage for " + message.getSessionConfig().getDestination().calculateHash().toBase64()); - _runner.doSend(msg); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("after sending sessionStatusMessage for " + message.getSessionConfig().getDestination().calculateHash().toBase64()); - _runner.sessionEstablished(message.getSessionConfig()); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("after sessionEstablished for " + message.getSessionConfig().getDestination().calculateHash().toBase64()); - } catch (I2CPMessageException ime) { - _log.error("Error writing out the session status message", ime); - } + sendStatusMessage(SessionStatusMessage.STATUS_CREATED); + _runner.sessionEstablished(message.getSessionConfig()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("after sessionEstablished for " + message.getSessionConfig().getDestination().calculateHash().toBase64()); _context.jobQueue().addJob(new CreateSessionJob(_context, _runner)); } @@ -249,10 +244,36 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi */ private void handleReconfigureSession(I2CPMessageReader reader, ReconfigureSessionMessage message) { if (_log.shouldLog(Log.INFO)) - _log.info("Updating options - session " + _runner.getSessionId()); + _log.info("Updating options - old: " + _runner.getConfig() + " new: " + message.getSessionConfig()); + if (!message.getSessionConfig().getDestination().equals(_runner.getConfig().getDestination())) { + _log.error("Dest mismatch"); + sendStatusMessage(SessionStatusMessage.STATUS_INVALID); + _runner.stopRunning(); + return; + } _runner.getConfig().getOptions().putAll(message.getSessionConfig().getOptions()); + ClientTunnelSettings settings = new ClientTunnelSettings(); + Properties props = new Properties(); + props.putAll(_runner.getConfig().getOptions()); + settings.readFromProperties(props); + _context.tunnelManager().setInboundSettings(_runner.getConfig().getDestination().calculateHash(), + settings.getInboundSettings()); + _context.tunnelManager().setOutboundSettings(_runner.getConfig().getDestination().calculateHash(), + settings.getOutboundSettings()); + sendStatusMessage(SessionStatusMessage.STATUS_UPDATED); } + private void sendStatusMessage(int status) { + SessionStatusMessage msg = new SessionStatusMessage(); + msg.setSessionId(_runner.getSessionId()); + msg.setStatus(status); + try { + _runner.doSend(msg); + } catch (I2CPMessageException ime) { + _log.error("Error writing out the session status message", ime); + } + } + // this *should* be mod 65536, but UnsignedInteger is still b0rked. FIXME private final static int MAX_SESSION_ID = 32767; From 7ec29b0c5a34513905b65d8b5c3b00f2a4394e86 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 2 Feb 2009 18:03:16 +0000 Subject: [PATCH 103/191] use concurrent --- .../router/client/ClientConnectionRunner.java | 55 +++-------- .../i2p/router/client/ClientWriterRunner.java | 98 ++++++++----------- 2 files changed, 52 insertions(+), 101 deletions(-) diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java index 189568ead..af30a5d3d 100644 --- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java +++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java @@ -11,6 +11,7 @@ package net.i2p.router.client; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; +import java.util.concurrent.ConcurrentHashMap; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -59,7 +60,7 @@ public class ClientConnectionRunner { /** user's config */ private SessionConfig _config; /** static mapping of MessageId to Payload, storing messages for retrieval */ - private Map _messages; + private Map _messages; /** lease set request state, or null if there is no request pending on at the moment */ private LeaseRequestState _leaseRequest; /** currently allocated leaseSet, or null if none is allocated */ @@ -88,7 +89,7 @@ public class ClientConnectionRunner { _manager = manager; _socket = socket; _config = null; - _messages = new HashMap(); + _messages = new ConcurrentHashMap(); _alreadyProcessed = new ArrayList(); _acceptedPending = new HashSet(); _dead = false; @@ -106,7 +107,7 @@ public class ClientConnectionRunner { _reader = new I2CPMessageReader(_socket.getInputStream(), new ClientMessageEventListener(_context, this)); _writer = new ClientWriterRunner(_context, this); I2PThread t = new I2PThread(_writer); - t.setName("Writer " + ++__id); + t.setName("I2CP Writer " + ++__id); t.setDaemon(true); t.setPriority(I2PThread.MAX_PRIORITY); t.start(); @@ -128,9 +129,7 @@ public class ClientConnectionRunner { if (_reader != null) _reader.stopReading(); if (_writer != null) _writer.stopWriting(); if (_socket != null) try { _socket.close(); } catch (IOException ioe) { } - synchronized (_messages) { - _messages.clear(); - } + _messages.clear(); if (_manager != null) _manager.unregisterConnection(this); if (_currentLeaseSet != null) @@ -164,50 +163,18 @@ public class ClientConnectionRunner { } /** already closed? */ boolean isDead() { return _dead; } + /** message body */ Payload getPayload(MessageId id) { - Payload rv = null; - long beforeLock = _context.clock().now(); - long inLock = 0; - synchronized (_messages) { - inLock = _context.clock().now(); - rv = (Payload)_messages.get(id); - } - long afterLock = _context.clock().now(); - - if (afterLock - beforeLock > 50) { - _log.warn("alreadyAccepted.locking took too long: " + (afterLock-beforeLock) - + " overall, synchronized took " + (inLock - beforeLock)); - } - return rv; + return _messages.get(id); } + void setPayload(MessageId id, Payload payload) { - long beforeLock = _context.clock().now(); - long inLock = 0; - synchronized (_messages) { - inLock = _context.clock().now(); - _messages.put(id, payload); - } - long afterLock = _context.clock().now(); - - if (afterLock - beforeLock > 50) { - _log.warn("setPayload.locking took too long: " + (afterLock-beforeLock) - + " overall, synchronized took " + (inLock - beforeLock)); - } + _messages.put(id, payload); } + void removePayload(MessageId id) { - long beforeLock = _context.clock().now(); - long inLock = 0; - synchronized (_messages) { - inLock = _context.clock().now(); - _messages.remove(id); - } - long afterLock = _context.clock().now(); - - if (afterLock - beforeLock > 50) { - _log.warn("removePayload.locking took too long: " + (afterLock-beforeLock) - + " overall, synchronized took " + (inLock - beforeLock)); - } + _messages.remove(id); } void sessionEstablished(SessionConfig config) { diff --git a/router/java/src/net/i2p/router/client/ClientWriterRunner.java b/router/java/src/net/i2p/router/client/ClientWriterRunner.java index bf1364877..49fcddcc2 100644 --- a/router/java/src/net/i2p/router/client/ClientWriterRunner.java +++ b/router/java/src/net/i2p/router/client/ClientWriterRunner.java @@ -1,9 +1,13 @@ package net.i2p.router.client; -import java.util.ArrayList; -import java.util.List; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; import net.i2p.data.i2cp.I2CPMessage; +import net.i2p.data.i2cp.I2CPMessageImpl; +import net.i2p.data.i2cp.I2CPMessageException; import net.i2p.router.RouterContext; import net.i2p.util.Log; @@ -13,26 +17,18 @@ import net.i2p.util.Log; * the client reads from their i2cp socket, causing all sorts of bad shit to * happen) * + * @author zzz modded to use concurrent */ class ClientWriterRunner implements Runnable { - private List _messagesToWrite; - private List _messagesToWriteTimes; + private BlockingQueue _messagesToWrite; private ClientConnectionRunner _runner; - private RouterContext _context; private Log _log; private long _id; private static long __id = 0; - private static final long MAX_WAIT = 5*1000; - - /** lock on this when updating the class level data structs */ - private Object _dataLock = new Object(); - public ClientWriterRunner(RouterContext context, ClientConnectionRunner runner) { - _context = context; _log = context.logManager().getLog(ClientWriterRunner.class); - _messagesToWrite = new ArrayList(4); - _messagesToWriteTimes = new ArrayList(4); + _messagesToWrite = new LinkedBlockingQueue(); _runner = runner; _id = ++__id; } @@ -42,11 +38,9 @@ class ClientWriterRunner implements Runnable { * */ public void addMessage(I2CPMessage msg) { - synchronized (_dataLock) { - _messagesToWrite.add(msg); - _messagesToWriteTimes.add(new Long(_context.clock().now())); - _dataLock.notifyAll(); - } + try { + _messagesToWrite.put(msg); + } catch (InterruptedException ie) {} if (_log.shouldLog(Log.DEBUG)) _log.debug("["+_id+"] addMessage completed for " + msg.getClass().getName()); } @@ -56,47 +50,37 @@ class ClientWriterRunner implements Runnable { * */ public void stopWriting() { - synchronized (_dataLock) { - _dataLock.notifyAll(); + _messagesToWrite.clear(); + try { + _messagesToWrite.put(new PoisonMessage()); + } catch (InterruptedException ie) {} + } + + public void run() { + I2CPMessage msg; + while (!_runner.getIsDead()) { + try { + msg = _messagesToWrite.take(); + } catch (InterruptedException ie) { + continue; + } + if (msg.getType() == PoisonMessage.MESSAGE_TYPE) + break; + _runner.writeMessage(msg); } } - public void run() { - List messages = new ArrayList(64); - List messageTimes = new ArrayList(64); - List switchList = null; - - while (!_runner.getIsDead()) { - synchronized (_dataLock) { - if (_messagesToWrite.size() <= 0) - try { _dataLock.wait(); } catch (InterruptedException ie) {} - - if (_messagesToWrite.size() > 0) { - switchList = _messagesToWrite; - _messagesToWrite = messages; - messages = switchList; - - switchList = _messagesToWriteTimes; - _messagesToWriteTimes = messageTimes; - messageTimes = switchList; - } - } - - if (messages.size() > 0) { - for (int i = 0; i < messages.size(); i++) { - I2CPMessage msg = (I2CPMessage)messages.get(i); - Long when = (Long)messageTimes.get(i); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("["+_id+"] writeMessage before writing " - + msg.getClass().getName()); - _runner.writeMessage(msg); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("["+_id+"] writeMessage time since addMessage(): " - + (_context.clock().now()-when.longValue()) + " for " - + msg.getClass().getName()); - } - } - messages.clear(); - messageTimes.clear(); + + /** + * End-of-stream msg used to stop the concurrent queue + * See http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/BlockingQueue.html + * + */ + private static class PoisonMessage extends I2CPMessageImpl { + public static final int MESSAGE_TYPE = 999999; + public int getType() { + return MESSAGE_TYPE; } + public void doReadMessage(InputStream buf, int size) throws I2CPMessageException, IOException {} + public byte[] doWriteMessage() throws I2CPMessageException, IOException { return null; } } } From d236b9b44a298e95a49a76ee419cc06c853d57c2 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 2 Feb 2009 19:25:29 +0000 Subject: [PATCH 104/191] more concurrent --- .../src/net/i2p/util/ConcurrentHashSet.java | 53 +++++++++++++++++ .../router/client/ClientConnectionRunner.java | 57 +++---------------- 2 files changed, 60 insertions(+), 50 deletions(-) create mode 100644 core/java/src/net/i2p/util/ConcurrentHashSet.java diff --git a/core/java/src/net/i2p/util/ConcurrentHashSet.java b/core/java/src/net/i2p/util/ConcurrentHashSet.java new file mode 100644 index 000000000..b7bea9ee1 --- /dev/null +++ b/core/java/src/net/i2p/util/ConcurrentHashSet.java @@ -0,0 +1,53 @@ +package net.i2p.util; + +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Implement on top of a ConcurrentHashMap with a dummy value. + * + * @author zzz + */ +public class ConcurrentHashSet extends AbstractSet implements Set { + private static final Object DUMMY = new Object(); + private Map _map; + + public ConcurrentHashSet() { + _map = new ConcurrentHashMap(); + } + public ConcurrentHashSet(int capacity) { + _map = new ConcurrentHashMap(capacity); + } + + public boolean add(E o) { + return _map.put(o, DUMMY) == null; + } + + public void clear() { + _map.clear(); + } + + public boolean contains(Object o) { + return _map.containsKey(o); + } + + public boolean isEmpty() { + return _map.isEmpty(); + } + + public boolean remove(Object o) { + return _map.remove(o) != null; + } + + public int size() { + return _map.size(); + } + + public Iterator iterator() { + return _map.keySet().iterator(); + } +} diff --git a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java index af30a5d3d..4f979c5c9 100644 --- a/router/java/src/net/i2p/router/client/ClientConnectionRunner.java +++ b/router/java/src/net/i2p/router/client/ClientConnectionRunner.java @@ -14,7 +14,6 @@ import java.net.Socket; import java.util.concurrent.ConcurrentHashMap; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -36,6 +35,7 @@ import net.i2p.data.i2cp.SessionId; import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; +import net.i2p.util.ConcurrentHashSet; import net.i2p.util.I2PThread; import net.i2p.util.Log; import net.i2p.util.RandomSource; @@ -66,7 +66,7 @@ public class ClientConnectionRunner { /** currently allocated leaseSet, or null if none is allocated */ private LeaseSet _currentLeaseSet; /** set of messageIds created but not yet ACCEPTED */ - private Set _acceptedPending; + private Set _acceptedPending; /** thingy that does stuff */ private I2CPMessageReader _reader; /** @@ -91,7 +91,7 @@ public class ClientConnectionRunner { _config = null; _messages = new ConcurrentHashMap(); _alreadyProcessed = new ArrayList(); - _acceptedPending = new HashSet(); + _acceptedPending = new ConcurrentHashSet(); _dead = false; } @@ -242,19 +242,8 @@ public class ClientConnectionRunner { long expiration = 0; if (message instanceof SendMessageExpiresMessage) expiration = ((SendMessageExpiresMessage) message).getExpiration().getTime(); - long beforeLock = _context.clock().now(); - long inLock = 0; - synchronized (_acceptedPending) { - inLock = _context.clock().now(); - _acceptedPending.add(id); - } - long afterLock = _context.clock().now(); - - if (_log.shouldLog(Log.DEBUG)) { - _log.warn("distributeMessage.locking took: " + (afterLock-beforeLock) - + " overall, synchronized took " + (inLock - beforeLock)); - } - + _acceptedPending.add(id); + if (_log.shouldLog(Log.DEBUG)) _log.debug("** Receiving message [" + id.getMessageId() + "] with payload of size [" + payload.getSize() + "]" + " for session [" + _sessionId.getSessionId() @@ -291,18 +280,7 @@ public class ClientConnectionRunner { status.setStatus(MessageStatusMessage.STATUS_SEND_ACCEPTED); try { doSend(status); - long beforeLock = _context.clock().now(); - long inLock = 0; - synchronized (_acceptedPending) { - inLock = _context.clock().now(); - _acceptedPending.remove(id); - } - long afterLock = _context.clock().now(); - - if (afterLock - beforeLock > 50) { - _log.warn("ackSendMessage.locking took too long: " + (afterLock-beforeLock) - + " overall, synchronized took " + (inLock - beforeLock)); - } + _acceptedPending.remove(id); } catch (I2CPMessageException ime) { _log.error("Error writing out the message status message: " + ime); } @@ -504,28 +482,7 @@ public class ClientConnectionRunner { */ private boolean alreadyAccepted(MessageId id) { if (_dead) return false; - boolean isPending = false; - int pending = 0; - String buf = null; - long beforeLock = _context.clock().now(); - long inLock = 0; - synchronized (_acceptedPending) { - inLock = _context.clock().now(); - if (_acceptedPending.contains(id)) - isPending = true; - pending = _acceptedPending.size(); - buf = _acceptedPending.toString(); - } - long afterLock = _context.clock().now(); - - if (afterLock - beforeLock > 50) { - _log.warn("alreadyAccepted.locking took too long: " + (afterLock-beforeLock) - + " overall, synchronized took " + (inLock - beforeLock)); - } - if (pending >= 1) { - _log.warn("Pending acks: " + pending + ": " + buf); - } - return !isPending; + return !_acceptedPending.contains(id); } /** From ececf5407d0004999a22005d0f85c135a71ea870 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 3 Feb 2009 15:15:09 +0000 Subject: [PATCH 105/191] concurrentify shitlist --- .../src/net/i2p/util/ConcurrentHashSet.java | 7 + router/java/src/net/i2p/router/Shitlist.java | 124 +++++++----------- 2 files changed, 57 insertions(+), 74 deletions(-) diff --git a/core/java/src/net/i2p/util/ConcurrentHashSet.java b/core/java/src/net/i2p/util/ConcurrentHashSet.java index b7bea9ee1..2db9e195e 100644 --- a/core/java/src/net/i2p/util/ConcurrentHashSet.java +++ b/core/java/src/net/i2p/util/ConcurrentHashSet.java @@ -50,4 +50,11 @@ public class ConcurrentHashSet extends AbstractSet implements Set { public Iterator iterator() { return _map.keySet().iterator(); } + + public boolean addAll(Collection c) { + boolean rv = false; + for (E e : c) + rv |= _map.put(e, DUMMY) == null; + return rv; + } } diff --git a/router/java/src/net/i2p/router/Shitlist.java b/router/java/src/net/i2p/router/Shitlist.java index 2005366c2..4f866c7f7 100644 --- a/router/java/src/net/i2p/router/Shitlist.java +++ b/router/java/src/net/i2p/router/Shitlist.java @@ -10,6 +10,7 @@ package net.i2p.router; import java.io.IOException; import java.io.Writer; +import java.util.concurrent.ConcurrentHashMap; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -23,6 +24,7 @@ import java.util.TreeMap; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.router.peermanager.PeerProfile; +import net.i2p.util.ConcurrentHashSet; import net.i2p.util.Log; /** @@ -34,7 +36,7 @@ import net.i2p.util.Log; public class Shitlist { private Log _log; private RouterContext _context; - private Map _entries; + private Map _entries; private static class Entry { /** when it should expire, per the i2p clock */ @@ -42,7 +44,7 @@ public class Shitlist { /** why they were shitlisted */ String cause; /** what transports they were shitlisted for (String), or null for all transports */ - Set transports; + Set transports; } public final static long SHITLIST_DURATION_MS = 20*60*1000; @@ -54,49 +56,38 @@ public class Shitlist { public Shitlist(RouterContext context) { _context = context; _log = context.logManager().getLog(Shitlist.class); - _entries = new HashMap(32); + _entries = new ConcurrentHashMap(8); _context.jobQueue().addJob(new Cleanup(_context)); } private class Cleanup extends JobImpl { - private List _toUnshitlist; public Cleanup(RouterContext ctx) { super(ctx); - _toUnshitlist = new ArrayList(4); - getTiming().setStartAfter(_context.clock().now() + SHITLIST_CLEANER_START_DELAY); + getTiming().setStartAfter(ctx.clock().now() + SHITLIST_CLEANER_START_DELAY); } public String getName() { return "Cleanup shitlist"; } public void runJob() { - _toUnshitlist.clear(); long now = getContext().clock().now(); - synchronized (_entries) { - for (Iterator iter = _entries.keySet().iterator(); iter.hasNext(); ) { - Hash peer = (Hash)iter.next(); - Entry entry = (Entry)_entries.get(peer); - if (entry.expireOn <= now) { - iter.remove(); - _toUnshitlist.add(peer); - } + for (Iterator iter = _entries.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry e = (Map.Entry) iter.next(); + if (e.getValue().expireOn <= now) { + iter.remove(); + Hash peer = e.getKey(); + PeerProfile prof = _context.profileOrganizer().getProfile(peer); + if (prof != null) + prof.unshitlist(); + _context.messageHistory().unshitlist(peer); + if (_log.shouldLog(Log.INFO)) + _log.info("Unshitlisting router (expired) " + peer.toBase64()); } } - for (int i = 0; i < _toUnshitlist.size(); i++) { - Hash peer = (Hash)_toUnshitlist.get(i); - PeerProfile prof = _context.profileOrganizer().getProfile(peer); - if (prof != null) - prof.unshitlist(); - _context.messageHistory().unshitlist(peer); - if (_log.shouldLog(Log.INFO)) - _log.info("Unshitlisting router (expired) " + peer.toBase64()); - } requeue(30*1000); } } public int getRouterCount() { - synchronized (_entries) { - return _entries.size(); - } + return _entries.size(); } public boolean shitlistRouter(Hash peer) { @@ -143,12 +134,11 @@ public class Shitlist { e.cause = reason; e.transports = null; if (transport != null) { - e.transports = new HashSet(1); + e.transports = new ConcurrentHashSet(1); e.transports.add(transport); } - synchronized (_entries) { - Entry old = (Entry)_entries.get(peer); + Entry old = _entries.get(peer); if (old != null) { wasAlready = true; // take the oldest expiration and cause, combine transports @@ -166,7 +156,6 @@ public class Shitlist { } } _entries.put(peer, e); - } if (transport == null) { // we hate the peer on *any* transport @@ -190,20 +179,19 @@ public class Shitlist { _log.debug("Calling unshitlistRouter " + peer.toBase64() + (transport != null ? "/" + transport : "")); boolean fully = false; - Entry e; - synchronized (_entries) { - e = (Entry)_entries.remove(peer); - if ( (e == null) || (e.transports == null) || (transport == null) || (e.transports.size() <= 1) ) { - // fully unshitlisted + + Entry e = _entries.remove(peer); + if ( (e == null) || (e.transports == null) || (transport == null) || (e.transports.size() <= 1) ) { + // fully unshitlisted + fully = true; + } else { + e.transports.remove(transport); + if (e.transports.size() <= 0) fully = true; - } else { - e.transports.remove(transport); - if (e.transports.size() <= 0) - fully = true; - else - _entries.put(peer, e); - } + else + _entries.put(peer, e); } + if (fully) { if (realUnshitlist) { PeerProfile prof = _context.profileOrganizer().getProfile(peer); @@ -221,25 +209,18 @@ public class Shitlist { public boolean isShitlisted(Hash peer, String transport) { boolean rv = false; boolean unshitlist = false; - synchronized (_entries) { - Entry entry = (Entry)_entries.get(peer); - if (entry == null) { - rv = false; - } else { - if (entry.expireOn <= _context.clock().now()) { - _entries.remove(peer); - unshitlist = true; - rv = false; - } else { - if (entry.transports == null) { - rv = true; - } else if (entry.transports.contains(transport)) { - rv = true; - } else { - rv = false; - } - } - } + + Entry entry = _entries.get(peer); + if (entry == null) { + rv = false; + } else if (entry.expireOn <= _context.clock().now()) { + _entries.remove(peer); + unshitlist = true; + rv = false; + } else if (entry.transports == null) { + rv = true; + } else { + rv = entry.transports.contains(transport); } if (unshitlist) { @@ -255,10 +236,7 @@ public class Shitlist { } public boolean isShitlistedForever(Hash peer) { - Entry entry; - synchronized (_entries) { - entry = (Entry)_entries.get(peer); - } + Entry entry = _entries.get(peer); return entry != null && entry.expireOn > _context.clock().now() + SHITLIST_DURATION_MAX; } @@ -271,17 +249,15 @@ public class Shitlist { public void renderStatusHTML(Writer out) throws IOException { StringBuffer buf = new StringBuffer(1024); buf.append("

    Shitlist

    "); - Map entries = new TreeMap(new HashComparator()); + Map entries = new TreeMap(new HashComparator()); - synchronized (_entries) { - entries.putAll(_entries); - } + entries.putAll(_entries); + buf.append("
      "); - for (Iterator iter = entries.entrySet().iterator(); iter.hasNext(); ) { - Map.Entry mentry = (Map.Entry)iter.next(); - Hash key = (Hash)mentry.getKey(); - Entry entry = (Entry)mentry.getValue(); + for (Map.Entry e : entries.entrySet()) { + Hash key = e.getKey(); + Entry entry = e.getValue(); buf.append("
    • ").append(key.toBase64()).append(""); buf.append(" (netdb)"); buf.append(" expiring in "); From 3b9fec185798c530053bad3464f5a96965d4b972 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 3 Feb 2009 15:34:47 +0000 Subject: [PATCH 106/191] save a little space --- core/java/src/net/i2p/util/Clock.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/java/src/net/i2p/util/Clock.java b/core/java/src/net/i2p/util/Clock.java index 66d20721c..87cf9d639 100644 --- a/core/java/src/net/i2p/util/Clock.java +++ b/core/java/src/net/i2p/util/Clock.java @@ -28,7 +28,7 @@ public class Clock implements Timestamper.UpdateListener { _context = context; _offset = 0; _alreadyChanged = false; - _listeners = new HashSet(64); + _listeners = new HashSet(1); _timestamper = new Timestamper(context, this); _startedOn = System.currentTimeMillis(); _statCreated = false; @@ -149,4 +149,4 @@ public class Clock implements Timestamper.UpdateListener { public static interface ClockUpdateListener { public void offsetChanged(long delta); } -} \ No newline at end of file +} From 3d8cb3b90d077797447874bc492b356451939d31 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 4 Feb 2009 14:04:52 +0000 Subject: [PATCH 107/191] print torrent and peer count --- .../src/org/klomp/snark/web/I2PSnarkServlet.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) 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 c791ad2fb..d42d3b520 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -63,7 +63,7 @@ public class I2PSnarkServlet extends HttpServlet { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("text/html; charset=UTF-8"); - long stats[] = {0,0,0,0}; + long stats[] = {0,0,0,0,0}; String nonce = req.getParameter("nonce"); if ( (nonce != null) && (nonce.equals(String.valueOf(_nonce))) ) @@ -143,8 +143,10 @@ public class I2PSnarkServlet extends HttpServlet { if (snarks.size() <= 0) { out.write(TABLE_EMPTY); } else if (snarks.size() > 1) { - out.write(TABLE_TOTAL); - out.write(" " + formatSize(stats[0]) + "\n" + + out.write("\n" + + " Totals (" + snarks.size() + " torrents, " + stats[4] + " connected peers)\n" + + "  \n" + + " " + formatSize(stats[0]) + "\n" + " " + formatSize(stats[1]) + "\n" + " " + formatSize(stats[2]) + "ps\n" + " " + formatSize(stats[3]) + "ps\n" + @@ -439,6 +441,7 @@ public class I2PSnarkServlet extends HttpServlet { if (snark.coordinator != null) { err = snark.coordinator.trackerProblems; curPeers = snark.coordinator.getPeerCount(); + stats[4] += curPeers; knownPeers = snark.coordinator.trackerSeenPeers; } @@ -577,7 +580,7 @@ public class I2PSnarkServlet extends HttpServlet { client = "Azureus"; else if ("CwsL".equals(ch)) client = "I2PSnarkXL"; - else if ("AUZV".equals(ch) || "AkZV".equals(ch) || "A0ZV".equals(ch)) + else if ("ZV".equals(ch.substring(2,4))) client = "Robert"; else client = "Unknown (" + ch + ')'; @@ -858,11 +861,6 @@ public class I2PSnarkServlet extends HttpServlet { " Down Rate\n" + " Up Rate\n"; - private static final String TABLE_TOTAL = "\n" + - "Totals\n" + - "  \n" + - "  \n"; - private static final String TABLE_EMPTY = "" + "No torrents\n"; From 5946c35a885d1fe32f5fcf3d27f3d62a71a92367 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 4 Feb 2009 14:16:36 +0000 Subject: [PATCH 108/191] avoid illegalstateexception --- router/java/src/net/i2p/router/Shitlist.java | 29 ++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/router/java/src/net/i2p/router/Shitlist.java b/router/java/src/net/i2p/router/Shitlist.java index 4f866c7f7..29d384de9 100644 --- a/router/java/src/net/i2p/router/Shitlist.java +++ b/router/java/src/net/i2p/router/Shitlist.java @@ -61,25 +61,32 @@ public class Shitlist { } private class Cleanup extends JobImpl { + private List _toUnshitlist; public Cleanup(RouterContext ctx) { super(ctx); + _toUnshitlist = new ArrayList(4); getTiming().setStartAfter(ctx.clock().now() + SHITLIST_CLEANER_START_DELAY); } public String getName() { return "Cleanup shitlist"; } public void runJob() { + _toUnshitlist.clear(); long now = getContext().clock().now(); - for (Iterator iter = _entries.entrySet().iterator(); iter.hasNext(); ) { - Map.Entry e = (Map.Entry) iter.next(); - if (e.getValue().expireOn <= now) { - iter.remove(); - Hash peer = e.getKey(); - PeerProfile prof = _context.profileOrganizer().getProfile(peer); - if (prof != null) - prof.unshitlist(); - _context.messageHistory().unshitlist(peer); - if (_log.shouldLog(Log.INFO)) - _log.info("Unshitlisting router (expired) " + peer.toBase64()); + try { + for (Iterator iter = _entries.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry e = (Map.Entry) iter.next(); + if (e.getValue().expireOn <= now) { + iter.remove(); + _toUnshitlist.add(e.getKey()); + } } + } catch (IllegalStateException ise) {} // next time... + for (Hash peer : _toUnshitlist) { + PeerProfile prof = _context.profileOrganizer().getProfile(peer); + if (prof != null) + prof.unshitlist(); + _context.messageHistory().unshitlist(peer); + if (_log.shouldLog(Log.INFO)) + _log.info("Unshitlisting router (expired) " + peer.toBase64()); } requeue(30*1000); From 69f051da41a37d5e28736ed96f1e0da3b371699b Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 4 Feb 2009 14:17:10 +0000 Subject: [PATCH 109/191] concurrentify TunnelDispatcher --- .../i2p/router/tunnel/TunnelDispatcher.java | 142 ++++++------------ 1 file changed, 44 insertions(+), 98 deletions(-) diff --git a/router/java/src/net/i2p/router/tunnel/TunnelDispatcher.java b/router/java/src/net/i2p/router/tunnel/TunnelDispatcher.java index 8bb4781fe..de29a9540 100644 --- a/router/java/src/net/i2p/router/tunnel/TunnelDispatcher.java +++ b/router/java/src/net/i2p/router/tunnel/TunnelDispatcher.java @@ -2,8 +2,8 @@ package net.i2p.router.tunnel; import java.io.IOException; import java.io.Writer; +import java.util.concurrent.ConcurrentHashMap; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,12 +30,11 @@ import net.i2p.util.Log; public class TunnelDispatcher implements Service { private RouterContext _context; private Log _log; - private Map _outboundGateways; - private Map _outboundEndpoints; - private Map _participants; - private Map _inboundGateways; - /** id to HopConfig */ - private Map _participatingConfig; + private Map _outboundGateways; + private Map _outboundEndpoints; + private Map _participants; + private Map _inboundGateways; + private Map _participatingConfig; /** what is the date/time on which the last non-locally-created tunnel expires? */ private long _lastParticipatingExpiration; private BloomFilterIVValidator _validator; @@ -48,11 +47,11 @@ public class TunnelDispatcher implements Service { public TunnelDispatcher(RouterContext ctx) { _context = ctx; _log = ctx.logManager().getLog(TunnelDispatcher.class); - _outboundGateways = new HashMap(); - _outboundEndpoints = new HashMap(); - _participants = new HashMap(); - _inboundGateways = new HashMap(); - _participatingConfig = new HashMap(); + _outboundGateways = new ConcurrentHashMap(); + _outboundEndpoints = new ConcurrentHashMap(); + _participants = new ConcurrentHashMap(); + _inboundGateways = new ConcurrentHashMap(); + _participatingConfig = new ConcurrentHashMap(); _lastParticipatingExpiration = 0; _lastDropTime = 0; _validator = null; @@ -158,17 +157,13 @@ public class TunnelDispatcher implements Service { //TunnelGateway gw = new TunnelGateway(_context, preproc, sender, receiver); TunnelGateway gw = new PumpedTunnelGateway(_context, preproc, sender, receiver, _pumper); TunnelId outId = cfg.getConfig(0).getSendTunnel(); - synchronized (_outboundGateways) { - _outboundGateways.put(outId, gw); - } + _outboundGateways.put(outId, gw); _context.statManager().addRateData("tunnel.joinOutboundGateway", 1, 0); _context.messageHistory().tunnelJoined("outbound", cfg); } else { TunnelGatewayZeroHop gw = new TunnelGatewayZeroHop(_context, cfg); TunnelId outId = cfg.getConfig(0).getSendTunnel(); - synchronized (_outboundGateways) { - _outboundGateways.put(outId, gw); - } + _outboundGateways.put(outId, gw); _context.statManager().addRateData("tunnel.joinOutboundGatewayZeroHop", 1, 0); _context.messageHistory().tunnelJoined("outboundZeroHop", cfg); } @@ -183,17 +178,13 @@ public class TunnelDispatcher implements Service { if (cfg.getLength() > 1) { TunnelParticipant participant = new TunnelParticipant(_context, new InboundEndpointProcessor(_context, cfg, _validator)); TunnelId recvId = cfg.getConfig(cfg.getLength()-1).getReceiveTunnel(); - synchronized (_participants) { - _participants.put(recvId, participant); - } + _participants.put(recvId, participant); _context.statManager().addRateData("tunnel.joinInboundEndpoint", 1, 0); _context.messageHistory().tunnelJoined("inboundEndpoint", cfg); } else { TunnelGatewayZeroHop gw = new TunnelGatewayZeroHop(_context, cfg); TunnelId recvId = cfg.getConfig(0).getReceiveTunnel(); - synchronized (_inboundGateways) { - _inboundGateways.put(recvId, gw); - } + _inboundGateways.put(recvId, gw); _context.statManager().addRateData("tunnel.joinInboundEndpointZeroHop", 1, 0); _context.messageHistory().tunnelJoined("inboundEndpointZeroHop", cfg); } @@ -208,12 +199,8 @@ public class TunnelDispatcher implements Service { _log.info("Joining as participant: " + cfg); TunnelId recvId = cfg.getReceiveTunnel(); TunnelParticipant participant = new TunnelParticipant(_context, cfg, new HopProcessor(_context, cfg, _validator)); - synchronized (_participants) { - _participants.put(recvId, participant); - } - synchronized (_participatingConfig) { - _participatingConfig.put(recvId, cfg); - } + _participants.put(recvId, participant); + _participatingConfig.put(recvId, cfg); _context.messageHistory().tunnelJoined("participant", cfg); _context.statManager().addRateData("tunnel.joinParticipant", 1, 0); if (cfg.getExpiration() > _lastParticipatingExpiration) @@ -229,12 +216,8 @@ public class TunnelDispatcher implements Service { _log.info("Joining as outbound endpoint: " + cfg); TunnelId recvId = cfg.getReceiveTunnel(); OutboundTunnelEndpoint endpoint = new OutboundTunnelEndpoint(_context, cfg, new HopProcessor(_context, cfg, _validator)); - synchronized (_outboundEndpoints) { - _outboundEndpoints.put(recvId, endpoint); - } - synchronized (_participatingConfig) { - _participatingConfig.put(recvId, cfg); - } + _outboundEndpoints.put(recvId, endpoint); + _participatingConfig.put(recvId, cfg); _context.messageHistory().tunnelJoined("outboundEndpoint", cfg); _context.statManager().addRateData("tunnel.joinOutboundEndpoint", 1, 0); @@ -256,12 +239,8 @@ public class TunnelDispatcher implements Service { //TunnelGateway gw = new TunnelGateway(_context, preproc, sender, receiver); TunnelGateway gw = new PumpedTunnelGateway(_context, preproc, sender, receiver, _pumper); TunnelId recvId = cfg.getReceiveTunnel(); - synchronized (_inboundGateways) { - _inboundGateways.put(recvId, gw); - } - synchronized (_participatingConfig) { - _participatingConfig.put(recvId, cfg); - } + _inboundGateways.put(recvId, gw); + _participatingConfig.put(recvId, cfg); _context.messageHistory().tunnelJoined("inboundGateway", cfg); _context.statManager().addRateData("tunnel.joinInboundGateway", 1, 0); @@ -271,9 +250,7 @@ public class TunnelDispatcher implements Service { } public int getParticipatingCount() { - synchronized (_participatingConfig) { - return _participatingConfig.size(); - } + return _participatingConfig.size(); } /** what is the date/time on which the last non-locally-created tunnel expires? */ @@ -287,14 +264,9 @@ public class TunnelDispatcher implements Service { TunnelId recvId = cfg.getConfig(cfg.getLength()-1).getReceiveTunnel(); if (_log.shouldLog(Log.DEBUG)) _log.debug("removing our own inbound " + cfg); - TunnelParticipant participant = null; - synchronized (_participants) { - participant = (TunnelParticipant)_participants.remove(recvId); - } + TunnelParticipant participant = _participants.remove(recvId); if (participant == null) { - synchronized (_inboundGateways) { - _inboundGateways.remove(recvId); - } + _inboundGateways.remove(recvId); } else { // update stats based off getCompleteCount() + getFailedCount() for (int i = 0; i < cfg.getLength(); i++) { @@ -311,10 +283,7 @@ public class TunnelDispatcher implements Service { if (_log.shouldLog(Log.DEBUG)) _log.debug("removing our own outbound " + cfg); TunnelId outId = cfg.getConfig(0).getSendTunnel(); - TunnelGateway gw = null; - synchronized (_outboundGateways) { - gw = (TunnelGateway)_outboundGateways.remove(outId); - } + TunnelGateway gw = _outboundGateways.remove(outId); if (gw != null) { // update stats based on gw.getMessagesSent() } @@ -339,26 +308,17 @@ public class TunnelDispatcher implements Service { if (_log.shouldLog(Log.DEBUG)) _log.debug("removing " + cfg); - boolean removed = false; - synchronized (_participatingConfig) { - removed = (null != _participatingConfig.remove(recvId)); - } + boolean removed = (null != _participatingConfig.remove(recvId)); if (!removed) { if (_log.shouldLog(Log.WARN)) _log.warn("Participating tunnel, but no longer listed in participatingConfig? " + cfg); } - synchronized (_participants) { - removed = (null != _participants.remove(recvId)); - } + removed = (null != _participants.remove(recvId)); if (removed) return; - synchronized (_inboundGateways) { - removed = (null != _inboundGateways.remove(recvId)); - } + removed = (null != _inboundGateways.remove(recvId)); if (removed) return; - synchronized (_outboundEndpoints) { - removed = (null != _outboundEndpoints.remove(recvId)); - } + _outboundEndpoints.remove(recvId); } /** @@ -372,10 +332,7 @@ public class TunnelDispatcher implements Service { */ public void dispatch(TunnelDataMessage msg, Hash recvFrom) { long before = System.currentTimeMillis(); - TunnelParticipant participant = null; - synchronized (_participants) { - participant = (TunnelParticipant)_participants.get(msg.getTunnelIdObj()); - } + TunnelParticipant participant = _participants.get(msg.getTunnelIdObj()); if (participant != null) { // we are either just a random participant or the inbound endpoint if (_log.shouldLog(Log.DEBUG)) @@ -385,10 +342,7 @@ public class TunnelDispatcher implements Service { participant.dispatch(msg, recvFrom); _context.statManager().addRateData("tunnel.dispatchParticipant", 1, 0); } else { - OutboundTunnelEndpoint endpoint = null; - synchronized (_outboundEndpoints) { - endpoint = (OutboundTunnelEndpoint)_outboundEndpoints.get(msg.getTunnelIdObj()); - } + OutboundTunnelEndpoint endpoint = _outboundEndpoints.get(msg.getTunnelIdObj()); if (endpoint != null) { // we are the outobund endpoint if (_log.shouldLog(Log.DEBUG)) @@ -421,10 +375,7 @@ public class TunnelDispatcher implements Service { */ public void dispatch(TunnelGatewayMessage msg) { long before = System.currentTimeMillis(); - TunnelGateway gw = null; - synchronized (_inboundGateways) { - gw = (TunnelGateway)_inboundGateways.get(msg.getTunnelId()); - } + TunnelGateway gw = _inboundGateways.get(msg.getTunnelId()); if (gw != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("dispatch where we are the inbound gateway: " + gw + ": " + msg); @@ -489,10 +440,7 @@ public class TunnelDispatcher implements Service { public void dispatchOutbound(I2NPMessage msg, TunnelId outboundTunnel, TunnelId targetTunnel, Hash targetPeer) { if (outboundTunnel == null) throw new IllegalArgumentException("wtf, null outbound tunnel?"); long before = _context.clock().now(); - TunnelGateway gw = null; - synchronized (_outboundGateways) { - gw = (TunnelGateway)_outboundGateways.get(outboundTunnel); - } + TunnelGateway gw = _outboundGateways.get(outboundTunnel); if (gw != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("dispatch outbound through " + outboundTunnel.getTunnelId() @@ -538,10 +486,8 @@ public class TunnelDispatcher implements Service { _context.statManager().addRateData("tunnel.dispatchOutboundTime", dispatchTime, dispatchTime); } - public List listParticipatingTunnels() { - synchronized (_participatingConfig) { - return new ArrayList(_participatingConfig.values()); - } + public List listParticipatingTunnels() { + return new ArrayList(_participatingConfig.values()); } /** @@ -554,7 +500,7 @@ public class TunnelDispatcher implements Service { * and computing the average from that. */ public void updateParticipatingStats() { - List participating = listParticipatingTunnels(); + List participating = listParticipatingTunnels(); int size = participating.size(); long count = 0; long bw = 0; @@ -563,7 +509,7 @@ public class TunnelDispatcher implements Service { long tooYoung = _context.clock().now() - 60*1000; long tooOld = tooYoung - 9*60*1000; for (int i = 0; i < size; i++) { - HopConfig cfg = (HopConfig)participating.get(i); + HopConfig cfg = participating.get(i); long c = cfg.getRecentMessagesCount(); bw += c; bwOut += cfg.getRecentSentMessagesCount(); @@ -645,7 +591,7 @@ public class TunnelDispatcher implements Service { public void dropBiggestParticipating() { - List partTunnels = listParticipatingTunnels(); + List partTunnels = listParticipatingTunnels(); if ((partTunnels == null) || (partTunnels.size() == 0)) { if (_log.shouldLog(Log.ERROR)) _log.error("Not dropping tunnel, since partTunnels was null or had 0 items!"); @@ -668,7 +614,7 @@ public class TunnelDispatcher implements Service { for (int i=0; i _configs; + private List _times; public LeaveTunnel(RouterContext ctx) { super(ctx); @@ -765,12 +711,12 @@ public class TunnelDispatcher implements Service { synchronized (LeaveTunnel.this) { if (_configs.size() <= 0) return; - nextTime = (Long)_times.get(0); + nextTime = _times.get(0); if (nextTime.longValue() <= now) { - cur = (HopConfig)_configs.remove(0); + cur = _configs.remove(0); _times.remove(0); if (_times.size() > 0) - nextTime = (Long)_times.get(0); + nextTime = _times.get(0); else nextTime = null; } else { From a6dc27adaf97c84f2b3192904f74f88be1a57070 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 4 Feb 2009 14:32:09 +0000 Subject: [PATCH 110/191] Bound and concurrentify SYN queue to hopefully prevent explosion --- .../client/streaming/ConnectionHandler.java | 117 ++++++++++++------ 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java index a123708e4..d468372ec 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java @@ -1,5 +1,7 @@ package net.i2p.client.streaming; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.ArrayList; import java.util.List; @@ -10,24 +12,36 @@ import net.i2p.util.SimpleTimer; /** * Receive new connection attempts + * + * Use a bounded queue to limit the damage from SYN floods, + * router overload, or a slow client + * + * @author zzz modded to use concurrent and bound queue size */ class ConnectionHandler { private I2PAppContext _context; private Log _log; private ConnectionManager _manager; - private List _synQueue; + private LinkedBlockingQueue _synQueue; private boolean _active; private int _acceptTimeout; /** max time after receiveNewSyn() and before the matched accept() */ private static final int DEFAULT_ACCEPT_TIMEOUT = 3*1000; + + /** + * This is both SYNs and subsequent packets, and with an initial window size of 12, + * this is a backlog of 5 to 64 Syns, which seems like plenty for now + * Don't make this too big because the removal by all the TimeoutSyns is O(n**2) - sortof. + */ + private static final int MAX_QUEUE_SIZE = 64; /** Creates a new instance of ConnectionHandler */ public ConnectionHandler(I2PAppContext context, ConnectionManager mgr) { _context = context; _log = context.logManager().getLog(ConnectionHandler.class); _manager = mgr; - _synQueue = new ArrayList(5); + _synQueue = new LinkedBlockingQueue(MAX_QUEUE_SIZE); _active = false; _acceptTimeout = DEFAULT_ACCEPT_TIMEOUT; } @@ -35,9 +49,11 @@ class ConnectionHandler { public void setActive(boolean active) { if (_log.shouldLog(Log.DEBUG)) _log.debug("setActive(" + active + ") called"); - synchronized (_synQueue) { - _active = active; - _synQueue.notifyAll(); // so we break from the accept() + _active = active; + if (!active) { + try { + _synQueue.put(new PoisonPacket()); // so we break from the accept() - waits until space is available + } catch (InterruptedException ie) {} } } public boolean getActive() { return _active; } @@ -45,6 +61,11 @@ class ConnectionHandler { /** * Non-SYN packets with a zero SendStreamID may also be queued here so * that they don't get thrown away while the SYN packet before it is queued. + * + * Additional overload protection may be required here... + * We don't have a 3-way handshake, so the SYN fully opens a connection. + * Does that make us more or less vulnerable to SYN flooding? + * */ public void receiveNewSyn(Packet packet) { if (!_active) { @@ -55,10 +76,15 @@ class ConnectionHandler { } if (_log.shouldLog(Log.DEBUG)) _log.debug("Receive new SYN: " + packet + ": timeout in " + _acceptTimeout); - SimpleScheduler.getInstance().addEvent(new TimeoutSyn(packet), _acceptTimeout); - synchronized (_synQueue) { - _synQueue.add(packet); - _synQueue.notifyAll(); + // also check if expiration of the head is long past for overload detection with peek() ? + boolean success = _synQueue.offer(packet); // fail immediately if full + if (success) { + SimpleScheduler.getInstance().addEvent(new TimeoutSyn(packet), _acceptTimeout); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Dropping new SYN request, as the queue is full"); + if (packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) + sendReset(packet); } } @@ -82,41 +108,44 @@ class ConnectionHandler { return null; if (!_active) { // fail all the ones we had queued up - synchronized (_synQueue) { - for (int i = 0; i < _synQueue.size(); i++) { - Packet packet = (Packet)_synQueue.get(i); - sendReset(packet); - } - _synQueue.clear(); + while(true) { + Packet packet = _synQueue.poll(); // fails immediately if empty + if (packet == null || packet.getOptionalDelay() == PoisonPacket.MAX_DELAY_REQUEST) + break; + sendReset(packet); } return null; } Packet syn = null; - synchronized (_synQueue) { - while ( _active && (_synQueue.size() <= 0) ) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Accept("+ timeoutMs+"): active=" + _active + " queue: " - + _synQueue.size()); - if (timeoutMs <= 0) { - try { _synQueue.wait(); } catch (InterruptedException ie) {} - } else { - long remaining = expiration - _context.clock().now(); -// BUGFIX -// The specified amount of real time has elapsed, more or less. -// If timeout is zero, however, then real time is not taken into consideration -// and the thread simply waits until notified. - if (remaining < 1) - break; - try { _synQueue.wait(remaining); } catch (InterruptedException ie) {} - } - } - if (_active && _synQueue.size() > 0) { - syn = (Packet)_synQueue.remove(0); + while ( _active && syn == null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Accept("+ timeoutMs+"): active=" + _active + " queue: " + + _synQueue.size()); + if (timeoutMs <= 0) { + try { + syn = _synQueue.take(); // waits forever + } catch (InterruptedException ie) {} + } else { + long remaining = expiration - _context.clock().now(); + // (dont think this applies anymore for LinkedBlockingQueue) + // BUGFIX + // The specified amount of real time has elapsed, more or less. + // If timeout is zero, however, then real time is not taken into consideration + // and the thread simply waits until notified. + if (remaining < 1) + break; + try { + syn = _synQueue.poll(remaining, TimeUnit.MILLISECONDS); // waits the specified time max + } catch (InterruptedException ie) {} + break; } } if (syn != null) { + if (syn.getOptionalDelay() == PoisonPacket.MAX_DELAY_REQUEST) + return null; + // deal with forged / invalid syn packets // Handle both SYN and non-SYN packets in the queue @@ -179,10 +208,7 @@ class ConnectionHandler { } public void timeReached() { - boolean removed = false; - synchronized (_synQueue) { - removed = _synQueue.remove(_synPacket); - } + boolean removed = _synQueue.remove(_synPacket); if (removed) { if (_synPacket.isFlagSet(Packet.FLAG_SYNCHRONIZE)) @@ -196,4 +222,17 @@ class ConnectionHandler { } } } + + /** + * Simple end-of-queue marker. + * The standard class limits the delay to MAX_DELAY_REQUEST so + * an evil user can't use this to shut us down + */ + private static class PoisonPacket extends Packet { + public static final int MAX_DELAY_REQUEST = Packet.MAX_DELAY_REQUEST + 1; + + public PoisonPacket() { + setOptionalDelay(MAX_DELAY_REQUEST); + } + } } From a82de3d1cf7c802585dae34b04544f6070fb7f36 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 4 Feb 2009 17:18:00 +0000 Subject: [PATCH 111/191] Netdb: Remove all DataPublisher stuff --- .../networkdb/kademlia/DataPublisherJob.java | 101 ---------- .../kademlia/DataRepublishingSelectorJob.java | 175 ------------------ .../KademliaNetworkDatabaseFacade.java | 105 ----------- .../router/networkdb/kademlia/StoreJob.java | 1 - 4 files changed, 382 deletions(-) delete mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/DataPublisherJob.java delete mode 100644 router/java/src/net/i2p/router/networkdb/kademlia/DataRepublishingSelectorJob.java diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/DataPublisherJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/DataPublisherJob.java deleted file mode 100644 index 307ae5f79..000000000 --- a/router/java/src/net/i2p/router/networkdb/kademlia/DataPublisherJob.java +++ /dev/null @@ -1,101 +0,0 @@ -package net.i2p.router.networkdb.kademlia; -/* - * free (adj.): unencumbered; not under the control of others - * Written by jrandom in 2003 and released into the public domain - * with no warranty of any kind, either expressed or implied. - * It probably won't make your computer catch on fire, or eat - * your children, but it might. Use at your own risk. - * - */ - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -import net.i2p.data.DataStructure; -import net.i2p.data.Hash; -import net.i2p.data.LeaseSet; -import net.i2p.router.JobImpl; -import net.i2p.router.Router; -import net.i2p.router.RouterContext; -import net.i2p.util.Log; - -class DataPublisherJob extends JobImpl { - private Log _log; - private KademliaNetworkDatabaseFacade _facade; - private final static long RERUN_DELAY_MS = 120*1000; - private final static int MAX_SEND_PER_RUN = 1; // publish no more than 2 at a time - private final static long STORE_TIMEOUT = 60*1000; // give 'er a minute to send the data - - public DataPublisherJob(RouterContext ctx, KademliaNetworkDatabaseFacade facade) { - super(ctx); - _log = ctx.logManager().getLog(DataPublisherJob.class); - _facade = facade; - getTiming().setStartAfter(ctx.clock().now()+RERUN_DELAY_MS); // not immediate... - } - - public String getName() { return "Data Publisher Job"; } - public void runJob() { - Set toSend = selectKeysToSend(); - if (_log.shouldLog(Log.INFO)) - _log.info("Keys being published in this timeslice: " + toSend); - for (Iterator iter = toSend.iterator(); iter.hasNext(); ) { - Hash key = (Hash)iter.next(); - DataStructure data = _facade.getDataStore().get(key); - if (data == null) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Trying to send a key we dont have? " + key); - continue; - } - if (data instanceof LeaseSet) { - LeaseSet ls = (LeaseSet)data; - if (!ls.isCurrent(Router.CLOCK_FUDGE_FACTOR)) { - if (_log.shouldLog(Log.WARN)) - _log.warn("Not publishing a lease that isn't current - " + key, - new Exception("Publish expired lease?")); - } - if (!getContext().clientManager().shouldPublishLeaseSet(key)) - continue; - } - _facade.sendStore(key, data, null, null, STORE_TIMEOUT, null); - //StoreJob store = new StoreJob(getContext(), _facade, key, data, null, null, STORE_TIMEOUT); - //getContext().jobQueue().addJob(store); - } - requeue(RERUN_DELAY_MS); - } - - private Set selectKeysToSend() { - Set explicit = _facade.getExplicitSendKeys(); - Set toSend = new HashSet(MAX_SEND_PER_RUN); - - // if there's nothing we *need* to send, only send 10% of the time - if (explicit.size() <= 0) { - if (getContext().random().nextInt(10) > 0) - return toSend; - } - - if (explicit.size() < MAX_SEND_PER_RUN) { - toSend.addAll(explicit); - _facade.removeFromExplicitSend(explicit); - - Set passive = _facade.getPassivelySendKeys(); - Set psend = new HashSet(passive.size()); - for (Iterator iter = passive.iterator(); iter.hasNext(); ) { - if (toSend.size() >= MAX_SEND_PER_RUN) break; - Hash key = (Hash)iter.next(); - toSend.add(key); - psend.add(key); - } - _facade.removeFromPassiveSend(psend); - } else { - for (Iterator iter = explicit.iterator(); iter.hasNext(); ) { - if (toSend.size() >= MAX_SEND_PER_RUN) break; - Hash key = (Hash)iter.next(); - toSend.add(key); - } - _facade.removeFromExplicitSend(toSend); - } - - return toSend; - } -} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/DataRepublishingSelectorJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/DataRepublishingSelectorJob.java deleted file mode 100644 index 173522ab0..000000000 --- a/router/java/src/net/i2p/router/networkdb/kademlia/DataRepublishingSelectorJob.java +++ /dev/null @@ -1,175 +0,0 @@ -package net.i2p.router.networkdb.kademlia; -/* - * free (adj.): unencumbered; not under the control of others - * Written by jrandom in 2003 and released into the public domain - * with no warranty of any kind, either expressed or implied. - * It probably won't make your computer catch on fire, or eat - * your children, but it might. Use at your own risk. - * - */ - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.TreeMap; - -import net.i2p.data.Hash; -import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; -import net.i2p.router.JobImpl; -import net.i2p.router.Router; -import net.i2p.router.RouterContext; -import net.i2p.util.Log; - -class DataRepublishingSelectorJob extends JobImpl { - private Log _log; - private KademliaNetworkDatabaseFacade _facade; - - private final static long RERUN_DELAY_MS = 1*60*1000; - public final static int MAX_PASSIVE_POOL_SIZE = 10; // no need to have the pool be too big - - /** - * For every bucket away from us, resend period increases by 5 minutes - so we resend - * our own key every 5 minutes, and keys very far from us every 2.5 hours, increasing - * linearly - */ - public final static long RESEND_BUCKET_FACTOR = 5*60*1000; - - /** - * % chance any peer not specializing in the lease's key will broadcast it on each pass - * of this job /after/ waiting 5 minutes (one RESENT_BUCKET_FACTOR). In other words, - * .5% of routers will broadcast a particular unexpired lease to (say) 5 peers every - * minute. - * - */ - private final static int LEASE_REBROADCAST_PROBABILITY = 5; - /** - * LEASE_REBROADCAST_PROBABILITY out of LEASE_REBROADCAST_PROBABILITY_SCALE chance. - */ - private final static int LEASE_REBROADCAST_PROBABILITY_SCALE = 1000; - - public DataRepublishingSelectorJob(RouterContext ctx, KademliaNetworkDatabaseFacade facade) { - super(ctx); - _log = ctx.logManager().getLog(DataRepublishingSelectorJob.class); - _facade = facade; - getTiming().setStartAfter(ctx.clock().now()+RERUN_DELAY_MS); // not immediate... - } - - public String getName() { return "Data Publisher Job"; } - public void runJob() { - Set toSend = selectKeysToSend(); - if (_log.shouldLog(Log.INFO)) - _log.info("Keys being queued up for publishing: " + toSend); - _facade.queueForPublishing(toSend); - requeue(RERUN_DELAY_MS); - } - - /** - * Run through the entire data store, ranking how much we want to send each - * data point, and returning the ones we most want to send so that they can - * be placed in the passive send pool (without making the passive pool greater - * than the limit) - * - */ - private Set selectKeysToSend() { - Set alreadyQueued = new HashSet(128); - alreadyQueued.addAll(_facade.getPassivelySendKeys()); - - int toAdd = MAX_PASSIVE_POOL_SIZE - alreadyQueued.size(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Keys we need to queue up to fill the passive send pool: " + toAdd); - if (toAdd <= 0) return new HashSet(); - - alreadyQueued.addAll(_facade.getExplicitSendKeys()); - - Set keys = _facade.getDataStore().getKeys(); - keys.removeAll(alreadyQueued); - - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Total number of keys in the datastore: " + keys.size()); - - TreeMap toSend = new TreeMap(); - for (Iterator iter = keys.iterator(); iter.hasNext(); ) { - Hash key = (Hash)iter.next(); - Long lastPublished = _facade.getLastSent(key); - long publishRank = rankPublishNeed(key, lastPublished); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Publish rank for " + key + ": " + publishRank); - if (publishRank > 0) { - while (toSend.containsKey(new Long(publishRank))) - publishRank++; - toSend.put(new Long(publishRank), key); - } - } - Set rv = new HashSet(toAdd); - for (Iterator iter = toSend.values().iterator(); iter.hasNext(); ) { - if (rv.size() > toAdd) break; - Hash key = (Hash)iter.next(); - rv.add(key); - } - return rv; - } - - /** - * Higher values mean we want to publish it more, and values less than or equal to zero - * means we don't want to publish it - * - */ - private long rankPublishNeed(Hash key, Long lastPublished) { - int bucket = _facade.getKBuckets().pickBucket(key); - long sendPeriod = (bucket+1) * RESEND_BUCKET_FACTOR; - long now = getContext().clock().now(); - if (lastPublished.longValue() < now-sendPeriod) { - RouterInfo ri = _facade.lookupRouterInfoLocally(key); - if (ri != null) { - if (ri.isCurrent(2 * ExpireRoutersJob.EXPIRE_DELAY)) { - // last time it was sent was before the last send period - return KBucketSet.NUM_BUCKETS - bucket; - } else { - if (_log.shouldLog(Log.INFO)) - _log.info("Not republishing router " + key - + " since it is really old [" - + (now-ri.getPublished()) + "ms]"); - return -2; - } - } else { - LeaseSet ls = _facade.lookupLeaseSetLocally(key); - if (ls != null) { - if (!getContext().clientManager().shouldPublishLeaseSet(ls.getDestination().calculateHash())) - return -3; - if (ls.isCurrent(Router.CLOCK_FUDGE_FACTOR)) { - // last time it was sent was before the last send period - return KBucketSet.NUM_BUCKETS - bucket; - } else { - if (_log.shouldLog(Log.INFO)) - _log.info("Not republishing leaseSet " + key - + " since it is really old [" - + (now-ls.getEarliestLeaseDate()) + "ms]"); - return -3; - } - } else { - if (_log.shouldLog(Log.WARN)) - _log.warn("Key " + key + " is not a leaseSet or routerInfo, definitely not publishing it"); - return -5; - } - } - } else { - // its been published since the last period we want to publish it - - if (now - RESEND_BUCKET_FACTOR > lastPublished.longValue()) { - if (_facade.lookupRouterInfoLocally(key) != null) { - // randomize the chance of rebroadcast for leases if we haven't - // sent it within 5 minutes - int val = getContext().random().nextInt(LEASE_REBROADCAST_PROBABILITY_SCALE); - if (val <= LEASE_REBROADCAST_PROBABILITY) { - if (_log.shouldLog(Log.INFO)) - _log.info("Randomized rebroadcast of leases tells us to send " - + key + ": " + val); - return 1; - } - } - } - return -1; - } - } -} diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java index 23ef78d86..82e3a7f70 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java @@ -53,10 +53,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { private DataStore _ds; // hash to DataStructure mapping, persisted when necessary /** where the data store is pushing the data */ private String _dbDir; - private Set _explicitSendKeys; // set of Hash objects that should be published ASAP - private Set _passiveSendKeys; // set of Hash objects that should be published when there's time private Set _exploreKeys; // set of Hash objects that we should search on (to fill up a bucket, not to get data) - private Map _lastSent; // Hash to Long (date last sent, or <= 0 for never) private boolean _initialized; /** Clock independent time of when we started up */ private long _started; @@ -153,53 +150,6 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _exploreJob.updateExploreSchedule(); } - public Set getExplicitSendKeys() { - if (!_initialized) return null; - synchronized (_explicitSendKeys) { - return new HashSet(_explicitSendKeys); - } - } - public Set getPassivelySendKeys() { - if (!_initialized) return null; - synchronized (_passiveSendKeys) { - return new HashSet(_passiveSendKeys); - } - } - public void removeFromExplicitSend(Set toRemove) { - if (!_initialized) return; - synchronized (_explicitSendKeys) { - _explicitSendKeys.removeAll(toRemove); - } - } - public void removeFromPassiveSend(Set toRemove) { - if (!_initialized) return; - synchronized (_passiveSendKeys) { - _passiveSendKeys.removeAll(toRemove); - } - } - public void queueForPublishing(Set toSend) { - if (!_initialized) return; - synchronized (_passiveSendKeys) { - _passiveSendKeys.addAll(toSend); - } - } - - public Long getLastSent(Hash key) { - if (!_initialized) return null; - synchronized (_lastSent) { - if (!_lastSent.containsKey(key)) - _lastSent.put(key, new Long(0)); - return (Long)_lastSent.get(key); - } - } - - public void noteKeySent(Hash key) { - if (!_initialized) return; - synchronized (_lastSent) { - _lastSent.put(key, new Long(_context.clock().now())); - } - } - public Set getExploreKeys() { if (!_initialized) return null; synchronized (_exploreKeys) { @@ -226,10 +176,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _initialized = false; _kb = null; _ds = null; - _explicitSendKeys = null; - _passiveSendKeys = null; _exploreKeys = null; - _lastSent = null; } public void restart() { @@ -244,9 +191,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { else _enforceNetId = DEFAULT_ENFORCE_NETID; _ds.restart(); - synchronized (_explicitSendKeys) { _explicitSendKeys.clear(); } synchronized (_exploreKeys) { _exploreKeys.clear(); } - synchronized (_passiveSendKeys) { _passiveSendKeys.clear(); } _initialized = true; @@ -273,10 +218,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _kb = new KBucketSet(_context, ri.getIdentity().getHash()); _ds = new PersistentDataStore(_context, dbDir, this); //_ds = new TransientDataStore(); - _explicitSendKeys = new HashSet(64); - _passiveSendKeys = new HashSet(64); _exploreKeys = new HashSet(64); - _lastSent = new HashMap(1024); _dbDir = dbDir; createHandlers(); @@ -284,9 +226,6 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _initialized = true; _started = System.currentTimeMillis(); - // read the queues and publish appropriately - if (false) - _context.jobQueue().addJob(new DataPublisherJob(_context, this)); // expire old leases _context.jobQueue().addJob(new ExpireLeasesJob(_context, this)); @@ -298,9 +237,6 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { ////_context.jobQueue().addJob(new ExpireRoutersJob(_context, this)); if (!_quiet) { - // fill the passive queue periodically - // Is this pointless too??? - _context.jobQueue().addJob(new DataRepublishingSelectorJob(_context, this)); // fill the search queue with random keys in buckets that are too small // Disabled since KBucketImpl.generateRandomKey() is b0rked, // and anyway, we want to search for a completely random key, @@ -532,9 +468,6 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { if (!_context.clientManager().shouldPublishLeaseSet(h)) return; - synchronized (_explicitSendKeys) { - _explicitSendKeys.add(h); - } RepublishLeaseSetJob j = null; synchronized (_publishingLeaseSets) { j = (RepublishLeaseSetJob)_publishingLeaseSets.get(h); @@ -563,9 +496,6 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { if (_context.router().isHidden()) return; // DE-nied! Hash h = localRouterInfo.getIdentity().getHash(); store(h, localRouterInfo); - synchronized (_explicitSendKeys) { - _explicitSendKeys.add(h); - } } /** @@ -658,10 +588,6 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { throw new IllegalArgumentException("Invalid store attempt - " + err); _ds.put(key, leaseSet); - synchronized (_lastSent) { - if (!_lastSent.containsKey(key)) - _lastSent.put(key, new Long(0)); - } // Iterate through the old failure / success count, copying over the old // values (if any tunnels overlap between leaseSets). no need to be @@ -770,10 +696,6 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _context.peerManager().setCapabilities(key, routerInfo.getCapabilities()); _ds.put(key, routerInfo); - synchronized (_lastSent) { - if (!_lastSent.containsKey(key)) - _lastSent.put(key, new Long(0)); - } if (rv == null) _kb.add(key); return rv; @@ -808,15 +730,6 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _ds.remove(dbEntry); else _ds.removeLease(dbEntry); - synchronized (_lastSent) { - _lastSent.remove(dbEntry); - } - synchronized (_explicitSendKeys) { - _explicitSendKeys.remove(dbEntry); - } - synchronized (_passiveSendKeys) { - _passiveSendKeys.remove(dbEntry); - } } /** don't use directly - see F.N.D.F. override */ @@ -833,30 +746,12 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } _ds.remove(peer); - synchronized (_lastSent) { - _lastSent.remove(peer); - } - synchronized (_explicitSendKeys) { - _explicitSendKeys.remove(peer); - } - synchronized (_passiveSendKeys) { - _passiveSendKeys.remove(peer); - } } public void unpublish(LeaseSet localLeaseSet) { if (!_initialized) return; Hash h = localLeaseSet.getDestination().calculateHash(); DataStructure data = _ds.remove(h); - synchronized (_lastSent) { - _lastSent.remove(h); - } - synchronized (_explicitSendKeys) { - _explicitSendKeys.remove(h); - } - synchronized (_passiveSendKeys) { - _passiveSendKeys.remove(h); - } if (data == null) { if (_log.shouldLog(Log.WARN)) diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java index 18d18f13e..cc4c51329 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java @@ -437,7 +437,6 @@ class StoreJob extends JobImpl { _log.debug(getJobId() + ": State of successful send: " + _state); if (_onSuccess != null) getContext().jobQueue().addJob(_onSuccess); - _facade.noteKeySent(_state.getTarget()); _state.complete(true); getContext().statManager().addRateData("netDb.storePeers", _state.getAttempted().size(), _state.getWhenCompleted()-_state.getWhenStarted()); } From a7d4b3d6baf77a17f37a6788d9e41af234c157ae Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 6 Feb 2009 04:22:44 +0000 Subject: [PATCH 112/191] * I2PTunnel & I2CP: - Fix tunnel reduction/restore, hook in the GUI - Hook leaseset encryption into the GUI - Implement saves for all the new stuff - Add cancel button - Add b32 display for non-http servers - Prep for CONNECT - Fix error msg when connection goes away --- .../src/net/i2p/i2ptunnel/web/EditBean.java | 43 +++--- .../src/net/i2p/i2ptunnel/web/IndexBean.java | 134 ++++++++++++++---- apps/i2ptunnel/jsp/editClient.jsp | 3 +- apps/i2ptunnel/jsp/editServer.jsp | 3 +- apps/i2ptunnel/jsp/index.jsp | 20 ++- .../src/net/i2p/client/I2PSessionImpl.java | 6 +- .../client/RequestLeaseSetMessageHandler.java | 7 +- .../src/net/i2p/client/SessionIdleTimer.java | 17 ++- .../net/i2p/data/i2cp/I2CPMessageHandler.java | 7 +- .../router/tunnel/pool/TunnelPoolManager.java | 20 ++- 10 files changed, 182 insertions(+), 78 deletions(-) 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 e8bda1a29..e62bd2500 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -8,12 +8,10 @@ package net.i2p.i2ptunnel.web; * */ -import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Properties; -import java.util.Set; import java.util.StringTokenizer; import net.i2p.i2ptunnel.TunnelController; @@ -107,15 +105,15 @@ public class EditBean extends IndexBean { } public boolean getReduce(int tunnel) { - return false; + return getBooleanProperty(tunnel, "i2cp.reduceOnIdle"); } public int getReduceCount(int tunnel) { - return getProperty(tunnel, "inbound.reduceQuantity", 1); + return getProperty(tunnel, "i2cp.reduceQuantity", 1); } public int getReduceTime(int tunnel) { - return getProperty(tunnel, "reduceIdleTime", 20); + return getProperty(tunnel, "i2cp.reduceIdleTime", 20*60*1000) / (60*1000); } public int getCert(int tunnel) { @@ -131,31 +129,31 @@ public class EditBean extends IndexBean { } public boolean getEncrypt(int tunnel) { - return false; + return getBooleanProperty(tunnel, "i2cp.encryptLeaseSet"); } public String getEncryptKey(int tunnel) { - return getProperty(tunnel, "encryptKey", ""); + return getProperty(tunnel, "i2cp.leaseSetKey", ""); } public boolean getAccess(int tunnel) { - return false; + return getBooleanProperty(tunnel, "i2cp.enableAccessList"); } public String getAccessList(int tunnel) { - return getProperty(tunnel, "accessList", ""); + return getProperty(tunnel, "i2cp.accessList", "").replaceAll(",", "\n"); } public boolean getClose(int tunnel) { - return false; + return getBooleanProperty(tunnel, "i2cp.closeOnIdle"); } public int getCloseTime(int tunnel) { - return getProperty(tunnel, "closeIdleTime", 30); + return getProperty(tunnel, "i2cp.closeIdleTime", 30*60*1000) / (60*1000); } public boolean getNewDest(int tunnel) { - return false; + return getBooleanProperty(tunnel, "i2cp.newDestOnResume"); } private int getProperty(int tunnel, String prop, int def) { @@ -183,6 +181,17 @@ public class EditBean extends IndexBean { return def; } + /** default is false */ + private boolean getBooleanProperty(int tunnel, String prop) { + TunnelController tun = getController(tunnel); + if (tun != null) { + Properties opts = getOptions(tun); + if (opts != null) + return Boolean.valueOf(opts.getProperty(prop)).booleanValue(); + } + return false; + } + public String getI2CPHost(int tunnel) { TunnelController tun = getController(tunnel); if (tun != null) @@ -199,14 +208,6 @@ public class EditBean extends IndexBean { return "7654"; } - private static final String noShowProps[] = { - "inbound.length", "outbound.length", "inbound.lengthVariance", "outbound.lengthVariance", - "inbound.backupQuantity", "outbound.backupQuantity", "inbound.quantity", "outbound.quantity", - "inbound.nickname", "outbound.nickname", "i2p.streaming.connectDelay", "i2p.streaming.maxWindowSize" - }; - private static final Set noShowSet = new HashSet(noShowProps.length); - static { noShowSet.addAll(Arrays.asList(noShowProps)); } - public String getCustomOptions(int tunnel) { TunnelController tun = getController(tunnel); if (tun != null) { @@ -216,7 +217,7 @@ public class EditBean extends IndexBean { int i = 0; for (Iterator iter = opts.keySet().iterator(); iter.hasNext(); ) { String key = (String)iter.next(); - if (noShowSet.contains(key)) + if (_noShowSet.contains(key)) continue; String val = opts.getProperty(key); if (i != 0) buf.append(' '); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 1500150e3..1aca37bf5 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -8,13 +8,19 @@ package net.i2p.i2ptunnel.web; * */ +import java.util.concurrent.ConcurrentHashMap; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.StringTokenizer; import net.i2p.I2PAppContext; import net.i2p.i2ptunnel.TunnelController; import net.i2p.i2ptunnel.TunnelControllerGroup; +import net.i2p.util.ConcurrentHashSet; import net.i2p.util.Log; /** @@ -57,6 +63,8 @@ public class IndexBean { private boolean _sharedClient; private boolean _privKeyGenerate; private boolean _removeConfirmed; + private Set _booleanOptions; + private Map _otherOptions; public static final int RUNNING = 1; public static final int STARTING = 2; @@ -85,6 +93,8 @@ public class IndexBean { } catch (NumberFormatException nfe) {} _nextNonce = _context.random().nextLong(); System.setProperty(PROP_NONCE, Long.toString(_nextNonce)); + _booleanOptions = new ConcurrentHashSet(4); + _otherOptions = new ConcurrentHashMap(4); } public long getNextNonce() { return _nextNonce; } @@ -326,6 +336,7 @@ public class IndexBean { return ( ("client".equals(type)) || ("httpclient".equals(type)) || ("sockstunnel".equals(type)) || + ("connectclient".equals(type)) || ("ircclient".equals(type))); } @@ -360,6 +371,7 @@ public class IndexBean { else if ("server".equals(internalType)) return "Standard server"; else if ("httpserver".equals(internalType)) return "HTTP server"; else if ("sockstunnel".equals(internalType)) return "SOCKS proxy"; + else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy"; else return internalType; } @@ -406,13 +418,12 @@ public class IndexBean { public String getClientDestination(int tunnel) { TunnelController tun = getController(tunnel); if (tun == null) return ""; - if ("client".equals(tun.getType())||"ircclient".equals(tun.getType())) { - if (tun.getTargetDestination() != null) - return tun.getTargetDestination(); - else - return ""; - } - else return tun.getProxyList(); + String rv; + if ("client".equals(tun.getType())||"ircclient".equals(tun.getType())) + rv = tun.getTargetDestination(); + else + rv = tun.getProxyList(); + return rv != null ? rv : ""; } public String getServerTarget(int tunnel) { @@ -429,11 +440,8 @@ public class IndexBean { String rv = tun.getMyDestination(); if (rv != null) return rv; - else - return ""; - } else { - return ""; } + return ""; } public String getDestHashBase32(int tunnel) { @@ -442,11 +450,8 @@ public class IndexBean { String rv = tun.getMyDestHashBase32(); if (rv != null) return rv; - else - return ""; - } else { - return ""; } + return ""; } /// @@ -568,6 +573,49 @@ public class IndexBean { _profile = profile; } + public void setReduce(String moo) { + _booleanOptions.add("i2cp.reduceOnIdle"); + } + public void setClose(String moo) { + _booleanOptions.add("i2cp.closeOnIdle"); + } + public void setEncrypt(String moo) { + _booleanOptions.add("i2cp.encryptLeaseSet"); + } + public void setAccess(String moo) { + _booleanOptions.add("i2cp.enableAccessList"); + } + public void setNewDest(String moo) { + _booleanOptions.add("i2cp.newDestOnResume"); + } + + public void setReduceTime(String val) { + if (val != null) { + try { + _otherOptions.put("i2cp.reduceIdleTime", "" + (Integer.parseInt(val.trim()) * 60*1000)); + } catch (NumberFormatException nfe) {} + } + } + public void setReduceCount(String val) { + if (val != null) + _otherOptions.put("i2cp.reduceQuantity", val.trim()); + } + public void setEncryptKey(String val) { + if (val != null) + _otherOptions.put("i2cp.leaseSetKey", val.trim()); + } + public void setAccessList(String val) { + if (val != null) + _otherOptions.put("i2cp.accessList", val.trim().replaceAll("\r\n", ",").replaceAll("\n", ",").replaceAll(" ", ",")); + } + public void setCloseTime(String val) { + if (val != null) { + try { + _otherOptions.put("i2cp.closeIdleTime", "" + (Integer.parseInt(val.trim()) * 60*1000)); + } catch (NumberFormatException nfe) {} + } + } + /** * Based on all provided data, create a set of configuration parameters * suitable for use in a TunnelController. This will replace (not add to) @@ -593,6 +641,11 @@ public class IndexBean { config.setProperty("option.outbound.nickname", _name); } config.setProperty("sharedClient", _sharedClient + ""); + for (String p : _booleanClientOpts) + config.setProperty("option." + p, "" + _booleanOptions.contains(p)); + for (String p : _otherClientOpts) + if (_otherOptions.containsKey(p)) + config.setProperty("option." + p, _otherOptions.get(p)); } else { // generic server stuff if (_targetHost != null) @@ -601,9 +654,14 @@ public class IndexBean { config.setProperty("targetPort", _targetPort); if (_privKeyFile != null) config.setProperty("privKeyFile", _privKeyFile); + for (String p : _booleanServerOpts) + config.setProperty("option." + p, "" + _booleanOptions.contains(p)); + for (String p : _otherServerOpts) + if (_otherOptions.containsKey(p)) + config.setProperty("option." + p, _otherOptions.get(p)); } - if ("httpclient".equals(_type)) { + if ("httpclient".equals(_type) || "connectclient".equals(_type)) { if (_proxyList != null) config.setProperty("proxyList", _proxyList); } else if ("ircclient".equals(_type) || "client".equals(_type)) { @@ -617,6 +675,32 @@ public class IndexBean { return config; } + private static final String _noShowOpts[] = { + "inbound.length", "outbound.length", "inbound.lengthVariance", "outbound.lengthVariance", + "inbound.backupQuantity", "outbound.backupQuantity", "inbound.quantity", "outbound.quantity", + "inbound.nickname", "outbound.nickname", "i2p.streaming.connectDelay", "i2p.streaming.maxWindowSize" + }; + private static final String _booleanClientOpts[] = { + "i2cp.reduceOnIdle", "i2cp.closeOnIdle", "i2cp.newDestOnResume" + }; + private static final String _booleanServerOpts[] = { + "i2cp.reduceOnIdle", "i2cp.encryptLeaseSet", "i2cp.enableAccessList" + }; + private static final String _otherClientOpts[] = { + "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.closeIdleTime" + }; + private static final String _otherServerOpts[] = { + "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.leaseSetKey", "i2cp.accessList" + }; + protected static final Set _noShowSet = new HashSet(); + static { + _noShowSet.addAll(Arrays.asList(_noShowOpts)); + _noShowSet.addAll(Arrays.asList(_booleanClientOpts)); + _noShowSet.addAll(Arrays.asList(_booleanServerOpts)); + _noShowSet.addAll(Arrays.asList(_otherClientOpts)); + _noShowSet.addAll(Arrays.asList(_otherServerOpts)); + } + private void updateConfigGeneric(Properties config) { config.setProperty("type", _type); if (_name != null) @@ -639,19 +723,9 @@ public class IndexBean { if ( (eq <= 0) || (eq >= pair.length()) ) continue; String key = pair.substring(0, eq); + if (_noShowSet.contains(key)) + continue; String val = pair.substring(eq+1); - if ("inbound.length".equals(key)) continue; - if ("outbound.length".equals(key)) continue; - if ("inbound.quantity".equals(key)) continue; - if ("outbound.quantity".equals(key)) continue; - if ("inbound.lengthVariance".equals(key)) continue; - if ("outbound.lengthVariance".equals(key)) continue; - if ("inbound.backupQuantity".equals(key)) continue; - if ("outbound.backupQuantity".equals(key)) continue; - if ("inbound.nickname".equals(key)) continue; - if ("outbound.nickname".equals(key)) continue; - if ("i2p.streaming.connectDelay".equals(key)) continue; - if ("i2p.streaming.maxWindowSize".equals(key)) continue; config.setProperty("option." + key, val); } } @@ -679,14 +753,14 @@ public class IndexBean { else config.setProperty("option.i2p.streaming.connectDelay", "0"); if (_name != null) { - if ( ((!"client".equals(_type)) && (!"httpclient".equals(_type))&& (!"ircclient".equals(_type))) || (!_sharedClient) ) { + if ( (!isClient(_type)) || (!_sharedClient) ) { config.setProperty("option.inbound.nickname", _name); config.setProperty("option.outbound.nickname", _name); } else { config.setProperty("option.inbound.nickname", CLIENT_NICKNAME); config.setProperty("option.outbound.nickname", CLIENT_NICKNAME); } - } + } if ("interactive".equals(_profile)) // This was 1 which doesn't make much sense // The real way to make it interactive is to make the streaming lib diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index f5e1fac7f..3e4c3ecd8 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -116,7 +116,7 @@
    - <% if ("httpclient".equals(tunnelType)) { + <% if ("httpclient".equals(tunnelType) || "connectclient".equals(tunnelType)) { %>
    diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index e6cc0497d..596848617 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -281,7 +281,7 @@ - + (Restrict to these clients only) @@ -385,6 +385,7 @@ + diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index 082549307..a06177dd4 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -114,7 +114,13 @@ <% if (!"sockstunnel".equals(indexBean.getInternalType(curClient))) { %>
    - +
    <% } %> @@ -143,6 +149,7 @@ + @@ -162,10 +169,10 @@
    -
    +
    -
    +
    @@ -181,7 +188,7 @@ <%=indexBean.getTunnelName(curServer)%>
    -
    +
    <% @@ -195,11 +202,14 @@ } %>
    -
    +
    <% if ("httpserver".equals(indexBean.getInternalType(curServer)) && indexBean.getTunnelStatus(curServer) == IndexBean.RUNNING) { %> Preview + <% + } else if (indexBean.getTunnelStatus(curServer) == IndexBean.RUNNING) { + %>Base32 Address:
    <%=indexBean.getDestHashBase32(curServer)%>.b32.i2p
    <% } else { %>No Preview diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index 2c8582a4f..b5f4e8ab3 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -442,8 +442,8 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa long before = System.currentTimeMillis(); _sessionListener.messageAvailable(I2PSessionImpl.this, msgId.intValue(), size.intValue()); long duration = System.currentTimeMillis() - before; - if ((duration > 100) && _log.shouldLog(Log.WARN)) - _log.warn("Message availability notification for " + msgId.intValue() + " took " + if ((duration > 100) && _log.shouldLog(Log.INFO)) + _log.info("Message availability notification for " + msgId.intValue() + " took " + duration + " to " + _sessionListener); } catch (Exception e) { _log.log(Log.CRIT, "Error notifying app of message availability", e); @@ -678,6 +678,8 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa _lastActivity = _context.clock().now(); if (_isReduced) { _isReduced = false; + if (_log.shouldLog(Log.WARN)) + _log.warn(getPrefix() + "Restoring original tunnel quantity"); try { _producer.updateTunnels(this, 0); } catch (I2PSessionException ise) { diff --git a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java index 6163771e3..7a8bd200e 100644 --- a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java +++ b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java @@ -79,15 +79,16 @@ class RequestLeaseSetMessageHandler extends HandlerImpl { leaseSet.setEncryptionKey(li.getPublicKey()); leaseSet.setSigningKey(li.getSigningPublicKey()); - String sk = session.getOptions().getProperty("i2cp.sessionKey"); - if (sk != null) { + boolean encrypt = Boolean.valueOf(session.getOptions().getProperty("i2cp.encryptLeaseset")).booleanValue(); + String sk = session.getOptions().getProperty("i2cp.leaseSetKey"); + if (encrypt && sk != null) { SessionKey key = new SessionKey(); try { key.fromBase64(sk); leaseSet.encrypt(key); _context.keyRing().put(session.getMyDestination().calculateHash(), key); } catch (DataFormatException dfe) { - _log.error("Bad session key: " + sk); + _log.error("Bad leaseset key: " + sk); } } try { diff --git a/core/java/src/net/i2p/client/SessionIdleTimer.java b/core/java/src/net/i2p/client/SessionIdleTimer.java index 1babd9551..354a2b633 100644 --- a/core/java/src/net/i2p/client/SessionIdleTimer.java +++ b/core/java/src/net/i2p/client/SessionIdleTimer.java @@ -31,6 +31,7 @@ public class SessionIdleTimer implements SimpleTimer.TimedEvent { private boolean _shutdownEnabled; private long _shutdownTime; private long _minimumTime; + private long _lastActive; /** * reduce, shutdown, or both must be true @@ -44,6 +45,7 @@ public class SessionIdleTimer implements SimpleTimer.TimedEvent { throw new IllegalArgumentException("At least one must be enabled"); Properties props = session.getOptions(); _minimumTime = Long.MAX_VALUE; + _lastActive = 0; if (reduce) { _reduceQuantity = 1; String p = props.getProperty("i2cp.reduceQuantity"); @@ -83,10 +85,16 @@ public class SessionIdleTimer implements SimpleTimer.TimedEvent { long lastActivity = _session.lastActivity(); if (_log.shouldLog(Log.INFO)) _log.info("Fire idle timer, last activity: " + DataHelper.formatDuration(now - lastActivity) + " ago "); + long nextDelay = 0; if (_shutdownEnabled && now - lastActivity >= _shutdownTime) { if (_log.shouldLog(Log.WARN)) _log.warn("Closing on idle " + _session); _session.destroySession(); + return; + } else if (lastActivity <= _lastActive && !_shutdownEnabled) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Still idle, sleeping again " + _session); + nextDelay = _reduceTime; } else if (_reduceEnabled && now - lastActivity >= _reduceTime) { if (_log.shouldLog(Log.WARN)) _log.warn("Reducing quantity on idle " + _session); @@ -96,11 +104,14 @@ public class SessionIdleTimer implements SimpleTimer.TimedEvent { _log.error("bork idle reduction " + ise); } _session.setReduced(); + _lastActive = lastActivity; if (_shutdownEnabled) - SimpleScheduler.getInstance().addEvent(this, _shutdownTime - (now - lastActivity)); - // else sessionimpl must reschedule?? + nextDelay = _shutdownTime - (now - lastActivity); + else + nextDelay = _reduceTime; } else { - SimpleScheduler.getInstance().addEvent(this, _minimumTime - (now - lastActivity)); + nextDelay = _minimumTime - (now - lastActivity); } + SimpleScheduler.getInstance().addEvent(this, nextDelay); } } diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java index 294059bdb..61d865053 100644 --- a/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageHandler.java @@ -34,8 +34,13 @@ public class I2CPMessageHandler { * message - if it is an unknown type or has improper formatting, etc. */ public static I2CPMessage readMessage(InputStream in) throws IOException, I2CPMessageException { + int length = -1; + try { + length = (int) DataHelper.readLong(in, 4); + } catch (DataFormatException dfe) { + throw new IOException("Connection closed"); + } try { - int length = (int) DataHelper.readLong(in, 4); if (length < 0) throw new I2CPMessageException("Invalid message length specified"); int type = (int) DataHelper.readLong(in, 1); I2CPMessage msg = createMessage(in, length, type); diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java index c6b1f5b9a..58637ce63 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPoolManager.java @@ -237,22 +237,20 @@ public class TunnelPoolManager implements TunnelManagerFacade { return null; } public void setInboundSettings(Hash client, TunnelPoolSettings settings) { - - TunnelPool pool = null; - synchronized (_clientInboundPools) { - pool = (TunnelPool)_clientInboundPools.get(client); - } - if (pool != null) - pool.setSettings(settings); + setSettings(_clientInboundPools, client, settings); } public void setOutboundSettings(Hash client, TunnelPoolSettings settings) { - + setSettings(_clientOutboundPools, client, settings); + } + private void setSettings(Map pools, Hash client, TunnelPoolSettings settings) { TunnelPool pool = null; - synchronized (_clientOutboundPools) { - pool = (TunnelPool)_clientOutboundPools.get(client); + synchronized (pools) { + pool = (TunnelPool)pools.get(client); } - if (pool != null) + if (pool != null) { + settings.setDestination(client); // prevent spoofing or unset dest pool.setSettings(settings); + } } public void restart() { From bdf7dda3b409fbd818c4641fe69874c70c887200 Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 6 Feb 2009 13:14:10 +0000 Subject: [PATCH 113/191] Use the right error msg when a b32 address fails to resolve --- .../java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index a3d6f75e4..4b5379ac1 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -560,7 +560,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable Destination dest = I2PTunnel.destFromName(destination); if (dest == null) { - l.log("Could not resolve " + destination + "."); + //l.log("Could not resolve " + destination + "."); if (_log.shouldLog(Log.WARN)) _log.warn("Unable to resolve " + destination + " (proxy? " + usingWWWProxy + ", request: " + targetRequest); String str; @@ -570,6 +570,8 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true); else if(ahelper != 0) str = FileUtil.readTextFile("docs/dnfb-header.ht", 100, true); + else if (destination.length() == 60 && destination.endsWith(".b32.i2p")) + str = FileUtil.readTextFile("docs/dnf-header.ht", 100, true); else { str = FileUtil.readTextFile("docs/dnfh-header.ht", 100, true); showAddrHelper = true; From e7bccb2f4731b3c164e6b5d232311852c7be294d Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 6 Feb 2009 15:45:34 +0000 Subject: [PATCH 114/191] fix idle property names --- core/java/src/net/i2p/client/SessionIdleTimer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/java/src/net/i2p/client/SessionIdleTimer.java b/core/java/src/net/i2p/client/SessionIdleTimer.java index 354a2b633..f4661b73b 100644 --- a/core/java/src/net/i2p/client/SessionIdleTimer.java +++ b/core/java/src/net/i2p/client/SessionIdleTimer.java @@ -56,7 +56,7 @@ public class SessionIdleTimer implements SimpleTimer.TimedEvent { } catch (NumberFormatException nfe) {} } _reduceTime = DEFAULT_REDUCE_TIME; - p = props.getProperty("i2cp.reduceTime"); + p = props.getProperty("i2cp.reduceIdleTime"); if (p != null) { try { _reduceTime = Math.max(Long.parseLong(p), MINIMUM_TIME); @@ -66,7 +66,7 @@ public class SessionIdleTimer implements SimpleTimer.TimedEvent { } if (shutdown) { _shutdownTime = DEFAULT_CLOSE_TIME; - String p = props.getProperty("i2cp.closeTime"); + String p = props.getProperty("i2cp.closeIdleTime"); if (p != null) { try { _shutdownTime = Math.max(Long.parseLong(p), MINIMUM_TIME); From 28a14782a6e2b6671c8f5ee6f1b29e1c10a79bd2 Mon Sep 17 00:00:00 2001 From: dream Date: Fri, 6 Feb 2009 18:39:51 +0000 Subject: [PATCH 115/191] debian package instructions As Debian's package building system is rather complicated and requires root access unconditionally for some reason, doing it from ant isn't really feasible. However to build any debian package anywhere is the same system, so including helpful documentation on how to use that system as an ant build target would be most useful in this case. Hopefully Debian users will only have to deal with the already built .deb anyway. --- build.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.xml b/build.xml index b4a374697..24e18ffb1 100644 --- a/build.xml +++ b/build.xml @@ -16,6 +16,10 @@ + + + + From 06e1305df28087dffdb737d5c51d8580db0111de Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 6 Feb 2009 21:19:45 +0000 Subject: [PATCH 116/191] prevent race NPE http://forum.i2p/viewtopic.php?t=3066 --- core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java index 53650ec19..13b01a67a 100644 --- a/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java +++ b/core/java/src/net/i2p/data/i2cp/I2CPMessageReader.java @@ -134,9 +134,11 @@ public class I2CPMessageReader { public void cancelRunner() { _doRun = false; _stayAlive = false; - if (_stream != null) { + // prevent race NPE + InputStream in = _stream; + if (in != null) { try { - _stream.close(); + in.close(); } catch (IOException ioe) { _log.error("Error closing the stream", ioe); } @@ -164,6 +166,7 @@ public class I2CPMessageReader { _listener.disconnected(I2CPMessageReader.this); cancelRunner(); } catch (OutOfMemoryError oom) { + // ooms seen here... maybe log and keep going? throw oom; } catch (Exception e) { _log.log(Log.CRIT, "Unhandled error reading I2CP stream", e); @@ -182,4 +185,4 @@ public class I2CPMessageReader { // boom bye bye bad bwoy } } -} \ No newline at end of file +} From b125276be91fa05438b639f3e67d12420790d353 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 7 Feb 2009 21:24:53 +0000 Subject: [PATCH 117/191] correct comment --- core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java index 1e77f3636..5dc4a5d71 100644 --- a/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java +++ b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java @@ -19,8 +19,7 @@ import net.i2p.data.Payload; import net.i2p.util.Log; /** - * Defines the message a client sends to a router to ask it to deliver - * a new message + * Defines the payload message a router sends to the client * * @author jrandom */ From 6504e1f91dd20ea3f6630513b7828f5f5d0d00db Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 7 Feb 2009 21:25:27 +0000 Subject: [PATCH 118/191] export symbol --- apps/streaming/java/src/net/i2p/client/streaming/Packet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Packet.java b/apps/streaming/java/src/net/i2p/client/streaming/Packet.java index 7a7a0a6c9..9d4177256 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/Packet.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/Packet.java @@ -144,7 +144,7 @@ public class Packet { public static final int FLAG_NO_ACK = (1 << 10); public static final int DEFAULT_MAX_SIZE = 32*1024; - private static final int MAX_DELAY_REQUEST = 65535; + protected static final int MAX_DELAY_REQUEST = 65535; public Packet() { } From 7acaa964af667690d657e10d56889ba2b5a8783b Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 7 Feb 2009 21:27:20 +0000 Subject: [PATCH 119/191] -3 --- history.txt | 22 +++++++++++++++++++ .../src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index f94b1369a..45d89effa 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,25 @@ +2009-02-07 zzz + * ClientConnectionRunner, Shitlist, TunnelDispatcher: + Update using concurrent + * Streaming ConnectionHandler: Bound SYN queue and + use concurrent to prevent blowup + * HTTP Proxy: Fix error msg for b32 addresses + * I2CP: Implement optional reduce tunnels on idle - not hooked + in to i2ptunnel GUI yet - still needs tweaks + * I2CP MessageReader: Prevent rare NPE + * I2CP Writer: Rewrite using concurrent + * i2psnark: Add torrent and connection count + * I2PTunnel & I2CP: + - Fix tunnel reduction/restore, hook in the GUI + - Hook leaseset encryption into the GUI + - Implement saves for all the new stuff + - Add cancel button + - Add b32 display for non-http servers + - Prep for CONNECT + - Fix error msg when connection goes away + * NetDb: Remove all DataPublisher stuff + * Wrapper: Remove dup timeout + 2009-02-02 sponge * Final? cleanups to Slackbuilds. * ant target for Slackbuilds. diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index a727710d6..a34e9238c 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 2; + public final static long BUILD = 3; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From f344c9e0be56ba0307fbdef0039ff994a6429a8b Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 9 Feb 2009 12:55:35 +0000 Subject: [PATCH 120/191] plug connection leak --- .../src/net/i2p/client/streaming/Connection.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java index ee93d20ee..a59e12610 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java @@ -855,10 +855,12 @@ public class Connection { return; } // if one of us can't talk... - if ( (_closeSentOn > 0) || (_closeReceivedOn > 0) ) { - if (_log.shouldLog(Log.DEBUG)) _log.debug("Inactivity timeout reached, but we are closing"); - return; - } + // No - not true - data and acks are still going back and forth. + // Prevent zombie connections by keeping the inactivity timer. + //if ( (_closeSentOn > 0) || (_closeReceivedOn > 0) ) { + // if (_log.shouldLog(Log.DEBUG)) _log.debug("Inactivity timeout reached, but we are closing"); + // return; + //} if (_log.shouldLog(Log.DEBUG)) _log.debug("Inactivity timeout reached, with action=" + _options.getInactivityAction()); @@ -959,9 +961,9 @@ public class Connection { } if (getResetSent()) - buf.append(" reset sent"); + buf.append(" reset sent ").append(DataHelper.formatDuration(_context.clock().now() - getResetSentOn())).append(" ago"); if (getResetReceived()) - buf.append(" reset received"); + buf.append(" reset received ").append(DataHelper.formatDuration(_context.clock().now() - getDisconnectScheduledOn())).append(" ago"); if (getCloseSentOn() > 0) { buf.append(" close sent "); long timeSinceClose = _context.clock().now() - getCloseSentOn(); @@ -969,7 +971,7 @@ public class Connection { buf.append(" ago"); } if (getCloseReceivedOn() > 0) - buf.append(" close received"); + buf.append(" close received ").append(DataHelper.formatDuration(_context.clock().now() - getCloseReceivedOn())).append(" ago"); buf.append(" sent: ").append(1 + _lastSendId); if (_inputStream != null) buf.append(" rcvd: ").append(1 + _inputStream.getHighestBlockId() - missing); From cdab99bd25ebf5260d1da26b9d922bf584062873 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 9 Feb 2009 12:56:53 +0000 Subject: [PATCH 121/191] concurrentify _availableMessages --- .../src/net/i2p/client/I2PSessionImpl.java | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index b5f4e8ab3..00da88aa2 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -14,6 +14,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException; +import java.util.concurrent.ConcurrentHashMap; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -81,7 +82,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa /** class that generates new messages */ protected I2CPMessageProducer _producer; /** map of Long --> MessagePayloadMessage */ - private Map _availableMessages; + private Map _availableMessages; protected I2PClientMessageHandlerMap _handlerMap; @@ -139,7 +140,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa _closing = false; _producer = new I2CPMessageProducer(context); _availabilityNotifier = new AvailabilityNotifier(); - _availableMessages = new HashMap(); + _availableMessages = new ConcurrentHashMap(); try { readDestination(destKeyStream); } catch (DataFormatException dfe) { @@ -152,7 +153,6 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa loadConfig(options); _sessionId = null; _leaseSet = null; - _context.statManager().createRateStat("client.availableMessages", "How many messages are available for the current client", "ClientMessages", new long[] { 60*1000, 10*60*1000 }); } /** @@ -309,15 +309,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa * */ public byte[] receiveMessage(int msgId) throws I2PSessionException { - int remaining = 0; - MessagePayloadMessage msg = null; - synchronized (_availableMessages) { - msg = (MessagePayloadMessage) _availableMessages.remove(new Long(msgId)); - remaining = _availableMessages.size(); - } - _context.statManager().addRateData("client.availableMessages", remaining, 0); + MessagePayloadMessage msg = _availableMessages.remove(new Long(msgId)); if (msg == null) { - _log.error("Receive message " + msgId + " had no matches, remaining=" + remaining); + _log.error("Receive message " + msgId + " had no matches"); return null; } updateActivity(); @@ -357,12 +351,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa */ public void addNewMessage(MessagePayloadMessage msg) { Long mid = new Long(msg.getMessageId()); - int avail = 0; - synchronized (_availableMessages) { - _availableMessages.put(mid, msg); - avail = _availableMessages.size(); - } - _context.statManager().addRateData("client.availableMessages", avail, 0); + _availableMessages.put(mid, msg); long id = msg.getMessageId(); byte data[] = msg.getPayload().getUnencryptedData(); if ((data == null) || (data.length <= 0)) { @@ -382,16 +371,9 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa public VerifyUsage(Long id) { _msgId = id; } public void timeReached() { - MessagePayloadMessage removed = null; - int remaining = 0; - synchronized (_availableMessages) { - removed = (MessagePayloadMessage)_availableMessages.remove(_msgId); - remaining = _availableMessages.size(); - } - if (removed != null) { - _log.log(Log.CRIT, "Message NOT removed! id=" + _msgId + ": " + removed + ": remaining: " + remaining); - _context.statManager().addRateData("client.availableMessages", remaining, 0); - } + MessagePayloadMessage removed = _availableMessages.remove(_msgId); + if (removed != null && !isClosed()) + _log.log(Log.CRIT, "Message NOT removed! id=" + _msgId + ": " + removed); } } From f9d8a2d79b11a9f63b4de7761100d0b7cbeca93e Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 9 Feb 2009 14:34:23 +0000 Subject: [PATCH 122/191] allow smaller leasesets --- .../i2p/router/tunnel/pool/TunnelPool.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java index 0fca44cc4..06a9b4999 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPool.java @@ -468,7 +468,9 @@ public class TunnelPool { if (_tunnels.size() < wanted) { if (_log.shouldLog(Log.WARN)) _log.warn(toString() + ": Not enough tunnels (" + _tunnels.size() + ", wanted " + wanted + ")"); - return null; + // see comment below + if (_tunnels.size() <= 0) + return null; } long expireAfter = _context.clock().now(); // + _settings.getRebuildPeriod(); @@ -492,15 +494,26 @@ public class TunnelPool { leases.add(lease); } + // Go ahead and use less leases for now, hopefully a new tunnel will be built soon + // and we will get called again to generate a full leaseset. + // For clients with high tunnel count or length, + // this will make startup considerably faster, and reduce loss of leaseset + // when one tunnel is lost, thus making us much more robust. + // This also helps when returning to full lease count after reduce-on-idle + // or close-on-idle. + // So we will generate a succession of leases at startup. That's OK. + // Do we want a config option for this, or are there times when we shouldn't do this? if (leases.size() < wanted) { if (_log.shouldLog(Log.WARN)) _log.warn(toString() + ": Not enough leases (" + leases.size() + ", wanted " + wanted + ")"); - return null; + if (leases.size() <= 0) + return null; } LeaseSet ls = new LeaseSet(); Iterator iter = leases.iterator(); - for (int i = 0; i < wanted; i++) + int count = Math.min(leases.size(), wanted); + for (int i = 0; i < count; i++) ls.addLease((Lease) iter.next()); if (_log.shouldLog(Log.INFO)) _log.info(toString() + ": built new leaseSet: " + ls); From 39a1958bf411c80d35a6d1433e434d89757f30e3 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 9 Feb 2009 14:55:48 +0000 Subject: [PATCH 123/191] fix dest save broken in 0.7 --- apps/susidns/src/jsp/addressbook.jsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/susidns/src/jsp/addressbook.jsp b/apps/susidns/src/jsp/addressbook.jsp index 435ce2ec5..05a012936 100644 --- a/apps/susidns/src/jsp/addressbook.jsp +++ b/apps/susidns/src/jsp/addressbook.jsp @@ -160,7 +160,7 @@

    Add new destination:

    -Hostname: Destination:
    +Hostname: Destination:

    From 7756e20b860581681a817ccb7b4c9fc74d78c507 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 9 Feb 2009 16:52:54 +0000 Subject: [PATCH 124/191] enforce max leaseset publish frequency --- .../kademlia/KademliaNetworkDatabaseFacade.java | 6 +++++- .../networkdb/kademlia/RepublishLeaseSetJob.java | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java index 82e3a7f70..6ff82c6a4 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java @@ -456,6 +456,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } } + private static final long PUBLISH_DELAY = 3*1000; public void publish(LeaseSet localLeaseSet) { if (!_initialized) return; Hash h = localLeaseSet.getDestination().calculateHash(); @@ -476,7 +477,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _publishingLeaseSets.put(h, j); } } - j.getTiming().setStartAfter(_context.clock().now()); + // Don't spam the floodfills. In addition, always delay a few seconds since there may + // be another leaseset change coming along momentarily. + long nextTime = Math.max(j.lastPublished() + j.REPUBLISH_LEASESET_TIMEOUT, _context.clock().now() + PUBLISH_DELAY); + j.getTiming().setStartAfter(nextTime); _context.jobQueue().addJob(j); } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java index 47753998c..ac191a96c 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/RepublishLeaseSetJob.java @@ -23,15 +23,18 @@ import net.i2p.util.Log; public class RepublishLeaseSetJob extends JobImpl { private Log _log; private final static long REPUBLISH_LEASESET_DELAY = 5*60*1000; - private final static long REPUBLISH_LEASESET_TIMEOUT = 60*1000; + public final static long REPUBLISH_LEASESET_TIMEOUT = 60*1000; private Hash _dest; private KademliaNetworkDatabaseFacade _facade; + /** this is actually last attempted publish */ + private long _lastPublished; public RepublishLeaseSetJob(RouterContext ctx, KademliaNetworkDatabaseFacade facade, Hash destHash) { super(ctx); _log = ctx.logManager().getLog(RepublishLeaseSetJob.class); _facade = facade; _dest = destHash; + _lastPublished = 0; //getTiming().setStartAfter(ctx.clock().now()+REPUBLISH_LEASESET_DELAY); getContext().statManager().createRateStat("netDb.republishLeaseSetCount", "How often we republish a leaseSet?", "NetworkDatabase", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); } @@ -52,6 +55,7 @@ public class RepublishLeaseSetJob extends JobImpl { } else { getContext().statManager().addRateData("netDb.republishLeaseSetCount", 1, 0); _facade.sendStore(_dest, ls, new OnRepublishSuccess(getContext()), new OnRepublishFailure(getContext(), this), REPUBLISH_LEASESET_TIMEOUT, null); + _lastPublished = getContext().clock().now(); //getContext().jobQueue().addJob(new StoreJob(getContext(), _facade, _dest, ls, new OnSuccess(getContext()), new OnFailure(getContext()), REPUBLISH_LEASESET_TIMEOUT)); } } else { @@ -82,6 +86,10 @@ public class RepublishLeaseSetJob extends JobImpl { requeue(getContext().random().nextInt(60*1000)); } + public long lastPublished() { + return _lastPublished; + } + class OnRepublishSuccess extends JobImpl { public OnRepublishSuccess(RouterContext ctx) { super(ctx); } public String getName() { return "Publish leaseSet successful"; } From 8591dfe71cf9128d5a8e6405008052596f1c58ae Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 10 Feb 2009 02:34:48 +0000 Subject: [PATCH 125/191] i2psnark tmp files take 3 --- .../java/src/org/klomp/snark/I2PSnarkUtil.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 26ed5860f..b7e623060 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -23,6 +23,7 @@ import net.i2p.data.DataFormatException; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.util.EepGet; +import net.i2p.util.FileUtil; import net.i2p.util.Log; import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; @@ -49,6 +50,7 @@ public class I2PSnarkUtil { private int _maxUploaders; private int _maxUpBW; private int _maxConnections; + private File _tmpDir; public static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers"; public static final boolean DEFAULT_USE_OPENTRACKERS = true; @@ -68,6 +70,12 @@ public class I2PSnarkUtil { _maxUploaders = Snark.MAX_TOTAL_UPLOADERS; _maxUpBW = DEFAULT_MAX_UP_BW; _maxConnections = MAX_CONNECTIONS; + // This is used for both announce replies and .torrent file downloads, + // so it must be available even if not connected to I2CP. + // so much for multiple instances + _tmpDir = new File("tmp", "i2psnark"); + FileUtil.rmdir(_tmpDir, false); + _tmpDir.mkdirs(); } /** @@ -95,6 +103,7 @@ public class I2PSnarkUtil { _i2cpHost = i2cpHost; if (i2cpPort > 0) _i2cpPort = i2cpPort; + // can't remove any options this way... if (opts != null) _opts.putAll(opts); _configured = true; @@ -167,6 +176,10 @@ public class I2PSnarkUtil { _manager = null; _shitlist.clear(); 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... + _tmpDir.mkdirs(); } /** connect to the given destination */ @@ -205,7 +218,8 @@ public class I2PSnarkUtil { _log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy); File out = null; try { - out = File.createTempFile("i2psnark", "url", new File(".")); + // we could use the system tmp dir but deleteOnExit() doesn't seem to work on all platforms... + out = File.createTempFile("i2psnark", null, _tmpDir); } catch (IOException ioe) { ioe.printStackTrace(); if (out != null) From 806e2f88c822a1a0205e5e27f4fb42616fbc400e Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 12 Feb 2009 16:50:20 +0000 Subject: [PATCH 126/191] Dont buffer all the POST data so we wont OOM on huge POSTs. Use unbuffered read for the first line, and for all the headers if POST --- .../i2p/i2ptunnel/I2PTunnelHTTPClient.java | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index 4b5379ac1..b42376dbf 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -5,6 +5,7 @@ package net.i2p.i2ptunnel; import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; @@ -21,6 +22,7 @@ import net.i2p.I2PException; import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocketOptions; import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.util.EventDispatcher; import net.i2p.util.FileUtil; @@ -131,16 +133,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable "Your browser is misconfigured. Do not use the proxy to access the router console or other localhost destinations.
    ") .getBytes(); - private final static int MAX_POSTBYTES = 20*1024*1024; // arbitrary but huge - all in memory, no temp file - private final static byte[] ERR_MAXPOST = - ("HTTP/1.1 503 Bad POST\r\n"+ - "Content-Type: text/html; charset=iso-8859-1\r\n"+ - "Cache-control: no-cache\r\n"+ - "\r\n"+ - "

    I2P ERROR: REQUEST DENIED

    "+ - "The maximum POST size is " + MAX_POSTBYTES + " bytes.
    ") - .getBytes(); - /** used to assign unique IDs to the threads / clients. no logic or functionality */ private static volatile long __clientId = 0; @@ -232,6 +224,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable private static long __requestId = 0; protected void clientConnectionRun(Socket s) { + InputStream in = null; OutputStream out = null; String targetRequest = null; boolean usingWWWProxy = false; @@ -239,11 +232,12 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable long requestId = ++__requestId; try { out = s.getOutputStream(); - BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream(), "ISO-8859-1")); + InputReader reader = new InputReader(s.getInputStream()); String line, method = null, protocol = null, host = null, destination = null; StringBuffer newRequest = new StringBuffer(); int ahelper = 0; - while ((line = br.readLine()) != null) { + while ((line = reader.readLine(method)) != null) { + line = line.trim(); if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix(requestId) + "Line=[" + line + "]"); @@ -257,7 +251,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix(requestId) + "Method is null for [" + line + "]"); - line = line.trim(); int pos = line.indexOf(" "); if (pos == -1) break; method = line.substring(0, pos); @@ -514,30 +507,11 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable newRequest.append("Connection: close\r\n\r\n"); break; } else { - newRequest.append(line.trim()).append("\r\n"); // HTTP spec + newRequest.append(line).append("\r\n"); // HTTP spec } } if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix(requestId) + "NewRequest header: [" + newRequest.toString() + "]"); - - int postbytes = 0; - while (br.ready()) { // empty the buffer (POST requests) - int i = br.read(); - if (i != -1) { - newRequest.append((char) i); - if (++postbytes > MAX_POSTBYTES) { - if (out != null) { - out.write(ERR_MAXPOST); - out.write("

    Generated on: ".getBytes()); - out.write(new Date().toString().getBytes()); - out.write("\n".getBytes()); - out.flush(); - } - s.close(); - return; - } - } - } if (method == null || destination == null) { l.log("No HTTP method found in the request."); @@ -610,8 +584,8 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable l.log(ex.getMessage()); handleHTTPClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); closeSocket(s); - } catch (OutOfMemoryError oom) { // mainly for huge POSTs - IOException ex = new IOException("OOM (in POST?)"); + } catch (OutOfMemoryError oom) { + IOException ex = new IOException("OOM"); _log.info("getPrefix(requestId) + Error trying to connect", ex); l.log(ex.getMessage()); handleHTTPClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); @@ -619,6 +593,29 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable } } + /** + * Read the first line unbuffered. + * After that, switch to a BufferedReader, unless the method is "POST". + * We can't use BufferedReader for POST because we can't have readahead, + * since we are passing the stream on to I2PTunnelRunner for the POST data. + * + */ + private static class InputReader { + BufferedReader _br; + InputStream _s; + public InputReader(InputStream s) { + _br = null; + _s = s; + } + String readLine(String method) throws IOException { + if (method == null || "POST".equals(method)) + return DataHelper.readLine(_s); + if (_br == null) + _br = new BufferedReader(new InputStreamReader(_s, "ISO-8859-1")); + return _br.readLine(); + } + } + private final static String getHostName(String host) { if (host == null) return null; try { @@ -630,7 +627,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable } } - private class OnTimeout implements Runnable { + private static class OnTimeout implements Runnable { private Socket _socket; private OutputStream _out; private String _target; @@ -708,11 +705,12 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable } } - private void handleHTTPClientException(Exception ex, OutputStream out, String targetRequest, + private static void handleHTTPClientException(Exception ex, OutputStream out, String targetRequest, boolean usingWWWProxy, String wwwProxy, long requestId) { - if (_log.shouldLog(Log.WARN)) - _log.warn(getPrefix(requestId) + "Error sending to " + wwwProxy + " (proxy? " + usingWWWProxy + ", request: " + targetRequest, ex); + // static + //if (_log.shouldLog(Log.WARN)) + // _log.warn(getPrefix(requestId) + "Error sending to " + wwwProxy + " (proxy? " + usingWWWProxy + ", request: " + targetRequest, ex); if (out != null) { try { String str; @@ -727,16 +725,18 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable header = ERR_DESTINATION_UNKNOWN; writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy, false); } catch (IOException ioe) { - _log.warn(getPrefix(requestId) + "Error writing out the 'destination was unknown' " + "message", ioe); + // static + //_log.warn(getPrefix(requestId) + "Error writing out the 'destination was unknown' " + "message", ioe); } } else { - _log.warn(getPrefix(requestId) + "Client disconnected before we could say that destination " + "was unknown", ex); + // static + //_log.warn(getPrefix(requestId) + "Client disconnected before we could say that destination " + "was unknown", ex); } } private final static String SUPPORTED_HOSTS[] = { "i2p", "www.i2p.com", "i2p."}; - private boolean isSupportedAddress(String host, String protocol) { + private static boolean isSupportedAddress(String host, String protocol) { if ((host == null) || (protocol == null)) return false; boolean found = false; String lcHost = host.toLowerCase(); From 7b12f700dd8c45c8bc24be4321e9a4b1bca590f9 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 12 Feb 2009 17:10:47 +0000 Subject: [PATCH 127/191] plug a tunnel build leak --- .../java/src/net/i2p/router/tunnel/pool/BuildHandler.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java index f6b4d3478..0699d3ff4 100644 --- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java @@ -249,6 +249,8 @@ class BuildHandler { int record = order.indexOf(Integer.valueOf(i)); if (record < 0) { _log.error("Bad status index " + i); + // don't leak + _exec.buildComplete(cfg, cfg.getTunnelPool()); return; } int howBad = statuses[record]; @@ -294,9 +296,9 @@ class BuildHandler { _context.messageHistory().tunnelParticipantRejected(peer, "peer rejected after " + rtt + " with " + howBad + ": " + cfg.toString()); } } + _exec.buildComplete(cfg, cfg.getTunnelPool()); if (allAgree) { // wikked, completely build - _exec.buildComplete(cfg, cfg.getTunnelPool()); if (cfg.isInbound()) _context.tunnelDispatcher().joinInbound(cfg); else @@ -313,7 +315,6 @@ class BuildHandler { _context.statManager().addRateData("tunnel.buildClientSuccess", rtt, rtt); } else { // someone is no fun - _exec.buildComplete(cfg, cfg.getTunnelPool()); if (cfg.getDestination() == null) _context.statManager().addRateData("tunnel.buildExploratoryReject", rtt, rtt); else @@ -322,6 +323,8 @@ class BuildHandler { } else { if (_log.shouldLog(Log.WARN)) _log.warn(msg.getUniqueId() + ": Tunnel reply could not be decrypted for tunnel " + cfg); + // don't leak + _exec.buildComplete(cfg, cfg.getTunnelPool()); } } From 6b0a2464dd4245f1fdfccfe4dfe2095b1a473aa8 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 14 Feb 2009 19:49:00 +0000 Subject: [PATCH 128/191] Add licenses to all packages --- LICENSE.txt | 179 ++++++++++ build.xml | 17 +- licenses/LICENSE-Addressbook.txt | 20 ++ licenses/LICENSE-Apache1.1.txt | 60 ++++ licenses/LICENSE-Apache2.0.txt | 202 +++++++++++ licenses/LICENSE-BSD.txt | 27 ++ licenses/LICENSE-ElGamalDSA.txt | 28 ++ licenses/LICENSE-GPLv2.txt | 340 +++++++++++++++++++ licenses/LICENSE-HashCash.txt | 2 + licenses/LICENSE-I2PTunnel.txt | 29 ++ licenses/LICENSE-LGPLv2.1.txt | 504 ++++++++++++++++++++++++++++ licenses/LICENSE-Ministreaming.txt | 10 + licenses/LICENSE-SHA256.txt | 26 ++ licenses/LICENSE-SNTP.txt | 29 ++ licenses/LICENSE-Wrapper.txt | 41 +++ licenses/NOTICE-Ant.txt | 15 + licenses/NOTICE-Commons-Logging.txt | 3 + 17 files changed, 1525 insertions(+), 7 deletions(-) create mode 100644 LICENSE.txt create mode 100644 licenses/LICENSE-Addressbook.txt create mode 100644 licenses/LICENSE-Apache1.1.txt create mode 100644 licenses/LICENSE-Apache2.0.txt create mode 100644 licenses/LICENSE-BSD.txt create mode 100644 licenses/LICENSE-ElGamalDSA.txt create mode 100644 licenses/LICENSE-GPLv2.txt create mode 100644 licenses/LICENSE-HashCash.txt create mode 100644 licenses/LICENSE-I2PTunnel.txt create mode 100644 licenses/LICENSE-LGPLv2.1.txt create mode 100644 licenses/LICENSE-Ministreaming.txt create mode 100644 licenses/LICENSE-SHA256.txt create mode 100644 licenses/LICENSE-SNTP.txt create mode 100644 licenses/LICENSE-Wrapper.txt create mode 100644 licenses/NOTICE-Ant.txt create mode 100644 licenses/NOTICE-Commons-Logging.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..324f532c6 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,179 @@ +This product includes both public domain code and licensed code as described below. +For all code, unless otherwise stated in the appropriate license, the following applies: + + + NO WARRANTY + + BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + + +LICENSES +-------- + +Core: +Public domain except as listed below: + + ElGamal and DSA code: + Copyright (c) 2003, TheCrypto + See licenses/LICENSE-ElGamalDSA.txt + + SHA256 and HMAC-SHA256: + Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle + See licenses/LICENSE-SHA256.txt + + AES code: + Under the Cryptix (MIT) license, written by the Cryptix team + (That's what our website says but all our AES code looks like it is public domain) + + Crypto filters: + From the xlattice app - http://xlattice.sourceforge.net/ + See licenses/LICENSE-BSD.txt + + SNTP code: + Copyright (c) 2004, Adam Buckley + See licenses/LICENSE-SNTP.txt + + PRNG: + Copyright (C) 2001, 2002, Free Software Foundation, Inc. + See licenses/LICENSE-LGPLv2.1.txt + + GMP 4.1.3: + Copyright 1991, 1996, 1999, 2000 Free Software Foundation, Inc. + See licenses/LICENSE-LGPLv2.1.txt + + HashCash code: + Copyright 2006 Gregory Rubin grrubin@gmail.com + See licenses/LICENSE-HashCash.txt + + + +Router: +Public domain + + + +Installer: + Launch4j: + Copyright (C) 2005 Grzegorz Kowal + See licenses/LICENSE-GPLv2.txt + + Izpack: + See licenses/LICENSE-Apache1.1.txt + + + +Wrapper: +Copyright (c) 1999, 2004 Tanuki Software +See licenses/LICENSE-Wrapper.txt + + + +Applications: + + Addressbook: + Copyright (c) 2004 Ragnarok + See licenses/LICENSE-Addressbook.txt + + BOB: + Copyright (C) sponge + DWTFYWTPL + + I2PSnark: + Copyright (C) 2003 Mark J. Wielaard + See licenses/LICENSE-GPLv2.txt + + I2PTunnel: + (c) 2003 - 2004 mihi + GPLv2 with exception. + See licenses/LICENSE-I2PTunnel.txt + See licenses/LICENSE-GPLv2.txt + + I2PTunnel SOCKS Proxy: + Copyright (c) 2004 by human + GPLv2 with exception. + See licenses/LICENSE-I2PTunnel.txt + See licenses/LICENSE-GPLv2.txt + + Jetty 5.1.12: + Copyright 2000-2004 Mort Bay Consulting Pty. Ltd. + See licenses/LICENSE-Apache1.1.txt + See licenses/LICENSE-Apache2.0.txt + See licenses/NOTICE-Ant.txt + See licenses/NOTICE-Commons-Logging.txt + + JRobin 1.4.0: + See licenses/LICENSE-LGPLv2.1.txt + + Ministreaming Lib: + By mihi. + See licenses/LICENSE-BSD.txt + + Proxyscript: + By Cervantes. + Public domain. + + Router console: + Public domain. + + SAM: + Public domain. + + Streaming Lib: + Public domain. + + SusiDNS: + Copyright (C) 2005 + See licenses/LICENSE-GPLv2.txt + + SusiMail: + Copyright (C) 2004-2005 + See licenses/LICENSE-GPLv2.txt + + Systray: + Public domain. + Bundles systray4j code: + See licenses/LICENSE-GPLv2.txt + + + +Other Applications and Libraries +-------------------------------- +The following applications and libraries are not used or bundled in +binary packages, therefore the licenses are not included in binary +distributions. See the source package for the additional license information. + + Atalk: + Public domain + + SAM C Library: + Copyright (c) 2004, Matthew P. Cashdollar + See apps/sam/c/doc/license.txt + + SAM C# Library: + Public domain. + See apps/sam/csharp/README + + SAM Perl Library: + See licenses/LICENSE-GPLv2.txt + + SAM Python Library: + Public domain. diff --git a/build.xml b/build.xml index 24e18ffb1..a9f58ef20 100644 --- a/build.xml +++ b/build.xml @@ -303,13 +303,11 @@ - + + + + + @@ -345,6 +343,11 @@ + + + + + diff --git a/licenses/LICENSE-Addressbook.txt b/licenses/LICENSE-Addressbook.txt new file mode 100644 index 000000000..5b44d6a6b --- /dev/null +++ b/licenses/LICENSE-Addressbook.txt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2004 Ragnarok + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ diff --git a/licenses/LICENSE-Apache1.1.txt b/licenses/LICENSE-Apache1.1.txt new file mode 100644 index 000000000..cea737d38 --- /dev/null +++ b/licenses/LICENSE-Apache1.1.txt @@ -0,0 +1,60 @@ +/* + * $Header: /home/cvs/jakarta-commons/el/LICENSE.txt,v 1.1.1.1 2003/02/04 00:22:24 luehe Exp $ + * $Revision: 1.1.1.1 $ + * $Date: 2003/02/04 00:22:24 $ + * + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 1999-2002 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Commons", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ diff --git a/licenses/LICENSE-Apache2.0.txt b/licenses/LICENSE-Apache2.0.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/licenses/LICENSE-Apache2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/licenses/LICENSE-BSD.txt b/licenses/LICENSE-BSD.txt new file mode 100644 index 000000000..59a9311c9 --- /dev/null +++ b/licenses/LICENSE-BSD.txt @@ -0,0 +1,27 @@ +Copyright (c) 2009, The I2P Project +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of the I2P nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/LICENSE-ElGamalDSA.txt b/licenses/LICENSE-ElGamalDSA.txt new file mode 100644 index 000000000..6bf735772 --- /dev/null +++ b/licenses/LICENSE-ElGamalDSA.txt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2003, TheCrypto + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the TheCrypto may be used to endorse or promote + * products derived from this software without specific prior written + * permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ diff --git a/licenses/LICENSE-GPLv2.txt b/licenses/LICENSE-GPLv2.txt new file mode 100644 index 000000000..14db8fc79 --- /dev/null +++ b/licenses/LICENSE-GPLv2.txt @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/licenses/LICENSE-HashCash.txt b/licenses/LICENSE-HashCash.txt new file mode 100644 index 000000000..7aa3f3f71 --- /dev/null +++ b/licenses/LICENSE-HashCash.txt @@ -0,0 +1,2 @@ + Copyright 2006 Gregory Rubin grrubin@gmail.com + Permission is given to use, modify, and or distribute this code so long as this message remains attached diff --git a/licenses/LICENSE-I2PTunnel.txt b/licenses/LICENSE-I2PTunnel.txt new file mode 100644 index 000000000..52af2190e --- /dev/null +++ b/licenses/LICENSE-I2PTunnel.txt @@ -0,0 +1,29 @@ +/* + * I2PTunnel + * (c) 2003 - 2004 mihi + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + * + * In addition, as a special exception, mihi gives permission to link + * the code of this program with the proprietary Java implementation + * provided by Sun (or other vendors as well), and distribute linked + * combinations including the two. You must obey the GNU General + * Public License in all respects for all of the code used other than + * the proprietary Java implementation. If you modify this file, you + * may extend this exception to your version of the file, but you are + * not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ diff --git a/licenses/LICENSE-LGPLv2.1.txt b/licenses/LICENSE-LGPLv2.1.txt new file mode 100644 index 000000000..5ab7695ab --- /dev/null +++ b/licenses/LICENSE-LGPLv2.1.txt @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/licenses/LICENSE-Ministreaming.txt b/licenses/LICENSE-Ministreaming.txt new file mode 100644 index 000000000..50a6a709a --- /dev/null +++ b/licenses/LICENSE-Ministreaming.txt @@ -0,0 +1,10 @@ +$Id$ + +the i2p/apps/ministreaming module is the root of the +ministreaming library, and everything within it +is released according to the terms of the I2P +license policy. That means everything contained +within the i2p/apps/ministreaming module is released +under a BSD license unless otherwise marked. +Alternate licenses that may be used include Cryptix, +MIT, as well as code granted into the public domain. diff --git a/licenses/LICENSE-SHA256.txt b/licenses/LICENSE-SHA256.txt new file mode 100644 index 000000000..1eb884a89 --- /dev/null +++ b/licenses/LICENSE-SHA256.txt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2000 - 2004 The Legion Of The Bouncy Castle + * (http://www.bouncycastle.org) + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software + * without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + */ diff --git a/licenses/LICENSE-SNTP.txt b/licenses/LICENSE-SNTP.txt new file mode 100644 index 000000000..26c104971 --- /dev/null +++ b/licenses/LICENSE-SNTP.txt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2004, Adam Buckley + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of Adam Buckley nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ diff --git a/licenses/LICENSE-Wrapper.txt b/licenses/LICENSE-Wrapper.txt new file mode 100644 index 000000000..039b8927d --- /dev/null +++ b/licenses/LICENSE-Wrapper.txt @@ -0,0 +1,41 @@ +Copyright (c) 1999, 2004 Tanuki Software + +Permission is hereby granted, free of charge, to any person +obtaining a copy of the Java Service Wrapper and associated +documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sub-license, +and/or sell copies of the Software, and to permit persons to +whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + +Portions of the Software have been derived from source code +developed by Silver Egg Technology under the following license: + +Copyright (c) 2001 Silver Egg Technology + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sub-license, and/or +sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + diff --git a/licenses/NOTICE-Ant.txt b/licenses/NOTICE-Ant.txt new file mode 100644 index 000000000..1fb6dde47 --- /dev/null +++ b/licenses/NOTICE-Ant.txt @@ -0,0 +1,15 @@ + ========================================================================= + == NOTICE file corresponding to the section 4 d of == + == the Apache License, Version 2.0, == + == in this case for the Apache Ant distribution. == + ========================================================================= + + This product includes software developed by + The Apache Software Foundation (http://www.apache.org/). + + This product includes also software developed by : + - the W3C consortium (http://www.w3c.org) , + - the SAX project (http://www.saxproject.org) + + Please read the different LICENSE files present in the root directory of + this distribution. diff --git a/licenses/NOTICE-Commons-Logging.txt b/licenses/NOTICE-Commons-Logging.txt new file mode 100644 index 000000000..439eb83b2 --- /dev/null +++ b/licenses/NOTICE-Commons-Logging.txt @@ -0,0 +1,3 @@ +This product includes software developed by +The Apache Software Foundation (http://www.apache.org/). + From cc3165bf729e0fe7954fe70a7fb1f8170e195308 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 15 Feb 2009 05:11:35 +0000 Subject: [PATCH 129/191] * Streaming lib: - Move ConEvent from SimpleTimer to SimpleScheduler - Move RetransmissionTimer (ResendPacketEvent) from SimpleTimer to new SimpleTimer2 - Move ActivityTimer and Flusher from SimpleTimer to RetransmissionTimer - SimpleTimer2 allows specifying "fuzz" to reduce timer queue churn further --- apps/BOB/src/net/i2p/BOB/BOB.java | 3 +- apps/BOB/src/net/i2p/BOB/Main.java | 5 +- .../net/i2p/client/streaming/Connection.java | 38 ++- .../client/streaming/MessageOutputStream.java | 13 +- .../net/i2p/client/streaming/PacketLocal.java | 16 +- .../client/streaming/RetransmissionTimer.java | 6 +- .../i2p/client/streaming/SchedulerImpl.java | 4 +- .../net/i2p/client/streaming/TCBShare.java | 14 +- .../src/net/i2p/util/SimpleScheduler.java | 4 +- core/java/src/net/i2p/util/SimpleTimer2.java | 252 ++++++++++++++++++ .../src/net/i2p/router/tunnel/FlushTimer.java | 11 +- 11 files changed, 322 insertions(+), 44 deletions(-) create mode 100644 core/java/src/net/i2p/util/SimpleTimer2.java diff --git a/apps/BOB/src/net/i2p/BOB/BOB.java b/apps/BOB/src/net/i2p/BOB/BOB.java index cd4ccfdcb..cc1428f8c 100644 --- a/apps/BOB/src/net/i2p/BOB/BOB.java +++ b/apps/BOB/src/net/i2p/BOB/BOB.java @@ -34,7 +34,6 @@ import java.util.Properties; import net.i2p.client.I2PClient; import net.i2p.client.streaming.RetransmissionTimer; import net.i2p.util.Log; -import net.i2p.util.SimpleTimer; /** * * ################################################################################
    @@ -151,7 +150,7 @@ public class BOB { String configLocation = System.getProperty(PROP_CONFIG_LOCATION, "bob.config"); // This is here just to ensure there is no interference with our threadgroups. - SimpleTimer Y = RetransmissionTimer.getInstance(); + RetransmissionTimer Y = RetransmissionTimer.getInstance(); i = Y.hashCode(); { try { diff --git a/apps/BOB/src/net/i2p/BOB/Main.java b/apps/BOB/src/net/i2p/BOB/Main.java index 26823ff39..2d81fb30e 100644 --- a/apps/BOB/src/net/i2p/BOB/Main.java +++ b/apps/BOB/src/net/i2p/BOB/Main.java @@ -24,7 +24,6 @@ package net.i2p.BOB; import net.i2p.client.streaming.RetransmissionTimer; -import net.i2p.util.SimpleTimer; /** * Start from command line @@ -39,8 +38,8 @@ public class Main { */ public static void main(String[] args) { // THINK THINK THINK THINK THINK THINK - SimpleTimer Y = RetransmissionTimer.getInstance(); + RetransmissionTimer Y = RetransmissionTimer.getInstance(); BOB.main(args); - Y.removeSimpleTimer(); + Y.stop(); } } diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java index a59e12610..503760ed1 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java @@ -14,6 +14,7 @@ import net.i2p.data.Destination; import net.i2p.util.Log; import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; +import net.i2p.util.SimpleTimer2; /** * Maintain the state controlling a streaming connection between two @@ -69,6 +70,7 @@ public class Connection { /** how many messages have been resent and not yet ACKed? */ private int _activeResends; private ConEvent _connectionEvent; + private int _randomWait; private long _lifetimeBytesSent; private long _lifetimeBytesReceived; @@ -124,6 +126,7 @@ public class Connection { _isInbound = false; _updatedShareOpts = false; _connectionEvent = new ConEvent(); + _randomWait = _context.random().nextInt(10*1000); // just do this once to reduce usage _context.statManager().createRateStat("stream.con.windowSizeAtCongestion", "How large was our send window when we send a dup?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); _context.statManager().createRateStat("stream.chokeSizeBegin", "How many messages were outstanding when we started to choke?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); _context.statManager().createRateStat("stream.chokeSizeEnd", "How many messages were outstanding when we stopped being choked?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); @@ -325,7 +328,8 @@ public class Connection { if (_log.shouldLog(Log.DEBUG)) _log.debug("Resend in " + timeout + " for " + packet, new Exception("Sent by")); - RetransmissionTimer.getInstance().addEvent(new ResendPacketEvent(packet, timeout + _context.clock().now()), timeout); + // schedules itself + ResendPacketEvent rpe = new ResendPacketEvent(packet, timeout); } _context.statManager().getStatLog().addData(Packet.toId(_sendStreamId), "stream.rtt", _options.getRTT(), _options.getWindowSize()); @@ -526,7 +530,7 @@ public class Connection { if (_receiver != null) _receiver.destroy(); if (_activityTimer != null) - SimpleTimer.getInstance().removeEvent(_activityTimer); + _activityTimer.cancel(); //_activityTimer = null; if (_inputStream != null) _inputStream.streamErrorOccurred(new IOException("disconnected!")); @@ -822,15 +826,18 @@ public class Connection { return; } long howLong = _options.getInactivityTimeout(); - howLong += _context.random().nextInt(30*1000); // randomize it a bit, so both sides don't do it at once + howLong += _randomWait; // randomize it a bit, so both sides don't do it at once if (_log.shouldLog(Log.DEBUG)) _log.debug("Resetting the inactivity timer to " + howLong, new Exception(toString())); // this will get rescheduled, and rescheduled, and rescheduled... - RetransmissionTimer.getInstance().removeEvent(_activityTimer); - RetransmissionTimer.getInstance().addEvent(_activityTimer, howLong); + _activityTimer.reschedule(howLong, false); // use the later of current and previous timeout } - private class ActivityTimer implements SimpleTimer.TimedEvent { + private class ActivityTimer extends SimpleTimer2.TimedEvent { + public ActivityTimer() { + super(RetransmissionTimer.getInstance()); + setFuzz(5*1000); // sloppy timer, don't reschedule unless at least 5s later + } public void timeReached() { // uh, nothing more to do... if (!_connected) { @@ -841,7 +848,7 @@ public class Connection { long left = getTimeLeft(); if (left > 0) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Inactivity timeout reached, but there is time left (" + left + ")"); - RetransmissionTimer.getInstance().addEvent(ActivityTimer.this, left); + schedule(left); return; } // these are either going to time out or cause further rescheduling @@ -1010,14 +1017,17 @@ public class Connection { /** * Coordinate the resends of a given packet + * */ - public class ResendPacketEvent implements SimpleTimer.TimedEvent { + public class ResendPacketEvent extends SimpleTimer2.TimedEvent { private PacketLocal _packet; private long _nextSendTime; - public ResendPacketEvent(PacketLocal packet, long sendTime) { + public ResendPacketEvent(PacketLocal packet, long delay) { + super(RetransmissionTimer.getInstance()); _packet = packet; - _nextSendTime = sendTime; + _nextSendTime = delay + _context.clock().now(); packet.setResendPacketEvent(ResendPacketEvent.this); + schedule(delay); } public long getNextSendTime() { return _nextSendTime; } @@ -1025,6 +1035,10 @@ public class Connection { /** * Retransmit the packet if we need to. * + * ackImmediately() above calls directly in here, so + * we have to use forceReschedule() instead of schedule() below, + * to prevent duplicates in the timer queue. + * * @param penalize true if this retransmission is caused by a timeout, false if we * are just sending this packet instead of an ACK * @return true if the packet was sent, false if it was not @@ -1057,7 +1071,7 @@ public class Connection { if (_log.shouldLog(Log.INFO)) _log.info("Delaying resend of " + _packet + " as there are " + _activeResends + " active resends already in play"); - RetransmissionTimer.getInstance().addEvent(ResendPacketEvent.this, 1000); + forceReschedule(1000); _nextSendTime = 1000 + _context.clock().now(); return false; } @@ -1144,7 +1158,7 @@ public class Connection { if (_log.shouldLog(Log.DEBUG)) _log.debug("Scheduling resend in " + timeout + "ms for " + _packet); - RetransmissionTimer.getInstance().addEvent(ResendPacketEvent.this, timeout); + forceReschedule(timeout); } // acked during resending (... or somethin') diff --git a/apps/streaming/java/src/net/i2p/client/streaming/MessageOutputStream.java b/apps/streaming/java/src/net/i2p/client/streaming/MessageOutputStream.java index 4a19d5e09..63d1caee7 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/MessageOutputStream.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/MessageOutputStream.java @@ -8,7 +8,7 @@ import net.i2p.I2PAppContext; import net.i2p.data.ByteArray; import net.i2p.util.ByteCache; import net.i2p.util.Log; -import net.i2p.util.SimpleTimer; +import net.i2p.util.SimpleTimer2; /** * A stream that we can shove data into that fires off those bytes @@ -200,13 +200,20 @@ public class MessageOutputStream extends OutputStream { * Flush data that has been enqued but not flushed after a certain * period of inactivity */ - private class Flusher implements SimpleTimer.TimedEvent { + private class Flusher extends SimpleTimer2.TimedEvent { private boolean _enqueued; + public Flusher() { + super(RetransmissionTimer.getInstance()); + } public void enqueue() { // no need to be overly worried about duplicates - it would just // push it further out if (!_enqueued) { - RetransmissionTimer.getInstance().addEvent(_flusher, _passiveFlushDelay); + // Maybe we could just use schedule() here - or even SimpleScheduler - not sure... + // To be safe, use forceReschedule() so we don't get lots of duplicates + // We've seen the queue blow up before, maybe it was this before the rewrite... + // So perhaps it IS wise to be "overly worried" ... + forceReschedule(_passiveFlushDelay); if (_log.shouldLog(Log.DEBUG)) _log.debug("Enqueueing the flusher for " + _passiveFlushDelay + "ms out"); } else { diff --git a/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java b/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java index f0288df5f..cbb89e79e 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/PacketLocal.java @@ -6,7 +6,7 @@ import net.i2p.I2PAppContext; import net.i2p.data.Destination; import net.i2p.data.SessionKey; import net.i2p.util.Log; -import net.i2p.util.SimpleTimer; +import net.i2p.util.SimpleTimer2; /** * coordinate local attributes about a packet - send time, ack time, number of @@ -27,7 +27,7 @@ public class PacketLocal extends Packet implements MessageOutputStream.WriteStat private long _cancelledOn; private volatile int _nackCount; private volatile boolean _retransmitted; - private SimpleTimer.TimedEvent _resendEvent; + private SimpleTimer2.TimedEvent _resendEvent; public PacketLocal(I2PAppContext ctx, Destination to) { this(ctx, to, null); @@ -93,7 +93,7 @@ public class PacketLocal extends Packet implements MessageOutputStream.WriteStat releasePayload(); notifyAll(); } - SimpleTimer.getInstance().removeEvent(_resendEvent); + _resendEvent.cancel(); } public void cancelled() { synchronized (this) { @@ -101,11 +101,11 @@ public class PacketLocal extends Packet implements MessageOutputStream.WriteStat releasePayload(); notifyAll(); } - SimpleTimer.getInstance().removeEvent(_resendEvent); + _resendEvent.cancel(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Cancelled! " + toString(), new Exception("cancelled")); } - public SimpleTimer.TimedEvent getResendEvent() { return _resendEvent; } + public SimpleTimer2.TimedEvent getResendEvent() { return _resendEvent; } /** how long after packet creation was it acked? * @return how long after packet creation the packet was ACKed in ms @@ -122,15 +122,15 @@ public class PacketLocal extends Packet implements MessageOutputStream.WriteStat public void incrementNACKs() { int cnt = ++_nackCount; - SimpleTimer.TimedEvent evt = _resendEvent; + SimpleTimer2.TimedEvent evt = _resendEvent; if ( (cnt >= Connection.FAST_RETRANSMIT_THRESHOLD) && (evt != null) && (!_retransmitted)) { _retransmitted = true; - RetransmissionTimer.getInstance().addEvent(evt, 0); + evt.reschedule(0); } } public int getNACKs() { return _nackCount; } - public void setResendPacketEvent(SimpleTimer.TimedEvent evt) { _resendEvent = evt; } + public void setResendPacketEvent(SimpleTimer2.TimedEvent evt) { _resendEvent = evt; } @Override public StringBuffer formatAsString() { diff --git a/apps/streaming/java/src/net/i2p/client/streaming/RetransmissionTimer.java b/apps/streaming/java/src/net/i2p/client/streaming/RetransmissionTimer.java index c52c373b1..92c4cf1c2 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/RetransmissionTimer.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/RetransmissionTimer.java @@ -1,12 +1,12 @@ package net.i2p.client.streaming; -import net.i2p.util.SimpleTimer; +import net.i2p.util.SimpleTimer2; /** * */ -public class RetransmissionTimer extends SimpleTimer { +public class RetransmissionTimer extends SimpleTimer2 { private static final RetransmissionTimer _instance = new RetransmissionTimer(); - public static final SimpleTimer getInstance() { return _instance; } + public static final RetransmissionTimer getInstance() { return _instance; } protected RetransmissionTimer() { super("StreamingTimer"); } } diff --git a/apps/streaming/java/src/net/i2p/client/streaming/SchedulerImpl.java b/apps/streaming/java/src/net/i2p/client/streaming/SchedulerImpl.java index 3d29880f0..e02a1b413 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/SchedulerImpl.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/SchedulerImpl.java @@ -2,7 +2,7 @@ package net.i2p.client.streaming; import net.i2p.I2PAppContext; import net.i2p.util.Log; -import net.i2p.util.SimpleTimer; +import net.i2p.util.SimpleScheduler; /** * Base scheduler @@ -17,6 +17,6 @@ abstract class SchedulerImpl implements TaskScheduler { } protected void reschedule(long msToWait, Connection con) { - SimpleTimer.getInstance().addEvent(con.getConnectionEvent(), msToWait); + SimpleScheduler.getInstance().addEvent(con.getConnectionEvent(), msToWait); } } diff --git a/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java b/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java index 1562f948e..7c8df3e3e 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/TCBShare.java @@ -7,7 +7,7 @@ import java.util.concurrent.ConcurrentHashMap; import net.i2p.I2PAppContext; import net.i2p.data.Destination; import net.i2p.util.Log; -import net.i2p.util.SimpleTimer; +import net.i2p.util.SimpleTimer2; /** * Share important TCP Control Block parameters across Connections @@ -38,11 +38,11 @@ public class TCBShare { _log = ctx.logManager().getLog(TCBShare.class); _cache = new ConcurrentHashMap(4); _cleaner = new CleanEvent(); - SimpleTimer.getInstance().addEvent(_cleaner, CLEAN_TIME); + _cleaner.schedule(CLEAN_TIME); } public void stop() { - SimpleTimer.getInstance().removeEvent(_cleaner); + _cleaner.cancel(); } public void updateOptsFromShare(Connection con) { @@ -124,14 +124,16 @@ public class TCBShare { } } - private class CleanEvent implements SimpleTimer.TimedEvent { - public CleanEvent() {} + private class CleanEvent extends SimpleTimer2.TimedEvent { + public CleanEvent() { + super(RetransmissionTimer.getInstance()); + } public void timeReached() { for (Iterator iter = _cache.keySet().iterator(); iter.hasNext(); ) { if (_cache.get(iter.next()).isExpired()) iter.remove(); } - SimpleTimer.getInstance().addEvent(CleanEvent.this, CLEAN_TIME); + schedule(CLEAN_TIME); } } } diff --git a/core/java/src/net/i2p/util/SimpleScheduler.java b/core/java/src/net/i2p/util/SimpleScheduler.java index 91415102c..becf10099 100644 --- a/core/java/src/net/i2p/util/SimpleScheduler.java +++ b/core/java/src/net/i2p/util/SimpleScheduler.java @@ -13,8 +13,8 @@ import net.i2p.I2PAppContext; * appropriate time. The method that is fired however should NOT block (otherwise * they b0rk the timer). * - * This is like SimpleScheduler but addEvent() for an existing event adds a second - * job. Events cannot be cancelled or rescheduled. + * This is like SimpleTimer but addEvent() for an existing event adds a second + * job. Unlike SimpleTimer, events cannot be cancelled or rescheduled. * * For events that cannot or will not be cancelled or rescheduled - * for example, a call such as: diff --git a/core/java/src/net/i2p/util/SimpleTimer2.java b/core/java/src/net/i2p/util/SimpleTimer2.java new file mode 100644 index 000000000..6239ed42f --- /dev/null +++ b/core/java/src/net/i2p/util/SimpleTimer2.java @@ -0,0 +1,252 @@ +package net.i2p.util; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadFactory; +import java.util.Map; + +import net.i2p.I2PAppContext; + +/** + * Simple event scheduler - toss an event on the queue and it gets fired at the + * appropriate time. The method that is fired however should NOT block (otherwise + * they b0rk the timer). + * + * This rewrites the old SimpleTimer to use the java.util.concurrent.ScheduledThreadPoolExecutor. + * SimpleTimer has problems with lock contention; + * this should work a lot better. + * + * This supports cancelling and arbitrary rescheduling. + * If you don't need that, use SimpleScheduler instead. + * + * SimpleTimer is deprecated, use this or SimpleScheduler. + * + * @author zzz + */ +public class SimpleTimer2 { + private static final SimpleTimer2 _instance = new SimpleTimer2(); + public static SimpleTimer2 getInstance() { return _instance; } + private static final int THREADS = 4; + private I2PAppContext _context; + private static Log _log; // static so TimedEvent can use it + private ScheduledThreadPoolExecutor _executor; + private String _name; + private int _count; + + protected SimpleTimer2() { this("SimpleTimer2"); } + protected SimpleTimer2(String name) { + _context = I2PAppContext.getGlobalContext(); + _log = _context.logManager().getLog(SimpleTimer2.class); + _name = name; + _count = 0; + _executor = new CustomScheduledThreadPoolExecutor(THREADS, new CustomThreadFactory()); + } + + /** + * Removes the SimpleTimer. + */ + public void stop() { + _executor.shutdownNow(); + } + + private class CustomScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor { + public CustomScheduledThreadPoolExecutor(int threads, ThreadFactory factory) { + super(threads, factory); + } + + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t != null) // shoudn't happen, caught in RunnableEvent.run() + _log.log(Log.CRIT, "wtf, event borked: " + r, t); + } + } + + private class CustomThreadFactory implements ThreadFactory { + public Thread newThread(Runnable r) { + Thread rv = Executors.defaultThreadFactory().newThread(r); + rv.setName(_name + ' ' + (++_count) + '/' + THREADS); + rv.setDaemon(true); + return rv; + } + } + + private ScheduledFuture schedule(TimedEvent t, long timeoutMs) { + return _executor.schedule(t, timeoutMs, TimeUnit.MILLISECONDS); + } + + /** + * Similar to SimpleTimer.TimedEvent but users must extend instead of implement, + * and all schedule and cancel methods are through this class rather than SimpleTimer2. + * + * To convert over, change implements SimpleTimer.TimedEvent to extends SimpleTimer2.TimedEvent, + * and be sure to call super(SimpleTimer2.getInstance(), timeoutMs) in the constructor + * (or super(SimpleTimer2.getInstance()); .... schedule(timeoutMs); if there is other stuff + * in your constructor) + * + * Other porting: + * SimpleTimer.getInstance().addEvent(new foo(), timeout) => new foo(SimpleTimer2.getInstance(), timeout) + * SimpleTimer.getInstance().addEvent(this, timeout) => schedule(timeout) + * SimpleTimer.getInstance().addEvent(foo, timeout) => foo.reschedule(timeout) + * SimpleTimer.getInstance().removeEvent(foo) => foo.cancel() + * + * There's no global locking, but for scheduling, we synchronize on this + * to reduce the chance of duplicates on the queue. + * + * schedule(ms) can get create duplicates + * reschedule(ms) and reschedule(ms, true) can lose the timer + * reschedule(ms, false) and forceReschedule(ms) are relatively safe from either + * + */ + public static abstract class TimedEvent implements Runnable { + private SimpleTimer2 _pool; + private int _fuzz; + protected static final int DEFAULT_FUZZ = 3; + private ScheduledFuture _future; // _executor.remove() doesn't work so we have to use this + // ... and I expect cancelling this way is more efficient + + /** must call schedule() later */ + public TimedEvent(SimpleTimer2 pool) { + _pool = pool; + _fuzz = DEFAULT_FUZZ; + } + /** automatically schedules, don't use this one if you have other things to do first */ + public TimedEvent(SimpleTimer2 pool, long timeoutMs) { + this(pool); + schedule(timeoutMs); + } + + /** + * Don't bother rescheduling if +/- this many ms or less. + * Use this to reduce timer queue and object churn for a sloppy timer like + * an inactivity timer. + * Default 3 ms. + */ + public void setFuzz(int fuzz) { + _fuzz = fuzz; + } + + /** + * More efficient than reschedule(). + * Only call this after calling the non-scheduling constructor, + * or from within timeReached(), or you will get duplicates on the queue. + * Otherwise use reschedule(). + */ + public synchronized void schedule(long timeoutMs) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Scheduling: " + this + " timeout = " + timeoutMs); + if (timeoutMs <= 0 && _log.shouldLog(Log.WARN)) + timeoutMs = 1; // otherwise we may execute before _future is updated, which is fine + // except it triggers 'early execution' warning logging + _future = _pool.schedule(this, timeoutMs); + } + + /** + * Use the earliest of the new time and the old time + * Do not call from within timeReached() + * + * @param timeoutMs + */ + public void reschedule(long timeoutMs) { + reschedule(timeoutMs, true); + } + + /** + * useEarliestTime must be false if called from within timeReached(), as + * it won't be rescheduled, in favor of the currently running task + * + * @param timeoutMs + * @param useEarliestTime if its already scheduled, use the earlier of the + * two timeouts, else use the later + */ + public synchronized void reschedule(long timeoutMs, boolean useEarliestTime) { + long oldTimeout; + boolean scheduled = _future != null && !_future.isDone(); + if (scheduled) + oldTimeout = _future.getDelay(TimeUnit.MILLISECONDS); + else + oldTimeout = timeoutMs; + // don't bother rescheduling if within _fuzz ms + if ((oldTimeout - _fuzz > timeoutMs && useEarliestTime) || + (oldTimeout + _fuzz < timeoutMs && !useEarliestTime)|| + (!scheduled)) { + if (scheduled) { + if (_log.shouldLog(Log.INFO)) + _log.info("Re-scheduling: " + this + " timeout = " + timeoutMs + " old timeout was " + oldTimeout); + cancel(); + } + schedule(timeoutMs); + } + } + + /** + * Always use the new time - ignores fuzz + * @param timeoutMs + */ + public synchronized void forceReschedule(long timeoutMs) { + cancel(); + schedule(timeoutMs); + } + + /** returns true if cancelled */ + public synchronized boolean cancel() { + if (_future == null) + return false; + return _future.cancel(false); + } + + public void run() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Running: " + this); + long before = System.currentTimeMillis(); + long delay = 0; + if (_future != null) + delay = _future.getDelay(TimeUnit.MILLISECONDS); + else if (_log.shouldLog(Log.WARN)) + _log.warn(_pool + " wtf, no _future " + this); + // This can be an incorrect warning especially after a schedule(0) + if (_log.shouldLog(Log.WARN) && delay > 100) + _log.warn(_pool + " wtf, early execution " + delay + ": " + this); + else if (_log.shouldLog(Log.WARN) && delay < -1000) + _log.warn(" wtf, late execution " + delay + ": " + this + _pool.debug()); + try { + timeReached(); + } catch (Throwable t) { + _log.log(Log.CRIT, _pool + " wtf, event borked: " + this, t); + } + long time = System.currentTimeMillis() - before; + if (time > 500 && _log.shouldLog(Log.WARN)) + _log.warn(_pool + " wtf, event execution took " + time + ": " + this); + long completed = _pool.getCompletedTaskCount(); + if (_log.shouldLog(Log.INFO) && completed % 250 == 0) + _log.info(_pool.debug()); + } + + /** + * Simple interface for events to be queued up and notified on expiration + * the time requested has been reached (this call should NOT block, + * otherwise the whole SimpleTimer gets backed up) + * + */ + public abstract void timeReached(); + } + + public String toString() { + return _name; + } + + private long getCompletedTaskCount() { + return _executor.getCompletedTaskCount(); + } + + private String debug() { + _executor.purge(); // Remove cancelled tasks from the queue so we get a good queue size stat + return + " Pool: " + _name + + " Active: " + _executor.getActiveCount() + '/' + _executor.getPoolSize() + + " Completed: " + _executor.getCompletedTaskCount() + + " Queued: " + _executor.getQueue().size(); + } +} + diff --git a/router/java/src/net/i2p/router/tunnel/FlushTimer.java b/router/java/src/net/i2p/router/tunnel/FlushTimer.java index b18799ac6..b55384b80 100644 --- a/router/java/src/net/i2p/router/tunnel/FlushTimer.java +++ b/router/java/src/net/i2p/router/tunnel/FlushTimer.java @@ -6,7 +6,12 @@ import net.i2p.util.SimpleTimer; * */ class FlushTimer extends SimpleTimer { - private static final FlushTimer _instance = new FlushTimer(); - public static final SimpleTimer getInstance() { return _instance; } - protected FlushTimer() { super("TunnelFlushTimer"); } + /* + Streaming lib has been moved from SimpleTimer to SimpleTimer2, eliminating the congestion. + So there's not much left using SimpleTimer, and FlushTimer doesn't need its own 4 threads any more + (if it ever did?...) + */ + //private static final FlushTimer _instance = new FlushTimer(); + //public static final SimpleTimer getInstance() { return _instance; } + //protected FlushTimer() { super("TunnelFlushTimer"); } } From 374360c7b4a9f20c354935755ae683beaa83db94 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 15 Feb 2009 05:15:25 +0000 Subject: [PATCH 130/191] save a little space --- .../java/src/net/i2p/router/networkdb/kademlia/KBucketImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KBucketImpl.java b/router/java/src/net/i2p/router/networkdb/kademlia/KBucketImpl.java index 51507c520..5d8e186f5 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/KBucketImpl.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KBucketImpl.java @@ -41,7 +41,7 @@ class KBucketImpl implements KBucket { public KBucketImpl(I2PAppContext context, Hash local) { _context = context; _log = context.logManager().getLog(KBucketImpl.class); - _entries = new ArrayList(64); //new HashSet(); + _entries = new ArrayList(0); //all but the last 1 or 2 buckets will be empty _lastShuffle = context.clock().now(); setLocal(local); } From 775ab9a7bf42b1c32009ba6aa32b4d5626fd2409 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 15 Feb 2009 05:17:18 +0000 Subject: [PATCH 131/191] * I2PTunnel: - Display destination even when stopped - Enable key generation, dest modification, and hashcash estimation in the GUI - Add new CONNECT client --- .../java/src/net/i2p/i2ptunnel/I2PTunnel.java | 62 ++- .../i2p/i2ptunnel/I2PTunnelConnectClient.java | 369 ++++++++++++++++++ .../net/i2p/i2ptunnel/TunnelController.java | 20 +- .../src/net/i2p/i2ptunnel/web/IndexBean.java | 135 ++++++- apps/i2ptunnel/jsp/editServer.jsp | 20 +- apps/i2ptunnel/jsp/index.jsp | 2 +- .../java/src/net/i2p/data/PrivateKeyFile.java | 170 +++++--- 7 files changed, 707 insertions(+), 71 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java index 3b279a679..b72ae18b3 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java @@ -244,6 +244,8 @@ public class I2PTunnel implements Logging, EventDispatcher { runIrcClient(args, l); } else if ("sockstunnel".equals(cmdname)) { runSOCKSTunnel(args, l); + } else if ("connectclient".equals(cmdname)) { + runConnectClient(args, l); } else if ("config".equals(cmdname)) { runConfig(args, l); } else if ("listen_on".equals(cmdname)) { @@ -296,6 +298,7 @@ public class I2PTunnel implements Logging, EventDispatcher { l.log("client [, []"); l.log("ircclient [, []"); l.log("httpclient [] []"); + l.log("connectclient [] []"); l.log("lookup "); l.log("quit"); l.log("close [forced] |all"); @@ -555,7 +558,7 @@ public class I2PTunnel implements Logging, EventDispatcher { return; } - String proxy = "squid.i2p"; + String proxy = ""; boolean isShared = true; if (args.length > 1) { if ("true".equalsIgnoreCase(args[1].trim())) { @@ -595,11 +598,66 @@ public class I2PTunnel implements Logging, EventDispatcher { l.log(" (optional) indicates if this client shares tunnels with other clients (true of false)"); l.log(" (optional) indicates a proxy server to be used"); l.log(" when trying to access an address out of the .i2p domain"); - l.log(" (the default proxy is squid.i2p)."); notifyEvent("httpclientTaskId", Integer.valueOf(-1)); } } + /** + * Run a CONNECT client on the given port number + * + * @param args {portNumber[, sharedClient][, proxy to be used for the WWW]} + * @param l logger to receive events and output + */ + public void runConnectClient(String args[], Logging l) { + if (args.length >= 1 && args.length <= 3) { + int port = -1; + try { + port = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe); + return; + } + + String proxy = ""; + boolean isShared = true; + if (args.length > 1) { + if ("true".equalsIgnoreCase(args[1].trim())) { + isShared = true; + if (args.length == 3) + proxy = args[2]; + } else if ("false".equalsIgnoreCase(args[1].trim())) { + _log.warn("args[1] == [" + args[1] + "] and rejected explicitly"); + isShared = false; + if (args.length == 3) + proxy = args[2]; + } else if (args.length == 3) { + isShared = false; // not "true" + proxy = args[2]; + _log.warn("args[1] == [" + args[1] + "] but rejected"); + } else { + // isShared not specified, default to true + isShared = true; + proxy = args[1]; + } + } + + I2PTunnelTask task; + ownDest = !isShared; + try { + task = new I2PTunnelConnectClient(port, l, ownDest, proxy, (EventDispatcher) this, this); + addtask(task); + } catch (IllegalArgumentException iae) { + _log.error(getPrefix() + "Invalid I2PTunnel config to create an httpclient [" + host + ":"+ port + "]", iae); + } + } else { + l.log("connectclient [] []"); + l.log(" creates a client that for SSL/HTTPS requests."); + l.log(" (optional) indicates if this client shares tunnels with other clients (true of false)"); + l.log(" (optional) indicates a proxy server to be used"); + l.log(" when trying to access an address out of the .i2p domain"); + } + } + /** * Run an IRC client on the given port number * diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java new file mode 100644 index 000000000..bf7ebf0a7 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java @@ -0,0 +1,369 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.StringTokenizer; + +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.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.util.EventDispatcher; +import net.i2p.util.FileUtil; +import net.i2p.util.Log; + +/** + * Supports the following: + * (where protocol is generally HTTP/1.1 but is ignored) + * (where host is one of: + * example.i2p + * 52chars.b32.i2p + * 516+charsbase64 + * example.com (sent to one of the configured proxies) + * ) + * + * (port and protocol are ignored for i2p destinations) + * CONNECT host + * CONNECT host protocol + * CONNECT host:port + * CONNECT host:port protocol (this is the standard) + * + * Additional lines after the CONNECT line but before the blank line are ignored and stripped. + * The CONNECT line is removed for .i2p accesses + * but passed along for outproxy accesses. + * + * Ref: + * INTERNET-DRAFT Ari Luotonen + * Expires: September 26, 1997 Netscape Communications Corporation + * March 26, 1997 + * Tunneling SSL Through a WWW Proxy + * + * @author zzz a stripped-down I2PTunnelHTTPClient + */ +public class I2PTunnelConnectClient extends I2PTunnelClientBase implements Runnable { + private static final Log _log = new Log(I2PTunnelConnectClient.class); + + private List _proxyList; + + private final static byte[] ERR_DESTINATION_UNKNOWN = + ("HTTP/1.1 503 Service Unavailable\r\n"+ + "Content-Type: text/html; charset=iso-8859-1\r\n"+ + "Cache-control: no-cache\r\n"+ + "\r\n"+ + "

    I2P ERROR: DESTINATION NOT FOUND

    "+ + "That I2P Destination was not found. "+ + "The host (or the outproxy, if you're using one) could also "+ + "be temporarily offline. You may want to retry. "+ + "Could not find the following Destination:

    ") + .getBytes(); + + private final static byte[] ERR_NO_OUTPROXY = + ("HTTP/1.1 503 Service Unavailable\r\n"+ + "Content-Type: text/html; charset=iso-8859-1\r\n"+ + "Cache-control: no-cache\r\n"+ + "\r\n"+ + "

    I2P ERROR: No outproxy found

    "+ + "Your request was for a site outside of I2P, but you have no "+ + "HTTP outproxy configured. Please configure an outproxy in I2PTunnel") + .getBytes(); + + private final static byte[] ERR_BAD_PROTOCOL = + ("HTTP/1.1 405 Bad Method\r\n"+ + "Content-Type: text/html; charset=iso-8859-1\r\n"+ + "Cache-control: no-cache\r\n"+ + "\r\n"+ + "

    I2P ERROR: METHOD NOT ALLOWED

    "+ + "The request uses a bad protocol. "+ + "The Connect Proxy supports CONNECT requests ONLY. Other methods such as GET are not allowed - Maybe you wanted the HTTP Proxy?.
    ") + .getBytes(); + + private final static byte[] ERR_LOCALHOST = + ("HTTP/1.1 403 Access Denied\r\n"+ + "Content-Type: text/html; charset=iso-8859-1\r\n"+ + "Cache-control: no-cache\r\n"+ + "\r\n"+ + "

    I2P ERROR: REQUEST DENIED

    "+ + "Your browser is misconfigured. Do not use the proxy to access the router console or other localhost destinations.
    ") + .getBytes(); + + private final static byte[] SUCCESS_RESPONSE = + ("HTTP/1.1 200 Connection Established\r\n"+ + "Proxy-agent: I2P\r\n"+ + "\r\n") + .getBytes(); + + /** used to assign unique IDs to the threads / clients. no logic or functionality */ + private static volatile long __clientId = 0; + + /** + * @throws IllegalArgumentException if the I2PTunnel does not contain + * valid config to contact the router + */ + public I2PTunnelConnectClient(int localPort, Logging l, boolean ownDest, + String wwwProxy, EventDispatcher notifyThis, + I2PTunnel tunnel) throws IllegalArgumentException { + super(localPort, ownDest, l, notifyThis, "HTTPHandler " + (++__clientId), tunnel); + + if (waitEventValue("openBaseClientResult").equals("error")) { + notifyEvent("openConnectClientResult", "error"); + return; + } + + _proxyList = new ArrayList(); + if (wwwProxy != null) { + StringTokenizer tok = new StringTokenizer(wwwProxy, ","); + while (tok.hasMoreTokens()) + _proxyList.add(tok.nextToken().trim()); + } + + setName(getLocalPort() + " -> ConnectClient [Outproxy list: " + wwwProxy + "]"); + + startRunning(); + } + + private String getPrefix(long requestId) { return "Client[" + _clientId + "/" + requestId + "]: "; } + + private String selectProxy() { + synchronized (_proxyList) { + int size = _proxyList.size(); + if (size <= 0) + return null; + int index = I2PAppContext.getGlobalContext().random().nextInt(size); + return _proxyList.get(index); + } + } + + private static final int DEFAULT_READ_TIMEOUT = 60*1000; + + /** + * create the default options (using the default timeout, etc) + * + */ + protected I2PSocketOptions getDefaultOptions() { + Properties defaultOpts = getTunnel().getClientOptions(); + if (!defaultOpts.contains(I2PSocketOptions.PROP_READ_TIMEOUT)) + defaultOpts.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, ""+DEFAULT_READ_TIMEOUT); + if (!defaultOpts.contains("i2p.streaming.inactivityTimeout")) + defaultOpts.setProperty("i2p.streaming.inactivityTimeout", ""+DEFAULT_READ_TIMEOUT); + I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts); + if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT)) + opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT); + return opts; + } + + private static long __requestId = 0; + protected void clientConnectionRun(Socket s) { + InputStream in = null; + OutputStream out = null; + String targetRequest = null; + boolean usingWWWProxy = false; + String currentProxy = null; + long requestId = ++__requestId; + try { + out = s.getOutputStream(); + in = s.getInputStream(); + String line, method = null, host = null, destination = null, restofline = null; + StringBuffer newRequest = new StringBuffer(); + int ahelper = 0; + while (true) { + // Use this rather than BufferedReader because we can't have readahead, + // since we are passing the stream on to I2PTunnelRunner + line = DataHelper.readLine(in); + line = line.trim(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug(getPrefix(requestId) + "Line=[" + line + "]"); + + if (method == null) { // first line CONNECT blah.i2p:80 HTTP/1.1 + int pos = line.indexOf(" "); + if (pos == -1) break; // empty first line + method = line.substring(0, pos); + String request = line.substring(pos + 1); + + pos = request.indexOf(":"); + if (pos == -1) + pos = request.indexOf(" "); + if (pos == -1) { + host = request; + restofline = ""; + } else { + host = request.substring(0, pos); + restofline = request.substring(pos); // ":80 HTTP/1.1" or " HTTP/1.1" + } + + if (host.toLowerCase().endsWith(".i2p")) { + // Destination gets the host name + destination = host; + } else if (host.indexOf(".") != -1) { + // The request must be forwarded to a outproxy + currentProxy = selectProxy(); + if (currentProxy == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn(getPrefix(requestId) + "Host wants to be outproxied, but we dont have any!"); + writeErrorMessage(ERR_NO_OUTPROXY, out); + s.close(); + return; + } + destination = currentProxy; + usingWWWProxy = true; + newRequest.append("CONNECT ").append(host).append(restofline).append("\r\n\r\n"); // HTTP spec + } else if (host.toLowerCase().equals("localhost")) { + writeErrorMessage(ERR_LOCALHOST, out); + s.close(); + return; + } else { // full b64 address (hopefully) + destination = host; + } + targetRequest = host; + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug(getPrefix(requestId) + "METHOD:" + method + ":"); + _log.debug(getPrefix(requestId) + "HOST :" + host + ":"); + _log.debug(getPrefix(requestId) + "REST :" + restofline + ":"); + _log.debug(getPrefix(requestId) + "DEST :" + destination + ":"); + } + + } else if (line.length() > 0) { + // Additional lines - shouldn't be too many. Firefox sends: + // User-Agent: blabla + // Proxy-Connection: keep-alive + // Host: blabla.i2p + // + // We could send these (filtered like in HTTPClient) on to the outproxy, + // but for now just chomp them all. + line = null; + } else { + // do it + break; + } + } + + if (destination == null || !"CONNECT".equalsIgnoreCase(method)) { + writeErrorMessage(ERR_BAD_PROTOCOL, out); + s.close(); + return; + } + + Destination dest = I2PTunnel.destFromName(destination); + if (dest == null) { + String str; + byte[] header; + if (usingWWWProxy) + str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true); + else + str = FileUtil.readTextFile("docs/dnfh-header.ht", 100, true); + if (str != null) + header = str.getBytes(); + else + header = ERR_DESTINATION_UNKNOWN; + writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination); + s.close(); + return; + } + + I2PSocket i2ps = createI2PSocket(dest, getDefaultOptions()); + byte[] data = null; + byte[] response = null; + if (usingWWWProxy) + data = newRequest.toString().getBytes("ISO-8859-1"); + else + response = SUCCESS_RESPONSE; + Runnable onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId); + I2PTunnelRunner runner = new I2PTunnelRunner(s, i2ps, sockLock, data, response, mySockets, onTimeout); + } catch (SocketException ex) { + _log.info(getPrefix(requestId) + "Error trying to connect", ex); + handleConnectClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); + closeSocket(s); + } catch (IOException ex) { + _log.info(getPrefix(requestId) + "Error trying to connect", ex); + handleConnectClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); + closeSocket(s); + } catch (I2PException ex) { + _log.info("getPrefix(requestId) + Error trying to connect", ex); + handleConnectClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); + closeSocket(s); + } catch (OutOfMemoryError oom) { + IOException ex = new IOException("OOM"); + _log.info("getPrefix(requestId) + Error trying to connect", ex); + handleConnectClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); + closeSocket(s); + } + } + + private static class OnTimeout implements Runnable { + private Socket _socket; + private OutputStream _out; + private String _target; + private boolean _usingProxy; + private String _wwwProxy; + private long _requestId; + public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) { + _socket = s; + _out = out; + _target = target; + _usingProxy = usingProxy; + _wwwProxy = wwwProxy; + _requestId = id; + } + public void run() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Timeout occured requesting " + _target); + handleConnectClientException(new RuntimeException("Timeout"), _out, + _target, _usingProxy, _wwwProxy, _requestId); + closeSocket(_socket); + } + } + + private static void writeErrorMessage(byte[] errMessage, OutputStream out) throws IOException { + if (out == null) + return; + out.write(errMessage); + out.write("\n\n".getBytes()); + out.flush(); + } + + private static void writeErrorMessage(byte[] errMessage, OutputStream out, String targetRequest, + boolean usingWWWProxy, String wwwProxy) throws IOException { + if (out != null) { + out.write(errMessage); + if (targetRequest != null) { + out.write(targetRequest.getBytes()); + if (usingWWWProxy) + out.write(("
    WWW proxy: " + wwwProxy).getBytes()); + } + out.write("
    ".getBytes()); + out.write("\n\n".getBytes()); + out.flush(); + } + } + + private static void handleConnectClientException(Exception ex, OutputStream out, String targetRequest, + boolean usingWWWProxy, String wwwProxy, long requestId) { + if (out == null) + return; + try { + String str; + byte[] header; + if (usingWWWProxy) + str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true); + else + str = FileUtil.readTextFile("docs/dnf-header.ht", 100, true); + if (str != null) + header = str.getBytes(); + else + header = ERR_DESTINATION_UNKNOWN; + writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy); + } catch (IOException ioe) {} + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index f8592fcd3..3c9640ce5 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -73,7 +73,7 @@ public class TunnelController implements Logging { File keyFile = new File(getPrivKeyFile()); if (keyFile.exists()) { - log("Not overwriting existing private keys in " + keyFile.getAbsolutePath()); + //log("Not overwriting existing private keys in " + keyFile.getAbsolutePath()); return; } else { File parent = keyFile.getParentFile(); @@ -87,6 +87,7 @@ public class TunnelController implements Logging { String destStr = dest.toBase64(); log("Private key created and saved in " + keyFile.getAbsolutePath()); log("New destination: " + destStr); + log("Base32: " + Base32.encode(dest.calculateHash().getData()) + ".b32.i2p"); } catch (I2PException ie) { if (_log.shouldLog(Log.ERROR)) _log.error("Error creating new destination", ie); @@ -139,6 +140,8 @@ public class TunnelController implements Logging { startIrcClient(); } else if("sockstunnel".equals(type)) { startSocksClient(); + } else if("connectclient".equals(type)) { + startConnectClient(); } else if ("client".equals(type)) { startClient(); } else if ("server".equals(type)) { @@ -166,6 +169,21 @@ public class TunnelController implements Logging { _running = true; } + private void startConnectClient() { + setI2CPOptions(); + setSessionOptions(); + setListenOn(); + String listenPort = getListenPort(); + String proxyList = getProxyList(); + String sharedClient = getSharedClient(); + if (proxyList == null) + _tunnel.runConnectClient(new String[] { listenPort, sharedClient }, this); + else + _tunnel.runConnectClient(new String[] { listenPort, sharedClient, proxyList }, this); + acquire(); + _running = true; + } + private void startIrcClient() { setI2CPOptions(); setSessionOptions(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 1aca37bf5..46b555772 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -18,6 +18,11 @@ import java.util.Set; import java.util.StringTokenizer; import net.i2p.I2PAppContext; +import net.i2p.data.Base32; +import net.i2p.data.Certificate; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKeyFile; +import net.i2p.data.SessionKey; import net.i2p.i2ptunnel.TunnelController; import net.i2p.i2ptunnel.TunnelControllerGroup; import net.i2p.util.ConcurrentHashSet; @@ -65,6 +70,9 @@ public class IndexBean { private boolean _removeConfirmed; private Set _booleanOptions; private Map _otherOptions; + private int _hashCashValue; + private int _certType; + private String _certSigner; public static final int RUNNING = 1; public static final int STARTING = 2; @@ -156,6 +164,12 @@ public class IndexBean { else if ("Delete this proxy".equals(_action) || // IE workaround: (_action.toLowerCase().indexOf("delete") >= 0)) return deleteTunnel(); + else if ("Estimate".equals(_action)) + return PrivateKeyFile.estimateHashCashTime(_hashCashValue); + else if ("Modify".equals(_action)) + return modifyDestination(); + else if ("Generate".equals(_action)) + return generateNewEncryptionKey(); else return "Action " + _action + " unknown"; } @@ -370,7 +384,7 @@ public class IndexBean { else if ("ircclient".equals(internalType)) return "IRC client"; else if ("server".equals(internalType)) return "Standard server"; else if ("httpserver".equals(internalType)) return "HTTP server"; - else if ("sockstunnel".equals(internalType)) return "SOCKS proxy"; + else if ("sockstunnel".equals(internalType)) return "SOCKS 5 proxy"; else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy"; else return internalType; } @@ -440,6 +454,16 @@ public class IndexBean { String rv = tun.getMyDestination(); if (rv != null) return rv; + // if not running, do this the hard way + String keyFile = tun.getPrivKeyFile(); + if (keyFile != null && keyFile.trim().length() > 0) { + PrivateKeyFile pkf = new PrivateKeyFile(keyFile); + try { + Destination d = pkf.getDestination(); + if (d != null) + return d.toBase64(); + } catch (Exception e) {} + } } return ""; } @@ -616,6 +640,115 @@ public class IndexBean { } } + /** params needed for hashcash and dest modification */ + public void setEffort(String val) { + if (val != null) { + try { + _hashCashValue = Integer.parseInt(val.trim()); + } catch (NumberFormatException nfe) {} + } + } + public void setCert(String val) { + if (val != null) { + try { + _certType = Integer.parseInt(val.trim()); + } catch (NumberFormatException nfe) {} + } + } + public void setSigner(String val) { + _certSigner = val; + } + + /** Modify or create a destination */ + private String modifyDestination() { + if (_privKeyFile == null || _privKeyFile.trim().length() <= 0) + return "Private Key File not specified"; + + TunnelController tun = getController(_tunnel); + Properties config = getConfig(); + if (config == null) + return "Invalid params"; + if (tun == null) { + // creating new + tun = new TunnelController(config, "", true); + _group.addController(tun); + saveChanges(); + } else if (tun.getIsRunning() || tun.getIsStarting()) { + return "Tunnel must be stopped before modifying destination"; + } + PrivateKeyFile pkf = new PrivateKeyFile(_privKeyFile); + try { + pkf.createIfAbsent(); + } catch (Exception e) { + return "Create private key file failed: " + e; + } + switch (_certType) { + case Certificate.CERTIFICATE_TYPE_NULL: + case Certificate.CERTIFICATE_TYPE_HIDDEN: + pkf.setCertType(_certType); + break; + case Certificate.CERTIFICATE_TYPE_HASHCASH: + pkf.setHashCashCert(_hashCashValue); + break; + case Certificate.CERTIFICATE_TYPE_SIGNED: + if (_certSigner == null || _certSigner.trim().length() <= 0) + return "No signing destination specified"; + // find the signer's key file... + String signerPKF = null; + for (int i = 0; i < getTunnelCount(); i++) { + TunnelController c = getController(i); + if (_certSigner.equals(c.getConfig("").getProperty("name")) || + _certSigner.equals(c.getConfig("").getProperty("spoofedHost"))) { + signerPKF = c.getConfig("").getProperty("privKeyFile"); + break; + } + } + if (signerPKF == null || signerPKF.length() <= 0) + return "Signing destination " + _certSigner + " not found"; + if (_privKeyFile.equals(signerPKF)) + return "Self-signed destinations not allowed"; + Certificate c = pkf.setSignedCert(new PrivateKeyFile(signerPKF)); + if (c == null) + return "Signing failed - does signer destination exist?"; + break; + default: + return "Unknown certificate type"; + } + Destination newdest; + try { + pkf.write(); + newdest = pkf.getDestination(); + } catch (Exception e) { + return "Modification failed: " + e; + } + return "Destination modified - " + + "New Base32 is " + Base32.encode(newdest.calculateHash().getData()) + ".b32.i2p " + + "New Destination is " + newdest.toBase64(); + } + + /** New key */ + private String generateNewEncryptionKey() { + TunnelController tun = getController(_tunnel); + Properties config = getConfig(); + if (config == null) + return "Invalid params"; + if (tun == null) { + // creating new + tun = new TunnelController(config, "", true); + _group.addController(tun); + saveChanges(); + } else if (tun.getIsRunning() || tun.getIsStarting()) { + return "Tunnel must be stopped before modifying leaseset encryption key"; + } + byte[] data = new byte[SessionKey.KEYSIZE_BYTES]; + _context.random().nextBytes(data); + SessionKey sk = new SessionKey(data); + setEncryptKey(sk.toBase64()); + setEncrypt(""); + saveChanges(); + return "New Leaseset Encryption Key: " + sk.toBase64(); + } + /** * Based on all provided data, create a set of configuration parameters * suitable for use in a TunnelController. This will replace (not add to) diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index 596848617..82ac69dc5 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -254,12 +254,18 @@ class="tickbox" />
    -
    +
    - - (Users will require this key) + +
    +
    + + + (Tunnel must be stopped first)
    @@ -319,7 +325,7 @@
    @@ -331,14 +337,14 @@
    class="tickbox" /> - +
    - +
    @@ -359,7 +365,7 @@ - + (Tunnel must be stopped first)
    diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index a06177dd4..b96236ae1 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -148,7 +148,7 @@ - + diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java index 7680204d1..d9e52aecc 100644 --- a/core/java/src/net/i2p/data/PrivateKeyFile.java +++ b/core/java/src/net/i2p/data/PrivateKeyFile.java @@ -77,74 +77,25 @@ public class PrivateKeyFile { verifySignature(d); if (args.length == 1) return; - Certificate c = new Certificate(); if (args[0].equals("-n")) { // Cert constructor generates a null cert + pkf.setCertType(Certificate.CERTIFICATE_TYPE_NULL); } else if (args[0].equals("-u")) { - c.setCertificateType(99); + pkf.setCertType(99); } else if (args[0].equals("-x")) { - c.setCertificateType(Certificate.CERTIFICATE_TYPE_HIDDEN); + pkf.setCertType(Certificate.CERTIFICATE_TYPE_HIDDEN); } else if (args[0].equals("-h")) { int hashEffort = HASH_EFFORT; if (args.length == 3) hashEffort = Integer.parseInt(args[1]); System.out.println("Estimating hashcash generation time, stand by..."); - // takes a lot longer than the estimate usually... - // maybe because the resource string is much longer than used in the estimate? - long low = HashCash.estimateTime(hashEffort); - System.out.println("It is estimated this will take " + DataHelper.formatDuration(low) + - " to " + DataHelper.formatDuration(4*low)); - - long begin = System.currentTimeMillis(); - System.out.println("Starting hashcash generation now..."); - String resource = d.getPublicKey().toBase64() + d.getSigningPublicKey().toBase64(); - HashCash hc = HashCash.mintCash(resource, hashEffort); - System.out.println("Generation took: " + DataHelper.formatDuration(System.currentTimeMillis() - begin)); - System.out.println("Full Hashcash is: " + hc); - // Take the resource out of the stamp - String hcs = hc.toString(); - int end1 = 0; - for (int i = 0; i < 3; i++) { - end1 = 1 + hcs.indexOf(':', end1); - if (end1 < 0) { - System.out.println("Bad hashcash"); - return; - } - } - int start2 = hcs.indexOf(':', end1); - if (start2 < 0) { - System.out.println("Bad hashcash"); - return; - } - hcs = hcs.substring(0, end1) + hcs.substring(start2); - System.out.println("Short Hashcash is: " + hcs); - - c.setCertificateType(Certificate.CERTIFICATE_TYPE_HASHCASH); - c.setPayload(hcs.getBytes()); + System.out.println(estimateHashCashTime(hashEffort)); + pkf.setHashCashCert(hashEffort); } else if (args.length == 3 && args[0].equals("-s")) { // Sign dest1 with dest2's Signing Private Key - File f2 = new File(args[2]); - I2PClient client2 = I2PClientFactory.createClient(); - PrivateKeyFile pkf2 = new PrivateKeyFile(f2, client2); - Destination d2 = pkf2.getDestination(); - SigningPrivateKey spk2 = pkf2.getSigningPrivKey(); - System.out.println("Signing With Dest:"); - System.out.println(pkf2.toString()); - - int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert - byte[] data = new byte[len]; - System.arraycopy(d.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES); - System.arraycopy(d.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES); - byte[] payload = new byte[Hash.HASH_LENGTH + Signature.SIGNATURE_BYTES]; - byte[] sig = DSAEngine.getInstance().sign(new ByteArrayInputStream(data), spk2).getData(); - System.arraycopy(sig, 0, payload, 0, Signature.SIGNATURE_BYTES); - // Add dest2's Hash for reference - byte[] h2 = d2.calculateHash().getData(); - System.arraycopy(h2, 0, payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH); - c.setCertificateType(Certificate.CERTIFICATE_TYPE_SIGNED); - c.setPayload(payload); + PrivateKeyFile pkf2 = new PrivateKeyFile(args[2]); + pkf.setSignedCert(pkf2); } - d.setCertificate(c); // do this rather than just change the existing cert so the hash is recalculated System.out.println("New signed destination is:"); System.out.println(pkf); pkf.write(); @@ -154,7 +105,10 @@ public class PrivateKeyFile { } } - + public PrivateKeyFile(String file) { + this(new File(file), I2PClientFactory.createClient()); + } + public PrivateKeyFile(File file, I2PClient client) { this.file = file; this.client = client; @@ -176,7 +130,7 @@ public class PrivateKeyFile { return getDestination(); } - /** Also sets the local privKay and signingPrivKey */ + /** Also sets the local privKey and signingPrivKey */ public Destination getDestination() throws I2PSessionException, IOException, DataFormatException { if (dest == null) { I2PSession s = open(); @@ -188,6 +142,86 @@ public class PrivateKeyFile { } return this.dest; } + + public void setDestination(Destination d) { + this.dest = d; + } + + /** change cert type - caller must also call write() */ + public Certificate setCertType(int t) { + if (this.dest == null) + throw new IllegalArgumentException("Dest is null"); + Certificate c = new Certificate(); + c.setCertificateType(t); + this.dest.setCertificate(c); + return c; + } + + /** change to hashcash cert - caller must also call write() */ + public Certificate setHashCashCert(int effort) { + Certificate c = setCertType(Certificate.CERTIFICATE_TYPE_HASHCASH); + long begin = System.currentTimeMillis(); + System.out.println("Starting hashcash generation now..."); + String resource = this.dest.getPublicKey().toBase64() + this.dest.getSigningPublicKey().toBase64(); + HashCash hc; + try { + hc = HashCash.mintCash(resource, effort); + } catch (Exception e) { + return null; + } + System.out.println("Generation took: " + DataHelper.formatDuration(System.currentTimeMillis() - begin)); + System.out.println("Full Hashcash is: " + hc); + // Take the resource out of the stamp + String hcs = hc.toString(); + int end1 = 0; + for (int i = 0; i < 3; i++) { + end1 = 1 + hcs.indexOf(':', end1); + if (end1 < 0) { + System.out.println("Bad hashcash"); + return null; + } + } + int start2 = hcs.indexOf(':', end1); + if (start2 < 0) { + System.out.println("Bad hashcash"); + return null; + } + hcs = hcs.substring(0, end1) + hcs.substring(start2); + System.out.println("Short Hashcash is: " + hcs); + + c.setPayload(hcs.getBytes()); + return c; + } + + /** sign this dest by dest found in pkf2 - caller must also call write() */ + public Certificate setSignedCert(PrivateKeyFile pkf2) { + Certificate c = setCertType(Certificate.CERTIFICATE_TYPE_SIGNED); + Destination d2; + try { + d2 = pkf2.getDestination(); + } catch (Exception e) { + return null; + } + if (d2 == null) + return null; + SigningPrivateKey spk2 = pkf2.getSigningPrivKey(); + System.out.println("Signing With Dest:"); + System.out.println(pkf2.toString()); + + int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert + byte[] data = new byte[len]; + System.arraycopy(this.dest.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES); + System.arraycopy(this.dest.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES); + byte[] payload = new byte[Hash.HASH_LENGTH + Signature.SIGNATURE_BYTES]; + byte[] sig = DSAEngine.getInstance().sign(new ByteArrayInputStream(data), spk2).getData(); + System.arraycopy(sig, 0, payload, 0, Signature.SIGNATURE_BYTES); + // Add dest2's Hash for reference + byte[] h2 = d2.calculateHash().getData(); + System.arraycopy(h2, 0, payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH); + c.setCertificateType(Certificate.CERTIFICATE_TYPE_SIGNED); + c.setPayload(payload); + return c; + } public PrivateKey getPrivKey() { return this.privKey; @@ -238,7 +272,25 @@ public class PrivateKeyFile { return s.toString(); } - + public static String estimateHashCashTime(int hashEffort) { + if (hashEffort <= 0 || hashEffort > 160) + return "Bad HashCash value: " + hashEffort; + long low = Long.MAX_VALUE; + try { + low = HashCash.estimateTime(hashEffort); + } catch (Exception e) {} + // takes a lot longer than the estimate usually... + // maybe because the resource string is much longer than used in the estimate? + return "It is estimated that generating a HashCash Certificate with value " + hashEffort + + " for the Destination will take " + + ((low < 1000l * 24l * 60l * 60l * 1000l) + ? + "approximately " + DataHelper.formatDuration(low) + + " to " + DataHelper.formatDuration(4*low) + : + "longer than three years!" + ); + } /** * Sample code to verify a 3rd party signature. From 129fc5b8384a6a581417e03cba24e7bc597bdcf4 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 15 Feb 2009 14:27:46 +0000 Subject: [PATCH 132/191] Backport rev 1c20e222438c8098ed49a4e5a5a609f0d2cf14c5 before the prop forward --- .../src/net/i2p/client/streaming/Connection.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java index 503760ed1..3beed8c8f 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java @@ -126,6 +126,7 @@ public class Connection { _isInbound = false; _updatedShareOpts = false; _connectionEvent = new ConEvent(); + _hardDisconnected = false; _randomWait = _context.random().nextInt(10*1000); // just do this once to reduce usage _context.statManager().createRateStat("stream.con.windowSizeAtCongestion", "How large was our send window when we send a dup?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); _context.statManager().createRateStat("stream.chokeSizeBegin", "How many messages were outstanding when we started to choke?", "Stream", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); @@ -154,7 +155,7 @@ public class Connection { * @return true if the packet should be sent */ boolean packetSendChoke(long timeoutMs) { - if (false) return true; + // if (false) return true; // <--- what the fuck?? long start = _context.clock().now(); long writeExpire = start + timeoutMs; boolean started = false; @@ -168,9 +169,9 @@ public class Connection { // no need to wait until the other side has ACKed us before sending the first few wsize // packets through - // if (!_connected) - // return false; - + // Incorrect assumption, the constructor defaults _connected to true --Sponge + if (!_connected) + return false; started = true; if ( (_outboundPackets.size() >= _options.getWindowSize()) || (_activeResends > 0) || (_lastSendId - _highestAckedThrough > _options.getWindowSize()) ) { @@ -186,12 +187,12 @@ public class Connection { if (_log.shouldLog(Log.DEBUG)) _log.debug("Outbound window is full (" + _outboundPackets.size() + "/" + _options.getWindowSize() + "/" + _activeResends + "), waiting " + timeLeft); - try { _outboundPackets.wait(timeLeft); } catch (InterruptedException ie) {} + try { _outboundPackets.wait(Math.min(timeLeft,250l)); } catch (InterruptedException ie) {} } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Outbound window is full (" + _outboundPackets.size() + "/" + _activeResends + "), waiting indefinitely"); - try { _outboundPackets.wait(10*1000); } catch (InterruptedException ie) {} + try { _outboundPackets.wait(250); } catch (InterruptedException ie) {} //10*1000 } } else { _context.statManager().addRateData("stream.chokeSizeEnd", _outboundPackets.size(), _context.clock().now() - start); @@ -494,7 +495,6 @@ public class Connection { synchronized (_connectLock) { _connectLock.notifyAll(); } if (_log.shouldLog(Log.DEBUG)) _log.debug("Disconnecting " + toString(), new Exception("discon")); - if (!cleanDisconnect) { _hardDisconnected = true; if (_log.shouldLog(Log.WARN)) @@ -1017,7 +1017,6 @@ public class Connection { /** * Coordinate the resends of a given packet - * */ public class ResendPacketEvent extends SimpleTimer2.TimedEvent { private PacketLocal _packet; From 609e70692d6c43475964a205e4b0a5e27b2f5617 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 15 Feb 2009 14:50:15 +0000 Subject: [PATCH 133/191] -4 --- history.txt | 24 +++++++++++++++++++ .../src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index 45d89effa..9c0c5d294 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,27 @@ +2009-02-15 zzz + * Add licenses to all packages + * I2PSession: Concurrent _messagesReceived + * i2psnark: tmp file removal try #3 + * I2PTunnel: + - Don't buffer POST data in HTTPClient + - Display destination even when stopped + - Enable key generation, dest modification, and + hashcash estimation in the GUI + - Add new CONNECT client + * NetDb: Enforce 60s minimum leaseset publish interval + * Streaming lib: + - Plug connection leak + - Move ConEvent from SimpleTimer to SimpleScheduler + - Move RetransmissionTimer (ResendPacketEvent) + from SimpleTimer to new SimpleTimer2 + - Move ActivityTimer and Flusher from SimpleTimer to RetransmissionTimer + - SimpleTimer2 allows specifying "fuzz" to reduce + timer queue churn further + * Susidns: Fix save of new dest broken in 0.7 + * TunnelPool: + - Allow leasesets with reduced leases for robustness and startup speed + - Plug in-progress build leak + 2009-02-07 zzz * ClientConnectionRunner, Shitlist, TunnelDispatcher: Update using concurrent diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index a34e9238c..5f58c2cdd 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 3; + public final static long BUILD = 4; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From e151ef74e10a8de1f8690300cf8a8d7f24a8a0fb Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 16 Feb 2009 19:42:28 +0000 Subject: [PATCH 134/191] * Streaming lib: Plug timer leak, don't send keepalives after close, don't disconnect hard after close --- .../net/i2p/client/streaming/Connection.java | 25 +++++++++++++------ .../client/streaming/ConnectionHandler.java | 3 ++- .../src/net/i2p/client/streaming/Packet.java | 2 ++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java index 3beed8c8f..6b99fdc00 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java @@ -839,6 +839,8 @@ public class Connection { setFuzz(5*1000); // sloppy timer, don't reschedule unless at least 5s later } public void timeReached() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Fire inactivity timer on " + Connection.this.toString()); // uh, nothing more to do... if (!_connected) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Inactivity timeout reached, but we are already closed"); @@ -864,6 +866,9 @@ public class Connection { // if one of us can't talk... // No - not true - data and acks are still going back and forth. // Prevent zombie connections by keeping the inactivity timer. + // Not sure why... receiving a close but never sending one? + // If so we can probably re-enable this for _closeSentOn. + // For further investigation... //if ( (_closeSentOn > 0) || (_closeReceivedOn > 0) ) { // if (_log.shouldLog(Log.DEBUG)) _log.debug("Inactivity timeout reached, but we are closing"); // return; @@ -873,15 +878,17 @@ public class Connection { // bugger it, might as well do the hard work now switch (_options.getInactivityAction()) { - case ConnectionOptions.INACTIVITY_ACTION_SEND: - if (_log.shouldLog(Log.WARN)) - _log.warn("Sending some data due to inactivity"); - _receiver.send(null, 0, 0, true); - break; case ConnectionOptions.INACTIVITY_ACTION_NOOP: if (_log.shouldLog(Log.WARN)) _log.warn("Inactivity timer expired, but we aint doin' shit"); break; + case ConnectionOptions.INACTIVITY_ACTION_SEND: + if (_closeSentOn <= 0 && _closeReceivedOn <= 0) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Sending some data due to inactivity"); + _receiver.send(null, 0, 0, true); + break; + } // else fall through case ConnectionOptions.INACTIVITY_ACTION_DISCONNECT: // fall through default: @@ -897,7 +904,9 @@ public class Connection { _inputStream.streamErrorOccurred(new IOException("Inactivity timeout")); _outputStream.streamErrorOccurred(new IOException("Inactivity timeout")); - disconnect(false); + // Clean disconnect if we have already scheduled one + // (generally because we already sent a close) + disconnect(_disconnectScheduledOn >= 0); break; } } @@ -1046,7 +1055,9 @@ public class Connection { if (_packet.getAckTime() > 0) return false; - if (_resetSent || _resetReceived) { + if (_resetSent || _resetReceived || !_connected) { + if(_log.shouldLog(Log.WARN) && (!_resetSent) && (!_resetReceived)) + _log.warn("??? no resets but not connected: " + _packet); // don't think this is possible _packet.cancelled(); return false; } diff --git a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java index d468372ec..b96eaea63 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/ConnectionHandler.java @@ -71,7 +71,8 @@ class ConnectionHandler { if (!_active) { if (_log.shouldLog(Log.WARN)) _log.warn("Dropping new SYN request, as we're not listening"); - sendReset(packet); + if (packet.isFlagSet(Packet.FLAG_SYNCHRONIZE)) + sendReset(packet); return; } if (_log.shouldLog(Log.DEBUG)) diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Packet.java b/apps/streaming/java/src/net/i2p/client/streaming/Packet.java index 9d4177256..8fc0f02fa 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/Packet.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/Packet.java @@ -41,6 +41,8 @@ import net.i2p.util.Log; *
  • {@link #FLAG_DELAY_REQUESTED}: 2 byte integer
  • *
  • {@link #FLAG_MAX_PACKET_SIZE_INCLUDED}: 2 byte integer
  • *
  • {@link #FLAG_PROFILE_INTERACTIVE}: no option data
  • + *
  • {@link #FLAG_ECHO}: no option data
  • + *
  • {@link #FLAG_NO_ACK}: no option data
  • * * *

    If the signature is included, it uses the Destination's DSA key From fd32d77976e08b08c4e17879d0952aa625192af2 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 16 Feb 2009 19:43:59 +0000 Subject: [PATCH 135/191] -5 --- history.txt | 4 ++++ router/java/src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index 9c0c5d294..62c4430e9 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,7 @@ +2009-02-16 zzz + * Streaming lib: Plug timer leak, don't send keepalives + after close, don't disconnect hard after close + 2009-02-15 zzz * Add licenses to all packages * I2PSession: Concurrent _messagesReceived diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 5f58c2cdd..5203f2360 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 4; + public final static long BUILD = 5; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From f3143d8b3d65e7add8dcf9ac5182652182fde878 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 18 Feb 2009 20:54:55 +0000 Subject: [PATCH 136/191] case insensitive sort on stat groups --- core/java/src/net/i2p/stat/StatManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/java/src/net/i2p/stat/StatManager.java b/core/java/src/net/i2p/stat/StatManager.java index 4c5c69c79..56af55f71 100644 --- a/core/java/src/net/i2p/stat/StatManager.java +++ b/core/java/src/net/i2p/stat/StatManager.java @@ -1,5 +1,6 @@ package net.i2p.stat; +import java.text.Collator; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -178,7 +179,7 @@ public class StatManager { /** Group name (String) to a Set of stat names, ordered alphabetically */ public Map getStatsByGroup() { - Map groups = new TreeMap(); + Map groups = new TreeMap(Collator.getInstance()); for (Iterator iter = _frequencyStats.values().iterator(); iter.hasNext();) { FrequencyStat stat = (FrequencyStat) iter.next(); if (!groups.containsKey(stat.getGroupName())) groups.put(stat.getGroupName(), new TreeSet()); From fbe7e42f46b08fc1d4d8cf15676d0779c986e5d3 Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 20 Feb 2009 17:53:17 +0000 Subject: [PATCH 137/191] fixed a NPE --- core/java/src/net/i2p/data/PrivateKeyFile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java index d9e52aecc..b5d68ee41 100644 --- a/core/java/src/net/i2p/data/PrivateKeyFile.java +++ b/core/java/src/net/i2p/data/PrivateKeyFile.java @@ -261,7 +261,7 @@ public class PrivateKeyFile { public String toString() { StringBuffer s = new StringBuffer(128); s.append("Dest: "); - s.append(this.dest.toBase64()); + s.append(this.dest != null ? this.dest.toBase64() : "null"); s.append("\nContains: "); s.append(this.dest); s.append("\nPrivate Key: "); From f4c3607c4d7725997fc5d9627d70a305d3a2d0e4 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 22 Feb 2009 00:35:24 +0000 Subject: [PATCH 138/191] * I2PTunnel: - Add new IRCServer tunnel type - Catch OOMs in HTTPServer - Name the IRCClient filter threads --- .../java/src/net/i2p/i2ptunnel/I2PTunnel.java | 49 +++++ .../i2p/i2ptunnel/I2PTunnelHTTPServer.java | 9 + .../net/i2p/i2ptunnel/I2PTunnelIRCClient.java | 4 +- .../net/i2p/i2ptunnel/I2PTunnelIRCServer.java | 193 ++++++++++++++++++ .../net/i2p/i2ptunnel/TunnelController.java | 15 +- .../src/net/i2p/i2ptunnel/web/IndexBean.java | 1 + apps/i2ptunnel/jsp/edit.jsp | 4 +- apps/i2ptunnel/jsp/index.jsp | 1 + 8 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java index b72ae18b3..18b517cd2 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java @@ -234,6 +234,8 @@ public class I2PTunnel implements Logging, EventDispatcher { runServer(args, l); } else if ("httpserver".equals(cmdname)) { runHttpServer(args, l); + } else if ("ircserver".equals(cmdname)) { + runIrcServer(args, l); } else if ("textserver".equals(cmdname)) { runTextServer(args, l); } else if ("client".equals(cmdname)) { @@ -383,6 +385,53 @@ public class I2PTunnel implements Logging, EventDispatcher { } } + /** + * Same args as runServer + * (we should stop duplicating all this code...) + */ + public void runIrcServer(String args[], Logging l) { + if (args.length == 3) { + InetAddress serverHost = null; + int portNum = -1; + File privKeyFile = null; + try { + serverHost = InetAddress.getByName(args[0]); + } catch (UnknownHostException uhe) { + l.log("unknown host"); + _log.error(getPrefix() + "Error resolving " + args[0], uhe); + notifyEvent("serverTaskId", Integer.valueOf(-1)); + return; + } + + try { + portNum = Integer.parseInt(args[1]); + } catch (NumberFormatException nfe) { + l.log("invalid port"); + _log.error(getPrefix() + "Port specified is not valid: " + args[1], nfe); + notifyEvent("serverTaskId", Integer.valueOf(-1)); + return; + } + + privKeyFile = new File(args[2]); + if (!privKeyFile.canRead()) { + l.log("private key file does not exist"); + _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[2]); + notifyEvent("serverTaskId", Integer.valueOf(-1)); + return; + } + I2PTunnelServer serv = new I2PTunnelIRCServer(serverHost, portNum, privKeyFile, args[2], l, (EventDispatcher) this, this); + serv.setReadTimeout(readTimeout); + serv.startRunning(); + addtask(serv); + notifyEvent("serverTaskId", Integer.valueOf(serv.getId())); + return; + } else { + l.log("server "); + l.log(" creates a server that sends all incoming data\n" + " of its destination to host:port."); + notifyEvent("serverTaskId", Integer.valueOf(-1)); + } + } + /** * Run the HTTP server pointing at the host and port specified using the private i2p * destination loaded from the specified file, replacing the HTTP headers diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java index 658dd5e32..536844af9 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java @@ -124,8 +124,17 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { _log.error("Error while closing the received i2p con", ex); } } catch (IOException ex) { + try { + socket.close(); + } catch (IOException ioe) {} if (_log.shouldLog(Log.WARN)) _log.warn("Error while receiving the new HTTP request", ex); + } catch (OutOfMemoryError oom) { + try { + socket.close(); + } catch (IOException ioe) {} + if (_log.shouldLog(Log.ERROR)) + _log.error("OOM in HTTP server", oom); } long afterHandle = getTunnel().getContext().clock().now(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java index e6708aa21..5b223b1a4 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java @@ -83,9 +83,9 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable i2ps = createI2PSocket(dest); i2ps.setReadTimeout(readTimeout); StringBuffer expectedPong = new StringBuffer(); - Thread in = new I2PThread(new IrcInboundFilter(s,i2ps, expectedPong)); + Thread in = new I2PThread(new IrcInboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " in"); in.start(); - Thread out = new I2PThread(new IrcOutboundFilter(s,i2ps, expectedPong)); + Thread out = new I2PThread(new IrcOutboundFilter(s,i2ps, expectedPong), "IRC Client " + __clientId + " out"); out.start(); } catch (Exception ex) { if (_log.shouldLog(Log.ERROR)) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java new file mode 100644 index 000000000..970d90ff0 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java @@ -0,0 +1,193 @@ +package net.i2p.i2ptunnel; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.crypto.SHA256Generator; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.util.EventDispatcher; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +/** + * Simple extension to the I2PTunnelServer that filters the registration + * sequence to pass the destination hash of the client through as the hostname, + * so an IRC Server may track users across nick changes. + * + * Of course, this requires the ircd actually use the hostname sent by + * the client rather than the IP. It is common for ircds to ignore the + * hostname in the USER message (unless it's coming from another server) + * since it is easily spoofed. So you have to fix or, if you are lucky, + * configure your ircd first. At least in unrealircd and ngircd this is + * not configurable. + * + * There are three options for mangling the desthash. Put the option in the + * "custom options" section of i2ptunnel. + * - ircserver.cloakKey unset: Cloak with a random value that is persistent for + * the life of this tunnel. This is the default. + * - ircserver.cloakKey=none: Don't cloak. Users may be correlated with their + * (probably) shared clients destination. + * Of course if the ircd does cloaking than this is ok. + * - ircserver.cloakKey=somepassphrase: Cloak with the hash of the passphrase. Use this to + * have consistent mangling across restarts, or to + * have multiple IRC servers cloak consistently to + * be able to track users even when they switch servers. + * Note: don't quote or put spaces in the passphrase, + * the i2ptunnel gui can't handle it. + * + * There is no outbound filtering. + * + * @author zzz + */ +public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { + + private static final Log _log = new Log(I2PTunnelIRCServer.class); + private static final String PROP_CLOAK="ircserver.cloakKey"; + private boolean _cloak; + private byte[] _cloakKey; // 32 bytes of stuff to scramble the dest with + + /** + * @throws IllegalArgumentException if the I2PTunnel does not contain + * valid config to contact the router + */ + public I2PTunnelIRCServer(InetAddress host, int port, String privData, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { + super(host, port, privData, l, notifyThis, tunnel); + initCloak(tunnel); + } + + public I2PTunnelIRCServer(InetAddress host, int port, File privkey, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { + super(host, port, privkey, privkeyname, l, notifyThis, tunnel); + initCloak(tunnel); + } + + public I2PTunnelIRCServer(InetAddress host, int port, InputStream privData, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { + super(host, port, privData, privkeyname, l, notifyThis, tunnel); + initCloak(tunnel); + } + + /** generate a random 32 bytes, or the hash of the passphrase */ + private void initCloak(I2PTunnel tunnel) { + Properties opts = tunnel.getClientOptions(); + String passphrase = opts.getProperty(PROP_CLOAK); + _cloak = passphrase == null || !"none".equals(passphrase); + if (_cloak) { + if (passphrase == null) { + _cloakKey = new byte[Hash.HASH_LENGTH]; + tunnel.getContext().random().nextBytes(_cloakKey); + } else { + _cloakKey = SHA256Generator.getInstance().calculateHash(passphrase.trim().getBytes()).getData(); + } + } + } + + protected void blockingHandle(I2PSocket socket) { + try { + // give them 15 seconds to send in the request + socket.setReadTimeout(15*1000); + InputStream in = socket.getInputStream(); + String modifiedRegistration = filterRegistration(in, cloakDest(socket.getPeerDestination())); + socket.setReadTimeout(readTimeout); + Socket s = new Socket(remoteHost, remotePort); + new I2PTunnelRunner(s, socket, slock, null, modifiedRegistration.getBytes(), null); + } catch (SocketException ex) { + try { + socket.close(); + } catch (IOException ioe) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Error while closing the received i2p con", ex); + } + } catch (IOException ex) { + try { + socket.close(); + } catch (IOException ioe) {} + if (_log.shouldLog(Log.WARN)) + _log.warn("Error while receiving the new IRC Connection", ex); + } catch (OutOfMemoryError oom) { + try { + socket.close(); + } catch (IOException ioe) {} + if (_log.shouldLog(Log.ERROR)) + _log.error("OOM in IRC server", oom); + } + } + + /** + * (Optionally) append 32 bytes of crap to the destination then return + * the first few characters of the hash of the whole thing, + ".i2p". + * Or do we want the full hash if the ircd is going to use this for + * nickserv auto-login? Or even Base32 if it will be used in a + * case-insensitive manner? + * + */ + String cloakDest(Destination d) { + Hash h; + if (_cloak) { + byte[] b = new byte[d.size() + _cloakKey.length]; + System.arraycopy(b, 0, d.toByteArray(), 0, d.size()); + System.arraycopy(b, d.size(), _cloakKey, 0, _cloakKey.length); + h = SHA256Generator.getInstance().calculateHash(b); + } else { + h = d.calculateHash(); + } + return h.toBase64().substring(0, 8) + ".i2p"; + } + + /** keep reading until we see USER or SERVER */ + private String filterRegistration(InputStream in, String newHostname) throws IOException { + StringBuffer buf = new StringBuffer(128); + int lineCount = 0; + + while (true) { + String s = DataHelper.readLine(in); + if (s == null) + throw new IOException("EOF reached before the end of the headers [" + buf.toString() + "]"); + if (++lineCount > 10) + throw new IOException("Too many lines before USER or SERVER, giving up"); + s = s.trim(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Got line: " + s); + + String field[]=s.split(" ",5); + String command; + int idx=0; + + if(field[0].charAt(0)==':') + idx++; + + try { command = field[idx++]; } + catch (IndexOutOfBoundsException ioobe) // wtf, server sent borked command? + { + throw new IOException("Dropping defective message: index out of bounds while extracting command."); + } + + if ("USER".equalsIgnoreCase(command)) { + if (field.length < idx + 4) + throw new IOException("Too few parameters in USER message: " + s); + // USER zzz1 hostname localhost :zzz + // => + // USER zzz1 abcd1234.i2p localhost :zzz + // this whole class is for these two lines... + buf.append("USER ").append(field[idx]).append(' ').append(newHostname).append(".i2p "); + buf.append(field[idx+2]).append(' ').append(field[idx+3]).append("\r\n"); + break; + } + buf.append(s).append("\r\n"); + if ("SERVER".equalsIgnoreCase(command)) + break; + } + if (_log.shouldLog(Log.DEBUG)) + _log.debug("All done, sending: " + buf.toString()); + return buf.toString(); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 3c9640ce5..82b253985 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -58,7 +58,7 @@ public class TunnelController implements Logging { setConfig(config, prefix); _messages = new ArrayList(4); _running = false; - if (createKey && ("server".equals(getType()) || "httpserver".equals(getType())) ) + if (createKey && getType().endsWith("server")) createPrivateKey(); _starting = getStartOnLoad(); } @@ -148,6 +148,8 @@ public class TunnelController implements Logging { startServer(); } else if ("httpserver".equals(type)) { startHttpServer(); + } else if ("ircserver".equals(type)) { + startIrcServer(); } else { if (_log.shouldLog(Log.ERROR)) _log.error("Cannot start tunnel - unknown type [" + type + "]"); @@ -274,6 +276,17 @@ public class TunnelController implements Logging { _running = true; } + private void startIrcServer() { + setI2CPOptions(); + setSessionOptions(); + String targetHost = getTargetHost(); + String targetPort = getTargetPort(); + String privKeyFile = getPrivKeyFile(); + _tunnel.runIrcServer(new String[] { targetHost, targetPort, privKeyFile }, this); + acquire(); + _running = true; + } + private void setListenOn() { String listenOn = getListenOnInterface(); if ( (listenOn != null) && (listenOn.length() > 0) ) { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 46b555772..87d8e26f6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -386,6 +386,7 @@ public class IndexBean { else if ("httpserver".equals(internalType)) return "HTTP server"; else if ("sockstunnel".equals(internalType)) return "SOCKS 5 proxy"; else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy"; + else if ("ircserver".equals(internalType)) return "IRC server"; else return internalType; } diff --git a/apps/i2ptunnel/jsp/edit.jsp b/apps/i2ptunnel/jsp/edit.jsp index 67fdf016c..b58798b20 100644 --- a/apps/i2ptunnel/jsp/edit.jsp +++ b/apps/i2ptunnel/jsp/edit.jsp @@ -16,10 +16,8 @@ String tun = request.getParameter("tunnel"); int curTunnel = -1; if (EditBean.isClient(type)) { %><% - } else if ("server".equals(type) || "httpserver".equals(type)) { - %><% } else { - %>Invalid tunnel type<% + %><% } } %> diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index b96236ae1..77303267c 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -260,6 +260,7 @@

    From 3603cc23ee0725256a0321e6fba59c190c1e37fe Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 22 Feb 2009 02:58:00 +0000 Subject: [PATCH 139/191] add socks 4/4a support --- .../i2p/i2ptunnel/socks/SOCKS4aServer.java | 283 ++++++++++++++++++ .../i2ptunnel/socks/SOCKSServerFactory.java | 4 + .../src/net/i2p/i2ptunnel/web/IndexBean.java | 2 +- apps/i2ptunnel/jsp/index.jsp | 2 +- 4 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java new file mode 100644 index 000000000..2745cb0fa --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java @@ -0,0 +1,283 @@ +/* I2PSOCKSTunnel is released under the terms of the GNU GPL, + * with an additional exception. For further details, see the + * licensing terms in I2PTunnel.java. + * + * Copyright (c) 2004 by human + */ +package net.i2p.i2ptunnel.socks; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.List; + +import net.i2p.I2PAppContext; +import net.i2p.I2PException; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.data.DataFormatException; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.util.HexDump; +import net.i2p.util.Log; + +/* + * Class that manages SOCKS 4/4a connections, and forwards them to + * destination hosts or (eventually) some outproxy. + * + * @author zzz modded from SOCKS5Server + */ +public class SOCKS4aServer extends SOCKSServer { + private static final Log _log = new Log(SOCKS4aServer.class); + + private Socket clientSock = null; + private boolean setupCompleted = false; + + /** + * Create a SOCKS4a server that communicates with the client using + * the specified socket. This method should not be invoked + * directly: new SOCKS4aServer objects should be created by using + * SOCKSServerFactory.createSOCSKServer(). It is assumed that the + * SOCKS VER field has been stripped from the input stream of the + * client socket. + * + * @param clientSock client socket + */ + public SOCKS4aServer(Socket clientSock) { + this.clientSock = clientSock; + } + + public Socket getClientSocket() throws SOCKSException { + setupServer(); + + return clientSock; + } + + protected void setupServer() throws SOCKSException { + if (setupCompleted) { return; } + + DataInputStream in; + DataOutputStream out; + try { + in = new DataInputStream(clientSock.getInputStream()); + out = new DataOutputStream(clientSock.getOutputStream()); + + manageRequest(in, out); + } catch (IOException e) { + throw new SOCKSException("Connection error (" + e.getMessage() + ")"); + } + + setupCompleted = true; + } + + /** + * SOCKS4a request management. This method assumes that all the + * stuff preceding or enveloping the actual request + * has been stripped out of the input/output streams. + */ + private void manageRequest(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException { + + int command = in.readByte() & 0xff; + switch (command) { + case Command.CONNECT: + break; + case Command.BIND: + _log.debug("BIND command is not supported!"); + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + throw new SOCKSException("BIND command not supported"); + default: + _log.debug("unknown command in request (" + Integer.toHexString(command) + ")"); + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + throw new SOCKSException("Invalid command in request"); + } + + connPort = in.readUnsignedShort(); + if (connPort == 0) { + _log.debug("trying to connect to TCP port 0? Dropping!"); + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + throw new SOCKSException("Invalid port number in request"); + } + + connHostName = new String(""); + boolean alreadyWarned = false; + for (int i = 0; i < 4; ++i) { + int octet = in.readByte() & 0xff; + connHostName += Integer.toString(octet); + if (i != 3) { + connHostName += "."; + if (octet != 0 && !alreadyWarned) { + _log.warn("IPV4 address type in request: " + connHostName + ". Is your client secure?"); + alreadyWarned = true; + } + } + } + + // discard user name + readString(in); + + // SOCKS 4a + if (connHostName.startsWith("0.0.0.") && !connHostName.equals("0.0.0.0")) + connHostName = readString(in); + } + + private String readString(DataInputStream in) throws IOException { + StringBuffer sb = new StringBuffer(16); + char c; + while ((c = (char) (in.readByte() & 0xff)) != 0) + sb.append(c); + return sb.toString(); + } + + protected void confirmConnection() throws SOCKSException { + DataInputStream in; + DataOutputStream out; + try { + out = new DataOutputStream(clientSock.getOutputStream()); + + sendRequestReply(Reply.SUCCEEDED, InetAddress.getByName("127.0.0.1"), 1, out); + } catch (IOException e) { + throw new SOCKSException("Connection error (" + e.getMessage() + ")"); + } + } + + /** + * Send the specified reply to a request of the client. Either + * one of inetAddr or domainName can be null, depending on + * addressType. + */ + private void sendRequestReply(int replyCode, InetAddress inetAddr, + int bindPort, DataOutputStream out) throws IOException { + ByteArrayOutputStream reps = new ByteArrayOutputStream(); + DataOutputStream dreps = new DataOutputStream(reps); + + // Reserved byte, should be 0x00 + dreps.write(0x00); + dreps.write(replyCode); + dreps.writeShort(bindPort); + dreps.write(inetAddr.getAddress()); + + byte[] reply = reps.toByteArray(); + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug("Sending request reply:\n" + HexDump.dump(reply)); + } + + out.write(reply); + } + + /** + * Get an I2PSocket that can be used to send/receive 8-bit clean data + * to/from the destination of the SOCKS connection. + * + * @return an I2PSocket connected with the destination + */ + public I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException { + setupServer(); + + if (connHostName == null) { + _log.error("BUG: destination host name has not been initialized!"); + throw new SOCKSException("BUG! See the logs!"); + } + if (connPort == 0) { + _log.error("BUG: destination port has not been initialized!"); + throw new SOCKSException("BUG! See the logs!"); + } + + DataOutputStream out; // for errors + try { + out = new DataOutputStream(clientSock.getOutputStream()); + } catch (IOException e) { + throw new SOCKSException("Connection error (" + e.getMessage() + ")"); + } + + // FIXME: here we should read our config file, select an + // outproxy, and instantiate the proper socket class that + // handles the outproxy itself (SOCKS4a, SOCKS4a, HTTP CONNECT...). + I2PSocket destSock; + + try { + if (connHostName.toLowerCase().endsWith(".i2p")) { + _log.debug("connecting to " + connHostName + "..."); + // 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(I2PTunnel.destFromName(connHostName)); + } else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) { + String err = "No localhost accesses allowed through the Socks Proxy"; + _log.error(err); + try { + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + } catch (IOException ioe) {} + throw new SOCKSException(err); + } else if (connPort == 80) { + // rewrite GET line to include hostname??? or add Host: line??? + // or forward to local eepProxy (but that's a Socket not an I2PSocket) + // use eepProxy configured outproxies? + String err = "No handler for HTTP outproxy implemented - to: " + connHostName; + _log.error(err); + try { + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + } catch (IOException ioe) {} + throw new SOCKSException(err); + } else { + List proxies = t.getProxies(connPort); + if (proxies == null || proxies.size() <= 0) { + String err = "No outproxy configured for port " + connPort + " and no default configured either"; + _log.error(err); + try { + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + } catch (IOException ioe) {} + throw new SOCKSException(err); + } + int p = I2PAppContext.getGlobalContext().random().nextInt(proxies.size()); + String proxy = proxies.get(p); + _log.debug("connecting to port " + connPort + " proxy " + proxy + " for " + connHostName + "..."); + // this isn't going to work, these need to be socks outproxies so we need + // to do a socks session to them? + destSock = t.createI2PSocket(I2PTunnel.destFromName(proxy)); + } + confirmConnection(); + _log.debug("connection confirmed - exchanging data..."); + } catch (DataFormatException e) { + try { + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + } catch (IOException ioe) {} + throw new SOCKSException("Error in destination format"); + } catch (SocketException e) { + try { + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + } catch (IOException ioe) {} + throw new SOCKSException("Error connecting (" + + e.getMessage() + ")"); + } catch (IOException e) { + try { + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + } catch (IOException ioe) {} + throw new SOCKSException("Error connecting (" + + e.getMessage() + ")"); + } catch (I2PException e) { + try { + sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); + } catch (IOException ioe) {} + throw new SOCKSException("Error connecting (" + + e.getMessage() + ")"); + } + + return destSock; + } + + /* + * Some namespaces to enclose SOCKS protocol codes + */ + private static class Command { + private static final int CONNECT = 0x01; + private static final int BIND = 0x02; + } + + private static class Reply { + private static final int SUCCEEDED = 0x5a; + private static final int CONNECTION_REFUSED = 0x5b; + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java index 67a52d688..80dfacb6a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServerFactory.java @@ -44,6 +44,10 @@ public class SOCKSServerFactory { int socksVer = in.readByte(); switch (socksVer) { + case 0x04: + // SOCKS version 4/4a + serv = new SOCKS4aServer(s); + break; case 0x05: // SOCKS version 5 serv = new SOCKS5Server(s); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 87d8e26f6..045ea5e58 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -384,7 +384,7 @@ public class IndexBean { else if ("ircclient".equals(internalType)) return "IRC client"; else if ("server".equals(internalType)) return "Standard server"; else if ("httpserver".equals(internalType)) return "HTTP server"; - else if ("sockstunnel".equals(internalType)) return "SOCKS 5 proxy"; + else if ("sockstunnel".equals(internalType)) return "SOCKS 4/4a/5 proxy"; else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy"; else if ("ircserver".equals(internalType)) return "IRC server"; else return internalType; diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index 77303267c..7787eb1f5 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -148,7 +148,7 @@ - + From 8bce2fd7a2cf10d25114dcfc3752a7e4b8b8b29f Mon Sep 17 00:00:00 2001 From: sponge Date: Sun, 22 Feb 2009 07:04:31 +0000 Subject: [PATCH 140/191] Hopeful BOB fixes for orphaned tunnels. Additional comments in TCPio addressing performance. --- apps/BOB/src/net/i2p/BOB/I2Plistener.java | 2 +- apps/BOB/src/net/i2p/BOB/MUXlisten.java | 37 ++++++++++++++++++----- apps/BOB/src/net/i2p/BOB/TCPio.java | 19 ++++++++++++ apps/BOB/src/net/i2p/BOB/TCPlistener.java | 2 +- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/apps/BOB/src/net/i2p/BOB/I2Plistener.java b/apps/BOB/src/net/i2p/BOB/I2Plistener.java index 1561b7a22..c59683270 100644 --- a/apps/BOB/src/net/i2p/BOB/I2Plistener.java +++ b/apps/BOB/src/net/i2p/BOB/I2Plistener.java @@ -70,7 +70,7 @@ public class I2Plistener implements Runnable { boolean g = false; I2PSocket sessSocket = null; - serverSocket.setSoTimeout(100); + serverSocket.setSoTimeout(50); database.getReadLock(); info.getReadLock(); if(info.exists("INPORT")) { diff --git a/apps/BOB/src/net/i2p/BOB/MUXlisten.java b/apps/BOB/src/net/i2p/BOB/MUXlisten.java index bd52e27fd..89ab53fe6 100644 --- a/apps/BOB/src/net/i2p/BOB/MUXlisten.java +++ b/apps/BOB/src/net/i2p/BOB/MUXlisten.java @@ -173,7 +173,7 @@ die: { boolean spin = true; while(spin) { try { - Thread.sleep(1000); //sleep for 1000 ms (One second) + Thread.sleep(200); //sleep for 200 ms (Two thenths second) } catch(InterruptedException e) { // nop } @@ -213,14 +213,21 @@ die: { } } // die + try { + Thread.sleep(500); //sleep for 500 ms (One half second) + } catch(InterruptedException ex) { + // nop + } // wait for child threads and thread groups to die // System.out.println("MUXlisten: waiting for children"); - while(tg.activeCount() + tg.activeGroupCount() != 0) { + if(tg.activeCount() + tg.activeGroupCount() != 0) { tg.interrupt(); // unwedge any blocking threads. - try { - Thread.sleep(100); //sleep for 100 ms (One tenth second) - } catch(InterruptedException ex) { - // nop + while(tg.activeCount() + tg.activeGroupCount() != 0) { + try { + Thread.sleep(100); //sleep for 100 ms (One tenth second) + } catch(InterruptedException ex) { + // nop + } } } tg.destroy(); @@ -260,17 +267,33 @@ die: { } // This is here to catch when something fucks up REALLY bad. if(tg != null) { - while(tg.activeCount() + tg.activeGroupCount() != 0) { + if(tg.activeCount() + tg.activeGroupCount() != 0) { tg.interrupt(); // unwedge any blocking threads. + while(tg.activeCount() + tg.activeGroupCount() != 0) { try { Thread.sleep(100); //sleep for 100 ms (One tenth second) } catch(InterruptedException ex) { // nop } + } } tg.destroy(); // Zap reference to the ThreadGroup so the JVM can GC it. tg = null; } + + // Lastly try to close things again. + if(this.come_in) { + try { + listener.close(); + } catch(IOException e) { + } + } + try { + socketManager.destroySocketManager(); + } catch(Exception e) { + // nop + } + } } diff --git a/apps/BOB/src/net/i2p/BOB/TCPio.java b/apps/BOB/src/net/i2p/BOB/TCPio.java index 25290bcdc..41bb7cbe4 100644 --- a/apps/BOB/src/net/i2p/BOB/TCPio.java +++ b/apps/BOB/src/net/i2p/BOB/TCPio.java @@ -56,9 +56,28 @@ public class TCPio implements Runnable { * Copy from source to destination... * and yes, we are totally OK to block here on writes, * The OS has buffers, and I intend to use them. + * We send an interrupt signal to the threadgroup to + * unwedge any pending writes. * */ public void run() { + /* + * NOTE: + * The write method of OutputStream calls the write method of + * one argument on each of the bytes to be written out. + * Subclasses are encouraged to override this method and provide + * a more efficient implementation. + * + * So, is this really a performance problem? + * Should we expand to several bytes? + * I don't believe there would be any gain, since read method + * has the same reccomendations. If anyone has a better way to + * do this, I'm interested in performance improvements. + * + * --Sponge + * + */ + int b; byte a[] = new byte[1]; boolean spin = true; diff --git a/apps/BOB/src/net/i2p/BOB/TCPlistener.java b/apps/BOB/src/net/i2p/BOB/TCPlistener.java index 99ae047d3..30380a55d 100644 --- a/apps/BOB/src/net/i2p/BOB/TCPlistener.java +++ b/apps/BOB/src/net/i2p/BOB/TCPlistener.java @@ -77,7 +77,7 @@ public class TCPlistener implements Runnable { } try { Socket server = new Socket(); - listener.setSoTimeout(1000); + listener.setSoTimeout(50); // Half of the expected time from MUXlisten info.releaseReadLock(); database.releaseReadLock(); while(spin) { From 532077a4c15b6bf4b2f634a7d695de0eb96e55d1 Mon Sep 17 00:00:00 2001 From: sponge Date: Sun, 22 Feb 2009 07:26:08 +0000 Subject: [PATCH 141/191] BOB version bump. Router Build bump. --- apps/BOB/src/net/i2p/BOB/DoCMDS.java | 2 +- history.txt | 4 ++++ router/java/src/net/i2p/router/RouterVersion.java | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/BOB/src/net/i2p/BOB/DoCMDS.java b/apps/BOB/src/net/i2p/BOB/DoCMDS.java index 6033e303b..099d69fec 100644 --- a/apps/BOB/src/net/i2p/BOB/DoCMDS.java +++ b/apps/BOB/src/net/i2p/BOB/DoCMDS.java @@ -46,7 +46,7 @@ public class DoCMDS implements Runnable { // FIX ME // I need a better way to do versioning, but this will do for now. - public static final String BMAJ = "00", BMIN = "00", BREV = "03", BEXT = ""; + public static final String BMAJ = "00", BMIN = "00", BREV = "04", BEXT = ""; public static final String BOBversion = BMAJ + "." + BMIN + "." + BREV + BEXT; private Socket server; private Properties props; diff --git a/history.txt b/history.txt index 62c4430e9..ec81d5a43 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,7 @@ +2009-02-22 sponge + * BOB: Orphan tunnel issue fix, bump BOB version + * bump to Build 6 + 2009-02-16 zzz * Streaming lib: Plug timer leak, don't send keepalives after close, don't disconnect hard after close diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 5203f2360..c08948969 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 5; + public final static long BUILD = 6; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From 720aa704c4c5b8750e123045131f5ce8a00cdb97 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 23 Feb 2009 05:09:44 +0000 Subject: [PATCH 142/191] port streamr to i2ptunnel --- LICENSE.txt | 4 + .../java/src/net/i2p/i2ptunnel/I2PTunnel.java | 82 +++++++ .../net/i2p/i2ptunnel/I2PTunnelIRCServer.java | 9 - .../net/i2p/i2ptunnel/TunnelController.java | 59 +++-- .../i2p/i2ptunnel/streamr/MultiSource.java | 60 +++++ .../src/net/i2p/i2ptunnel/streamr/Pinger.java | 59 +++++ .../i2ptunnel/streamr/StreamrConsumer.java | 64 +++++ .../i2ptunnel/streamr/StreamrProducer.java | 70 ++++++ .../net/i2p/i2ptunnel/streamr/Subscriber.java | 75 ++++++ .../src/net/i2p/i2ptunnel/udp/I2PSink.java | 70 ++++++ .../i2p/i2ptunnel/udp/I2PSinkAnywhere.java | 67 ++++++ .../src/net/i2p/i2ptunnel/udp/I2PSource.java | 123 ++++++++++ .../java/src/net/i2p/i2ptunnel/udp/Sink.java | 17 ++ .../src/net/i2p/i2ptunnel/udp/Source.java | 15 ++ .../src/net/i2p/i2ptunnel/udp/Stream.java | 15 ++ .../src/net/i2p/i2ptunnel/udp/UDPSink.java | 74 ++++++ .../src/net/i2p/i2ptunnel/udp/UDPSource.java | 83 +++++++ .../udpTunnel/I2PTunnelUDPClientBase.java | 218 ++++++++++++++++++ .../udpTunnel/I2PTunnelUDPServerBase.java | 212 +++++++++++++++++ .../src/net/i2p/i2ptunnel/web/IndexBean.java | 8 +- apps/i2ptunnel/jsp/editClient.jsp | 30 ++- apps/i2ptunnel/jsp/editServer.jsp | 8 + apps/i2ptunnel/jsp/index.jsp | 2 + 23 files changed, 1375 insertions(+), 49 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Pinger.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Subscriber.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSink.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSource.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Sink.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Source.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Stream.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java diff --git a/LICENSE.txt b/LICENSE.txt index 324f532c6..e93798548 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -113,6 +113,10 @@ Applications: See licenses/LICENSE-I2PTunnel.txt See licenses/LICENSE-GPLv2.txt + I2PTunnel UDP and Streamr: + By welterde. + See licenses/LICENSE-GPLv2.txt + Jetty 5.1.12: Copyright 2000-2004 Mort Bay Consulting Pty. Ltd. See licenses/LICENSE-Apache1.1.txt diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java index 18b517cd2..dc9dfd2fc 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java @@ -62,6 +62,8 @@ import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel; +import net.i2p.i2ptunnel.streamr.StreamrConsumer; +import net.i2p.i2ptunnel.streamr.StreamrProducer; import net.i2p.util.EventDispatcher; import net.i2p.util.EventDispatcherImpl; import net.i2p.util.Log; @@ -248,6 +250,10 @@ public class I2PTunnel implements Logging, EventDispatcher { runSOCKSTunnel(args, l); } else if ("connectclient".equals(cmdname)) { runConnectClient(args, l); + } else if ("streamrclient".equals(cmdname)) { + runStreamrClient(args, l); + } else if ("streamrserver".equals(cmdname)) { + runStreamrServer(args, l); } else if ("config".equals(cmdname)) { runConfig(args, l); } else if ("listen_on".equals(cmdname)) { @@ -800,6 +806,82 @@ public class I2PTunnel implements Logging, EventDispatcher { } } + /** + * Streamr client + * + * @param args {targethost, targetport, destinationString} + * @param l logger to receive events and output + */ + public void runStreamrClient(String args[], Logging l) { + if (args.length == 3) { + InetAddress host; + try { + host = InetAddress.getByName(args[0]); + } catch (UnknownHostException uhe) { + l.log("unknown host"); + _log.error(getPrefix() + "Error resolving " + args[0], uhe); + notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1)); + return; + } + + int port = -1; + try { + port = Integer.parseInt(args[1]); + } catch (NumberFormatException nfe) { + l.log("invalid port"); + _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe); + notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1)); + return; + } + + StreamrConsumer task = new StreamrConsumer(host, port, args[2], l, (EventDispatcher) this, this); + task.startRunning(); + addtask(task); + notifyEvent("streamrtunnelTaskId", Integer.valueOf(task.getId())); + } else { + l.log("streamrclient "); + l.log(" creates a tunnel that receives streaming data."); + notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1)); + } + } + + /** + * Streamr server + * + * @param args {port, privkeyfile} + * @param l logger to receive events and output + */ + public void runStreamrServer(String args[], Logging l) { + if (args.length == 2) { + int port = -1; + try { + port = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + l.log("invalid port"); + _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe); + notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1)); + return; + } + + File privKeyFile = new File(args[1]); + if (!privKeyFile.canRead()) { + l.log("private key file does not exist"); + _log.error(getPrefix() + "Private key file does not exist or is not readable: " + args[3]); + notifyEvent("serverTaskId", Integer.valueOf(-1)); + return; + } + + StreamrProducer task = new StreamrProducer(port, privKeyFile, args[1], l, (EventDispatcher) this, this); + task.startRunning(); + addtask(task); + notifyEvent("streamrtunnelTaskId", Integer.valueOf(task.getId())); + } else { + l.log("streamrserver "); + l.log(" creates a tunnel that sends streaming data."); + notifyEvent("streamrtunnelTaskId", Integer.valueOf(-1)); + } + } + /** * Specify the i2cp host and port * diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java index 970d90ff0..aa95e526c 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCServer.java @@ -61,21 +61,12 @@ public class I2PTunnelIRCServer extends I2PTunnelServer implements Runnable { * @throws IllegalArgumentException if the I2PTunnel does not contain * valid config to contact the router */ - public I2PTunnelIRCServer(InetAddress host, int port, String privData, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { - super(host, port, privData, l, notifyThis, tunnel); - initCloak(tunnel); - } public I2PTunnelIRCServer(InetAddress host, int port, File privkey, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { super(host, port, privkey, privkeyname, l, notifyThis, tunnel); initCloak(tunnel); } - public I2PTunnelIRCServer(InetAddress host, int port, InputStream privData, String privkeyname, Logging l, EventDispatcher notifyThis, I2PTunnel tunnel) { - super(host, port, privData, privkeyname, l, notifyThis, tunnel); - initCloak(tunnel); - } - /** generate a random 32 bytes, or the hash of the passphrase */ private void initCloak(I2PTunnel tunnel) { Properties opts = tunnel.getClientOptions(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 82b253985..6c5fa4eb9 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -134,6 +134,8 @@ public class TunnelController implements Logging { _log.warn("Cannot start the tunnel - no type specified"); return; } + setI2CPOptions(); + setSessionOptions(); if ("httpclient".equals(type)) { startHttpClient(); } else if("ircclient".equals(type)) { @@ -144,21 +146,26 @@ public class TunnelController implements Logging { startConnectClient(); } else if ("client".equals(type)) { startClient(); + } else if ("streamrclient".equals(type)) { + startStreamrClient(); } else if ("server".equals(type)) { startServer(); } else if ("httpserver".equals(type)) { startHttpServer(); } else if ("ircserver".equals(type)) { startIrcServer(); + } else if ("streamrserver".equals(type)) { + startStreamrServer(); } else { if (_log.shouldLog(Log.ERROR)) _log.error("Cannot start tunnel - unknown type [" + type + "]"); + return; } + acquire(); + _running = true; } private void startHttpClient() { - setI2CPOptions(); - setSessionOptions(); setListenOn(); String listenPort = getListenPort(); String proxyList = getProxyList(); @@ -167,13 +174,9 @@ public class TunnelController implements Logging { _tunnel.runHttpClient(new String[] { listenPort, sharedClient }, this); else _tunnel.runHttpClient(new String[] { listenPort, sharedClient, proxyList }, this); - acquire(); - _running = true; } private void startConnectClient() { - setI2CPOptions(); - setSessionOptions(); setListenOn(); String listenPort = getListenPort(); String proxyList = getProxyList(); @@ -182,31 +185,39 @@ public class TunnelController implements Logging { _tunnel.runConnectClient(new String[] { listenPort, sharedClient }, this); else _tunnel.runConnectClient(new String[] { listenPort, sharedClient, proxyList }, this); - acquire(); - _running = true; } private void startIrcClient() { - setI2CPOptions(); - setSessionOptions(); setListenOn(); String listenPort = getListenPort(); String dest = getTargetDestination(); String sharedClient = getSharedClient(); _tunnel.runIrcClient(new String[] { listenPort, dest, sharedClient }, this); - acquire(); - _running = true; } private void startSocksClient() { - setI2CPOptions(); - setSessionOptions(); setListenOn(); String listenPort = getListenPort(); String sharedClient = getSharedClient(); _tunnel.runSOCKSTunnel(new String[] { listenPort, sharedClient }, this); - acquire(); - _running = true; + } + + /* + * Streamr client is a UDP server, use the listenPort field for targetPort + * and the listenOnInterface field for the targetHost + */ + private void startStreamrClient() { + String targetHost = getListenOnInterface(); + String targetPort = getListenPort(); + String dest = getTargetDestination(); + _tunnel.runStreamrClient(new String[] { targetHost, targetPort, dest }, this); + } + + /** Streamr server is a UDP client, use the targetPort field for listenPort */ + private void startStreamrServer() { + String listenPort = getTargetPort(); + String privKeyFile = getPrivKeyFile(); + _tunnel.runStreamrServer(new String[] { listenPort, privKeyFile }, this); } /** @@ -242,49 +253,33 @@ public class TunnelController implements Logging { } private void startClient() { - setI2CPOptions(); - setSessionOptions(); setListenOn(); String listenPort = getListenPort(); String dest = getTargetDestination(); String sharedClient = getSharedClient(); _tunnel.runClient(new String[] { listenPort, dest, sharedClient }, this); - acquire(); - _running = true; } private void startServer() { - setI2CPOptions(); - setSessionOptions(); String targetHost = getTargetHost(); String targetPort = getTargetPort(); String privKeyFile = getPrivKeyFile(); _tunnel.runServer(new String[] { targetHost, targetPort, privKeyFile }, this); - acquire(); - _running = true; } private void startHttpServer() { - setI2CPOptions(); - setSessionOptions(); String targetHost = getTargetHost(); String targetPort = getTargetPort(); String spoofedHost = getSpoofedHost(); String privKeyFile = getPrivKeyFile(); _tunnel.runHttpServer(new String[] { targetHost, targetPort, spoofedHost, privKeyFile }, this); - acquire(); - _running = true; } private void startIrcServer() { - setI2CPOptions(); - setSessionOptions(); String targetHost = getTargetHost(); String targetPort = getTargetPort(); String privKeyFile = getPrivKeyFile(); _tunnel.runIrcServer(new String[] { targetHost, targetPort, privKeyFile }, this); - acquire(); - _running = true; } private void setListenOn() { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java new file mode 100644 index 000000000..13d9b5520 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java @@ -0,0 +1,60 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.streamr; + +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.List; + +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.udp.*; + +/** + * Sends to many Sinks + * @author welterde + * @author zzz modded for I2PTunnel + */ +public class MultiSource implements Source, Sink { + public MultiSource() { + this.sinks = new CopyOnWriteArrayList(); + } + + public void setSink(Sink sink) { + this.sink = sink; + } + + public void start() {} + + public void send(Destination ignored_from, byte[] data) { + for(Destination dest : this.sinks) { + this.sink.send(dest, data); + } + } + + public void add(Destination sink) { + this.sinks.add(sink); + } + + public void remove(Destination sink) { + this.sinks.remove(sink); + } + + + + + + + + + + + + + + + + private Sink sink; + private List sinks; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Pinger.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Pinger.java new file mode 100644 index 000000000..a3a797536 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Pinger.java @@ -0,0 +1,59 @@ +package net.i2p.i2ptunnel.streamr; + +import net.i2p.i2ptunnel.udp.*; + +/** + * + * @author welterde/zzz + */ +public class Pinger implements Source, Runnable { + public Pinger() { + this.thread = new Thread(this); + } + public void setSink(Sink sink) { + this.sink = sink; + } + + public void start() { + this.running = true; + this.waitlock = new Object(); + this.thread.start(); + } + + public void stop() { + this.running = false; + synchronized(this.waitlock) { + this.waitlock.notifyAll(); + } + // send unsubscribe-message + byte[] data = new byte[1]; + data[0] = 1; + this.sink.send(null, data); + } + + public void run() { + // send subscribe-message + byte[] data = new byte[1]; + data[0] = 0; + int i = 0; + while(this.running) { + //System.out.print("p"); + this.sink.send(null, data); + synchronized(this.waitlock) { + int delay = 10000; + if (i < 5) { + i++; + delay = 2000; + } + try { + this.waitlock.wait(delay); + } catch(InterruptedException ie) {} + } + } + } + + protected Sink sink; + protected Thread thread; + protected Object waitlock; + protected boolean running; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java new file mode 100644 index 000000000..3fc1d881b --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java @@ -0,0 +1,64 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.streamr; + +import java.net.InetAddress; + +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.i2ptunnel.Logging; +import net.i2p.i2ptunnel.udp.*; +import net.i2p.i2ptunnel.udpTunnel.I2PTunnelUDPClientBase; +import net.i2p.util.EventDispatcher; + +/** + * Compared to a standard I2PTunnel, + * this acts like a client on the I2P side (no privkey file) + * but a server on the UDP side (sends to a configured host/port) + * + * @author welterde + * @author zzz modded for I2PTunnel + */ +public class StreamrConsumer extends I2PTunnelUDPClientBase { + + public StreamrConsumer(InetAddress host, int port, String destination, + Logging l, EventDispatcher notifyThis, + I2PTunnel tunnel) { + super(destination, l, notifyThis, tunnel); + + // create udp-destination + this.sink = new UDPSink(host, port); + setSink(this.sink); + + // create pinger + this.pinger = new Pinger(); + this.pinger.setSink(this); + } + + public final void startRunning() { + super.startRunning(); + // send subscribe-message + this.pinger.start(); + } + + public boolean close(boolean forced) { + // send unsubscribe-message + this.pinger.stop(); + return super.close(forced); + } + + + + + + + + + + + private Sink sink; + private Pinger pinger; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java new file mode 100644 index 000000000..d722c5f95 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java @@ -0,0 +1,70 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.streamr; + +// system +import java.io.File; + +// i2p +import net.i2p.client.I2PSession; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.i2ptunnel.Logging; +import net.i2p.i2ptunnel.udp.*; +import net.i2p.i2ptunnel.udpTunnel.I2PTunnelUDPServerBase; +import net.i2p.util.EventDispatcher; + +/** + * Compared to a standard I2PTunnel, + * this acts like a server on the I2P side (persistent privkey file) + * but a client on the UDP side (receives on a configured port) + * + * @author welterde + * @author zzz modded for I2PTunnel + */ +public class StreamrProducer extends I2PTunnelUDPServerBase { + + public StreamrProducer(int port, + File privkey, String privkeyname, Logging l, + EventDispatcher notifyThis, I2PTunnel tunnel) { + // verify subscription requests + super(true, privkey, privkeyname, l, notifyThis, tunnel); + + // The broadcaster + this.multi = new MultiSource(); + this.multi.setSink(this); + + // The listener + this.subscriber = new Subscriber(this.multi); + setSink(this.subscriber); + + // now start udp-server + this.server = new UDPSource(port); + this.server.setSink(this.multi); + } + + public final void startRunning() { + super.startRunning(); + this.server.start(); + } + + public boolean close(boolean forced) { + // need some stop() methods in UDPSource and MultiSource + return super.close(forced); + } + + + + + + + + + + + private MultiSource multi; + private Source server; + private Sink subscriber; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Subscriber.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Subscriber.java new file mode 100644 index 000000000..97abdb889 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/Subscriber.java @@ -0,0 +1,75 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.streamr; + +// system +import java.io.File; +import java.util.Set; + +// i2p +import net.i2p.client.I2PSession; +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.i2ptunnel.Logging; +import net.i2p.i2ptunnel.udp.*; +import net.i2p.i2ptunnel.udpTunnel.I2PTunnelUDPServerBase; +import net.i2p.util.EventDispatcher; +import net.i2p.util.ConcurrentHashSet; + +/** + * server-mode + * @author welterde + * @author zzz modded from Producer for I2PTunnel + */ +public class Subscriber implements Sink { + + public Subscriber(MultiSource multi) { + this.multi = multi; + // subscriptions + this.subscriptions = new ConcurrentHashSet(); + } + + public void send(Destination dest, byte[] data) { + if(dest == null || data.length < 1) { + // invalid packet + // TODO: write to log + } else { + byte ctrl = data[0]; + if(ctrl == 0) { + if (!this.subscriptions.contains(dest)) { + // subscribe + System.out.println("Add subscription: " + dest.toBase64().substring(0,4)); + this.subscriptions.add(dest); + this.multi.add(dest); + } // else already subscribed + } else if(ctrl == 1) { + // unsubscribe + System.out.println("Remove subscription: " + dest.toBase64().substring(0,4)); + boolean removed = this.subscriptions.remove(dest); + if(removed) + multi.remove(dest); + } else { + // invalid packet + // TODO: write to log + } + } + } + + + + + + + + + + + private I2PSession sess; + private Source listener; + private Set subscriptions; + private MultiSource multi; + private Source server; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSink.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSink.java new file mode 100644 index 000000000..3cbccf139 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSink.java @@ -0,0 +1,70 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.udp; + +// i2p +import net.i2p.client.I2PSession; +import net.i2p.client.I2PSessionException; +import net.i2p.data.Destination; +import net.i2p.client.datagram.I2PDatagramMaker; + +/** + * Producer + * + * This sends to a fixed destination specified in the constructor + * + * @author welterde + */ +public class I2PSink implements Sink { + public I2PSink(I2PSession sess, Destination dest) { + this(sess, dest, false); + } + public I2PSink(I2PSession sess, Destination dest, boolean raw) { + this.sess = sess; + this.dest = dest; + this.raw = raw; + + // create maker + if (!raw) + this.maker = new I2PDatagramMaker(this.sess); + } + + /** @param src ignored */ + public synchronized void send(Destination src, byte[] data) { + //System.out.print("w"); + // create payload + byte[] payload; + if(!this.raw) + payload = this.maker.makeI2PDatagram(data); + else + payload = data; + + // send message + try { + this.sess.sendMessage(this.dest, payload); + } catch(I2PSessionException exc) { + // TODO: handle better + exc.printStackTrace(); + } + } + + + + + + + + + + + + + + protected boolean raw; + protected I2PSession sess; + protected Destination dest; + protected I2PDatagramMaker maker; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java new file mode 100644 index 000000000..09385d46f --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java @@ -0,0 +1,67 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.udp; + +// i2p +import net.i2p.client.I2PSession; +import net.i2p.client.I2PSessionException; +import net.i2p.data.Destination; +import net.i2p.client.datagram.I2PDatagramMaker; + +/** + * Producer + * + * This sends to any destination specified in send() + * + * @author zzz modded from I2PSink by welterde + */ +public class I2PSinkAnywhere implements Sink { + public I2PSinkAnywhere(I2PSession sess) { + this(sess, false); + } + public I2PSinkAnywhere(I2PSession sess, boolean raw) { + this.sess = sess; + this.raw = raw; + + // create maker + this.maker = new I2PDatagramMaker(this.sess); + } + + /** @param to - where it's going */ + public synchronized void send(Destination to, byte[] data) { + // create payload + byte[] payload; + if(!this.raw) + payload = this.maker.makeI2PDatagram(data); + else + payload = data; + + // send message + try { + this.sess.sendMessage(to, payload); + } catch(I2PSessionException exc) { + // TODO: handle better + exc.printStackTrace(); + } + } + + + + + + + + + + + + + + protected boolean raw; + protected I2PSession sess; + protected Destination dest; + protected I2PDatagramMaker maker; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSource.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSource.java new file mode 100644 index 000000000..0b5474777 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSource.java @@ -0,0 +1,123 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.udp; + +// system +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +// i2p +import net.i2p.client.I2PSession; +import net.i2p.client.I2PSessionListener; +import net.i2p.client.datagram.I2PDatagramDissector; + +/** + * + * @author welterde + */ +public class I2PSource implements Source, Runnable { + public I2PSource(I2PSession sess) { + this(sess, true, false); + } + public I2PSource(I2PSession sess, boolean verify) { + this(sess, verify, false); + } + public I2PSource(I2PSession sess, boolean verify, boolean raw) { + this.sess = sess; + this.sink = null; + this.verify = verify; + this.raw = raw; + + // create queue + this.queue = new ArrayBlockingQueue(256); + + // create listener + this.sess.setSessionListener(new Listener()); + + // create thread + this.thread = new Thread(this); + } + + public void setSink(Sink sink) { + this.sink = sink; + } + + public void start() { + this.thread.start(); + } + + public void run() { + // create dissector + I2PDatagramDissector diss = new I2PDatagramDissector(); + while(true) { + try { + // get id + int id = this.queue.take(); + + // receive message + byte[] msg = this.sess.receiveMessage(id); + + if(!this.raw) { + // load datagram into it + diss.loadI2PDatagram(msg); + + // now call sink + if(this.verify) + this.sink.send(diss.getSender(), diss.getPayload()); + else + this.sink.send(diss.extractSender(), diss.extractPayload()); + } else { + // verify is ignored + this.sink.send(null, msg); + } + //System.out.print("r"); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + + + + + + protected class Listener implements I2PSessionListener { + + public void messageAvailable(I2PSession sess, int id, long size) { + try { + queue.put(id); + } catch(Exception e) { + // ignore + } + } + + public void reportAbuse(I2PSession arg0, int arg1) { + // ignore + } + + public void disconnected(I2PSession arg0) { + // ignore + } + + public void errorOccurred(I2PSession arg0, String arg1, Throwable arg2) { + // ignore + } + + } + + + + + + + protected I2PSession sess; + protected BlockingQueue queue; + protected Sink sink; + protected Thread thread; + protected boolean verify; + protected boolean raw; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Sink.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Sink.java new file mode 100644 index 000000000..49e3e47a3 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Sink.java @@ -0,0 +1,17 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.udp; + +// i2p +import net.i2p.data.Destination; + +/** + * + * @author welterde + */ +public interface Sink { + public void send(Destination src, byte[] data); +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Source.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Source.java new file mode 100644 index 000000000..f65d03b19 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Source.java @@ -0,0 +1,15 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.udp; + +/** + * + * @author welterde + */ +public interface Source { + public void setSink(Sink sink); + public void start(); +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Stream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Stream.java new file mode 100644 index 000000000..b8b57e696 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/Stream.java @@ -0,0 +1,15 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.udp; + +/** + * + * @author welterde + */ +public interface Stream { + public void start(); + public void stop(); +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java new file mode 100644 index 000000000..15feba615 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java @@ -0,0 +1,74 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.udp; + +// system +import java.net.DatagramSocket; +import java.net.DatagramPacket; +import java.net.InetAddress; + +// i2p +import net.i2p.data.Destination; + +/** + * + * @author welterde + */ +public class UDPSink implements Sink { + public UDPSink(InetAddress host, int port) { + // create socket + try { + this.sock = new DatagramSocket(); + } catch(Exception e) { + // TODO: fail better + throw new RuntimeException("failed to open udp-socket", e); + } + + this.remoteHost = host; + + // remote port + this.remotePort = port; + } + + public void send(Destination src, byte[] data) { + // create packet + DatagramPacket packet = new DatagramPacket(data, data.length, this.remoteHost, this.remotePort); + + // send packet + try { + this.sock.send(packet); + } catch(Exception e) { + // TODO: fail a bit better + e.printStackTrace(); + } + } + + + + + + + + + + + + + + + + + + + + + + + protected DatagramSocket sock; + protected InetAddress remoteHost; + protected int remotePort; + +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java new file mode 100644 index 000000000..c54a984b0 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java @@ -0,0 +1,83 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ + +package net.i2p.i2ptunnel.udp; + +// system +import java.net.DatagramSocket; +import java.net.DatagramPacket; + +/** + * + * @author welterde + */ +public class UDPSource implements Source, Runnable { + public static final int MAX_SIZE = 15360; + public UDPSource(int port) { + this.sink = null; + + // create udp-socket + try { + this.sock = new DatagramSocket(port); + } catch(Exception e) { + throw new RuntimeException("failed to listen...", e); + } + + // create thread + this.thread = new Thread(this); + } + + public void setSink(Sink sink) { + this.sink = sink; + } + + public void start() { + this.thread.start(); + } + + public void run() { + // create packet + byte[] buf = new byte[MAX_SIZE]; + DatagramPacket pack = new DatagramPacket(buf, buf.length); + while(true) { + try { + // receive... + this.sock.receive(pack); + + // create new data array + byte[] nbuf = new byte[pack.getLength()]; + + // copy over + System.arraycopy(pack.getData(), 0, nbuf, 0, nbuf.length); + + // transfer to sink + this.sink.send(null, nbuf); + //System.out.print("i"); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + + + + + + + + + + + + + + + + + protected DatagramSocket sock; + protected Sink sink; + protected Thread thread; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java new file mode 100644 index 000000000..0123be6ea --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java @@ -0,0 +1,218 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel.udpTunnel; + +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.NoRouteToHostException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +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.client.I2PSession; +import net.i2p.client.I2PSessionException; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.i2ptunnel.I2PTunnelTask; +import net.i2p.i2ptunnel.Logging; +import net.i2p.i2ptunnel.udp.*; +import net.i2p.util.EventDispatcher; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +public abstract class I2PTunnelUDPClientBase extends I2PTunnelTask implements Source, Sink { + + private static final Log _log = new Log(I2PTunnelUDPClientBase.class); + protected I2PAppContext _context; + protected Logging l; + + static final long DEFAULT_CONNECT_TIMEOUT = 60 * 1000; + + private static volatile long __clientId = 0; + protected long _clientId; + + protected Destination dest = null; + + private boolean listenerReady = false; + + private ServerSocket ss; + + private Object startLock = new Object(); + private boolean startRunning = false; + + private byte[] pubkey; + + private String handlerName; + + private Object conLock = new Object(); + + /** How many connections will we allow to be in the process of being built at once? */ + private int _numConnectionBuilders; + /** How long will we allow sockets to sit in the _waitingSockets map before killing them? */ + private int _maxWaitTime; + + private I2PSession _session; + private Source _i2pSource; + private Sink _i2pSink; + private Destination _otherDest; + + /** + * Base client class that sets up an I2P Datagram client destination. + * The UDP side is not implemented here, as there are at least + * two possibilities: + * + * 1) UDP side is a "server" + * Example: Streamr Consumer + * - Configure a destination host and port + * - External application sends no data + * - Extending class must have a constructor with host and port arguments + * + * 2) UDP side is a client/server + * Example: SOCKS UDP (DNS requests?) + * - configure an inbound port and a destination host and port + * - External application sends and receives data + * - Extending class must have a constructor with host and 2 port arguments + * + * So the implementing class must create a UDPSource and/or UDPSink, + * and must call setSink(). + * + * @throws IllegalArgumentException if the I2CP configuration is b0rked so + * badly that we cant create a socketManager + * + * @author zzz with portions from welterde's streamr + */ + public I2PTunnelUDPClientBase(String destination, Logging l, EventDispatcher notifyThis, + I2PTunnel tunnel) throws IllegalArgumentException { + super("UDPServer", notifyThis, tunnel); + _clientId = ++__clientId; + this.l = l; + + _context = tunnel.getContext(); + + tunnel.getClientOptions().setProperty("i2cp.dontPublishLeaseSet", "true"); + + // create i2pclient and destination + I2PClient client = I2PClientFactory.createClient(); + Destination dest; + byte[] key; + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(512); + dest = client.createDestination(out); + key = out.toByteArray(); + } catch(Exception exc) { + throw new RuntimeException("failed to create i2p-destination", exc); + } + + // create a session + try { + ByteArrayInputStream in = new ByteArrayInputStream(key); + _session = client.createSession(in, tunnel.getClientOptions()); + } catch(Exception exc) { + throw new RuntimeException("failed to create session", exc); + } + + // Setup the source. Always expect raw unverified datagrams. + _i2pSource = new I2PSource(_session, false, true); + + // Setup the sink. Always send repliable datagrams. + if (destination != null && destination.length() > 0) { + try { + _otherDest = I2PTunnel.destFromName(destination); + } catch (DataFormatException dfe) {} + if (_otherDest == null) { + l.log("Could not resolve " + destination); + throw new RuntimeException("failed to create session - could not resolve " + destination); + } + _i2pSink = new I2PSink(_session, _otherDest, false); + } else { + _i2pSink = new I2PSinkAnywhere(_session, false); + } + + //configurePool(tunnel); + + } + + /** + * Actually start working on outgoing connections. + * Classes should override to start UDP side as well. + * + * Not specified in I2PTunnelTask but used in both + * I2PTunnelClientBase and I2PTunnelServer so let's + * implement it here too. + */ + public void startRunning() { + synchronized (startLock) { + try { + _session.connect(); + } catch(I2PSessionException exc) { + throw new RuntimeException("failed to connect session", exc); + } + start(); + startRunning = true; + startLock.notify(); + } + + if (open && listenerReady) { + notifyEvent("openBaseClientResult", "ok"); + } else { + l.log("Error listening - please see the logs!"); + notifyEvent("openBaseClientResult", "error"); + } + } + + /** + * I2PTunnelTask Methods + * + * Classes should override to close UDP side as well + */ + public boolean close(boolean forced) { + if (!open) return true; + if (_session != null) { + try { + _session.destroySession(); + } catch (I2PSessionException ise) {} + } + l.log("Closing client " + toString()); + return true; + } + + /** + * Source Methods + * + * Sets the receiver of the UDP datagrams from I2P + * Subclass must call this after constructor + * and before start() + */ + public void setSink(Sink s) { + _i2pSource.setSink(s); + } + + /** start the source */ + public void start() { + _i2pSource.start(); + } + + /** + * Sink Methods + * + * @param to - ignored if configured for a single destination + * (we use the dest specified in the constructor) + */ + public void send(Destination to, byte[] data) { + _i2pSink.send(to, data); + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java new file mode 100644 index 000000000..fe129fb13 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java @@ -0,0 +1,212 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel.udpTunnel; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.util.Iterator; +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.client.I2PSession; +import net.i2p.client.I2PSessionException; +import net.i2p.data.Base64; +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.i2ptunnel.I2PTunnelTask; +import net.i2p.i2ptunnel.Logging; +import net.i2p.i2ptunnel.udp.*; +import net.i2p.util.EventDispatcher; +import net.i2p.util.I2PThread; +import net.i2p.util.Log; + +public class I2PTunnelUDPServerBase extends I2PTunnelTask implements Source, Sink { + + private final static Log _log = new Log(I2PTunnelUDPServerBase.class); + + private Object lock = new Object(); + protected Object slock = new Object(); + + private static volatile long __serverId = 0; + + private Logging l; + + private static final long DEFAULT_READ_TIMEOUT = -1; // 3*60*1000; + /** default timeout to 3 minutes - override if desired */ + protected long readTimeout = DEFAULT_READ_TIMEOUT; + + private I2PSession _session; + private Source _i2pSource; + private Sink _i2pSink; + + /** + * Base client class that sets up an I2P Datagram server destination. + * The UDP side is not implemented here, as there are at least + * two possibilities: + * + * 1) UDP side is a "client" + * Example: Streamr Producer + * - configure an inbound port + * - External application receives no data + * - Extending class must have a constructor with a port argument + * + * 2) UDP side is a client/server + * Example: DNS + * - configure an inbound port and a destination host and port + * - External application sends and receives data + * - Extending class must have a constructor with host and 2 port arguments + * + * So the implementing class must create a UDPSource and/or UDPSink, + * and must call setSink(). + * + * @throws IllegalArgumentException if the I2CP configuration is b0rked so + * badly that we cant create a socketManager + * + * @author zzz with portions from welterde's streamr + */ + + public I2PTunnelUDPServerBase(boolean verify, File privkey, String privkeyname, Logging l, + EventDispatcher notifyThis, I2PTunnel tunnel) { + super("UDPServer <- " + privkeyname, notifyThis, tunnel); + FileInputStream fis = null; + try { + fis = new FileInputStream(privkey); + init(verify, fis, privkeyname, l); + } catch (IOException ioe) { + _log.error("Error starting server", ioe); + notifyEvent("openServerResult", "error"); + } finally { + if (fis != null) + try { fis.close(); } catch (IOException ioe) {} + } + } + + private void init(boolean verify, InputStream privData, String privkeyname, Logging l) { + this.l = l; + int portNum = 7654; + if (getTunnel().port != null) { + try { + portNum = Integer.parseInt(getTunnel().port); + } catch (NumberFormatException nfe) { + _log.log(Log.CRIT, "Invalid port specified [" + getTunnel().port + "], reverting to " + portNum); + } + } + + // create i2pclient + I2PClient client = I2PClientFactory.createClient(); + + try { + _session = client.createSession(privData, getTunnel().getClientOptions()); + } catch(I2PSessionException exc) { + throw new RuntimeException("failed to create session", exc); + } + + // Setup the source. Always expect repliable datagrams, optionally verify + _i2pSource = new I2PSource(_session, verify, false); + + // Setup the sink. Always send raw datagrams. + _i2pSink = new I2PSinkAnywhere(_session, true); + } + + /** + * Classes should override to start UDP side as well. + * + * Not specified in I2PTunnelTask but used in both + * I2PTunnelClientBase and I2PTunnelServer so let's + * implement it here too. + */ + public void startRunning() { + //synchronized (startLock) { + try { + _session.connect(); + } catch(I2PSessionException exc) { + throw new RuntimeException("failed to connect session", exc); + } + start(); + //} + + l.log("Ready!"); + notifyEvent("openServerResult", "ok"); + open = true; + } + + /** + * Set the read idle timeout for newly-created connections (in + * milliseconds). After this time expires without data being reached from + * the I2P network, the connection itself will be closed. + */ + public void setReadTimeout(long ms) { + readTimeout = ms; + } + + /** + * Get the read idle timeout for newly-created connections (in + * milliseconds). + * + * @return The read timeout used for connections + */ + public long getReadTimeout() { + return readTimeout; + } + + /** + * I2PTunnelTask Methods + * + * Classes should override to close UDP side as well + */ + public boolean close(boolean forced) { + if (!open) return true; + synchronized (lock) { + l.log("Shutting down server " + toString()); + try { + if (_session != null) { + _session.destroySession(); + } + } catch (I2PException ex) { + _log.error("Error destroying the session", ex); + } + l.log("Server shut down."); + open = false; + return true; + } + } + + /** + * Source Methods + * + * Sets the receiver of the UDP datagrams from I2P + * Subclass must call this after constructor + * and before start() + */ + public void setSink(Sink s) { + _i2pSource.setSink(s); + } + + /** start the source */ + public void start() { + _i2pSource.start(); + } + + /** + * Sink Methods + * + * @param to + * + */ + public void send(Destination to, byte[] data) { + _i2pSink.send(to, data); + } +} + diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 045ea5e58..6fcd9f2fe 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -351,6 +351,7 @@ public class IndexBean { ("httpclient".equals(type)) || ("sockstunnel".equals(type)) || ("connectclient".equals(type)) || + ("streamrclient".equals(type)) || ("ircclient".equals(type))); } @@ -387,6 +388,8 @@ public class IndexBean { else if ("sockstunnel".equals(internalType)) return "SOCKS 4/4a/5 proxy"; else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy"; else if ("ircserver".equals(internalType)) return "IRC server"; + else if ("streamrclient".equals(internalType)) return "Streamr client"; + else if ("streamrserver".equals(internalType)) return "Streamr server"; else return internalType; } @@ -434,7 +437,8 @@ public class IndexBean { TunnelController tun = getController(tunnel); if (tun == null) return ""; String rv; - if ("client".equals(tun.getType())||"ircclient".equals(tun.getType())) + if ("client".equals(tun.getType()) || "ircclient".equals(tun.getType()) || + "streamrclient".equals(tun.getType())) rv = tun.getTargetDestination(); else rv = tun.getProxyList(); @@ -798,7 +802,7 @@ public class IndexBean { if ("httpclient".equals(_type) || "connectclient".equals(_type)) { if (_proxyList != null) config.setProperty("proxyList", _proxyList); - } else if ("ircclient".equals(_type) || "client".equals(_type)) { + } else if ("ircclient".equals(_type) || "client".equals(_type) || "streamrclient".equals(_type)) { if (_targetDestination != null) config.setProperty("targetDestination", _targetDestination); } else if ("httpserver".equals(_type)) { diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index 3e4c3ecd8..6a796eb7d 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -75,7 +75,11 @@
    + <% if ("streamrclient".equals(tunnelType)) { %> + + <% } else { %> + <% } %>
    + <% String otherInterface = ""; + String clientInterface = editBean.getClientInterface(curTunnel); + if ("streamrclient".equals(tunnelType)) { + otherInterface = clientInterface; + } else { %>
    + <% } // streamrclient %>
    @@ -123,7 +139,7 @@
    - <% } else if ("client".equals(tunnelType) || "ircclient".equals(tunnelType)) { + <% } else if ("client".equals(tunnelType) || "ircclient".equals(tunnelType) || "streamrclient".equals(tunnelType)) { %>
    - <% } - %>
    + <% } %> + <% if (!"streamrclient".equals(tunnelType)) { %> +
    @@ -160,6 +177,7 @@ class="tickbox" /> (Share tunnels with other clients and irc/httpclients? Change requires restart of client proxy)
    + <% } // !streamrclient %>
    + <% if ("streamrserver".equals(tunnelType)) { %> + + <% } else { %> + <% } %>
    + <% if (!"streamrserver".equals(tunnelType)) { %>
    + <% } // !streamrserver %>
    + <% if (!"streamrserver".equals(tunnelType)) { %>
    + <% } // !streamrserver %>
    @@ -261,6 +262,7 @@ +
    From 7e21afe6a6c2ce3d84a8973393d5ac60154d7f90 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Feb 2009 22:59:59 +0000 Subject: [PATCH 143/191] sort the summary bar destinations --- .../src/net/i2p/router/web/SummaryHelper.java | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java index 2e56e858b..47a07c374 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java @@ -1,11 +1,15 @@ package net.i2p.router.web; +import java.text.Collator; import java.text.DateFormat; import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.Iterator; +import java.util.List; import java.util.Locale; -import java.util.Set; import net.i2p.data.DataHelper; import net.i2p.data.Destination; @@ -346,20 +350,16 @@ public class SummaryHelper extends HelperBase { * @return html section summary */ public String getDestinations() { - Set clients = _context.clientManager().listClients(); + // covert the set to a list so we can sort by name and not lose duplicates + List clients = new ArrayList(_context.clientManager().listClients()); + Collections.sort(clients, new AlphaComparator()); StringBuffer buf = new StringBuffer(512); buf.append("Local destinations
    "); for (Iterator iter = clients.iterator(); iter.hasNext(); ) { Destination client = (Destination)iter.next(); - TunnelPoolSettings in = _context.tunnelManager().getInboundSettings(client.calculateHash()); - TunnelPoolSettings out = _context.tunnelManager().getOutboundSettings(client.calculateHash()); - String name = (in != null ? in.getDestinationNickname() : null); - if (name == null) - name = (out != null ? out.getDestinationNickname() : null); - if (name == null) - name = client.calculateHash().toBase64().substring(0,6); + String name = getName(client); buf.append("* ").append(name).append("
    \n"); LeaseSet ls = _context.netDb().lookupLeaseSetLocally(client.calculateHash()); @@ -373,14 +373,38 @@ public class SummaryHelper extends HelperBase { buf.append("No leases
    \n"); } buf.append("Details "); + buf.append("\" target=\"_top\">Details "); buf.append("Config
    \n"); + buf.append("\" target=\"_top\">Config
    \n"); } buf.append("
    \n"); return buf.toString(); } + private class AlphaComparator implements Comparator { + public int compare(Object lhs, Object rhs) { + String lname = getName((Destination)lhs); + String rname = getName((Destination)rhs); + if (lname.equals("shared clients")) + return -1; + if (rname.equals("shared clients")) + return 1; + return Collator.getInstance().compare(lname, rname); + } + } + + private String getName(Destination d) { + TunnelPoolSettings in = _context.tunnelManager().getInboundSettings(d.calculateHash()); + String name = (in != null ? in.getDestinationNickname() : null); + if (name == null) { + TunnelPoolSettings out = _context.tunnelManager().getOutboundSettings(d.calculateHash()); + name = (out != null ? out.getDestinationNickname() : null); + if (name == null) + name = d.calculateHash().toBase64().substring(0,6); + } + return name; + } + /** * How many free inbound tunnels we have. * @@ -511,4 +535,5 @@ public class SummaryHelper extends HelperBase { public boolean updateAvailable() { return NewsFetcher.getInstance(_context).updateAvailable(); } + } From 7a684c160b50988cb9103d6a0033af64eb18bd08 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Feb 2009 23:15:26 +0000 Subject: [PATCH 144/191] * Routerconsole: - Thread hard shutdown and restart requests from the routerconsole, and add a delay even if no tunnels, to allow time for a UI response --- .../net/i2p/router/web/ConfigRestartBean.java | 19 +++++++++--- router/java/src/net/i2p/router/Router.java | 30 +++++++++++++++---- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java index 3c9fe1bbf..75a8108c5 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java @@ -26,12 +26,14 @@ public class ConfigRestartBean { if ( (nonce != null) && (systemNonce.equals(nonce)) && (action != null) ) { if ("shutdownImmediate".equals(action)) { ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD)); - ctx.router().shutdown(Router.EXIT_HARD); // never returns + //ctx.router().shutdown(Router.EXIT_HARD); // never returns + ctx.router().shutdownGracefully(Router.EXIT_HARD); // give the UI time to respond } else if ("cancelShutdown".equals(action)) { ctx.router().cancelGracefulShutdown(); } else if ("restartImmediate".equals(action)) { ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART)); - ctx.router().shutdown(Router.EXIT_HARD_RESTART); // never returns + //ctx.router().shutdown(Router.EXIT_HARD_RESTART); // never returns + ctx.router().shutdownGracefully(Router.EXIT_HARD_RESTART); // give the UI time to respond } else if ("restart".equals(action)) { ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART)); ctx.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART); @@ -79,9 +81,18 @@ public class ConfigRestartBean { } private static boolean isShuttingDown(RouterContext ctx) { - return Router.EXIT_GRACEFUL == ctx.router().scheduledGracefulExitCode(); + return Router.EXIT_GRACEFUL == ctx.router().scheduledGracefulExitCode() || + Router.EXIT_HARD == ctx.router().scheduledGracefulExitCode(); } private static boolean isRestarting(RouterContext ctx) { - return Router.EXIT_GRACEFUL_RESTART == ctx.router().scheduledGracefulExitCode(); + return Router.EXIT_GRACEFUL_RESTART == ctx.router().scheduledGracefulExitCode() || + Router.EXIT_HARD_RESTART == ctx.router().scheduledGracefulExitCode(); + } + /** this is for summaryframe.jsp */ + public static long getRestartTimeRemaining() { + RouterContext ctx = ContextHelper.getContext(null); + if (ctx.router().gracefulShutdownInProgress()) + return ctx.router().getShutdownTimeRemaining(); + return Long.MAX_VALUE/2; // summaryframe.jsp adds a safety factor so we don't want to overflow... } } diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index 033678924..77e4b1968 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -446,13 +446,14 @@ public class Router { */ private static final String _rebuildFiles[] = new String[] { "router.info", "router.keys", - "netDb/my.info", - "connectionTag.keys", + "netDb/my.info", // no longer used + "connectionTag.keys", // never used? "keyBackup/privateEncryption.key", "keyBackup/privateSigning.key", "keyBackup/publicEncryption.key", "keyBackup/publicSigning.key", - "sessionKeys.dat" }; + "sessionKeys.dat" // no longer used + }; static final String IDENTLOG = "identlog.txt"; public static void killKeys() { @@ -859,6 +860,10 @@ public class Router { public void shutdownGracefully() { shutdownGracefully(EXIT_GRACEFUL); } + /** + * Call this with EXIT_HARD or EXIT_HARD_RESTART for a non-blocking, + * hard, non-graceful shutdown with a brief delay to allow a UI response + */ public void shutdownGracefully(int exitCode) { _gracefulExitCode = exitCode; _config.setProperty(PROP_SHUTDOWN_IN_PROGRESS, "true"); @@ -887,7 +892,9 @@ public class Router { } /** How long until the graceful shutdown will kill us? */ public long getShutdownTimeRemaining() { - if (_gracefulExitCode <= 0) return -1; + if (_gracefulExitCode <= 0) return -1; // maybe Long.MAX_VALUE would be better? + if (_gracefulExitCode == EXIT_HARD || _gracefulExitCode == EXIT_HARD_RESTART) + return 0; long exp = _context.tunnelManager().getLastParticipatingExpiration(); if (exp < 0) return -1; @@ -906,9 +913,20 @@ public class Router { while (true) { boolean shutdown = (null != _config.getProperty(PROP_SHUTDOWN_IN_PROGRESS)); if (shutdown) { - if (_context.tunnelManager().getParticipatingCount() <= 0) { - if (_log.shouldLog(Log.CRIT)) + if (_gracefulExitCode == EXIT_HARD || _gracefulExitCode == EXIT_HARD_RESTART || + _context.tunnelManager().getParticipatingCount() <= 0) { + if (_gracefulExitCode == EXIT_HARD) + _log.log(Log.CRIT, "Shutting down after a brief delay"); + else if (_gracefulExitCode == EXIT_HARD_RESTART) + _log.log(Log.CRIT, "Restarting after a brief delay"); + else _log.log(Log.CRIT, "Graceful shutdown progress - no more tunnels, safe to die"); + // Allow time for a UI reponse + try { + synchronized (Thread.currentThread()) { + Thread.currentThread().wait(2*1000); + } + } catch (InterruptedException ie) {} shutdown(_gracefulExitCode); return; } else { From 559653f0ab532352503b527f38b326745f176940 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Feb 2009 23:18:12 +0000 Subject: [PATCH 145/191] clean up OCMOSJ cache cleaner --- .../OutboundClientMessageOneShotJob.java | 87 +++++++++++-------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java index 0515d5c34..0e858ef77 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java @@ -34,6 +34,8 @@ import net.i2p.router.Router; import net.i2p.router.RouterContext; import net.i2p.router.TunnelInfo; import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; +import net.i2p.util.SimpleTimer; /** * Send a client message out a random outbound tunnel and into a random inbound @@ -98,6 +100,10 @@ public class OutboundClientMessageOneShotJob extends JobImpl { */ private static final int BUNDLE_PROBABILITY_DEFAULT = 100; + private static final Object _initializeLock = new Object(); + private static boolean _initialized = false; + private static final int CLEAN_INTERVAL = 5*60*1000; + /** * Send the sucker */ @@ -105,20 +111,26 @@ public class OutboundClientMessageOneShotJob extends JobImpl { super(ctx); _log = ctx.logManager().getLog(OutboundClientMessageOneShotJob.class); - ctx.statManager().createFrequencyStat("client.sendMessageFailFrequency", "How often does a client fail to send a message?", "ClientMessages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.sendMessageSize", "How large are messages sent by the client?", "ClientMessages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.sendAckTime", "Message round trip time", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.timeoutCongestionTunnel", "How lagged our tunnels are when a send times out?", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.timeoutCongestionMessage", "How fast we process messages locally when a send times out?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.timeoutCongestionInbound", "How much faster we are receiving data than our average bps when a send times out?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.leaseSetFoundLocally", "How often we tried to look for a leaseSet and found it locally?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.leaseSetFoundRemoteTime", "How long we tried to look for a remote leaseSet (when we succeeded)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.leaseSetFailedRemoteTime", "How long we tried to look for a remote leaseSet (when we failed)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.dispatchPrepareTime", "How long until we've queued up the dispatch job (since we started)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.dispatchTime", "How long until we've dispatched the message (since we started)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.dispatchSendTime", "How long the actual dispatching takes?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.dispatchNoTunnels", "How long after start do we run out of tunnels to send/receive with?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); - ctx.statManager().createRateStat("client.dispatchNoACK", "Repeated message sends to a peer (no ack required)", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l }); + synchronized (_initializeLock) { + if (!_initialized) { + SimpleScheduler.getInstance().addPeriodicEvent(new OCMOSJCacheCleaner(ctx), CLEAN_INTERVAL, CLEAN_INTERVAL); + ctx.statManager().createFrequencyStat("client.sendMessageFailFrequency", "How often does a client fail to send a message?", "ClientMessages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.sendMessageSize", "How large are messages sent by the client?", "ClientMessages", new long[] { 60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.sendAckTime", "Message round trip time", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.timeoutCongestionTunnel", "How lagged our tunnels are when a send times out?", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.timeoutCongestionMessage", "How fast we process messages locally when a send times out?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.timeoutCongestionInbound", "How much faster we are receiving data than our average bps when a send times out?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.leaseSetFoundLocally", "How often we tried to look for a leaseSet and found it locally?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.leaseSetFoundRemoteTime", "How long we tried to look for a remote leaseSet (when we succeeded)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.leaseSetFailedRemoteTime", "How long we tried to look for a remote leaseSet (when we failed)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.dispatchPrepareTime", "How long until we've queued up the dispatch job (since we started)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.dispatchTime", "How long until we've dispatched the message (since we started)?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.dispatchSendTime", "How long the actual dispatching takes?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.dispatchNoTunnels", "How long after start do we run out of tunnels to send/receive with?", "ClientMessages", new long[] { 5*60*1000l, 60*60*1000l, 24*60*60*1000l }); + ctx.statManager().createRateStat("client.dispatchNoACK", "Repeated message sends to a peer (no ack required)", "ClientMessages", new long[] { 60*1000l, 5*60*1000l, 60*60*1000l }); + _initialized = true; + } + } long timeoutMs = OVERALL_TIMEOUT_MS_DEFAULT; _clientMessage = msg; _clientMessageId = msg.getMessageId(); @@ -201,7 +213,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * Key the cache on the source+dest pair. */ private static HashMap _leaseSetCache = new HashMap(); - private static long _lscleanTime = 0; private LeaseSet getReplyLeaseSet(boolean force) { LeaseSet newLS = getContext().netDb().lookupLeaseSetLocally(_from.calculateHash()); if (newLS == null) @@ -235,10 +246,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl { // If the last leaseSet we sent him is still good, don't bother sending again long now = getContext().clock().now(); synchronized (_leaseSetCache) { - if (now - _lscleanTime > 5*60*1000) { // clean out periodically - cleanLeaseSetCache(_leaseSetCache); - _lscleanTime = now; - } if (!force) { LeaseSet ls = (LeaseSet) _leaseSetCache.get(hashPair()); if (ls != null) { @@ -306,7 +313,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * */ private static HashMap _leaseCache = new HashMap(); - private static long _lcleanTime = 0; private boolean getNextLease() { _leaseSet = getContext().netDb().lookupLeaseSetLocally(_to.calculateHash()); if (_leaseSet == null) { @@ -319,10 +325,6 @@ public class OutboundClientMessageOneShotJob extends JobImpl { // Use the same lease if it's still good // Even if _leaseSet changed, _leaseSet.getEncryptionKey() didn't... synchronized (_leaseCache) { - if (now - _lcleanTime > 5*60*1000) { // clean out periodically - cleanLeaseCache(_leaseCache); - _lcleanTime = now; - } _lease = (Lease) _leaseCache.get(hashPair()); if (_lease != null) { // if outbound tunnel length == 0 && lease.firsthop.isBacklogged() don't use it ?? @@ -607,7 +609,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * (needed for cleanTunnelCache) * 44 = 32 * 4 / 3 */ - private Hash sourceFromHashPair(String s) { + private static Hash sourceFromHashPair(String s) { return new Hash(Base64.decode(s.substring(44, 88))); } @@ -648,8 +650,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * Clean out old leaseSets from a set. * Caller must synchronize on tc. */ - private void cleanLeaseSetCache(HashMap tc) { - long now = getContext().clock().now(); + private static void cleanLeaseSetCache(RouterContext ctx, HashMap tc) { + long now = ctx.clock().now(); List deleteList = new ArrayList(); for (Iterator iter = tc.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry)iter.next(); @@ -668,7 +670,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * Clean out old leases from a set. * Caller must synchronize on tc. */ - private void cleanLeaseCache(HashMap tc) { + private static void cleanLeaseCache(HashMap tc) { List deleteList = new ArrayList(); for (Iterator iter = tc.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry)iter.next(); @@ -687,13 +689,13 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * Clean out old tunnels from a set. * Caller must synchronize on tc. */ - private void cleanTunnelCache(HashMap tc) { + private static void cleanTunnelCache(RouterContext ctx, HashMap tc) { List deleteList = new ArrayList(); for (Iterator iter = tc.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry)iter.next(); String k = (String) entry.getKey(); TunnelInfo tunnel = (TunnelInfo) entry.getValue(); - if (!getContext().tunnelManager().isValidTunnel(sourceFromHashPair(k), tunnel)) + if (!ctx.tunnelManager().isValidTunnel(sourceFromHashPair(k), tunnel)) deleteList.add(k); } for (Iterator iter = deleteList.iterator(); iter.hasNext(); ) { @@ -702,6 +704,25 @@ public class OutboundClientMessageOneShotJob extends JobImpl { } } + private static class OCMOSJCacheCleaner implements SimpleTimer.TimedEvent { + private RouterContext _ctx; + private OCMOSJCacheCleaner(RouterContext ctx) { + _ctx = ctx; + } + public void timeReached() { + synchronized(_leaseSetCache) { + cleanLeaseSetCache(_ctx, _leaseSetCache); + } + synchronized(_leaseCache) { + cleanLeaseCache(_leaseCache); + } + synchronized(_tunnelCache) { + cleanTunnelCache(_ctx, _tunnelCache); + cleanTunnelCache(_ctx, _backloggedTunnelCache); + } + } + } + /** * Use the same outbound tunnel as we did for the same destination previously, * if possible, to keep the streaming lib happy @@ -712,16 +733,10 @@ public class OutboundClientMessageOneShotJob extends JobImpl { */ private static HashMap _tunnelCache = new HashMap(); private static HashMap _backloggedTunnelCache = new HashMap(); - private static long _cleanTime = 0; private TunnelInfo selectOutboundTunnel(Destination to) { TunnelInfo tunnel; long now = getContext().clock().now(); synchronized (_tunnelCache) { - if (now - _cleanTime > 5*60*1000) { // clean out periodically - cleanTunnelCache(_tunnelCache); - cleanTunnelCache(_backloggedTunnelCache); - _cleanTime = now; - } /** * If old tunnel is valid and no longer backlogged, use it. * This prevents an active anonymity attack, where a peer could tell From 6484005569a98cfa2bbb5ec4d3e869b5e51f2e06 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Feb 2009 23:28:53 +0000 Subject: [PATCH 146/191] I2PTunnel: First cut at SOCKS UDP (untested); also some streamr and UDP tweaks --- .../net/i2p/i2ptunnel/socks/MultiSink.java | 35 +++++++ .../net/i2p/i2ptunnel/socks/ReplyTracker.java | 36 +++++++ .../net/i2p/i2ptunnel/socks/SOCKS5Server.java | 84 +++++++++++++++-- .../net/i2p/i2ptunnel/socks/SOCKSHeader.java | 89 ++++++++++++++++++ .../net/i2p/i2ptunnel/socks/SOCKSUDPPort.java | 77 +++++++++++++++ .../i2p/i2ptunnel/socks/SOCKSUDPTunnel.java | 94 +++++++++++++++++++ .../i2ptunnel/socks/SOCKSUDPUnwrapper.java | 59 ++++++++++++ .../i2p/i2ptunnel/socks/SOCKSUDPWrapper.java | 49 ++++++++++ .../i2p/i2ptunnel/streamr/MultiSource.java | 4 + .../i2ptunnel/streamr/StreamrConsumer.java | 3 +- .../i2ptunnel/streamr/StreamrProducer.java | 5 +- .../src/net/i2p/i2ptunnel/udp/UDPSink.java | 21 +++-- .../src/net/i2p/i2ptunnel/udp/UDPSource.java | 14 ++- 13 files changed, 549 insertions(+), 21 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/MultiSink.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/ReplyTracker.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSHeader.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPPort.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPTunnel.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPUnwrapper.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPWrapper.java diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/MultiSink.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/MultiSink.java new file mode 100644 index 000000000..3c63758c1 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/MultiSink.java @@ -0,0 +1,35 @@ +package net.i2p.i2ptunnel.socks; + +import java.util.Map; + +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.udp.*; +import net.i2p.util.Log; + +/** + * Sends to one of many Sinks + * @author zzz modded from streamr/MultiSource + */ +public class MultiSink implements Source, Sink { + private static final Log _log = new Log(MultiSink.class); + + public MultiSink(Map cache) { + this.cache = cache; + } + + /** Don't use this - put sinks in the cache */ + public void setSink(Sink sink) {} + + public void start() {} + + public void send(Destination from, byte[] data) { + Sink s = this.cache.get(from); + if (s == null) { + _log.error("No where to go for " + from.calculateHash().toBase64().substring(0, 6)); + return; + } + s.send(from, data); + } + + private Map cache; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/ReplyTracker.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/ReplyTracker.java new file mode 100644 index 000000000..f6a124c95 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/ReplyTracker.java @@ -0,0 +1,36 @@ +package net.i2p.i2ptunnel.socks; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; + +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.udp.*; +import net.i2p.util.Log; + +/** + * Track who the reply goes to + * @author zzz + */ +public class ReplyTracker implements Source, Sink { + private static final Log _log = new Log(MultiSink.class); + + public ReplyTracker(Sink reply, Map cache) { + this.reply = reply; + this.cache = cache; + } + + public void setSink(Sink sink) { + this.sink = sink; + } + + public void start() {} + + public void send(Destination to, byte[] data) { + this.cache.put(to, this.reply); + this.sink.send(to, data); + } + + private Sink reply; + private Map cache; + private Sink sink; +} 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 38c50f266..5e5292607 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java @@ -13,12 +13,15 @@ import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; import java.util.List; import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.streaming.I2PSocket; import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; import net.i2p.i2ptunnel.I2PTunnel; import net.i2p.util.HexDump; import net.i2p.util.Log; @@ -67,7 +70,8 @@ public class SOCKS5Server extends SOCKSServer { out = new DataOutputStream(clientSock.getOutputStream()); init(in, out); - manageRequest(in, out); + if (manageRequest(in, out) == Command.UDP_ASSOCIATE) + handleUDP(in, out); } catch (IOException e) { throw new SOCKSException("Connection error (" + e.getMessage() + ")"); } @@ -111,7 +115,7 @@ public class SOCKS5Server extends SOCKSServer { * initialization, integrity/confidentiality encapsulations, etc) * has been stripped out of the input/output streams. */ - private void manageRequest(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException { + private int manageRequest(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException { int socksVer = in.readByte() & 0xff; if (socksVer != SOCKS_VERSION_5) { _log.debug("error in SOCKS5 request (protocol != 5? wtf?)"); @@ -127,9 +131,12 @@ public class SOCKS5Server extends SOCKSServer { sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("BIND command not supported"); case Command.UDP_ASSOCIATE: + /*** if(!Boolean.valueOf(tunnel.getOptions().getProperty("i2ptunnel.socks.allowUDP")).booleanValue()) { _log.debug("UDP ASSOCIATE command is not supported!"); sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("UDP ASSOCIATE command not supported"); + ***/ + break; default: _log.debug("unknown command in request (" + Integer.toHexString(command) + ")"); sendRequestReply(Reply.COMMAND_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); @@ -152,7 +159,8 @@ public class SOCKS5Server extends SOCKSServer { connHostName += "."; } } - _log.warn("IPV4 address type in request: " + connHostName + ". Is your client secure?"); + if (command != Command.UDP_ASSOCIATE) + _log.warn("IPV4 address type in request: " + connHostName + ". Is your client secure?"); break; case AddressType.DOMAINNAME: { @@ -168,9 +176,12 @@ public class SOCKS5Server extends SOCKSServer { _log.debug("DOMAINNAME address type in request: " + connHostName); break; case AddressType.IPV6: - _log.warn("IP V6 address type in request! Is your client secure?" + " (IPv6 is not supported, anyway :-)"); - sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); - throw new SOCKSException("IPV6 addresses not supported"); + if (command != Command.UDP_ASSOCIATE) { + _log.warn("IP V6 address type in request! Is your client secure?" + " (IPv6 is not supported, anyway :-)"); + sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + throw new SOCKSException("IPV6 addresses not supported"); + } + break; default: _log.debug("unknown address type in request (" + Integer.toHexString(command) + ")"); sendRequestReply(Reply.ADDRESS_TYPE_NOT_SUPPORTED, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); @@ -183,6 +194,7 @@ public class SOCKS5Server extends SOCKSServer { sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); throw new SOCKSException("Invalid port number in request"); } + return command; } protected void confirmConnection() throws SOCKSException { @@ -293,6 +305,13 @@ public class SOCKS5Server extends SOCKSServer { // Let's not due a new Dest for every request, huh? //I2PSocketManager sm = I2PSocketManagerFactory.createManager(); //destSock = sm.connect(I2PTunnel.destFromName(connHostName), null); + Destination dest = I2PTunnel.destFromName(connHostName); + if (dest == null) { + try { + sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + } catch (IOException ioe) {} + throw new SOCKSException("Host not found"); + } destSock = t.createI2PSocket(I2PTunnel.destFromName(connHostName)); } else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) { String err = "No localhost accesses allowed through the Socks Proxy"; @@ -358,6 +377,59 @@ public class SOCKS5Server extends SOCKSServer { return destSock; } + // This isn't really the right place for this, we can't stop the tunnel once it starts. + static SOCKSUDPTunnel _tunnel; + static Object _startLock = new Object(); + static byte[] dummyIP = new byte[4]; + /** + * We got a UDP associate command. + * Loop here looking for more, never return normally, + * or else I2PSocksTunnel will create a streaming lib connection. + * + * Do UDP Socks clients actually send more than one Associate request? + * RFC 1928 isn't clear... maybe not. + */ + private void handleUDP(DataInputStream in, DataOutputStream out) throws SOCKSException { + List ports = new ArrayList(1); + synchronized (_startLock) { + if (_tunnel == null) { + // tunnel options? + _tunnel = new SOCKSUDPTunnel(new I2PTunnel()); + _tunnel.startRunning(); + } + } + while (true) { + // Set it up. connHostName and connPort are the client's info. + InetAddress ia = null; + try { + ia = InetAddress.getByAddress(connHostName, dummyIP); + } catch (UnknownHostException uhe) {} // won't happen, no resolving done here + int myPort = _tunnel.add(ia, connPort); + ports.add(Integer.valueOf(myPort)); + try { + sendRequestReply(Reply.SUCCEEDED, AddressType.IPV4, InetAddress.getByName("127.0.0.1"), null, myPort, out); + } catch (IOException ioe) { break; } + + // wait for more ??? + try { + int command = manageRequest(in, out); + // don't do this... + if (command != Command.UDP_ASSOCIATE) + break; + } catch (IOException ioe) { break; } + catch (SOCKSException ioe) { break; } + } + + for (Integer i : ports) + _tunnel.remove(i); + + // Prevent I2PSocksTunnel from calling getDestinationI2PSocket() above + // to create a streaming lib connection... + // This isn't very elegant... + // + throw new SOCKSException("End of UDP Processing"); + } + /* * Some namespaces to enclose SOCKS protocol codes */ diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSHeader.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSHeader.java new file mode 100644 index 000000000..763b9aa10 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSHeader.java @@ -0,0 +1,89 @@ +package net.i2p.i2ptunnel.socks; + +import net.i2p.data.Base32; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.I2PTunnel; + +/** + * Save the SOCKS header from a datagram + * Ref: RFC 1928 + * + * @author zzz + */ +public class SOCKSHeader { + + /** + * @param data the whole packet + */ + public SOCKSHeader(byte[] data) { + if (data.length <= 8) + throw new IllegalArgumentException("Header too short: " + data.length); + if (data[0] != 0 || data[1] != 0) + throw new IllegalArgumentException("Not a SOCKS datagram?"); + if (data[2] != 0) + throw new IllegalArgumentException("We can't handle fragments!"); + int headerlen = 0; + int addressType = data[3]; + if (addressType == 1) { + // this will fail in getDestination() + headerlen = 6 + 4; + } else if (addressType == 3) { + headerlen = 6 + 1 + (data[4] & 0xff); + } else if (addressType == 4) { + // this will fail in getDestination() + // but future garlicat partial hash lookup possible? + headerlen = 6 + 16; + } else { + throw new IllegalArgumentException("Unknown address type: " + addressType); + } + if (data.length < headerlen) + throw new IllegalArgumentException("Header too short: " + data.length); + + this.header = new byte[headerlen]; + System.arraycopy(this.header, 0, data, 0, headerlen); + } + + private static final byte[] beg = {0,0,0,3,60}; + private static final byte[] end = {'.','b','3','2','.','i','2','p',0,0}; + + /** + * Make a dummy header from a dest, + * for those cases where we want to receive unsolicited datagrams. + * Unused for now. + */ + public SOCKSHeader(Destination dest) { + this.header = new byte[beg.length + 52 + end.length]; + System.arraycopy(this.header, 0, beg, 0, beg.length); + String b32 = Base32.encode(dest.calculateHash().getData()); + System.arraycopy(this.header, beg.length, b32.getBytes(), 0, 52); + System.arraycopy(this.header, beg.length + 52, end, 0, end.length); + } + + public String getHost() { + int addressType = this.header[3]; + if (addressType != 3) + return null; + int namelen = (this.header[4] & 0xff); + byte[] nameBytes = new byte[namelen]; + System.arraycopy(nameBytes, 0, this.header, 5, namelen); + return new String(nameBytes); + } + + public Destination getDestination() { + String name = getHost(); + if (name == null) + return null; + try { + // the naming service does caching (thankfully) + return I2PTunnel.destFromName(name); + } catch (DataFormatException dfe) {} + return null; + } + + public byte[] getBytes() { + return header; + } + + private byte[] header; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPPort.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPPort.java new file mode 100644 index 000000000..b56c9082f --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPPort.java @@ -0,0 +1,77 @@ +package net.i2p.i2ptunnel.socks; + +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; + +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.udp.*; + +/** + * Implements a UDP port and Socks encapsulation / decapsulation. + * This is for a single port. If there is demuxing for multiple + * ports, it happens outside of here. + * + * TX: + * UDPSource -> SOCKSUDPUnwrapper -> ReplyTracker ( -> I2PSink in SOCKSUDPTunnel) + * + * RX: + * UDPSink <- SOCKSUDPWrapper ( <- MultiSink <- I2PSource in SOCKSUDPTunnel) + * + * The Unwrapper passes headers to the Wrapper through a cache. + * The ReplyTracker passes sinks to MultiSink through a cache. + * + * @author zzz + */ +public class SOCKSUDPPort implements Source, Sink { + + public SOCKSUDPPort(InetAddress host, int port, Map replyMap) { + + // this passes the host and port from UDPUnwrapper to UDPWrapper + Map cache = new ConcurrentHashMap(4); + + // rcv from I2P and send to a port + this.wrapper = new SOCKSUDPWrapper(cache); + this.udpsink = new UDPSink(host, port); + this.wrapper.setSink(this.udpsink); + + // rcv from the same port and send to I2P + DatagramSocket sock = this.udpsink.getSocket(); + this.udpsource = new UDPSource(sock); + this.unwrapper = new SOCKSUDPUnwrapper(cache); + this.udpsource.setSink(this.unwrapper); + this.udptracker = new ReplyTracker(this, replyMap); + this.unwrapper.setSink(this.udptracker); + } + + /** Socks passes this back to the client on the TCP connection */ + public int getPort() { + return this.udpsink.getPort(); + } + + public void setSink(Sink sink) { + this.udptracker.setSink(sink); + } + + public void start() { + // the other Sources don't use start + this.udpsource.start(); + } + + public void stop() { + this.udpsink.stop(); + this.udpsource.stop(); + } + + public void send(Destination from, byte[] data) { + this.wrapper.send(from, data); + } + + + private UDPSink udpsink; + private UDPSource udpsource; + private SOCKSUDPWrapper wrapper; + private SOCKSUDPUnwrapper unwrapper; + private ReplyTracker udptracker; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPTunnel.java new file mode 100644 index 000000000..0adaa1950 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPTunnel.java @@ -0,0 +1,94 @@ +package net.i2p.i2ptunnel.socks; + +import java.net.InetAddress; +import java.util.concurrent.ConcurrentHashMap; +import java.util.Iterator; +import java.util.Map; + +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.I2PTunnel; +import net.i2p.i2ptunnel.Logging; +import net.i2p.i2ptunnel.udp.*; +import net.i2p.i2ptunnel.udpTunnel.I2PTunnelUDPClientBase; +import net.i2p.util.EventDispatcher; + +/** + * A Datagram Tunnel that can have multiple bidirectional ports on the UDP side. + * + * TX: + * (ReplyTracker in multiple SOCKSUDPPorts -> ) I2PSink + * + * RX: + * (SOCKSUDPWrapper in multiple SOCKSUDPPorts <- ) MultiSink <- I2PSource + * + * The reply from a dest goes to the last SOCKSUDPPort that sent to that dest. + * If multiple ports are talking to a dest at the same time, this isn't + * going to work very well. + * + * @author zzz modded from streamr/StreamrConsumer + */ +public class SOCKSUDPTunnel extends I2PTunnelUDPClientBase { + + /** + * Set up a tunnel with no UDP side yet. + * Use add() for each port. + */ + public SOCKSUDPTunnel(I2PTunnel tunnel) { + super(null, tunnel, tunnel, tunnel); + + this.ports = new ConcurrentHashMap(1); + this.cache = new ConcurrentHashMap(1); + this.demuxer = new MultiSink(this.cache); + setSink(this.demuxer); + } + + + /** @return the UDP port number */ + public int add(InetAddress host, int port) { + SOCKSUDPPort sup = new SOCKSUDPPort(host, port, this.cache); + this.ports.put(Integer.valueOf(sup.getPort()), sup); + sup.setSink(this); + sup.start(); + return sup.getPort(); + } + + public void remove(Integer port) { + SOCKSUDPPort sup = this.ports.remove(port); + if (sup != null) + sup.stop(); + for (Iterator iter = cache.entrySet().iterator(); iter.hasNext();) { + Map.Entry e = (Map.Entry) iter.next(); + if (e.getValue() == sup) + iter.remove(); + } + } + + public final void startRunning() { + super.startRunning(); + // demuxer start() doesn't do anything + startall(); + } + + public boolean close(boolean forced) { + stopall(); + return super.close(forced); + } + + /** you should really add() after startRunning() */ + private void startall() { + } + + private void stopall() { + for (SOCKSUDPPort sup : this.ports.values()) { + sup.stop(); + } + this.ports.clear(); + this.cache.clear(); + } + + + + private Map ports; + private Map cache; + private MultiSink demuxer; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPUnwrapper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPUnwrapper.java new file mode 100644 index 000000000..2720b6fd4 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPUnwrapper.java @@ -0,0 +1,59 @@ +package net.i2p.i2ptunnel.socks; + +import java.util.Map; + +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.udp.*; +import net.i2p.util.Log; + +/** + * Strip a SOCKS header off a datagram, convert it to a Destination + * Ref: RFC 1928 + * + * @author zzz + */ +public class SOCKSUDPUnwrapper implements Source, Sink { + private static final Log _log = new Log(SOCKSUDPUnwrapper.class); + + /** + * @param cache put headers here to pass to SOCKSUDPWrapper + */ + public SOCKSUDPUnwrapper(Map cache) { + this.cache = cache; + } + + public void setSink(Sink sink) { + this.sink = sink; + } + + public void start() {} + + /** + * + */ + public void send(Destination ignored_from, byte[] data) { + SOCKSHeader h; + try { + h = new SOCKSHeader(data); + } catch (IllegalArgumentException iae) { + _log.error(iae.toString()); + return; + } + Destination dest = h.getDestination(); + if (dest == null) { + // no, we aren't going to send non-i2p traffic to a UDP outproxy :) + _log.error("Destination not found: " + h.getHost()); + return; + } + + cache.put(dest, h); + + int headerlen = h.getBytes().length; + byte unwrapped[] = new byte[data.length - headerlen]; + System.arraycopy(unwrapped, 0, data, headerlen, unwrapped.length); + this.sink.send(dest, unwrapped); + } + + private Sink sink; + private Map cache; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPWrapper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPWrapper.java new file mode 100644 index 000000000..4ec836157 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSUDPWrapper.java @@ -0,0 +1,49 @@ +package net.i2p.i2ptunnel.socks; + +import java.util.Map; + +import net.i2p.data.Destination; +import net.i2p.i2ptunnel.udp.*; + +/** + * Put a SOCKS header on a datagram + * Ref: RFC 1928 + * + * @author zzz + */ +public class SOCKSUDPWrapper implements Source, Sink { + public SOCKSUDPWrapper(Map cache) { + this.cache = cache; + } + + public void setSink(Sink sink) { + this.sink = sink; + } + + public void start() {} + + /** + * Use the cached header, which should have the host string and port + * + */ + public void send(Destination from, byte[] data) { + if (this.sink == null) + return; + + SOCKSHeader h = cache.get(from); + if (h == null) { + // RFC 1928 says drop + // h = new SOCKSHeader(from); + return; + } + + byte[] header = h.getBytes(); + byte wrapped[] = new byte[header.length + data.length]; + System.arraycopy(wrapped, 0, header, 0, header.length); + System.arraycopy(wrapped, header.length, data, 0, data.length); + this.sink.send(from, wrapped); + } + + private Sink sink; + private Map cache; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java index 13d9b5520..5c5a08027 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/MultiSource.java @@ -27,6 +27,10 @@ public class MultiSource implements Source, Sink { public void start() {} + public void stop() { + this.sinks.clear(); + } + public void send(Destination ignored_from, byte[] data) { for(Destination dest : this.sinks) { this.sink.send(dest, data); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java index 3fc1d881b..02b443443 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java @@ -47,6 +47,7 @@ public class StreamrConsumer extends I2PTunnelUDPClientBase { public boolean close(boolean forced) { // send unsubscribe-message this.pinger.stop(); + this.sink.stop(); return super.close(forced); } @@ -59,6 +60,6 @@ public class StreamrConsumer extends I2PTunnelUDPClientBase { - private Sink sink; + private UDPSink sink; private Pinger pinger; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java index d722c5f95..c3963b6a6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java @@ -51,7 +51,8 @@ public class StreamrProducer extends I2PTunnelUDPServerBase { } public boolean close(boolean forced) { - // need some stop() methods in UDPSource and MultiSource + this.server.stop(); + this.multi.stop(); return super.close(forced); } @@ -65,6 +66,6 @@ public class StreamrProducer extends I2PTunnelUDPServerBase { private MultiSource multi; - private Source server; + private UDPSource server; private Sink subscriber; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java index 15feba615..d2e8e8924 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSink.java @@ -34,6 +34,8 @@ public class UDPSink implements Sink { } public void send(Destination src, byte[] data) { + // if data.length > this.sock.getSendBufferSize() ... + // create packet DatagramPacket packet = new DatagramPacket(data, data.length, this.remoteHost, this.remotePort); @@ -46,17 +48,18 @@ public class UDPSink implements Sink { } } + public int getPort() { + return this.sock.getLocalPort(); + } + /** to pass to UDPSource constructor */ + public DatagramSocket getSocket() { + return this.sock; + } - - - - - - - - - + public void stop() { + this.sock.close(); + } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java index c54a984b0..fc1dd5bf2 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/UDPSource.java @@ -28,6 +28,13 @@ public class UDPSource implements Source, Runnable { // create thread this.thread = new Thread(this); } + + /** use socket from UDPSink */ + public UDPSource(DatagramSocket sock) { + this.sink = null; + this.sock = sock; + this.thread = new Thread(this); + } public void setSink(Sink sink) { this.sink = sink; @@ -57,13 +64,14 @@ public class UDPSource implements Source, Runnable { //System.out.print("i"); } catch(Exception e) { e.printStackTrace(); + break; } } } - - - + public void stop() { + this.sock.close(); + } From 0d2812db5063bc3c88044dbd480e05599e5f29b1 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Feb 2009 23:32:38 +0000 Subject: [PATCH 147/191] add standard logging to NativeBigInteger --- .../src/net/i2p/util/NativeBigInteger.java | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/core/java/src/net/i2p/util/NativeBigInteger.java b/core/java/src/net/i2p/util/NativeBigInteger.java index 7a64e24e4..970de52c8 100644 --- a/core/java/src/net/i2p/util/NativeBigInteger.java +++ b/core/java/src/net/i2p/util/NativeBigInteger.java @@ -23,6 +23,9 @@ import freenet.support.CPUInformation.CPUInfo; import freenet.support.CPUInformation.IntelCPUInfo; import freenet.support.CPUInformation.UnknownCPUException; +import net.i2p.I2PAppContext; +import net.i2p.util.Log; + /** *

    BigInteger that takes advantage of the jbigi library for the modPow operation, * which accounts for a massive segment of the processing cost of asymmetric @@ -89,6 +92,9 @@ public class NativeBigInteger extends BigInteger { * do we want to dump some basic success/failure info to stderr during * initialization? this would otherwise use the Log component, but this makes * it easier for other systems to reuse this class + * + * Well, we really want to use Log so if you are one of those "other systems" + * then comment out the I2PAppContext usage below. */ private static final boolean _doLog = System.getProperty("jbigi.dontLog") == null; @@ -401,38 +407,32 @@ public class NativeBigInteger extends BigInteger { boolean loaded = loadGeneric("jbigi"); if (loaded) { _nativeOk = true; - if (_doLog) - System.err.println("INFO: Locally optimized native BigInteger loaded from the library path"); + info("Locally optimized native BigInteger library loaded from the library path"); } else { loaded = loadFromResource("jbigi"); if (loaded) { _nativeOk = true; - if (_doLog) - System.err.println("INFO: Locally optimized native BigInteger loaded from resource"); + info("Locally optimized native BigInteger library loaded from resource"); } else { loaded = loadFromResource(true); if (loaded) { _nativeOk = true; - if (_doLog) - System.err.println("INFO: Optimized native BigInteger library '"+getResourceName(true)+"' loaded from resource"); + info("Optimized native BigInteger library '"+getResourceName(true)+"' loaded from resource"); } else { loaded = loadGeneric(true); if (loaded) { _nativeOk = true; - if (_doLog) - System.err.println("INFO: Optimized native BigInteger library '"+getMiddleName(true)+"' loaded from somewhere in the path"); + info("Optimized native BigInteger library '"+getMiddleName(true)+"' loaded from somewhere in the path"); } else { loaded = loadFromResource(false); if (loaded) { _nativeOk = true; - if (_doLog) - System.err.println("INFO: Non-optimized native BigInteger library '"+getResourceName(false)+"' loaded from resource"); + info("Non-optimized native BigInteger library '"+getResourceName(false)+"' loaded from resource"); } else { loaded = loadGeneric(false); if (loaded) { _nativeOk = true; - if (_doLog) - System.err.println("INFO: Non-optimized native BigInteger library '"+getMiddleName(false)+"' loaded from somewhere in the path"); + info("Non-optimized native BigInteger library '"+getMiddleName(false)+"' loaded from somewhere in the path"); } else { _nativeOk = false; } @@ -442,16 +442,27 @@ public class NativeBigInteger extends BigInteger { } } } - if (_doLog && !_nativeOk) - System.err.println("INFO: Native BigInteger library jbigi not loaded - using pure java"); + if (!_nativeOk) { + warn("Native BigInteger library jbigi not loaded - using pure Java - " + + "poor performance may result - see http://www.i2p2.i2p/jbigi.html for help"); + } }catch(Exception e){ - if (_doLog) { - System.err.println("INFO: Native BigInteger library jbigi not loaded, reason: '"+e.getMessage()+"' - using pure java"); - e.printStackTrace(); - } + warn("Native BigInteger library jbigi not loaded, reason: '"+e.getMessage()+"' - using pure java"); } } + private static void info(String s) { + if(_doLog) + System.err.println("INFO: " + s); + I2PAppContext.getGlobalContext().logManager().getLog(NativeBigInteger.class).info(s); + } + + private static void warn(String s) { + if(_doLog) + System.err.println("WARNING: " + s); + I2PAppContext.getGlobalContext().logManager().getLog(NativeBigInteger.class).warn(s); + } + /** *

    Try loading it from an explictly build jbigi.dll / libjbigi.so first, before * looking into a jbigi.jar for any other libraries.

    From 84bd8274ad627e376c6e2320849d95b64f4d491e Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 25 Feb 2009 00:05:30 +0000 Subject: [PATCH 148/191] * Router: Move addShutdownTask from Router to I2PAppContext so that apps can register more easily --- .../src/org/klomp/snark/SnarkManager.java | 5 ++-- .../net/i2p/router/web/ConfigNetHandler.java | 2 +- .../net/i2p/router/web/ConfigRestartBean.java | 8 +++--- .../i2p/router/web/ConfigServiceHandler.java | 12 ++++----- .../src/net/i2p/router/web/UpdateHandler.java | 2 +- core/java/src/net/i2p/I2PAppContext.java | 12 +++++++++ router/java/src/net/i2p/router/Router.java | 26 ++++++------------- 7 files changed, 34 insertions(+), 33 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 7b62ace84..54367af1a 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -81,8 +81,7 @@ public class SnarkManager implements Snark.CompleteListener { I2PAppThread monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor"); monitor.setDaemon(true); monitor.start(); - if (_context instanceof RouterContext) - ((RouterContext)_context).router().addShutdownTask(new SnarkManagerShutdown()); + _context.addShutdownTask(new SnarkManagerShutdown()); } /** hook to I2PSnarkUtil for the servlet */ @@ -539,7 +538,7 @@ public class SnarkManager implements Snark.CompleteListener { String announce = info.getAnnounce(); // basic validation of url if ((!announce.startsWith("http://")) || - (announce.indexOf(".i2p/") < 0)) + (announce.indexOf(".i2p/") < 0)) // need to do better than this return "Non-i2p tracker in " + info.getName() + ", deleting it"; List files = info.getFiles(); if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java index 0ddcd58a9..a4fe7483e 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHandler.java @@ -237,7 +237,7 @@ public class ConfigNetHandler extends FormHandler { private void hiddenSwitch() { // Full restart required to generate new keys - _context.router().addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART)); + _context.addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART)); _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java index 75a8108c5..e8eb6b26d 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigRestartBean.java @@ -25,20 +25,20 @@ public class ConfigRestartBean { String systemNonce = getNonce(); if ( (nonce != null) && (systemNonce.equals(nonce)) && (action != null) ) { if ("shutdownImmediate".equals(action)) { - ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD)); + ctx.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD)); //ctx.router().shutdown(Router.EXIT_HARD); // never returns ctx.router().shutdownGracefully(Router.EXIT_HARD); // give the UI time to respond } else if ("cancelShutdown".equals(action)) { ctx.router().cancelGracefulShutdown(); } else if ("restartImmediate".equals(action)) { - ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART)); + ctx.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART)); //ctx.router().shutdown(Router.EXIT_HARD_RESTART); // never returns ctx.router().shutdownGracefully(Router.EXIT_HARD_RESTART); // give the UI time to respond } else if ("restart".equals(action)) { - ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART)); + ctx.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART)); ctx.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART); } else if ("shutdown".equals(action)) { - ctx.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL)); + ctx.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL)); ctx.router().shutdownGracefully(); } } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java index bd3bf7a5e..8d3e5725c 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigServiceHandler.java @@ -53,31 +53,31 @@ public class ConfigServiceHandler extends FormHandler { if (_action == null) return; if ("Shutdown gracefully".equals(_action)) { - _context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL)); + _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL)); _context.router().shutdownGracefully(); addFormNotice("Graceful shutdown initiated"); } else if ("Shutdown immediately".equals(_action)) { - _context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD)); + _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD)); _context.router().shutdown(Router.EXIT_HARD); addFormNotice("Shutdown immediately! boom bye bye bad bwoy"); } else if ("Cancel graceful shutdown".equals(_action)) { _context.router().cancelGracefulShutdown(); addFormNotice("Graceful shutdown cancelled"); } else if ("Graceful restart".equals(_action)) { - _context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART)); + _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART)); _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART); addFormNotice("Graceful restart requested"); } else if ("Hard restart".equals(_action)) { - _context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART)); + _context.addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART)); _context.router().shutdown(Router.EXIT_HARD_RESTART); addFormNotice("Hard restart requested"); } else if ("Rekey and Restart".equals(_action)) { addFormNotice("Rekeying after graceful restart"); - _context.router().addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART)); + _context.addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART)); _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART); } else if ("Rekey and Shutdown".equals(_action)) { addFormNotice("Rekeying after graceful shutdown"); - _context.router().addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL)); + _context.addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL)); _context.router().shutdownGracefully(Router.EXIT_GRACEFUL); } else if ("Run I2P on startup".equals(_action)) { installService(); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java index be39da2fd..83495f33e 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java @@ -185,7 +185,7 @@ public class UpdateHandler { } private void restart() { - _context.router().addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART)); + _context.addShutdownTask(new ConfigServiceHandler.UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART)); _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART); } diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java index 6b3b0fd5b..f26f74ab7 100644 --- a/core/java/src/net/i2p/I2PAppContext.java +++ b/core/java/src/net/i2p/I2PAppContext.java @@ -23,6 +23,7 @@ import net.i2p.crypto.SessionKeyManager; import net.i2p.data.RoutingKeyGenerator; import net.i2p.stat.StatManager; import net.i2p.util.Clock; +import net.i2p.util.ConcurrentHashSet; import net.i2p.util.FortunaRandomSource; import net.i2p.util.KeyRing; import net.i2p.util.LogManager; @@ -94,6 +95,7 @@ public class I2PAppContext { private volatile boolean _randomInitialized; private volatile boolean _keyGeneratorInitialized; protected volatile boolean _keyRingInitialized; // used in RouterContext + private Set _shutdownTasks; /** @@ -152,6 +154,7 @@ public class I2PAppContext { _elGamalAESEngineInitialized = false; _logManagerInitialized = false; _keyRingInitialized = false; + _shutdownTasks = new ConcurrentHashSet(0); } /** @@ -557,4 +560,13 @@ public class I2PAppContext { _randomInitialized = true; } } + + public void addShutdownTask(Runnable task) { + _shutdownTasks.add(task); + } + + public Set getShutdownTasks() { + return new HashSet(_shutdownTasks); + } + } diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index 77e4b1968..13e801458 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -65,7 +65,6 @@ public class Router { private I2PThread.OOMEventListener _oomListener; private ShutdownHook _shutdownHook; private I2PThread _gracefulShutdownDetector; - private Set _shutdownTasks; public final static String PROP_CONFIG_FILE = "router.configLocation"; @@ -171,7 +170,6 @@ public class Router { watchdog.setDaemon(true); watchdog.start(); - _shutdownTasks = new HashSet(0); } /** @@ -491,13 +489,12 @@ public class Router { */ public void rebuildNewIdentity() { killKeys(); - try { - for (Iterator iter = _shutdownTasks.iterator(); iter.hasNext(); ) { - Runnable task = (Runnable)iter.next(); + for (Runnable task : _context.getShutdownTasks()) { + try { task.run(); + } catch (Throwable t) { + _log.log(Log.CRIT, "Error running shutdown task", t); } - } catch (Throwable t) { - _log.log(Log.CRIT, "Error running shutdown task", t); } // hard and ugly finalShutdown(EXIT_HARD_RESTART); @@ -782,12 +779,6 @@ public class Router { buf.setLength(0); } - public void addShutdownTask(Runnable task) { - synchronized (_shutdownTasks) { - _shutdownTasks.add(task); - } - } - public static final int EXIT_GRACEFUL = 2; public static final int EXIT_HARD = 3; public static final int EXIT_OOM = 10; @@ -800,13 +791,12 @@ public class Router { I2PThread.removeOOMEventListener(_oomListener); // Run the shutdown hooks first in case they want to send some goodbye messages // Maybe we need a delay after this too? - try { - for (Iterator iter = _shutdownTasks.iterator(); iter.hasNext(); ) { - Runnable task = (Runnable)iter.next(); + for (Runnable task : _context.getShutdownTasks()) { + try { task.run(); + } catch (Throwable t) { + _log.log(Log.CRIT, "Error running shutdown task", t); } - } catch (Throwable t) { - _log.log(Log.CRIT, "Error running shutdown task", t); } try { _context.clientManager().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the client manager", t); } try { _context.jobQueue().shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the job queue", t); } From d222c7a9986dc7bfdfb9dc1159ae46df9388ba01 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 25 Feb 2009 01:18:38 +0000 Subject: [PATCH 149/191] move dest-to-hash conversion to new helper class --- .../i2p/router/web/ConfigKeyringHandler.java | 21 +---- core/java/src/net/i2p/util/ConvertToHash.java | 76 +++++++++++++++++++ 2 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 core/java/src/net/i2p/util/ConvertToHash.java diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java index 09f0905bf..b43bc4d1f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigKeyringHandler.java @@ -2,9 +2,9 @@ package net.i2p.router.web; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; -import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.SessionKey; +import net.i2p.util.ConvertToHash; /** * Support additions via B64 Destkey, B64 Desthash, or blahblah.i2p @@ -19,27 +19,12 @@ public class ConfigKeyringHandler extends FormHandler { addFormError("You must enter a destination and a key"); return; } - Hash h = new Hash(); - try { - h.fromBase64(_peer); - } catch (DataFormatException dfe) {} - if (h.getData() == null) { - try { - Destination d = new Destination(); - d.fromBase64(_peer); - h = d.calculateHash(); - } catch (DataFormatException dfe) {} - } - if (h.getData() == null) { - Destination d = _context.namingService().lookup(_peer); - if (d != null) - h = d.calculateHash(); - } + Hash h = ConvertToHash.getHash(_peer); SessionKey sk = new SessionKey(); try { sk.fromBase64(_key); } catch (DataFormatException dfe) {} - if (h.getData() != null && sk.getData() != null) { + if (h != null && h.getData() != null && sk.getData() != null) { _context.keyRing().put(h, sk); addFormNotice("Key for " + h.toBase64() + " added to keyring"); } else { diff --git a/core/java/src/net/i2p/util/ConvertToHash.java b/core/java/src/net/i2p/util/ConvertToHash.java new file mode 100644 index 000000000..087855640 --- /dev/null +++ b/core/java/src/net/i2p/util/ConvertToHash.java @@ -0,0 +1,76 @@ +package net.i2p.util; + +import net.i2p.I2PAppContext; +import net.i2p.data.Base32; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.data.Hash; + +/** + * Convert any kind of destination String to a hash + * Supported: + * Base64 dest + * Base64 dest.i2p + * Base64 Hash + * Base32 Hash + * Base32 desthash.b32.i2p + * example.i2p + * + * @return null on failure + * + * @author zzz + */ +public class ConvertToHash { + + public static Hash getHash(String peer) { + if (peer == null) + return null; + Hash h = new Hash(); + String peerLC = peer.toLowerCase(); + // b64 hash + if (peer.length() == 44 && !peerLC.endsWith(".i2p")) { + try { + h.fromBase64(peer); + } catch (DataFormatException dfe) {} + } + // b64 dest.i2p + if (h.getData() == null && peer.length() >= 520 && peerLC.endsWith(".i2p")) { + try { + Destination d = new Destination(); + d.fromBase64(peer.substring(0, peer.length() - 4)); + h = d.calculateHash(); + } catch (DataFormatException dfe) {} + } + // b64 dest + if (h.getData() == null && peer.length() >= 516 && !peerLC.endsWith(".i2p")) { + try { + Destination d = new Destination(); + d.fromBase64(peer); + h = d.calculateHash(); + } catch (DataFormatException dfe) {} + } + // b32 hash.b32.i2p + // do this here rather than in naming service so it will work + // even if the leaseset is not found + if (h.getData() == null && peer.length() == 60 && peerLC.endsWith(".b32.i2p")) { + byte[] b = Base32.decode(peer.substring(0, 52)); + if (b != null && b.length == Hash.HASH_LENGTH) + h.setData(b); + } + // b32 hash + if (h.getData() == null && peer.length() == 52 && !peerLC.endsWith(".i2p")) { + byte[] b = Base32.decode(peer); + if (b != null && b.length == Hash.HASH_LENGTH) + h.setData(b); + } + // example.i2p + if (h.getData() == null) { + Destination d = I2PAppContext.getGlobalContext().namingService().lookup(peer); + if (d != null) + h = d.calculateHash(); + } + if (h.getData() == null) + return null; + return h; + } +} From 56473c6b65bccf9b8d410141861ab20a1011c299 Mon Sep 17 00:00:00 2001 From: zzz Date: Wed, 25 Feb 2009 02:00:13 +0000 Subject: [PATCH 150/191] add reverse lookup by hash --- .../client/naming/HostsTxtNamingService.java | 32 +++++++++++++++++++ .../net/i2p/client/naming/NamingService.java | 2 ++ 2 files changed, 34 insertions(+) diff --git a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java index 9fa227f81..054bd9d8f 100644 --- a/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java +++ b/core/java/src/net/i2p/client/naming/HostsTxtNamingService.java @@ -16,8 +16,10 @@ import java.util.Set; import java.util.StringTokenizer; import net.i2p.I2PAppContext; +import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; +import net.i2p.data.Hash; import net.i2p.util.Log; /** @@ -135,4 +137,34 @@ public class HostsTxtNamingService extends NamingService { } return null; } + + @Override + public String reverseLookup(Hash h) { + List filenames = getFilenames(); + for (int i = 0; i < filenames.size(); i++) { + String hostsfile = (String)filenames.get(i); + Properties hosts = new Properties(); + try { + File f = new File(hostsfile); + if ( (f.exists()) && (f.canRead()) ) { + DataHelper.loadProps(hosts, f, true); + Set keyset = hosts.keySet(); + Iterator iter = keyset.iterator(); + while (iter.hasNext()) { + String host = (String)iter.next(); + String key = hosts.getProperty(host); + try { + Destination destkey = new Destination(); + destkey.fromBase64(key); + if (h.equals(destkey.calculateHash())) + return host; + } catch (DataFormatException dfe) {} + } + } + } catch (Exception ioe) { + _log.error("Error loading hosts file " + hostsfile, ioe); + } + } + return null; + } } diff --git a/core/java/src/net/i2p/client/naming/NamingService.java b/core/java/src/net/i2p/client/naming/NamingService.java index 5b61b1bcf..ee02ec911 100644 --- a/core/java/src/net/i2p/client/naming/NamingService.java +++ b/core/java/src/net/i2p/client/naming/NamingService.java @@ -16,6 +16,7 @@ import java.util.Map; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; +import net.i2p.data.Hash; import net.i2p.util.Log; /** @@ -61,6 +62,7 @@ public abstract class NamingService { * null if no reverse lookup is possible. */ public abstract String reverseLookup(Destination dest); + public String reverseLookup(Hash h) { return null; }; /** * Check if host name is valid Base64 encoded dest and return this From 6648e182aef3909b8927e968640c4a43a663e915 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 26 Feb 2009 14:45:45 +0000 Subject: [PATCH 151/191] * I2CP Client: Add support for muxing --- .../net/i2p/client/streaming/PacketQueue.java | 12 +- .../src/net/i2p/client/I2PClientImpl.java | 2 +- core/java/src/net/i2p/client/I2PSession.java | 22 ++ .../i2p/client/I2PSessionDemultiplexer.java | 135 ++++++++ .../src/net/i2p/client/I2PSessionImpl.java | 8 +- .../src/net/i2p/client/I2PSessionImpl2.java | 29 +- .../net/i2p/client/I2PSessionListener.java | 4 +- .../net/i2p/client/I2PSessionMuxedImpl.java | 319 ++++++++++++++++++ .../i2p/client/I2PSessionMuxedListener.java | 62 ++++ 9 files changed, 581 insertions(+), 12 deletions(-) create mode 100644 core/java/src/net/i2p/client/I2PSessionDemultiplexer.java create mode 100644 core/java/src/net/i2p/client/I2PSessionMuxedImpl.java create mode 100644 core/java/src/net/i2p/client/I2PSessionMuxedListener.java diff --git a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java index a56e7753d..e91cbdb7d 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/PacketQueue.java @@ -89,9 +89,17 @@ class PacketQueue { // so if we retransmit it will use a new tunnel/lease combo expires = rpe.getNextSendTime() - 500; if (expires > 0) - sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires); + // I2PSessionImpl2 + //sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires); + // I2PSessionMuxedImpl + sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, expires, + I2PSession.PROTO_STREAMING, I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED); else - sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent); + // I2PSessionImpl2 + //sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, 0); + // I2PSessionMuxedImpl + sent = _session.sendMessage(packet.getTo(), buf, 0, size, keyUsed, tagsSent, + I2PSession.PROTO_STREAMING, I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED); end = _context.clock().now(); if ( (end-begin > 1000) && (_log.shouldLog(Log.WARN)) ) diff --git a/core/java/src/net/i2p/client/I2PClientImpl.java b/core/java/src/net/i2p/client/I2PClientImpl.java index 4783458a3..5b1b44867 100644 --- a/core/java/src/net/i2p/client/I2PClientImpl.java +++ b/core/java/src/net/i2p/client/I2PClientImpl.java @@ -77,6 +77,6 @@ class I2PClientImpl implements I2PClient { * */ public I2PSession createSession(I2PAppContext context, InputStream destKeyStream, Properties options) throws I2PSessionException { - return new I2PSessionImpl2(context, destKeyStream, options); // thread safe + return new I2PSessionMuxedImpl(context, destKeyStream, options); // thread safe and muxed } } diff --git a/core/java/src/net/i2p/client/I2PSession.java b/core/java/src/net/i2p/client/I2PSession.java index d8c64f222..1776af5c0 100644 --- a/core/java/src/net/i2p/client/I2PSession.java +++ b/core/java/src/net/i2p/client/I2PSession.java @@ -40,6 +40,8 @@ public interface I2PSession { */ public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException; public boolean sendMessage(Destination dest, byte[] payload, int offset, int size) throws I2PSessionException; + /** See I2PSessionMuxedImpl for details */ + public boolean sendMessage(Destination dest, byte[] payload, int proto, int fromport, int toport) throws I2PSessionException; /** * Like sendMessage above, except the key used and the tags sent are exposed to the @@ -71,6 +73,12 @@ public interface I2PSession { public boolean sendMessage(Destination dest, byte[] payload, SessionKey keyUsed, Set tagsSent) throws I2PSessionException; public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent) throws I2PSessionException; public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expire) throws I2PSessionException; + /** See I2PSessionMuxedImpl for details */ + public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, + int proto, int fromport, int toport) throws I2PSessionException; + /** See I2PSessionMuxedImpl for details */ + public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expire, + int proto, int fromport, int toport) throws I2PSessionException; /** Receive a message that the router has notified the client about, returning * the payload. @@ -134,4 +142,18 @@ public interface I2PSession { * */ public Destination lookupDest(Hash h) throws I2PSessionException; + + /** See I2PSessionMuxedImpl for details */ + public void addSessionListener(I2PSessionListener lsnr, int proto, int port); + /** See I2PSessionMuxedImpl for details */ + public void addMuxedSessionListener(I2PSessionMuxedListener l, int proto, int port); + /** See I2PSessionMuxedImpl for details */ + public void removeListener(int proto, int port); + + public static final int PORT_ANY = 0; + public static final int PORT_UNSPECIFIED = 0; + public static final int PROTO_ANY = 0; + public static final int PROTO_UNSPECIFIED = 0; + public static final int PROTO_STREAMING = 6; + public static final int PROTO_DATAGRAM = 17; } diff --git a/core/java/src/net/i2p/client/I2PSessionDemultiplexer.java b/core/java/src/net/i2p/client/I2PSessionDemultiplexer.java new file mode 100644 index 000000000..9a1ff42e3 --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSessionDemultiplexer.java @@ -0,0 +1,135 @@ +package net.i2p.client; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.Map; + +import net.i2p.I2PAppContext; +import net.i2p.util.Log; + +/* + * public domain + */ + +/** + * Implement multiplexing with a 1-byte 'protocol' and a two-byte 'port'. + * Listeners register with either addListener() or addMuxedListener(), + * depending on whether they want to hear about the + * protocol, from port, and to port for every received message. + * + * This only calls one listener, not all that apply. + * + * @author zzz + */ +public class I2PSessionDemultiplexer implements I2PSessionMuxedListener { + private Log _log; + private Map _listeners; + + public I2PSessionDemultiplexer(I2PAppContext ctx) { + _log = ctx.logManager().getLog(I2PSessionDemultiplexer.class); + _listeners = new ConcurrentHashMap(); + } + + /** unused */ + public void messageAvailable(I2PSession session, int msgId, long size) {} + + public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport ) { + I2PSessionMuxedListener l = findListener(proto, toport); + if (l != null) + l.messageAvailable(session, msgId, size, proto, fromport, toport); + else { + // no listener, throw it out + _log.error("No listener found for proto: " + proto + " port: " + toport + "msg id: " + msgId + + " from pool of " + _listeners.size() + " listeners"); + try { + session.receiveMessage(msgId); + } catch (I2PSessionException ise) {} + } + } + + public void reportAbuse(I2PSession session, int severity) { + for (I2PSessionMuxedListener l : _listeners.values()) + l.reportAbuse(session, severity); + } + + public void disconnected(I2PSession session) { + for (I2PSessionMuxedListener l : _listeners.values()) + l.disconnected(session); + } + + public void errorOccurred(I2PSession session, String message, Throwable error) { + for (I2PSessionMuxedListener l : _listeners.values()) + l.errorOccurred(session, message, error); + } + + /** + * For those that don't need to hear about the protocol and ports + * in messageAvailable() + * (Streaming lib) + */ + public void addListener(I2PSessionListener l, int proto, int port) { + _listeners.put(key(proto, port), new NoPortsListener(l)); + } + + /** + * For those that do care + * UDP perhaps + */ + public void addMuxedListener(I2PSessionMuxedListener l, int proto, int port) { + _listeners.put(key(proto, port), l); + } + + public void removeListener(int proto, int port) { + _listeners.remove(key(proto, port)); + } + + /** find the one listener that most specifically matches the request */ + private I2PSessionMuxedListener findListener(int proto, int port) { + I2PSessionMuxedListener rv = getListener(proto, port); + if (rv != null) return rv; + if (port != I2PSession.PORT_ANY) { // try any port + rv = getListener(proto, I2PSession.PORT_ANY); + if (rv != null) return rv; + } + if (proto != I2PSession.PROTO_ANY) { // try any protocol + rv = getListener(I2PSession.PROTO_ANY, port); + if (rv != null) return rv; + } + if (proto != I2PSession.PROTO_ANY && port != I2PSession.PORT_ANY) { // try default + rv = getListener(I2PSession.PROTO_ANY, I2PSession.PORT_ANY); + } + return rv; + } + + private I2PSessionMuxedListener getListener(int proto, int port) { + return _listeners.get(key(proto, port)); + } + + private Integer key(int proto, int port) { + return Integer.valueOf(((port << 8) & 0xffff00) | proto); + } + + /** for those that don't care about proto and ports */ + private static class NoPortsListener implements I2PSessionMuxedListener { + private I2PSessionListener _l; + + public NoPortsListener(I2PSessionListener l) { + _l = l; + } + + public void messageAvailable(I2PSession session, int msgId, long size) { + throw new IllegalArgumentException("no"); + } + public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) { + _l.messageAvailable(session, msgId, size); + } + public void reportAbuse(I2PSession session, int severity) { + _l.reportAbuse(session, severity); + } + public void disconnected(I2PSession session) { + _l.disconnected(session); + } + public void errorOccurred(I2PSession session, String message, Throwable error) { + _l.errorOccurred(session, message, error); + } + } +} diff --git a/core/java/src/net/i2p/client/I2PSessionImpl.java b/core/java/src/net/i2p/client/I2PSessionImpl.java index 00da88aa2..0e13f2c56 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl.java @@ -77,12 +77,12 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa protected OutputStream _out; /** who we send events to */ - private I2PSessionListener _sessionListener; + protected I2PSessionListener _sessionListener; /** class that generates new messages */ protected I2CPMessageProducer _producer; /** map of Long --> MessagePayloadMessage */ - private Map _availableMessages; + protected Map _availableMessages; protected I2PClientMessageHandlerMap _handlerMap; @@ -366,14 +366,14 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa } SimpleScheduler.getInstance().addEvent(new VerifyUsage(mid), 30*1000); } - private class VerifyUsage implements SimpleTimer.TimedEvent { + protected class VerifyUsage implements SimpleTimer.TimedEvent { private Long _msgId; public VerifyUsage(Long id) { _msgId = id; } public void timeReached() { MessagePayloadMessage removed = _availableMessages.remove(_msgId); if (removed != null && !isClosed()) - _log.log(Log.CRIT, "Message NOT removed! id=" + _msgId + ": " + removed); + _log.error("Message NOT removed! id=" + _msgId + ": " + removed); } } diff --git a/core/java/src/net/i2p/client/I2PSessionImpl2.java b/core/java/src/net/i2p/client/I2PSessionImpl2.java index 56ef88974..9abce4b72 100644 --- a/core/java/src/net/i2p/client/I2PSessionImpl2.java +++ b/core/java/src/net/i2p/client/I2PSessionImpl2.java @@ -93,7 +93,7 @@ class I2PSessionImpl2 extends I2PSessionImpl { * set to false. */ private static final int DONT_COMPRESS_SIZE = 66; - private boolean shouldCompress(int size) { + protected boolean shouldCompress(int size) { if (size <= DONT_COMPRESS_SIZE) return false; String p = getOptions().getProperty("i2cp.gzip"); @@ -102,12 +102,35 @@ class I2PSessionImpl2 extends I2PSessionImpl { return SHOULD_COMPRESS; } + public void addSessionListener(I2PSessionListener lsnr, int proto, int port) { + throw new IllegalArgumentException("Use MuxedImpl"); + } + public void addMuxedSessionListener(I2PSessionMuxedListener l, int proto, int port) { + throw new IllegalArgumentException("Use MuxedImpl"); + } + public void removeListener(int proto, int port) { + throw new IllegalArgumentException("Use MuxedImpl"); + } + public boolean sendMessage(Destination dest, byte[] payload, int proto, int fromport, int toport) throws I2PSessionException { + throw new IllegalArgumentException("Use MuxedImpl"); + } + public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, + int proto, int fromport, int toport) throws I2PSessionException { + throw new IllegalArgumentException("Use MuxedImpl"); + } + public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, long expire, + int proto, int fromport, int toport) throws I2PSessionException { + throw new IllegalArgumentException("Use MuxedImpl"); + } + @Override public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException { return sendMessage(dest, payload, 0, payload.length); } public boolean sendMessage(Destination dest, byte[] payload, int offset, int size) throws I2PSessionException { - return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64), 0); + // we don't do end-to-end crypto any more + //return sendMessage(dest, payload, offset, size, new SessionKey(), new HashSet(64), 0); + return sendMessage(dest, payload, offset, size, null, null, 0); } @Override @@ -173,7 +196,7 @@ class I2PSessionImpl2 extends I2PSessionImpl { private static final int NUM_TAGS = 50; - private boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent, long expires) + protected boolean sendBestEffort(Destination dest, byte payload[], SessionKey keyUsed, Set tagsSent, long expires) throws I2PSessionException { SessionKey key = null; SessionKey newKey = null; diff --git a/core/java/src/net/i2p/client/I2PSessionListener.java b/core/java/src/net/i2p/client/I2PSessionListener.java index 4c78c6527..740ebeeab 100644 --- a/core/java/src/net/i2p/client/I2PSessionListener.java +++ b/core/java/src/net/i2p/client/I2PSessionListener.java @@ -20,7 +20,7 @@ public interface I2PSessionListener { * size # of bytes. * @param session session to notify * @param msgId message number available - * @param size size of the message + * @param size size of the message - why it's a long and not an int is a mystery */ void messageAvailable(I2PSession session, int msgId, long size); @@ -42,4 +42,4 @@ public interface I2PSessionListener { * */ void errorOccurred(I2PSession session, String message, Throwable error); -} \ No newline at end of file +} diff --git a/core/java/src/net/i2p/client/I2PSessionMuxedImpl.java b/core/java/src/net/i2p/client/I2PSessionMuxedImpl.java new file mode 100644 index 000000000..1cd7b072a --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSessionMuxedImpl.java @@ -0,0 +1,319 @@ +package net.i2p.client; + +/* + * public domain + */ + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.SessionKey; +import net.i2p.data.SessionTag; +import net.i2p.data.i2cp.MessagePayloadMessage; +import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; + +/** + * I2PSession with protocol and ports + * + * Streaming lib has been modified to send I2PSession.PROTO_STREAMING but + * still receives all. It sends with fromPort and toPort = 0, and receives on all ports. + * + * No datagram apps have been modified yet. + + * Therefore the compatibility situation is as follows: + * + * Compatibility: + * old streaming -> new streaming: sends proto anything, rcvs proto anything + * new streaming -> old streaming: sends PROTO_STREAMING, ignores rcvd proto + * old datagram -> new datagram: sends proto anything, rcvs proto anything + * new datagram -> old datagram: sends PROTO_DATAGRAM, ignores rcvd proto + * In all the above cases, streaming and datagram receive traffic for the other + * protocol, same as before. + * + * old datagram -> new muxed: doesn't work because the old sends proto 0 but the udp side + * of the mux registers with PROTO_DATAGRAM, so the datagrams + * go to the streaming side, same as before. + * old streaming -> new muxed: works + * + * Typical Usage: + * Streaming + datagrams: + * I2PSocketManager sockMgr = getSocketManager(); + * I2PSession session = sockMgr.getSession(); + * session.addMuxedSessionListener(myI2PSessionMuxedListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY); + * * or * + * session.addSessionListener(myI2PSessionListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY); + * session.sendMessage(dest, payload, I2PSession.PROTO_DATAGRAM, fromPort, toPort); + * + * Datagrams only, with multiple ports: + * I2PClient client = I2PClientFactory.createClient(); + * ... + * I2PSession session = client.createSession(...); + * session.addMuxedSessionListener(myI2PSessionMuxedListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY); + * * or * + * session.addSessionListener(myI2PSessionListener, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY); + * session.sendMessage(dest, payload, I2PSession.PROTO_DATAGRAM, fromPort, toPort); + * + * Multiple streaming ports: + * Needs some streaming lib hacking + * + * @author zzz + */ +class I2PSessionMuxedImpl extends I2PSessionImpl2 implements I2PSession { + private I2PSessionDemultiplexer _demultiplexer; + + public I2PSessionMuxedImpl(I2PAppContext ctx, InputStream destKeyStream, Properties options) throws I2PSessionException { + super(ctx, destKeyStream, options); + // also stored in _sessionListener but we keep it in _demultipexer + // as well so we don't have to keep casting + _demultiplexer = new I2PSessionDemultiplexer(ctx); + super.setSessionListener(_demultiplexer); + // discards the one in super(), sorry about that... (no it wasn't started yet) + _availabilityNotifier = new MuxedAvailabilityNotifier(); + } + + /** listen on all protocols and ports */ + @Override + public void setSessionListener(I2PSessionListener lsnr) { + _demultiplexer.addListener(lsnr, PROTO_ANY, PORT_ANY); + } + + /** + * Listen on specified protocol and port. + * + * An existing listener with the same proto and port is replaced. + * Only the listener with the best match is called back for each message. + * + * @param proto 1-254 or PROTO_ANY for all; recommended: + * I2PSession.PROTO_STREAMING + * I2PSession.PROTO_DATAGRAM + * 255 disallowed + * @param port 1-65535 or PORT_ANY for all + */ + public void addSessionListener(I2PSessionListener lsnr, int proto, int port) { + _demultiplexer.addListener(lsnr, proto, port); + } + + /** + * Listen on specified protocol and port, and receive notification + * of proto, fromPort, and toPort for every message. + * @param proto 1-254 or 0 for all; 255 disallowed + * @param port 1-65535 or 0 for all + */ + public void addMuxedSessionListener(I2PSessionMuxedListener l, int proto, int port) { + _demultiplexer.addMuxedListener(l, proto, port); + } + + /** removes the specified listener (only) */ + public void removeListener(int proto, int port) { + _demultiplexer.removeListener(proto, port); + } + + @Override + public boolean sendMessage(Destination dest, byte[] payload) throws I2PSessionException { + return sendMessage(dest, payload, 0, 0, null, null, 0, PROTO_UNSPECIFIED, PORT_UNSPECIFIED, PORT_UNSPECIFIED); + } + + @Override + public boolean sendMessage(Destination dest, byte[] payload, int proto, int fromport, int toport) throws I2PSessionException { + return sendMessage(dest, payload, 0, 0, null, null, 0, proto, fromport, toport); + } + + @Override + public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, + SessionKey keyUsed, Set tagsSent, long expires) + throws I2PSessionException { + return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0, PROTO_UNSPECIFIED, PORT_UNSPECIFIED, PORT_UNSPECIFIED); + } + + @Override + public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, SessionKey keyUsed, Set tagsSent, + int proto, int fromport, int toport) throws I2PSessionException { + return sendMessage(dest, payload, offset, size, keyUsed, tagsSent, 0, proto, fromport, toport); + } + + /** + * @param proto 1-254 or 0 for unset; recommended: + * I2PSession.PROTO_UNSPECIFIED + * I2PSession.PROTO_STREAMING + * I2PSession.PROTO_DATAGRAM + * 255 disallowed + * @param fromport 1-65535 or 0 for unset + * @param toport 1-65535 or 0 for unset + */ + public boolean sendMessage(Destination dest, byte[] payload, int offset, int size, + SessionKey keyUsed, Set tagsSent, long expires, + int proto, int fromPort, int toPort) + throws I2PSessionException { + if (isClosed()) throw new I2PSessionException("Already closed"); + updateActivity(); + + boolean sc = shouldCompress(size); + if (sc) + payload = DataHelper.compress(payload, offset, size); + else + payload = DataHelper.compress(payload, offset, size, DataHelper.NO_COMPRESSION); + + setProto(payload, proto); + setFromPort(payload, fromPort); + setToPort(payload, toPort); + + _context.statManager().addRateData("i2cp.tx.msgCompressed", payload.length, 0); + _context.statManager().addRateData("i2cp.tx.msgExpanded", size, 0); + return sendBestEffort(dest, payload, keyUsed, tagsSent, expires); + } + + /** + * Receive a payload message and let the app know its available + */ + @Override + public void addNewMessage(MessagePayloadMessage msg) { + Long mid = new Long(msg.getMessageId()); + _availableMessages.put(mid, msg); + long id = msg.getMessageId(); + byte data[] = msg.getPayload().getUnencryptedData(); + if ((data == null) || (data.length <= 0)) { + if (_log.shouldLog(Log.CRIT)) + _log.log(Log.CRIT, getPrefix() + "addNewMessage of a message with no unencrypted data", + new Exception("Empty message")); + return; + } + int size = data.length; + if (size < 10) { + _log.error(getPrefix() + "length too short for gzip header: " + size); + return; + } + ((MuxedAvailabilityNotifier)_availabilityNotifier).available(id, size, getProto(msg), + getFromPort(msg), getToPort(msg)); + SimpleScheduler.getInstance().addEvent(new VerifyUsage(mid), 30*1000); + } + + protected class MuxedAvailabilityNotifier extends AvailabilityNotifier { + private LinkedBlockingQueue _msgs; + private boolean _alive; + private static final int POISON_SIZE = -99999; + + public MuxedAvailabilityNotifier() { + _msgs = new LinkedBlockingQueue(); + } + + public void stopNotifying() { + _msgs.clear(); + if (_alive) { + _alive = false; + try { + _msgs.put(new MsgData(0, POISON_SIZE, 0, 0, 0)); + } catch (InterruptedException ie) {} + } + } + + /** unused */ + public void available(long msgId, int size) { throw new IllegalArgumentException("no"); } + + public void available(long msgId, int size, int proto, int fromPort, int toPort) { + try { + _msgs.put(new MsgData((int)(msgId & 0xffffffff), size, proto, fromPort, toPort)); + } catch (InterruptedException ie) {} + } + + public void run() { + _alive = true; + while (true) { + MsgData msg; + try { + msg = _msgs.take(); + } catch (InterruptedException ie) { + continue; + } + if (msg.size == POISON_SIZE) + break; + try { + _demultiplexer.messageAvailable(I2PSessionMuxedImpl.this, msg.id, + msg.size, msg.proto, msg.fromPort, msg.toPort); + } catch (Exception e) { + _log.error("Error notifying app of message availability"); + } + } + } + } + + /** let's keep this simple */ + private static class MsgData { + public int id, size, proto, fromPort, toPort; + public MsgData(int i, int s, int p, int f, int t) { + id = i; + size = s; + proto = p; + fromPort = f; + toPort = t; + } + } + + /** + * No, we couldn't put any protocol byte in front of everything and + * keep backward compatibility. But there are several bytes that + * are unused AND unchecked in the gzip header in releases <= 0.7. + * So let's use 5 of them for a protocol and two 2-byte ports. + * + * Following are all the methods to hide the + * protocol, fromPort, and toPort in the gzip header + * + * The fields used are all ignored on receive in ResettableGzipInputStream + * + * See also ResettableGzipOutputStream. + * Ref: RFC 1952 + * + */ + + /** OS byte in gzip header */ + private static final int PROTO_BYTE = 9; + + /** Upper two bytes of MTIME in gzip header */ + private static final int FROMPORT_BYTES = 4; + + /** Lower two bytes of MTIME in gzip header */ + private static final int TOPORT_BYTES = 6; + + /** Non-muxed sets the OS byte to 0xff */ + private static int getProto(MessagePayloadMessage msg) { + int rv = getByte(msg, PROTO_BYTE) & 0xff; + return rv == 0xff ? PROTO_UNSPECIFIED : rv; + } + + /** Non-muxed sets the MTIME bytes to 0 */ + private static int getFromPort(MessagePayloadMessage msg) { + return (((getByte(msg, FROMPORT_BYTES) & 0xff) << 8) | + (getByte(msg, FROMPORT_BYTES + 1) & 0xff)); + } + + /** Non-muxed sets the MTIME bytes to 0 */ + private static int getToPort(MessagePayloadMessage msg) { + return (((getByte(msg, TOPORT_BYTES) & 0xff) << 8) | + (getByte(msg, TOPORT_BYTES + 1) & 0xff)); + } + + private static int getByte(MessagePayloadMessage msg, int i) { + return msg.getPayload().getUnencryptedData()[i] & 0xff; + } + + private static void setProto(byte[] payload, int p) { + payload[PROTO_BYTE] = (byte) (p & 0xff); + } + + private static void setFromPort(byte[] payload, int p) { + payload[FROMPORT_BYTES] = (byte) ((p >> 8) & 0xff); + payload[FROMPORT_BYTES + 1] = (byte) (p & 0xff); + } + + private static void setToPort(byte[] payload, int p) { + payload[TOPORT_BYTES] = (byte) ((p >> 8) & 0xff); + payload[TOPORT_BYTES + 1] = (byte) (p & 0xff); + } +} diff --git a/core/java/src/net/i2p/client/I2PSessionMuxedListener.java b/core/java/src/net/i2p/client/I2PSessionMuxedListener.java new file mode 100644 index 000000000..118dc75ca --- /dev/null +++ b/core/java/src/net/i2p/client/I2PSessionMuxedListener.java @@ -0,0 +1,62 @@ +package net.i2p.client; + +/* + * public domain + */ + +/** + * Define a means for the router to asynchronously notify the client that a + * new message is available or the router is under attack. + * + * @author zzz extends I2PSessionListener + */ +public interface I2PSessionMuxedListener extends I2PSessionListener { + + /** + * Will be called only if you register via + * setSessionListener() or addSessionListener(). + * And if you are doing that, just use I2PSessionListener. + * + * If you register via addSessionListener(), + * this will be called only for the proto(s) and toport(s) you register for. + * + * @param session session to notify + * @param msgId message number available + * @param size size of the message - why it's a long and not an int is a mystery + */ + void messageAvailable(I2PSession session, int msgId, long size); + + /** + * Instruct the client that the given session has received a message + * + * Will be called only if you register via addMuxedSessionListener(). + * Will be called only for the proto(s) and toport(s) you register for. + * + * @param session session to notify + * @param msgId message number available + * @param size size of the message - why it's a long and not an int is a mystery + * @param proto 1-254 or 0 for unspecified + * @param fromport 1-65535 or 0 for unspecified + * @param toport 1-65535 or 0 for unspecified + */ + void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport); + + /** Instruct the client that the session specified seems to be under attack + * and that the client may wish to move its destination to another router. + * @param session session to report abuse to + * @param severity how bad the abuse is + */ + void reportAbuse(I2PSession session, int severity); + + /** + * Notify the client that the session has been terminated + * + */ + void disconnected(I2PSession session); + + /** + * Notify the client that some error occurred + * + */ + void errorOccurred(I2PSession session, String message, Throwable error); +} From 3733b78ccf727b7dd4090b5b884d37c68285bfc2 Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 27 Feb 2009 21:24:40 +0000 Subject: [PATCH 152/191] * I2PTunnelUDPClientBase: Fix client close, client target host * I2CP Mux: Fix UDP sends --- .../java/src/net/i2p/i2ptunnel/TunnelController.java | 9 ++++++++- .../net/i2p/i2ptunnel/streamr/StreamrConsumer.java | 1 + .../net/i2p/i2ptunnel/streamr/StreamrProducer.java | 1 + .../src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java | 3 ++- .../i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java | 12 ++---------- .../i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java | 3 +-- apps/i2ptunnel/jsp/editServer.jsp | 6 ++++-- .../java/src/net/i2p/client/I2PSessionMuxedImpl.java | 5 +++-- 8 files changed, 22 insertions(+), 18 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 6c5fa4eb9..9cb3762ac 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -213,8 +213,15 @@ public class TunnelController implements Logging { _tunnel.runStreamrClient(new String[] { targetHost, targetPort, dest }, this); } - /** Streamr server is a UDP client, use the targetPort field for listenPort */ + /** + * Streamr server is a UDP client, use the targetPort field for listenPort + * and the targetHost field for the listenOnInterface + */ private void startStreamrServer() { + String listenOn = getTargetHost(); + if ( (listenOn != null) && (listenOn.length() > 0) ) { + _tunnel.runListenOn(new String[] { listenOn }, this); + } String listenPort = getTargetPort(); String privKeyFile = getPrivKeyFile(); _tunnel.runStreamrServer(new String[] { listenPort, privKeyFile }, this); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java index 02b443443..87ea0eefe 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrConsumer.java @@ -42,6 +42,7 @@ public class StreamrConsumer extends I2PTunnelUDPClientBase { super.startRunning(); // send subscribe-message this.pinger.start(); + l.log("Streamr client ready"); } public boolean close(boolean forced) { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java index c3963b6a6..b801cb94f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/streamr/StreamrProducer.java @@ -48,6 +48,7 @@ public class StreamrProducer extends I2PTunnelUDPServerBase { public final void startRunning() { super.startRunning(); this.server.start(); + l.log("Streamr server ready"); } public boolean close(boolean forced) { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java index 09385d46f..58c5bfda4 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udp/I2PSinkAnywhere.java @@ -27,7 +27,8 @@ public class I2PSinkAnywhere implements Sink { this.raw = raw; // create maker - this.maker = new I2PDatagramMaker(this.sess); + if (!raw) + this.maker = new I2PDatagramMaker(this.sess); } /** @param to - where it's going */ diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java index 0123be6ea..c92da6ae8 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPClientBase.java @@ -141,9 +141,6 @@ public abstract class I2PTunnelUDPClientBase extends I2PTunnelTask implements So } else { _i2pSink = new I2PSinkAnywhere(_session, false); } - - //configurePool(tunnel); - } /** @@ -165,13 +162,7 @@ public abstract class I2PTunnelUDPClientBase extends I2PTunnelTask implements So startRunning = true; startLock.notify(); } - - if (open && listenerReady) { - notifyEvent("openBaseClientResult", "ok"); - } else { - l.log("Error listening - please see the logs!"); - notifyEvent("openBaseClientResult", "error"); - } + open = true; } /** @@ -187,6 +178,7 @@ public abstract class I2PTunnelUDPClientBase extends I2PTunnelTask implements So } catch (I2PSessionException ise) {} } l.log("Closing client " + toString()); + open = false; return true; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java index fe129fb13..8dcd66a36 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/udpTunnel/I2PTunnelUDPServerBase.java @@ -41,7 +41,7 @@ public class I2PTunnelUDPServerBase extends I2PTunnelTask implements Source, Sin private static volatile long __serverId = 0; - private Logging l; + protected Logging l; private static final long DEFAULT_READ_TIMEOUT = -1; // 3*60*1000; /** default timeout to 3 minutes - override if desired */ @@ -137,7 +137,6 @@ public class I2PTunnelUDPServerBase extends I2PTunnelTask implements Source, Sin start(); //} - l.log("Ready!"); notifyEvent("openServerResult", "ok"); open = true; } diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index 0e9f9c0ca..70a9df9f7 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -88,14 +88,16 @@ <% } %>
    - <% if (!"streamrserver".equals(tunnelType)) { %>
    - <% } // !streamrserver %>
    -
    - -
    -
    - - class="tickbox" /> -
    -
    - - class="tickbox" /> -
    -
    - - -
    - -
    -
    -
    -
    +
    + +
    +
    + + class="tickbox" /> +
    +
    + + +
    class="tickbox" /> + Enable + class="tickbox" /> + Disable +
    +
    +
    + + +
    + +
    +
    +
    + +
    + +
    +
    + + class="tickbox" /> +
    +
    + + +
    + +
    +
    +
    +
    - + (Tunnel must be stopped first)
    From 59b624a4a4fb9fd5857b424c46faeab6bf6b16fe Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 1 Mar 2009 20:44:01 +0000 Subject: [PATCH 157/191] add reasonable privkey file name default --- apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 e68d140c5..941edd0bd 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -60,8 +60,9 @@ public class EditBean extends IndexBean { TunnelController tun = getController(tunnel); if (tun != null && tun.getPrivKeyFile() != null) return tun.getPrivKeyFile(); - else - return ""; + if (tunnel < 0) + tunnel = _group.getControllers().size(); + return "i2ptunnel" + tunnel + "-privKeys.dat"; } public boolean startAutomatically(int tunnel) { From c455fa6309fb7c66e9159d99d9f0e162010cddfa Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 1 Mar 2009 20:45:16 +0000 Subject: [PATCH 158/191] * OCMOSJ: - Change from 5% reply requests to at least once per minute, in hopes of reducing IRC drops - More clean up of the cache cleaning --- .../OutboundClientMessageOneShotJob.java | 95 +++++++++++-------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java index 0e858ef77..20d69ea73 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java @@ -103,6 +103,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { private static final Object _initializeLock = new Object(); private static boolean _initialized = false; private static final int CLEAN_INTERVAL = 5*60*1000; + private static final int REPLY_REQUEST_INTERVAL = 60*1000; /** * Send the sucker @@ -212,7 +213,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * * Key the cache on the source+dest pair. */ - private static HashMap _leaseSetCache = new HashMap(); + private static HashMap _leaseSetCache = new HashMap(); private LeaseSet getReplyLeaseSet(boolean force) { LeaseSet newLS = getContext().netDb().lookupLeaseSetLocally(_from.calculateHash()); if (newLS == null) @@ -247,7 +248,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { long now = getContext().clock().now(); synchronized (_leaseSetCache) { if (!force) { - LeaseSet ls = (LeaseSet) _leaseSetCache.get(hashPair()); + LeaseSet ls = _leaseSetCache.get(hashPair()); if (ls != null) { if (ls.equals(newLS)) { // still good, send it 10% of the time @@ -312,7 +313,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * lease). * */ - private static HashMap _leaseCache = new HashMap(); + private static HashMap _leaseCache = new HashMap(); private boolean getNextLease() { _leaseSet = getContext().netDb().lookupLeaseSetLocally(_to.calculateHash()); if (_leaseSet == null) { @@ -325,7 +326,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { // Use the same lease if it's still good // Even if _leaseSet changed, _leaseSet.getEncryptionKey() didn't... synchronized (_leaseCache) { - _lease = (Lease) _leaseCache.get(hashPair()); + _lease = _leaseCache.get(hashPair()); if (_lease != null) { // if outbound tunnel length == 0 && lease.firsthop.isBacklogged() don't use it ?? if (!_lease.isExpired(Router.CLOCK_FUDGE_FACTOR)) { @@ -446,6 +447,14 @@ public class OutboundClientMessageOneShotJob extends JobImpl { } } + /** + * This cache is used to ensure that we request a reply every so often. + * Hopefully this allows the router to recognize a failed tunnel and switch, + * before upper layers like streaming lib fail, even for low-bandwidth + * connections like IRC. + */ + private static HashMap _lastReplyRequestCache = new HashMap(); + /** * Send the message to the specified tunnel by creating a new garlic message containing * the (already created) payload clove as well as a new delivery status message. This garlic @@ -456,18 +465,27 @@ public class OutboundClientMessageOneShotJob extends JobImpl { */ private void send() { if (_finished) return; - if (getContext().clock().now() >= _overallExpiration) { + long now = getContext().clock().now(); + if (now >= _overallExpiration) { dieFatal(); return; } int existingTags = GarlicMessageBuilder.estimateAvailableTags(getContext(), _leaseSet.getEncryptionKey()); _outTunnel = selectOutboundTunnel(_to); + // boolean wantACK = _wantACK || existingTags <= 30 || getContext().random().nextInt(100) < 5; // what's the point of 5% random? possible improvements or replacements: - // - wantACK if we changed their inbound lease (getNextLease() sets _wantACK) - // - wantACK if we changed our outbound tunnel (selectOutboundTunnel() sets _wantACK) - // - wantACK if we haven't in last 1m (requires a new static cache probably) - boolean wantACK = _wantACK || existingTags <= 30 || getContext().random().nextInt(100) < 5; + // DONE (getNextLease() is called before this): wantACK if we changed their inbound lease (getNextLease() sets _wantACK) + // DONE (selectOutboundTunnel() moved above here): wantACK if we changed our outbound tunnel (selectOutboundTunnel() sets _wantACK) + // DONE (added new cache): wantACK if we haven't in last 1m (requires a new static cache probably) + boolean wantACK; + synchronized (_lastReplyRequestCache) { + Long lastSent = _lastReplyRequestCache.get(hashPair()); + wantACK = _wantACK || existingTags <= 30 || + lastSent == null || lastSent.longValue() < now - REPLY_REQUEST_INTERVAL; + if (wantACK) + _lastReplyRequestCache.put(hashPair(), Long.valueOf(now)); + } PublicKey key = _leaseSet.getEncryptionKey(); SessionKey sessKey = new SessionKey(); @@ -501,7 +519,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { // we dont receive the reply? hmm...) if (_log.shouldLog(Log.WARN)) _log.warn(getJobId() + ": Unable to create the garlic message (no tunnels left or too lagged) to " + _toString); - getContext().statManager().addRateData("client.dispatchNoTunnels", getContext().clock().now() - _start, 0); + getContext().statManager().addRateData("client.dispatchNoTunnels", now - _start, 0); dieFatal(); return; } @@ -539,12 +557,12 @@ public class OutboundClientMessageOneShotJob extends JobImpl { } else { if (_log.shouldLog(Log.WARN)) _log.warn(getJobId() + ": Could not find any outbound tunnels to send the payload through... this might take a while"); - getContext().statManager().addRateData("client.dispatchNoTunnels", getContext().clock().now() - _start, 0); + getContext().statManager().addRateData("client.dispatchNoTunnels", now - _start, 0); dieFatal(); } _clientMessage = null; _clove = null; - getContext().statManager().addRateData("client.dispatchPrepareTime", getContext().clock().now() - _start, 0); + getContext().statManager().addRateData("client.dispatchPrepareTime", now - _start, 0); if (!wantACK) getContext().statManager().addRateData("client.dispatchNoACK", 1, 0); } @@ -582,7 +600,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { /** * This is the place where we make I2P go fast. * - * We have four static caches. + * We have five static caches. * - The LeaseSet cache is used to decide whether to bundle our own leaseset, * which minimizes overhead. * - The Lease cache is used to persistently send to the same lease for the destination, @@ -590,6 +608,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * - The Tunnel and BackloggedTunnel caches are used to persistently use the same outbound tunnel * for the same destination, * which keeps the streaming lib happy by minimizing out-of-order delivery. + * - The last reply requested cache ensures that a reply is requested every so often, + * so that failed tunnels are recognized. * */ @@ -629,17 +649,17 @@ public class OutboundClientMessageOneShotJob extends JobImpl { } if (_lease != null) { synchronized(_leaseCache) { - Lease l = (Lease) _leaseCache.get(key); + Lease l = _leaseCache.get(key); if (l != null && l.equals(_lease)) _leaseCache.remove(key); } } if (_outTunnel != null) { synchronized(_tunnelCache) { - TunnelInfo t =(TunnelInfo) _backloggedTunnelCache.get(key); + TunnelInfo t = _backloggedTunnelCache.get(key); if (t != null && t.equals(_outTunnel)) _backloggedTunnelCache.remove(key); - t = (TunnelInfo) _tunnelCache.get(key); + t = _tunnelCache.get(key); if (t != null && t.equals(_outTunnel)) _tunnelCache.remove(key); } @@ -652,17 +672,12 @@ public class OutboundClientMessageOneShotJob extends JobImpl { */ private static void cleanLeaseSetCache(RouterContext ctx, HashMap tc) { long now = ctx.clock().now(); - List deleteList = new ArrayList(); for (Iterator iter = tc.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry)iter.next(); String k = (String) entry.getKey(); LeaseSet l = (LeaseSet) entry.getValue(); if (l.getEarliestLeaseDate() < now) - deleteList.add(k); - } - for (Iterator iter = deleteList.iterator(); iter.hasNext(); ) { - String k = (String) iter.next(); - tc.remove(k); + iter.remove(); } } @@ -671,17 +686,12 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * Caller must synchronize on tc. */ private static void cleanLeaseCache(HashMap tc) { - List deleteList = new ArrayList(); for (Iterator iter = tc.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry)iter.next(); String k = (String) entry.getKey(); Lease l = (Lease) entry.getValue(); if (l.isExpired(Router.CLOCK_FUDGE_FACTOR)) - deleteList.add(k); - } - for (Iterator iter = deleteList.iterator(); iter.hasNext(); ) { - String k = (String) iter.next(); - tc.remove(k); + iter.remove(); } } @@ -690,17 +700,25 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * Caller must synchronize on tc. */ private static void cleanTunnelCache(RouterContext ctx, HashMap tc) { - List deleteList = new ArrayList(); for (Iterator iter = tc.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry)iter.next(); String k = (String) entry.getKey(); TunnelInfo tunnel = (TunnelInfo) entry.getValue(); if (!ctx.tunnelManager().isValidTunnel(sourceFromHashPair(k), tunnel)) - deleteList.add(k); + iter.remove(); } - for (Iterator iter = deleteList.iterator(); iter.hasNext(); ) { - String k = (String) iter.next(); - tc.remove(k); + } + + /** + * Clean out old reply times + * Caller must synchronize on tc. + */ + private static void cleanReplyCache(RouterContext ctx, HashMap tc) { + long now = ctx.clock().now(); + for (Iterator iter = tc.values().iterator(); iter.hasNext(); ) { + Long l = (Long) iter.next(); + if (l.longValue() < now - CLEAN_INTERVAL) + iter.remove(); } } @@ -720,6 +738,9 @@ public class OutboundClientMessageOneShotJob extends JobImpl { cleanTunnelCache(_ctx, _tunnelCache); cleanTunnelCache(_ctx, _backloggedTunnelCache); } + synchronized(_lastReplyRequestCache) { + cleanReplyCache(_ctx, _lastReplyRequestCache); + } } } @@ -731,8 +752,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * Key the caches on the source+dest pair. * */ - private static HashMap _tunnelCache = new HashMap(); - private static HashMap _backloggedTunnelCache = new HashMap(); + private static HashMap _tunnelCache = new HashMap(); + private static HashMap _backloggedTunnelCache = new HashMap(); private TunnelInfo selectOutboundTunnel(Destination to) { TunnelInfo tunnel; long now = getContext().clock().now(); @@ -743,7 +764,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { * if you were the originator by backlogging the tunnel, then removing the * backlog and seeing if traffic came back or not. */ - tunnel = (TunnelInfo) _backloggedTunnelCache.get(hashPair()); + tunnel = _backloggedTunnelCache.get(hashPair()); if (tunnel != null) { if (getContext().tunnelManager().isValidTunnel(_from.calculateHash(), tunnel)) { if (!getContext().commSystem().isBacklogged(tunnel.getPeer(1))) { @@ -758,7 +779,7 @@ public class OutboundClientMessageOneShotJob extends JobImpl { _backloggedTunnelCache.remove(hashPair()); } // Use the same tunnel unless backlogged - tunnel = (TunnelInfo) _tunnelCache.get(hashPair()); + tunnel = _tunnelCache.get(hashPair()); if (tunnel != null) { if (getContext().tunnelManager().isValidTunnel(_from.calculateHash(), tunnel)) { if (tunnel.getLength() <= 1 || !getContext().commSystem().isBacklogged(tunnel.getPeer(1))) From 8f5257d5dc818dde614a9a414b1835a579bcf6bb Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 1 Mar 2009 23:14:38 +0000 Subject: [PATCH 159/191] make persistent client dests work --- .../java/src/net/i2p/i2ptunnel/I2PTunnel.java | 24 ++++++---- .../net/i2p/i2ptunnel/I2PTunnelClient.java | 4 +- .../i2p/i2ptunnel/I2PTunnelClientBase.java | 48 +++++++++++++++---- .../net/i2p/i2ptunnel/I2PTunnelIRCClient.java | 4 +- .../net/i2p/i2ptunnel/TunnelController.java | 16 +++++-- apps/i2ptunnel/jsp/editClient.jsp | 9 ++++ 6 files changed, 80 insertions(+), 25 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java index dc9dfd2fc..b11f954e7 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java @@ -552,14 +552,14 @@ public class I2PTunnel implements Logging, EventDispatcher { * Integer port number if the client is listening * sharedClient parameter is a String "true" or "false" * - * @param args {portNumber, destinationBase64 or "file:filename"[, sharedClient]} + * @param args {portNumber, destinationBase64 or "file:filename"[, sharedClient [, privKeyFile]]} * @param l logger to receive events and output */ public void runClient(String args[], Logging l) { boolean isShared = true; - if (args.length == 3) + if (args.length >= 3) isShared = Boolean.valueOf(args[2].trim()).booleanValue(); - if ( (args.length == 2) || (args.length == 3) ) { + if (args.length >= 2) { int portNum = -1; try { portNum = Integer.parseInt(args[0]); @@ -572,7 +572,10 @@ public class I2PTunnel implements Logging, EventDispatcher { I2PTunnelTask task; ownDest = !isShared; try { - task = new I2PTunnelClient(portNum, args[1], l, ownDest, (EventDispatcher) this, this); + String privateKeyFile = null; + if (args.length >= 4) + privateKeyFile = args[3]; + task = new I2PTunnelClient(portNum, args[1], l, ownDest, (EventDispatcher) this, this, privateKeyFile); addtask(task); notifyEvent("clientTaskId", Integer.valueOf(task.getId())); } catch (IllegalArgumentException iae) { @@ -581,7 +584,7 @@ public class I2PTunnel implements Logging, EventDispatcher { notifyEvent("clientTaskId", Integer.valueOf(-1)); } } else { - l.log("client [,]|file:[ ]"); + l.log("client [,]|file:[ ] []"); l.log(" creates a client that forwards port to the pubkey.\n" + " use 0 as port to get a free port assigned. If you specify\n" + " a comma delimited list of pubkeys, it will rotate among them\n" @@ -720,11 +723,11 @@ public class I2PTunnel implements Logging, EventDispatcher { * Also sets "ircclientStatus" = "ok" or "error" after the client tunnel has started. * parameter sharedClient is a String, either "true" or "false" * - * @param args {portNumber,destinationBase64 or "file:filename" [, sharedClient]} + * @param args {portNumber,destinationBase64 or "file:filename" [, sharedClient [, privKeyFile]]} * @param l logger to receive events and output */ public void runIrcClient(String args[], Logging l) { - if (args.length >= 2 && args.length <= 3) { + if (args.length >= 2) { int port = -1; try { port = Integer.parseInt(args[0]); @@ -751,7 +754,10 @@ public class I2PTunnel implements Logging, EventDispatcher { I2PTunnelTask task; ownDest = !isShared; try { - task = new I2PTunnelIRCClient(port, args[1],l, ownDest, (EventDispatcher) this, this); + String privateKeyFile = null; + if (args.length >= 4) + privateKeyFile = args[3]; + task = new I2PTunnelIRCClient(port, args[1], l, ownDest, (EventDispatcher) this, this, privateKeyFile); addtask(task); notifyEvent("ircclientTaskId", Integer.valueOf(task.getId())); } catch (IllegalArgumentException iae) { @@ -760,7 +766,7 @@ public class I2PTunnel implements Logging, EventDispatcher { notifyEvent("ircclientTaskId", Integer.valueOf(-1)); } } else { - l.log("ircclient []"); + l.log("ircclient [ []]"); l.log(" creates a client that filter IRC protocol."); l.log(" (optional) indicates if this client shares tunnels with other clients (true of false)"); notifyEvent("ircclientTaskId", Integer.valueOf(-1)); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java index 4739a07f4..502bb28d5 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java @@ -31,8 +31,8 @@ public class I2PTunnelClient extends I2PTunnelClientBase { */ public I2PTunnelClient(int localPort, String destinations, Logging l, boolean ownDest, EventDispatcher notifyThis, - I2PTunnel tunnel) throws IllegalArgumentException { - super(localPort, ownDest, l, notifyThis, "SynSender", tunnel); + I2PTunnel tunnel, String pkf) throws IllegalArgumentException { + super(localPort, ownDest, l, notifyThis, "SynSender", tunnel, pkf); if (waitEventValue("openBaseClientResult").equals("error")) { notifyEvent("openClientResult", "error"); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java index c926156f4..fadbf9fa1 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java @@ -3,6 +3,7 @@ */ package net.i2p.i2ptunnel; +import java.io.FileInputStream; import java.io.IOException; import java.io.InterruptedIOException; import java.net.ConnectException; @@ -59,6 +60,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna private byte[] pubkey; private String handlerName; + private String privKeyFile; private Object conLock = new Object(); @@ -91,18 +93,28 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna // I2PTunnelClientBase(localPort, ownDest, l, (EventDispatcher)null); //} + public I2PTunnelClientBase(int localPort, boolean ownDest, Logging l, + EventDispatcher notifyThis, String handlerName, + I2PTunnel tunnel) throws IllegalArgumentException { + this(localPort, ownDest, l, notifyThis, handlerName, tunnel, null); + } + /** + * @param privKeyFile null to generate a transient key + * * @throws IllegalArgumentException if the I2CP configuration is b0rked so * badly that we cant create a socketManager */ public I2PTunnelClientBase(int localPort, boolean ownDest, Logging l, EventDispatcher notifyThis, String handlerName, - I2PTunnel tunnel) throws IllegalArgumentException{ + I2PTunnel tunnel, String pkf) throws IllegalArgumentException{ super(localPort + " (uninitialized)", notifyThis, tunnel); _clientId = ++__clientId; this.localPort = localPort; this.l = l; this.handlerName = handlerName + _clientId; + this.privKeyFile = pkf; + _context = tunnel.getContext(); _context.statManager().createRateStat("i2ptunnel.client.closeBacklog", "How many pending sockets remain when we close one due to backlog?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 }); @@ -195,28 +207,34 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna private static I2PSocketManager socketManager; protected synchronized I2PSocketManager getSocketManager() { - return getSocketManager(getTunnel()); + return getSocketManager(getTunnel(), this.privKeyFile); } protected static synchronized I2PSocketManager getSocketManager(I2PTunnel tunnel) { + return getSocketManager(tunnel, null); + } + protected static synchronized I2PSocketManager getSocketManager(I2PTunnel tunnel, String pkf) { if (socketManager != null) { I2PSession s = socketManager.getSession(); if ( (s == null) || (s.isClosed()) ) { _log.info("Building a new socket manager since the old one closed [s=" + s + "]"); - socketManager = buildSocketManager(tunnel); + socketManager = buildSocketManager(tunnel, pkf); } else { _log.info("Not building a new socket manager since the old one is open [s=" + s + "]"); } } else { _log.info("Building a new socket manager since there is no other one"); - socketManager = buildSocketManager(tunnel); + socketManager = buildSocketManager(tunnel, pkf); } return socketManager; } protected I2PSocketManager buildSocketManager() { - return buildSocketManager(getTunnel()); + return buildSocketManager(getTunnel(), this.privKeyFile); } protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel) { + return buildSocketManager(tunnel, null); + } + protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel, String pkf) { Properties props = new Properties(); props.putAll(tunnel.getClientOptions()); int portNum = 7654; @@ -230,10 +248,22 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna I2PSocketManager sockManager = null; while (sockManager == null) { - // if persistent dest - // sockManager = I2PSocketManagerFactory.createManager(privData, tunnel.host, portNum, props); - // else - sockManager = I2PSocketManagerFactory.createManager(tunnel.host, portNum, props); + if (pkf != null) { + // Persistent client dest + FileInputStream fis = null; + try { + fis = new FileInputStream(pkf); + sockManager = I2PSocketManagerFactory.createManager(fis, tunnel.host, portNum, props); + } catch (IOException ioe) { + _log.error("Error opening key file", ioe); + // this is going to loop but if we break we'll get a NPE + } finally { + if (fis != null) + try { fis.close(); } catch (IOException ioe) {} + } + } else { + sockManager = I2PSocketManagerFactory.createManager(tunnel.host, portNum, props); + } if (sockManager == null) { _log.log(Log.CRIT, "Unable to create socket manager"); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java index 5b223b1a4..732c222a7 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java @@ -39,12 +39,12 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable Logging l, boolean ownDest, EventDispatcher notifyThis, - I2PTunnel tunnel) throws IllegalArgumentException { + I2PTunnel tunnel, String pkf) throws IllegalArgumentException { super(localPort, ownDest, l, notifyThis, - "IRCHandler " + (++__clientId), tunnel); + "IRCHandler " + (++__clientId), tunnel, pkf); StringTokenizer tok = new StringTokenizer(destinations, ","); dests = new ArrayList(1); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 727b18158..419e5a899 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -192,7 +192,12 @@ public class TunnelController implements Logging { String listenPort = getListenPort(); String dest = getTargetDestination(); String sharedClient = getSharedClient(); - _tunnel.runIrcClient(new String[] { listenPort, dest, sharedClient }, this); + if (getPersistentClientKey()) { + String privKeyFile = getPrivKeyFile(); + _tunnel.runIrcClient(new String[] { listenPort, dest, sharedClient, privKeyFile }, this); + } else { + _tunnel.runIrcClient(new String[] { listenPort, dest, sharedClient }, this); + } } private void startSocksClient() { @@ -264,7 +269,12 @@ public class TunnelController implements Logging { String listenPort = getListenPort(); String dest = getTargetDestination(); String sharedClient = getSharedClient(); - _tunnel.runClient(new String[] { listenPort, dest, sharedClient }, this); + if (getPersistentClientKey()) { + String privKeyFile = getPrivKeyFile(); + _tunnel.runClient(new String[] { listenPort, dest, sharedClient, privKeyFile }, this); + } else { + _tunnel.runClient(new String[] { listenPort, dest, sharedClient }, this); + } } private void startServer() { @@ -395,7 +405,7 @@ public class TunnelController implements Logging { public String getProxyList() { return _config.getProperty("proxyList"); } public String getSharedClient() { return _config.getProperty("sharedClient", "true"); } public boolean getStartOnLoad() { return "true".equalsIgnoreCase(_config.getProperty("startOnLoad", "true")); } - public boolean getPersistentClientKey() { return Boolean.valueOf(_config.getProperty("persistentClientKey")).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/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index 3c046badd..178f564ef 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -351,6 +351,7 @@
    + <% if ("client".equals(tunnelType) || "ircclient".equals(tunnelType)) { %>
    +
    + + + (if known) +

    + <% } %>
    - (if known) + <% if (!"".equals(editBean.getDestinationBase64(curTunnel))) { %> + Add to local addressbook + <% } %>
    +
    + +
    +
    + + class="tickbox" /> +
    + +
    +
    +
    + <% if ("client".equals(tunnelType) || "ircclient".equals(tunnelType)) { %>
    @@ -329,7 +329,7 @@
    <% } // end iterating over required groups for the current stat %>
    diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index eb407ec16..b3917cced 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -326,7 +326,7 @@
    @@ -354,7 +354,7 @@
    From ca3b6eb00daf0ec644f8f4dba09b53d7f3a7a28e Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 13 Mar 2009 16:57:51 +0000 Subject: [PATCH 171/191] catch a reported NPE ? --- .../i2p/i2ptunnel/I2PTunnelHTTPServer.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java index 536844af9..d6cb40a25 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java @@ -171,7 +171,24 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { sender.start(); browserout = _browser.getOutputStream(); - serverin = _webserver.getInputStream(); + // NPE seen here in 0.7-7, caused by addition of socket.close() in the + // catch (IOException ioe) block above in blockingHandle() ??? + // CRIT [ad-130280.hc] net.i2p.util.I2PThread : Killing thread Thread-130280.hc + // java.lang.NullPointerException + // at java.io.FileInputStream.(FileInputStream.java:131) + // at java.net.SocketInputStream.(SocketInputStream.java:44) + // at java.net.PlainSocketImpl.getInputStream(PlainSocketImpl.java:401) + // at java.net.Socket$2.run(Socket.java:779) + // at java.security.AccessController.doPrivileged(Native Method) + // at java.net.Socket.getInputStream(Socket.java:776) + // at net.i2p.i2ptunnel.I2PTunnelHTTPServer$CompressedRequestor.run(I2PTunnelHTTPServer.java:174) + // at java.lang.Thread.run(Thread.java:619) + // at net.i2p.util.I2PThread.run(I2PThread.java:71) + try { + serverin = _webserver.getInputStream(); + } catch (NullPointerException npe) { + throw new IOException("getInputStream NPE"); + } CompressedResponseOutputStream compressedOut = new CompressedResponseOutputStream(browserout); Sender s = new Sender(compressedOut, serverin, "server: server to browser"); if (_log.shouldLog(Log.INFO)) From cf02abd19cf15c00f725866846e486ae5f6afcd3 Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 13 Mar 2009 16:58:23 +0000 Subject: [PATCH 172/191] allow .onion addresses for testing --- .../java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 2745cb0fa..23ec70c3f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS4aServer.java @@ -198,7 +198,8 @@ public class SOCKS4aServer extends SOCKSServer { I2PSocket destSock; try { - if (connHostName.toLowerCase().endsWith(".i2p")) { + if (connHostName.toLowerCase().endsWith(".i2p") || + connHostName.toLowerCase().endsWith(".onion")) { _log.debug("connecting to " + connHostName + "..."); // Let's not due a new Dest for every request, huh? //I2PSocketManager sm = I2PSocketManagerFactory.createManager(); @@ -224,7 +225,7 @@ public class SOCKS4aServer extends SOCKSServer { } else { List proxies = t.getProxies(connPort); if (proxies == null || proxies.size() <= 0) { - String err = "No outproxy configured for port " + connPort + " and no default configured either"; + String err = "No outproxy configured for port " + connPort + " and no default configured either - host: " + connHostName; _log.error(err); try { sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out); From 66eae60c48b1ad1958b68657f560dd1221521f60 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 14 Mar 2009 16:24:17 +0000 Subject: [PATCH 173/191] removed some hosts --- hosts.txt | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/hosts.txt b/hosts.txt index 95ca28af6..b30da6363 100644 --- a/hosts.txt +++ b/hosts.txt @@ -1,27 +1,6 @@ -tc.i2p=3RPLOkQGlq8anNyNWhjbMyHxpAvUyUJKbiUejI80DnPR59T3blc7-XrBhQ2iPbf-BRAR~v1j34Kpba1eDyhPk2gevsE6ULO1irarJ3~C9WcQH2wAbNiVwfWqbh6onQ~YmkSpGNwGHD6ytwbvTyXeBJcS8e6gmfNN-sYLn1aQu8UqWB3D6BmTfLtyS3eqWVk66Nrzmwy8E1Hvq5z~1lukYb~cyiDO1oZHAOLyUQtd9eN16yJY~2SRG8LiscpPMl9nSJUr6fmXMUubW-M7QGFH82Om-735PJUk6WMy1Hi9Vgh4Pxhdl7gfqGRWioFABdhcypb7p1Ca77p73uabLDFK-SjIYmdj7TwSdbNa6PCmzEvCEW~IZeZmnZC5B6pK30AdmD9vc641wUGce9xTJVfNRupf5L7pSsVIISix6FkKQk-FTW2RsZKLbuMCYMaPzLEx5gzODEqtI6Jf2teMd5xCz51RPayDJl~lJ-W0IWYfosnjM~KxYaqc4agviBuF5ZWeAAAA -dyad.i2p=W~JFpqSH8uopylox2V5hMbpcHSsb-dJkSKvdJ1vj~KQcUFJWXFyfbetBAukcGH5S559aK9oslU0qbVoMDlJITVC4OXfXSnVbJBP1IhsK8SvjSYicjmIi2fA~k4HvSh9Wxu~bg8yo~jgfHA8tjYppK9QKc56BpkJb~hx0nNGy4Ny9eW~6A5AwAmHvwdt5NqcREYRMjRd63dMGm8BcEe-6FbOyMo3dnIFcETWAe8TCeoMxm~S1n~6Jlinw3ETxv-L6lQkhFFWnC5zyzQ~4JhVxxT3taTMYXg8td4CBGmrS078jcjW63rlSiQgZBlYfN3iEYmurhuIEV9NXRcmnMrBOQUAoXPpVuRIxJbaQNDL71FO2iv424n4YjKs84suAho34GGQKq7WoL5V5KQgihfcl0f~xne-qP3FtpoPFeyA9x-sA2JWDAsxoZlfvgkiP5eyOn23prT9TJK47HCVilHSV11uTVaC4Jc5YsjoBCZadWbgQnMCKlZ4jk-bLE1PSWLg7AAAA -nightblade.i2p=nyErwSseXbsojcWtNkDyUul0YULtqr6qyWSzIp639Ygpe8juCdgPMLURVXcmlCvo~QPoHg6zt53KpgpGvB1-Wv2SGvc2Mvs~o8USw3ius8fP1URphqcBbulK8Ci0bgknt0kD0AfxqfMz-p~xk1QEMxq2kZEoB3oyIIFnQlpb2ByS74Lx8iKzXTrwWk19I3Dvu4nIq8CBDDwu3lYoCD2kC-jT5pjgglverGPEGN4o55LYVTtfSg4gAJFZeaE4KjBR5P1z7cca6UDjGMWfR0iCa8P3qpkY2ODYpk~8w2xgBbgDq-8Hzik~uraHc598ccS8QpwB0f0Jw~2PZcTjOPdZ-239U6p3tESXa7FXzRBCujv4Bx6CVFRhCmBHpyFnCD-MugZ~vR6XFSS2XBsCT~duXKq94HH2n1iAWslG4Vu44ut1JVhDPFzp~Dk7wujB0tCo2HXH2icRQxOWe37foU4LZSJ4oMpFDACBzwSfcZdIPsVRxGttKQx4yzgffR1Q~Jl7AAAA -bozo.i2p=ubMPUwY0op6B7Jr8SAjY2bQXze8m1sT6xF2N0cv43dIHwLTO0gUqn7FCP9jXZDodE9DR3fu8fG8x1Yz1SpXFk4WtFmuDuhdN7uaHuLIQ71PATC2GRhDS7NXqn7GsVZgQxhHKenaE5BKjIKt2amZ2~8CM0qBKTqwievUO-Y6zG~-8l~RpnAxDZUMOjKKy5R3~jEN9DFZCaKvXSNcOVFjZRGaD6d8NvkAJjndHdE3bFSJUDNv0qhhp09-mm~Se9C~FzjrAbhgappdNRiwQepXTWqRbjjt6lUPT2eJISPDxYxoeZkBGZa9XmfO9QH3hoMo0g~RbwLeBqtgeRGhVgFiC4pN8lFt3z7j8L-12575SUeOnJPIm3hQWXdTjKX1hqf4LopYBG84N95IeydPJegsmkIkAMzEb0d~-UZfVSP9yFgs37j~Fds5yxBsu-NFc6qmZihpXEd7jrfX1-HuJVmXFmwaZgyumRSRDbj714wxr7RP4Hb-liA3JrU-FbqNQFoTMAAAA duck.i2p=eFJdRYFmtjcpx9-Mw3JBdF6fwtb9cHRBR103Q8IGc91Jdfn0iYzK6Xx0oIzPvpbD4yOlPQm-C-7eTahrAkHa2FMCRTiiVq2a0nBp37W1uTvAToV-MKYPKTdFMxrXxvjS7qaSUXdJRcPaPexolfx-Gcjh~rN2tKCh0mz9beueiQ18~8qWGh6hUMb0yyA09ipL9vIkmHmooLwT9AZyzHXEzdLXZe1P~CG3L46QaXp9aTD7EkAwG6VBMjQrGiSJ-9FFhx4QcYAZWM-dfrtzbbVYfHxqQRBwzB27zLlaKVaqu4enC0N1cW1yy-cjnv0Wxokqe62B2uPzFYtloxQpBPfTLQZUfUzjskY-3Yg2AdSbEu37jYsnAJA95AlLz4t1W1vPTNiXzCaRqkkMX342SUkJK-HZE3wTjAgGPqJ9vMaC2YexPFViTxEO2Q0jDSjdPHG0D769qehkN5Bfb8MEI-yr3g9zLaY~w4r6T38ap33qfyISWlIJ5qCPcRVkeK8OqZ4vAAAA irc.duck.i2p=Bxqr5E7-56oJeya1lsDLBN6L1gKme-FUS6Bh~TQS3HswomK9rpjrYNeqBTBoE8TCFl161~FI3soWqbnmFhIdhskausZsO0ez5-4IXMJW8NTilWqXQ3OJxA20M9grohx3RjkZgXU1ooTx7wviSHtXQYiiqnzGnIzmmEZo5-Xx6VjXakctebWwbi2PrsE6XLxrxXBzB34l4KlVsyX504BJiOT6KXNaVZxvG61GfGVfNHdeXljMDE5d25UdFC6RJdDnJ3Z7Yb7EjAww78aowbR0VCfJDH~cB868-VOKIxmor3Rs7giaLXmUyW~GRtFX10COJj5V6BrhKs61XOXxfbyQKGVXZ0mM2A8cdZE1ftr96SZgGy~V8uUHKvoa1HpjMNrPL5Tr6EGfJxOxAy6PHwotn6a8UMnCZgEdTbQ6U3BTywU~x3SCAQvfOT~dl3sZ-5ujYWNFRp7RhdY-WHn1Kj59MfU-VpczGYdV3bRkwT5lpIjST~vopLfkYUeB6gcVSr49AAAA -jabber.duck.i2p=AzR8Zrms1sLXflvaZZ5CI2TS1cRvlO99D5Jh641-KS0lMSQPUnSApTfy9R4aPxZ3SWwpZPfSSGN-e7~kw8ucGQVB9EOHnht7WCz5UKC423vX4fFZqpTyqemDBx0CYXfhqRf-~7mjslXigR2nbQwh8NMl2bgLPknGe55wMlZPfsacS2WKNQYbdOvrgG6Zb6DK8RzTDrsum5h2jtIorv3CuMrNKLdfziXBDICep2Zp9jKDPBHgSsRgl6dW8A-5~WbkIPLpQwqeTzw90r8uJ9EIln~GlFulblJprCfLzxJ2LAFpxNcTkqzOJPElFfhjVrJOYbm6IfllBlHMVNbJjYTOsiJLqODxMiVt3pQx~AGFrx5mVtMUcz9cu9hVPnz2ZVmIDB~a4UGkexf~FCHhaQ6Emnr4nnZBiFSP3f4nASOMHLOs4SE3UKiYUB3ntmJssVdD1w-ZGrovHynkvXMjrJ9GhIeldFm0M5cOAhra0OtJagZ-bgphR0v99ADGPl7X5DoGAAAA -home.duck.i2p=dQmTRCAgcKlbt3VKdW-iFI4zd3WnDaM8ULsfIZb8oEdDuTq5Sl~1JguWH2Wl8PMRuxkhHpfOZ6IUJZtxo-rr3tfaDsYPl8SZSIbvGBs5QooLl2NNTPA~gQMJOHZkuM4KWMWYnFRWoaqAUt9KAmdyl5vgF20xv0rmSgRmm8t6c3QSTfNWSeSDYkRgEumaUKP-kRJnSKNeIOez1N2HQSlaDu~cH27E-VqEuAu8HppI87dV~v6nVKXR9-KgHzCdfHXxy2pxV9trJ8vmTEZO53ab11-BdDMoZjOv5sjMUADNDnhIW1RJ3c-ZcF9OotYsUrf7E1fRo3BwGg27LKhFnyBDmzI0a5uQJ9HCDlzSQ7yfHNMqNCtMfHgZE6Bw6F3tJfr5ISP1j5ibfj4k9tODyfWVqTionQfgrP5szHdyGu14Y4AtDW01-d9BNmBTHA53jCl~Jq6Qm5~CKcGZGS-05iyhmg9x1FtR-rW5Vf6o8uitSjtE5uPt~UHXacaGxQMJSy9vAAAA -nntp.duck.i2p=5SIb-1I6SVwFlW~XlYTNJU1NCpbyHJ3co8KdrpDxBosnHtdsZzq2qzYzGDfvkKK3WRoKmRGYCE77uXAvOQjEyWoTkkGeY2xl3B1t4t8K4j8dFvYtDJkRGePxGaY3MW-9ANGUQsGOhh6qq4lcQ7rv-AWyDftfZ3pGaHc79VukSo7-6OSh4WDw~0l~DjdFjQsZOVNTbKIfDxzSjKnkTpNc73nrLhRE5nmnMj7bljTzNtAHiVf~hFMndPxF80JZt6erLqy62-~XbevTWpn2KCTjYzhYUkwYW95-SW27ph5rIVZneizLEanSPtdUkDGhMEjjmy39Qr5rD5i9H-ioIP3NppRkjPwrtI9VJpsJtzv75uANIXy48RCBVXXA18ng8o7FVevdyjVb~C90IXpJF~mT7DI94rTT5QnzlpJVCid65kOoNzXFC80lP-iiwXMMBEcys8RGA9hdOvagkFDJ64l0GwrVpFGO2AVMsMenR0WHEvJOjNv62sT6rlntO3ZywhNXAAAA -pgp.duck.i2p=kRm2KRa2EiWO~XQFYxSg6UM9lXYHZ93IB80j3ShFhJOOZ4AN05BrTGjMeT9nhn5n1LMEUhy9HJuN-Lhkd89Mbhufk7di6M40wqySns2g0T7XUCFUgQ8kl4~BoY8M9U0pHpM3RJcCw8WJEFGEk~fX8tgC4XQB43UIXfrdKTlNfKhxZODE4UdvlFfFdOYMgH53x8UgMZUprn~URTRNg1uwcaXr2luMwts6HnDt1bDd8elitsWViOJiw42yAFMqBnf-7mhiTCsoYg-nsOiq0Jirt58cBjAGUL5ujZo~vfXkLslDKjHOP32Y7HIi5ANEsbbRr~8QrVUojnVoJwFXs8BQBTevRpGLkCSLnKa31jNSt3msOnEP-n729Jabwj8o3pdRk9e-3~~y9gfj4bcmpSH9sOJoqmipXDiqOvv0tL9twERqse3tAwjE1NgXTvl5e2Zc~F0xJ-L2aG~6BX175ihjjEiYGWRYaoEisHMZdMtivsAK92dKl1JVEkuDF3W8KjL8AAAA -scp.duck.i2p=AE2Ff7x-tJMI901UKEEXkcwb9~5KZKs--VBXEoZnnpC~mlgnqGsZr8MBvRvs6xAzP7xXLeL~cHQ~gvPFT60obEBitwH~JcKMahhJDGb0p23Z8B81QajXijypDpVfMDfFbMiqQGctXhnBidNKWe7HQEcJGZer-SCu3m8CiftcZ0t0g5Y67B2AtqLhza5xfepq6FQ-Tl6fpgS-UDcGUAqMpLUYfrBFB11oM09TRVsfNp~NIAvMdrvXiX8dTDUHJM4FRdpV2OsJiyDdgf5-W6s6ssxohviT5MdijUSYQw5sWj8~9Xkv~~aa11Dvty0E7K9IhXAeJlwBe3VuDizsAJGnFIU7PwZXV7-9-28Zgldarg1P-rh2QDvCqF1vjdyZe99BBcTiCqvE3zx2N-9eT~FeOURrgE~2rFBKVBZfuAASSORqiXIeAANKklCW2pGQrM3DkX8ybi93Weg~eBjwGQugO1FRZ-ISq7npRWruMiC7f~fWLqkmRUy9HahPNp8E41s9AAAA squid.i2p=8fiWZbRjOEzrj5n4jSqjN9UN54wTrsgEjqn7GRUQpLx1svf8lwckXPV5buP2VEYGo~83ftkIcDKyMLXkxSr8jqbb4yAEgPe2~w7OT~8LNvmVPz0xZhIO6fiw0WU4xD9x5PG1spYjWPnLFv7pynEvBpWFXaUlCacjWL2KkfViiGPXvveQqQIZs7VkxVD2HK-oT4yIjdqHpc7Y8nEV9xwds3-LX6to5p70jFe~kZJA2fjWHsSCm92TtPvoR3aTlL1VS3JUKpcH6BL5irsh-SKODEtDRCErPQI~j2SHzhcD6dMUsI7bm3AxivjFpSQHqyXLmLVdxECYsMET~nIHmuv8NYTHQQ0jM0XTQzwnQwEHjHRBd1~spR9uS2~LSnX4Pw~X1WTknJpPK1f4Cu1O44X4RYcLRCsxpEzytUBXA4BQizrbYgOJVGQa9-PNGxJeZsnNUZ3PxUi23Oh-c4jUaB0ZjyKTWJSpzj1GI6vc-gW-0ixGJ358TCSbKgqdBv~g~f7yAAAA -fillament.i2p=Pa50z7pU~ni5nWwUdaDZ5CJxG0fYjoarm9wlxnkgX~wHMX9RPgQAXz~r0Rr1Nadt2OA~dr9RMHswrMok0hutK3JZuFD707D7FjmWW2w979Ee9I3zxKyx9W5A2eE49PPT131NLa3uINXLXOYVA5frfDOmM75Dmvm533r8e2kloemJyj22HpvRiSXiQYgqYJGDMH3Hlnwk884eRkQu7P8DJL~hcuKpyY0FzLZtfxTNsdSavGjl7rKPMzJeP02-9TS5TkdHokZrstVM5Cn9ay1c8DQrMds7SPXJy13Ut34QRjb65JxRV0mrnY3teXewW6QFvFMXJCsf5C3i46t-9Fufy5D1H8cSd2Tx~Xl71MC5-1AJCcIS01Od23E9tFY3dU7IOSRhKC~FiAslyk3x-BnBSpKxbgl1w~LArBm5plNiCiUemJU88xYdn1UyukLer~yNrEHAWspckCRkXFwmUtPkaGNTvfwBSYns-skNHSd7MAUUoS-ewStBdmtnDgRkwSG9AAAA -eco.i2p=KhRG6BGxVPh-BUDDfIgy0570cppTdighytcaGVR0HzQo46tgRMBp9Shlpax5FQX4nLHn6qHQbdFFpFbAwe7CiDhURCVF9-CxYmPurGadxlMPHMjz9O3jHX0CQiv2iULsk4XPrYXF3PqBc4t1J6vVyBVO7uTUhDi0gF6sN1Ro-1GLcWcsoR8Kx-hb~Z4WqGD0QAROOBPHnSRSb236qVBkhFvSkfigfBq3jFgEsttadYJA9ZLSUj1XrFFRBjz~xkRra8kJQSZl5dbfg-eZMlL49h61U6Uta5n1~tL6sarmnl9CaTl2Qo27SKB1OmMLeZEteA5G0-~LiOjN0nxaKpwrCjKIOyvwbQy2QqE-GEb9m8SN8nC2bwYK9fH15pTMHY8GvPYGcUukbF6RhefwzkEJLZ~PaAECrZYuLsn9KE5C35uRnlWJiuJlJ25hG7da5tFMyDB95efzq5IMxPeI0pMigRfuVfRSaGDpNos6JxjfEIX8umk3jIJUPhz1d8gP4QgrAAAA -aum.i2p=09hSo56PTtkFLUEt1iUTO7zYTnO-B~ogsIsyyPWif6q1Iz4wz4JoBflAWZtedPmwGmH0nly4HYUS0gAADoUmBUnXwemmO6dxT-hPQkfEW-A7b3uEvYQWIN~kyFyg0Pa~FN6TaD9kGFttBN-GE4wxiHhXmWdzWNDVb0q5PVGnxMm6Jleik8xkd2Lgeexze8rIv8LCocAWx074USVkbCVQwoFi2P7EnjLq8odSz1cJAntbuCFeUZcjbslE3qmlcTFMCNCZXWKVzn7d5m4oszCQ83NidgekwxJ-S~iS6mBwIS0XKI--4iXiKXzzCFf0KtYfEWpvKCuqNJOcU8vQWAA2-i7~K28aLPzccDQn7acXWLKRTXF3tf0i6e-lSx-X6WTSWK-fuNitjAtKu~jqO10d~bCk7y~UPL-XwdH1XSTbk-Phhk7UoBTDiHY6zQHdD~iAzXER~9JXsJ4UoIrGFVabg7frzSt82CN7Ek5Li4AMz5gg3wq9H9HUa7xM5QfGIJpXAAAA -mp3.aum.i2p=vBOu1caCAajaL5WRMuwk4LwfXrlcn0WzA6iHUKV5ULhaBpJb9pR3SZpnQms2Ot2c5Fvu5I6Rp7WF6QRcyasAhUmC915ap~2~VG8KCDP0z3Quh1-eqGcmzErsIfXdh09j3CWuxN~fH84hd~KswqGudFkWtFTM9RcuQUGSC1efG7uF03uaDI-DKu7eb4VUV-hmpXb3Lqntgo5qSMBMmjyUND-f6RBoXnqM005mUZJpMoYfsBhnUEq37GG8u6P9T94nlMmtz9R3gNURpBJJKPlnEqCBN4mlE5rwspQ0ovxAlogVMhSCpQ4jr6cyWIbNx-nMzKGDj~hMQ0ndbVnWw3EDC3KsPnRnDv0yVz8Fc1YpoPwerHej7VnTupDKxc4T-j8XNA1dN8SfPmaKYEPfavlmy7HFAGcsbmeRZOq-PVvlDdrKNflug8Ysodd5XkDbh7y2k1pRDjwNBQ1EgDVAtL05-i9jerqekHkbFKJ6DlT76f06vj1R2v9qlQzAYjpcKbI3AAAA -ogg.aum.i2p=wR2ETKWn-mxsTurWwmSujvjpOiIjLg5TsldFUa4YFTgiRZdFIB-bXuK59shfnchlEgAZR0IR3~hH-O8bZ~j6wVBdZWq7bGTmyTxQ3MeYPdqK7wH7Jp147YUabFlqJkyI~DluwBDylJrIUyc2qw~ogJ67x-KyzIF7JLnoCC4E-T8Z0vmTAFWSa3XC-ncghrdZQCqEXaCMlG9PN~a7dcDq~qdWoNoyFcgLd0IQfE8JuJ1wSvmWUNEd9vkB2Zuu3EoSoDv4C53Fc0YhVACNug~VEEL-ZBGcCBcpVNud8dOMq-CbavkD5yKqHlvq~uzRi6BY5ajHI77uepJygkHcsm-8T0PXWXdc5ib4TtUI03tPkTar4Y2iVocY~oLk2jh7pQKZNioHJT4StWv9Pj8EWaVX4-emQB5kZOBwZItjo~EAGEoBT14NSM7CmKClgc6sg7fpvWF~-cNHkZsurBndni~~FKmUeWoO0FRQRF9Ao~C1DOt2V9oBbEW1~n6anjL5V~IyAAAA -fcp.entropy.i2p=jy0D13oJVmxSa1MltstV3FOfA5e2WAEEZJiYOJIZSUOcNnAkaR3ghE-AX4vuqyQPyOEUydpauD6cS8vfx4iZkb2U3ddlLcOU3YrFKdLrySpGtD~V126VO-9nOJFwDQOOaKAsiVGRKtMPLC64GkpU6TWSIhiVYWb7WmeAHXLLlR71DtgamAxEIlP3VhytxlS3vuvAoEH9ItsBwkv4N~7jec60WMQINl~c7uDDsuzKFY8wQlkHnLFQJCQ0VExfNYqK9nZ3x8TXNPmNKTMMQ1CUCowgwR783U7UAYqsxNrpkuWvTleadn7QcR9i2v4~L9zOeHd4nHBy8PAjO29g6nf6DIsYhg4c2HYnPYzktQ1NIElytmW83BhbXJXLgNBs1eI9gDaQmOiXi74FMgfg63IcXCYWeqCdwEzSouSphaXEHDcZZVTx7DE9R-1Bi4Dt~KvPOFsAoOqsjHCpHq1gS0u5HiL0hkSm1I1EMk4JBY0j4rM1nAt7e0ix~WiOz5jXlTVSAAAA -http.entropy.i2p=ON2Ud-B0-pJKbTR0Obpjp9wEG8grUpu55gEn5Mz3-dkVkPhHvHK6iLasr~P~Rf4kPPZvn-eK7z6rAVfsAytAJ9pcTH3lXERTjkd9FzVJJ0twbZSQ~XzX5d-24IPIMf00KegjnDkRJ82cRMKa-u4H-ayei~Y7xsSx64zC1eHv6qFxavtql3zRrS~du41~EHtpjqOtOo9Ea3lfFjhm2jUIJpYyVHqve3WbTfMBlguVALwGZIfenph7oQ1Hx~OnEtaviWuOEpupjm11LS9xqCNsccaEpJGvGt6ijxd9hrEuQZ5Ja~C0fNxf3xNtgRaUhakA8Xoo~jz8rCkV2vYQo58kj0E5xYrUQczomj8y-eDBZyq29BP8pfe2G1u3hpHA1z470LUeMPk8qVx8Cx8ZKmSK9XCvOl7WCnFS2~UUfzxbxSxPn9LfzxDZp05AVi9t~hJg-zkrL2n1wfEnScuUFapxarwK90rlAxNSnau-K61WfcXqyVMwDxl3leJOeVdHqhpbAAAA -www.mail.i2p=Y~V8YK2M-my6-Gw0lkrkJouxeqPuB03idp-4uT9pkIXCA5nki9m4YFfPObSPv0E7c2shBxwlUo-6beaRQ-7tCawJssDRc0C0PhRj12QUYYdtZP7JS8SQXy68gZIylY-wfyEXleIC4mYY5mSthhdUUfyo1lqzrdHc1NpjPBxRJcyMBFBGUeM7Of9E9M518jXpVl0bAmxSnr5dy7sgKAVNufzfqIBfEHnmL2ZYH78FoGnPybsV0F9~154emkmt89ZUbx0BuYvH3kT1zin8pSxKw1NqxvqYt7p8CElq1--U38rO9U5Y~kLB9f6F3RYJdkl28ANkvdgJUgqiHLVI5oPWATrJLAOokyGKhK4Xl4Bjp4SCuemxHwTOGyd-4Kl8cO41u3w1LksndX9stkV6U1X0gL9BeSIoa1997IgMLVbUiDMyCz7-cA0y2tc0EdQdlpc2y77nTdo7z23dMSJzWDXsrfmLhX7M24D70htLLc1dpwZ1BUEvM1uPqGfsBSrHdl-sAAAA -smtp.mail.i2p=gfSAYcvEsuU3oNGqeMpq1wZqH-whE1i~YCXEDwYzp8LrmukWsndvPER1~gF5QFrIp2RMXiietF3zEPtAJgevSG4ULxRU0s9MSAMXXlCACVhlf0m493J6kIYnkypOPC-Z8sulyF2kXM8BURLfSH57SS2uxLbx0hkc8j0kR9iys3ksxm5dgW-Rs7clAvLmmfASJkXZiU6DRhWbW84GbpAi9McE3ORhNLrWV5t1W4DXqzT0tzF2W0i8BEGns8XdOBQei9RAewzo5NRGPBmUl6ZKjEJ-2UtX189HPs7FcLknfsRxXhRcPQ1RombPezYCgcNhOgWY9owHq64mwGaDCnnpDSM01sdAuMlFfs1JJMoYNrILckjiHUNzV2XS8A0PIWdO4W0cT1EUs-V2a7Ocvg397HpR6Z4k-7fOrjs9yvpFsCPIEKYUD0mjr44N5pJIc61GGuNE~2ihQZGA3ju0OnUKTRZel3nK0rxl-qfFXsBsEB5vt-MTvKS73ZJdxUKWWzbWAAAA -pop.mail.i2p=aG6owmzirq7QZKYovpSVa4-WBLfI1uJ38cNmb6kkSkcS8A~JdoLWPj6eXieN8r3m7YJLxxyZhf3urmK9qJbiIPBp53M8bOSSkldpFz0NkQPWUWmYXiOrEsOBlugbJ8nNelDcOebqoieKBOTaF-WPJQil5C6RYdUy~PL50O6Qp-Hog1868zP26leYBBFiyzzWI3bOpOsgV~4bNXnqKQZeHXz1Ua2DkV-vDBpeamPzvNWQI6cNodf04PBKXK~TlduLZDK7v1yTt2LhPSBM5nE7ZRtS8KQIdh4o-nyYRmHjA~OQ70gowGpmsRqXHQxOpPro~C7w3gSe2N0zhqSHKstwoFJD-NmsBQ5opyOiKccATpWEQdAwmoICD6rw7TGG4XYXCtyTD2xxLffER0SEsJ-BJesKKhdm-qyEMAOQq00jatoEs9jBYoujFLFQMUgaDejRJdEHWkiGT~x4auosHGYavmrcm-0mdX0CWYgfjwVb~PORhBqHJ1G5IgRPjoZLuxiJAAAA -nm.i2p=UhUVbM972VwQgqS28SkGPtghCb~IpdpeMW7O9E7I3HtlB2I3XGbMeUAoya5RHsoG3TYxf~P6lA5IM5Z~mDZlcbZ~AG7255FE6Z3Jl9kfMArneou6AYaCfNBqNTRS-P7yX9s0Kss-vM63yBulxhS7CqmBKTZsXR27PNjJS0PMYsWBzciuy8UqUkE0YEhSLWSUYAXLP9FKs065CNjxsLumkkoF~MNUaBNEmbCrjpv5Ih9vrwz4XAJSE~S61qSMj6O-nvEPDVhTJJ5ymeoZnYMpIRt4r7FkCTH8vYSkXZkhqXUkLC11WPC33lw2wzh-irmIb5GQeab9o0-DuNQcvUnbK13Jxkq5XiilfK6kgKiPcEniqxMb-4paZAl8dj9Zp01LvhfjlS0c459Jv-gr7ZkjkX7hhTaEVvqwPyFgoVKnxQCitoZrK98WRKJwu7EQb-Kin2vzUsYfpGGI0aT68~gdr23oom2FsoZ~owVuur1h0bJr9mnCaMf6jaioQE7wezxgAAAA -mp3.tc.i2p=LRCWTiJouI6ohqtaVBNHWZe2ymjhtyj3z9KdeI2G2D9l0cYFsG0CRUVT5VPYOg~WykALRVBiL-2U24fbiQ28hhPdQgBMBDl9aQiZJM2hv~di0uVOdARhRSgCDgRQAWioAfpXeg6pyklzXU3TNLY4c2CRLe~9Y7wuLbK3lONsAApcxxKeHLrfGNkZZwJTKd7PcG78KAHU07E-TVNf4tQrOh8tSrHaMB1r7cQQv1Jl8mNUZWz4fGeNYEZ1wr04w74Em2~Z5K0VZ4mH5DhFGXc9ALYzZf6uOVzZKiUC0eOcdfGNdVUbIog2CYJGH69TgAX69d5vF~kEzvHSzX7RxUTt0y25Rlbi6rHSDF36xOfBrOUVnSPn5X~TdKLygz~zusYpRdGZwlsyOTKVTzJHKlU6Vp5Dofijj1bUwXDQ59XCpFUqEDA17nETiOO0H5jfieYBPS6Ke2cFTAhutSvaw~albCd1eV7RPqeGdw-vFfKoucDIEVUT40B5qalyFRKIxx3lAAAA mesh.firerabbit.i2p=BLt3pciNQcVIU-IGnTfSsrputh~b6drZpc1vH8qeA745XoE~nMCGtw8S7HGYX4uEbwk876nQRPV4qRwGtWWkWBs8BX9qX8NABoXFk4G-6NifB4TxEizC~ZAnXZ2uFs~nrqodhyCR8bBHJL0tzBYK3E36zc~SA-DKqQ9XSHWp7ScW7Z3cdQnYKma~x4u5eZmcS23uie3OIfOCk6pJOabtaE-YWRa1eUizhucI-ysm789GumjTo858vHR1mTQfrsPTqNri3yz0aIe~w06ifciq~UlNjVfx89lLEso1vmg8rfTQ-hwxS-qz-u3K5x5vtqdGp673vCvmEnQpU6GEycmkqoLCho9pNQzGbka-OVHg8fZqlFMeBfj2iLz~zlv170jvX6HTlMCNfBYnFqavs2RQJj7--dJ0g7JHReGMKL~TciQjxljrV5AoN-0afRzTZqtDg13PL4tltJm5U1~f-GcxlsjKLZAlv26LlZXsvTDU5plldsernv3fDcBev9UaKYCwAAAA chess.fillament.i2p=8xvXLwcBYu2MxqMAoG6DIahvkNAwBQs43kdZTg9SlouzC3JSQ25RHvspbrxWoGIVAlB~XCxVmBB6EolYEHoSZv2YCziOj4UVxEbVP7nHkh32-7Uc5T7xlcjk8-rsmZzdgz9NhxKVn2Uhp3xtcdVAiyG4mpXisvK-7NgHY-mXPNvj6goaH58roeDUZ5otJN7nCFmcRAUpaUk-nlQs8YfgSCCEsWKOWhsVnAaHwtqtDlpdTo1YKooACMRSa-DcV5W75Il80JEWBD79qpSAeONGAOMMPT~VEMwNNg001VG-UZbvyspZdxHaETw2yd7N9l3-mwI-sYcveTTnNXLWO8IjdgDLdUIP5qrIW6WS9XZIHRKqT2kpwEw7xsEgCA~qSNiZWeai8n6Zs0rLmdyeZeafVEEW9vr6CKcLZ5W7i1SMDqCKnzSbZdd2Hvpb9aWFf5foEjLt33n8w2KSaCUe4zmN~xuQMq2yiB-vQ9~5fgPmphlMxo3ca5MTCfbhshw5137YAAAA kaji.i2p=i-nivH~36AgabgzvS06oKHR~MLoy6NA0oSv8JuNJDLZB8FXEDzIiyzWISmw9XJm8I7QqZ1yFH8~xe84STCHHvMMIQQrfBmOUODLWbKZor~~KhiHdNLtfVZC5BpnXkBCJkklj~fMYSpWa0C~pVRrZl75RoGtBjDVP9P8hioTv5B6RC86g2ypBH5r093rY0wnzxSL8-ZuV3F~H48VYbqro8cRlbMmjx2oEsSHkDpQyjCMVkIYKaCijkSArqZTG~zX6op6Ng9CJwdrkjKSsbzjV6MLnE4aNv-jm2WaGGD5pR24h7e3ImDOGAr17tXRtmNX5ZEQ1udQp8qIhd8UMUumrnm962r8KJWK~9WNzcVeqDrIxaaxC7vcQmXxoPeEW2efbH0yKhVZ7OFu~I9cAapSe~aNWp9UK4URSpuJvOedt0axp3ORaaM-a5U7noW3Ao-HB83qfFEPU-6uUu16HNiPqCFMJiA0qODTOwHiyyx4HKQvbhjujh4mmknSbsuapdgR1AAAA From f70adf8da63afef2727a71026dedb30e0968454e Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 14 Mar 2009 20:25:50 +0000 Subject: [PATCH 174/191] disapproval of revision '3ae245c48c0f90b0e70cf800de354e012801f6cd' --- hosts.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/hosts.txt b/hosts.txt index b30da6363..95ca28af6 100644 --- a/hosts.txt +++ b/hosts.txt @@ -1,6 +1,27 @@ +tc.i2p=3RPLOkQGlq8anNyNWhjbMyHxpAvUyUJKbiUejI80DnPR59T3blc7-XrBhQ2iPbf-BRAR~v1j34Kpba1eDyhPk2gevsE6ULO1irarJ3~C9WcQH2wAbNiVwfWqbh6onQ~YmkSpGNwGHD6ytwbvTyXeBJcS8e6gmfNN-sYLn1aQu8UqWB3D6BmTfLtyS3eqWVk66Nrzmwy8E1Hvq5z~1lukYb~cyiDO1oZHAOLyUQtd9eN16yJY~2SRG8LiscpPMl9nSJUr6fmXMUubW-M7QGFH82Om-735PJUk6WMy1Hi9Vgh4Pxhdl7gfqGRWioFABdhcypb7p1Ca77p73uabLDFK-SjIYmdj7TwSdbNa6PCmzEvCEW~IZeZmnZC5B6pK30AdmD9vc641wUGce9xTJVfNRupf5L7pSsVIISix6FkKQk-FTW2RsZKLbuMCYMaPzLEx5gzODEqtI6Jf2teMd5xCz51RPayDJl~lJ-W0IWYfosnjM~KxYaqc4agviBuF5ZWeAAAA +dyad.i2p=W~JFpqSH8uopylox2V5hMbpcHSsb-dJkSKvdJ1vj~KQcUFJWXFyfbetBAukcGH5S559aK9oslU0qbVoMDlJITVC4OXfXSnVbJBP1IhsK8SvjSYicjmIi2fA~k4HvSh9Wxu~bg8yo~jgfHA8tjYppK9QKc56BpkJb~hx0nNGy4Ny9eW~6A5AwAmHvwdt5NqcREYRMjRd63dMGm8BcEe-6FbOyMo3dnIFcETWAe8TCeoMxm~S1n~6Jlinw3ETxv-L6lQkhFFWnC5zyzQ~4JhVxxT3taTMYXg8td4CBGmrS078jcjW63rlSiQgZBlYfN3iEYmurhuIEV9NXRcmnMrBOQUAoXPpVuRIxJbaQNDL71FO2iv424n4YjKs84suAho34GGQKq7WoL5V5KQgihfcl0f~xne-qP3FtpoPFeyA9x-sA2JWDAsxoZlfvgkiP5eyOn23prT9TJK47HCVilHSV11uTVaC4Jc5YsjoBCZadWbgQnMCKlZ4jk-bLE1PSWLg7AAAA +nightblade.i2p=nyErwSseXbsojcWtNkDyUul0YULtqr6qyWSzIp639Ygpe8juCdgPMLURVXcmlCvo~QPoHg6zt53KpgpGvB1-Wv2SGvc2Mvs~o8USw3ius8fP1URphqcBbulK8Ci0bgknt0kD0AfxqfMz-p~xk1QEMxq2kZEoB3oyIIFnQlpb2ByS74Lx8iKzXTrwWk19I3Dvu4nIq8CBDDwu3lYoCD2kC-jT5pjgglverGPEGN4o55LYVTtfSg4gAJFZeaE4KjBR5P1z7cca6UDjGMWfR0iCa8P3qpkY2ODYpk~8w2xgBbgDq-8Hzik~uraHc598ccS8QpwB0f0Jw~2PZcTjOPdZ-239U6p3tESXa7FXzRBCujv4Bx6CVFRhCmBHpyFnCD-MugZ~vR6XFSS2XBsCT~duXKq94HH2n1iAWslG4Vu44ut1JVhDPFzp~Dk7wujB0tCo2HXH2icRQxOWe37foU4LZSJ4oMpFDACBzwSfcZdIPsVRxGttKQx4yzgffR1Q~Jl7AAAA +bozo.i2p=ubMPUwY0op6B7Jr8SAjY2bQXze8m1sT6xF2N0cv43dIHwLTO0gUqn7FCP9jXZDodE9DR3fu8fG8x1Yz1SpXFk4WtFmuDuhdN7uaHuLIQ71PATC2GRhDS7NXqn7GsVZgQxhHKenaE5BKjIKt2amZ2~8CM0qBKTqwievUO-Y6zG~-8l~RpnAxDZUMOjKKy5R3~jEN9DFZCaKvXSNcOVFjZRGaD6d8NvkAJjndHdE3bFSJUDNv0qhhp09-mm~Se9C~FzjrAbhgappdNRiwQepXTWqRbjjt6lUPT2eJISPDxYxoeZkBGZa9XmfO9QH3hoMo0g~RbwLeBqtgeRGhVgFiC4pN8lFt3z7j8L-12575SUeOnJPIm3hQWXdTjKX1hqf4LopYBG84N95IeydPJegsmkIkAMzEb0d~-UZfVSP9yFgs37j~Fds5yxBsu-NFc6qmZihpXEd7jrfX1-HuJVmXFmwaZgyumRSRDbj714wxr7RP4Hb-liA3JrU-FbqNQFoTMAAAA duck.i2p=eFJdRYFmtjcpx9-Mw3JBdF6fwtb9cHRBR103Q8IGc91Jdfn0iYzK6Xx0oIzPvpbD4yOlPQm-C-7eTahrAkHa2FMCRTiiVq2a0nBp37W1uTvAToV-MKYPKTdFMxrXxvjS7qaSUXdJRcPaPexolfx-Gcjh~rN2tKCh0mz9beueiQ18~8qWGh6hUMb0yyA09ipL9vIkmHmooLwT9AZyzHXEzdLXZe1P~CG3L46QaXp9aTD7EkAwG6VBMjQrGiSJ-9FFhx4QcYAZWM-dfrtzbbVYfHxqQRBwzB27zLlaKVaqu4enC0N1cW1yy-cjnv0Wxokqe62B2uPzFYtloxQpBPfTLQZUfUzjskY-3Yg2AdSbEu37jYsnAJA95AlLz4t1W1vPTNiXzCaRqkkMX342SUkJK-HZE3wTjAgGPqJ9vMaC2YexPFViTxEO2Q0jDSjdPHG0D769qehkN5Bfb8MEI-yr3g9zLaY~w4r6T38ap33qfyISWlIJ5qCPcRVkeK8OqZ4vAAAA irc.duck.i2p=Bxqr5E7-56oJeya1lsDLBN6L1gKme-FUS6Bh~TQS3HswomK9rpjrYNeqBTBoE8TCFl161~FI3soWqbnmFhIdhskausZsO0ez5-4IXMJW8NTilWqXQ3OJxA20M9grohx3RjkZgXU1ooTx7wviSHtXQYiiqnzGnIzmmEZo5-Xx6VjXakctebWwbi2PrsE6XLxrxXBzB34l4KlVsyX504BJiOT6KXNaVZxvG61GfGVfNHdeXljMDE5d25UdFC6RJdDnJ3Z7Yb7EjAww78aowbR0VCfJDH~cB868-VOKIxmor3Rs7giaLXmUyW~GRtFX10COJj5V6BrhKs61XOXxfbyQKGVXZ0mM2A8cdZE1ftr96SZgGy~V8uUHKvoa1HpjMNrPL5Tr6EGfJxOxAy6PHwotn6a8UMnCZgEdTbQ6U3BTywU~x3SCAQvfOT~dl3sZ-5ujYWNFRp7RhdY-WHn1Kj59MfU-VpczGYdV3bRkwT5lpIjST~vopLfkYUeB6gcVSr49AAAA +jabber.duck.i2p=AzR8Zrms1sLXflvaZZ5CI2TS1cRvlO99D5Jh641-KS0lMSQPUnSApTfy9R4aPxZ3SWwpZPfSSGN-e7~kw8ucGQVB9EOHnht7WCz5UKC423vX4fFZqpTyqemDBx0CYXfhqRf-~7mjslXigR2nbQwh8NMl2bgLPknGe55wMlZPfsacS2WKNQYbdOvrgG6Zb6DK8RzTDrsum5h2jtIorv3CuMrNKLdfziXBDICep2Zp9jKDPBHgSsRgl6dW8A-5~WbkIPLpQwqeTzw90r8uJ9EIln~GlFulblJprCfLzxJ2LAFpxNcTkqzOJPElFfhjVrJOYbm6IfllBlHMVNbJjYTOsiJLqODxMiVt3pQx~AGFrx5mVtMUcz9cu9hVPnz2ZVmIDB~a4UGkexf~FCHhaQ6Emnr4nnZBiFSP3f4nASOMHLOs4SE3UKiYUB3ntmJssVdD1w-ZGrovHynkvXMjrJ9GhIeldFm0M5cOAhra0OtJagZ-bgphR0v99ADGPl7X5DoGAAAA +home.duck.i2p=dQmTRCAgcKlbt3VKdW-iFI4zd3WnDaM8ULsfIZb8oEdDuTq5Sl~1JguWH2Wl8PMRuxkhHpfOZ6IUJZtxo-rr3tfaDsYPl8SZSIbvGBs5QooLl2NNTPA~gQMJOHZkuM4KWMWYnFRWoaqAUt9KAmdyl5vgF20xv0rmSgRmm8t6c3QSTfNWSeSDYkRgEumaUKP-kRJnSKNeIOez1N2HQSlaDu~cH27E-VqEuAu8HppI87dV~v6nVKXR9-KgHzCdfHXxy2pxV9trJ8vmTEZO53ab11-BdDMoZjOv5sjMUADNDnhIW1RJ3c-ZcF9OotYsUrf7E1fRo3BwGg27LKhFnyBDmzI0a5uQJ9HCDlzSQ7yfHNMqNCtMfHgZE6Bw6F3tJfr5ISP1j5ibfj4k9tODyfWVqTionQfgrP5szHdyGu14Y4AtDW01-d9BNmBTHA53jCl~Jq6Qm5~CKcGZGS-05iyhmg9x1FtR-rW5Vf6o8uitSjtE5uPt~UHXacaGxQMJSy9vAAAA +nntp.duck.i2p=5SIb-1I6SVwFlW~XlYTNJU1NCpbyHJ3co8KdrpDxBosnHtdsZzq2qzYzGDfvkKK3WRoKmRGYCE77uXAvOQjEyWoTkkGeY2xl3B1t4t8K4j8dFvYtDJkRGePxGaY3MW-9ANGUQsGOhh6qq4lcQ7rv-AWyDftfZ3pGaHc79VukSo7-6OSh4WDw~0l~DjdFjQsZOVNTbKIfDxzSjKnkTpNc73nrLhRE5nmnMj7bljTzNtAHiVf~hFMndPxF80JZt6erLqy62-~XbevTWpn2KCTjYzhYUkwYW95-SW27ph5rIVZneizLEanSPtdUkDGhMEjjmy39Qr5rD5i9H-ioIP3NppRkjPwrtI9VJpsJtzv75uANIXy48RCBVXXA18ng8o7FVevdyjVb~C90IXpJF~mT7DI94rTT5QnzlpJVCid65kOoNzXFC80lP-iiwXMMBEcys8RGA9hdOvagkFDJ64l0GwrVpFGO2AVMsMenR0WHEvJOjNv62sT6rlntO3ZywhNXAAAA +pgp.duck.i2p=kRm2KRa2EiWO~XQFYxSg6UM9lXYHZ93IB80j3ShFhJOOZ4AN05BrTGjMeT9nhn5n1LMEUhy9HJuN-Lhkd89Mbhufk7di6M40wqySns2g0T7XUCFUgQ8kl4~BoY8M9U0pHpM3RJcCw8WJEFGEk~fX8tgC4XQB43UIXfrdKTlNfKhxZODE4UdvlFfFdOYMgH53x8UgMZUprn~URTRNg1uwcaXr2luMwts6HnDt1bDd8elitsWViOJiw42yAFMqBnf-7mhiTCsoYg-nsOiq0Jirt58cBjAGUL5ujZo~vfXkLslDKjHOP32Y7HIi5ANEsbbRr~8QrVUojnVoJwFXs8BQBTevRpGLkCSLnKa31jNSt3msOnEP-n729Jabwj8o3pdRk9e-3~~y9gfj4bcmpSH9sOJoqmipXDiqOvv0tL9twERqse3tAwjE1NgXTvl5e2Zc~F0xJ-L2aG~6BX175ihjjEiYGWRYaoEisHMZdMtivsAK92dKl1JVEkuDF3W8KjL8AAAA +scp.duck.i2p=AE2Ff7x-tJMI901UKEEXkcwb9~5KZKs--VBXEoZnnpC~mlgnqGsZr8MBvRvs6xAzP7xXLeL~cHQ~gvPFT60obEBitwH~JcKMahhJDGb0p23Z8B81QajXijypDpVfMDfFbMiqQGctXhnBidNKWe7HQEcJGZer-SCu3m8CiftcZ0t0g5Y67B2AtqLhza5xfepq6FQ-Tl6fpgS-UDcGUAqMpLUYfrBFB11oM09TRVsfNp~NIAvMdrvXiX8dTDUHJM4FRdpV2OsJiyDdgf5-W6s6ssxohviT5MdijUSYQw5sWj8~9Xkv~~aa11Dvty0E7K9IhXAeJlwBe3VuDizsAJGnFIU7PwZXV7-9-28Zgldarg1P-rh2QDvCqF1vjdyZe99BBcTiCqvE3zx2N-9eT~FeOURrgE~2rFBKVBZfuAASSORqiXIeAANKklCW2pGQrM3DkX8ybi93Weg~eBjwGQugO1FRZ-ISq7npRWruMiC7f~fWLqkmRUy9HahPNp8E41s9AAAA squid.i2p=8fiWZbRjOEzrj5n4jSqjN9UN54wTrsgEjqn7GRUQpLx1svf8lwckXPV5buP2VEYGo~83ftkIcDKyMLXkxSr8jqbb4yAEgPe2~w7OT~8LNvmVPz0xZhIO6fiw0WU4xD9x5PG1spYjWPnLFv7pynEvBpWFXaUlCacjWL2KkfViiGPXvveQqQIZs7VkxVD2HK-oT4yIjdqHpc7Y8nEV9xwds3-LX6to5p70jFe~kZJA2fjWHsSCm92TtPvoR3aTlL1VS3JUKpcH6BL5irsh-SKODEtDRCErPQI~j2SHzhcD6dMUsI7bm3AxivjFpSQHqyXLmLVdxECYsMET~nIHmuv8NYTHQQ0jM0XTQzwnQwEHjHRBd1~spR9uS2~LSnX4Pw~X1WTknJpPK1f4Cu1O44X4RYcLRCsxpEzytUBXA4BQizrbYgOJVGQa9-PNGxJeZsnNUZ3PxUi23Oh-c4jUaB0ZjyKTWJSpzj1GI6vc-gW-0ixGJ358TCSbKgqdBv~g~f7yAAAA +fillament.i2p=Pa50z7pU~ni5nWwUdaDZ5CJxG0fYjoarm9wlxnkgX~wHMX9RPgQAXz~r0Rr1Nadt2OA~dr9RMHswrMok0hutK3JZuFD707D7FjmWW2w979Ee9I3zxKyx9W5A2eE49PPT131NLa3uINXLXOYVA5frfDOmM75Dmvm533r8e2kloemJyj22HpvRiSXiQYgqYJGDMH3Hlnwk884eRkQu7P8DJL~hcuKpyY0FzLZtfxTNsdSavGjl7rKPMzJeP02-9TS5TkdHokZrstVM5Cn9ay1c8DQrMds7SPXJy13Ut34QRjb65JxRV0mrnY3teXewW6QFvFMXJCsf5C3i46t-9Fufy5D1H8cSd2Tx~Xl71MC5-1AJCcIS01Od23E9tFY3dU7IOSRhKC~FiAslyk3x-BnBSpKxbgl1w~LArBm5plNiCiUemJU88xYdn1UyukLer~yNrEHAWspckCRkXFwmUtPkaGNTvfwBSYns-skNHSd7MAUUoS-ewStBdmtnDgRkwSG9AAAA +eco.i2p=KhRG6BGxVPh-BUDDfIgy0570cppTdighytcaGVR0HzQo46tgRMBp9Shlpax5FQX4nLHn6qHQbdFFpFbAwe7CiDhURCVF9-CxYmPurGadxlMPHMjz9O3jHX0CQiv2iULsk4XPrYXF3PqBc4t1J6vVyBVO7uTUhDi0gF6sN1Ro-1GLcWcsoR8Kx-hb~Z4WqGD0QAROOBPHnSRSb236qVBkhFvSkfigfBq3jFgEsttadYJA9ZLSUj1XrFFRBjz~xkRra8kJQSZl5dbfg-eZMlL49h61U6Uta5n1~tL6sarmnl9CaTl2Qo27SKB1OmMLeZEteA5G0-~LiOjN0nxaKpwrCjKIOyvwbQy2QqE-GEb9m8SN8nC2bwYK9fH15pTMHY8GvPYGcUukbF6RhefwzkEJLZ~PaAECrZYuLsn9KE5C35uRnlWJiuJlJ25hG7da5tFMyDB95efzq5IMxPeI0pMigRfuVfRSaGDpNos6JxjfEIX8umk3jIJUPhz1d8gP4QgrAAAA +aum.i2p=09hSo56PTtkFLUEt1iUTO7zYTnO-B~ogsIsyyPWif6q1Iz4wz4JoBflAWZtedPmwGmH0nly4HYUS0gAADoUmBUnXwemmO6dxT-hPQkfEW-A7b3uEvYQWIN~kyFyg0Pa~FN6TaD9kGFttBN-GE4wxiHhXmWdzWNDVb0q5PVGnxMm6Jleik8xkd2Lgeexze8rIv8LCocAWx074USVkbCVQwoFi2P7EnjLq8odSz1cJAntbuCFeUZcjbslE3qmlcTFMCNCZXWKVzn7d5m4oszCQ83NidgekwxJ-S~iS6mBwIS0XKI--4iXiKXzzCFf0KtYfEWpvKCuqNJOcU8vQWAA2-i7~K28aLPzccDQn7acXWLKRTXF3tf0i6e-lSx-X6WTSWK-fuNitjAtKu~jqO10d~bCk7y~UPL-XwdH1XSTbk-Phhk7UoBTDiHY6zQHdD~iAzXER~9JXsJ4UoIrGFVabg7frzSt82CN7Ek5Li4AMz5gg3wq9H9HUa7xM5QfGIJpXAAAA +mp3.aum.i2p=vBOu1caCAajaL5WRMuwk4LwfXrlcn0WzA6iHUKV5ULhaBpJb9pR3SZpnQms2Ot2c5Fvu5I6Rp7WF6QRcyasAhUmC915ap~2~VG8KCDP0z3Quh1-eqGcmzErsIfXdh09j3CWuxN~fH84hd~KswqGudFkWtFTM9RcuQUGSC1efG7uF03uaDI-DKu7eb4VUV-hmpXb3Lqntgo5qSMBMmjyUND-f6RBoXnqM005mUZJpMoYfsBhnUEq37GG8u6P9T94nlMmtz9R3gNURpBJJKPlnEqCBN4mlE5rwspQ0ovxAlogVMhSCpQ4jr6cyWIbNx-nMzKGDj~hMQ0ndbVnWw3EDC3KsPnRnDv0yVz8Fc1YpoPwerHej7VnTupDKxc4T-j8XNA1dN8SfPmaKYEPfavlmy7HFAGcsbmeRZOq-PVvlDdrKNflug8Ysodd5XkDbh7y2k1pRDjwNBQ1EgDVAtL05-i9jerqekHkbFKJ6DlT76f06vj1R2v9qlQzAYjpcKbI3AAAA +ogg.aum.i2p=wR2ETKWn-mxsTurWwmSujvjpOiIjLg5TsldFUa4YFTgiRZdFIB-bXuK59shfnchlEgAZR0IR3~hH-O8bZ~j6wVBdZWq7bGTmyTxQ3MeYPdqK7wH7Jp147YUabFlqJkyI~DluwBDylJrIUyc2qw~ogJ67x-KyzIF7JLnoCC4E-T8Z0vmTAFWSa3XC-ncghrdZQCqEXaCMlG9PN~a7dcDq~qdWoNoyFcgLd0IQfE8JuJ1wSvmWUNEd9vkB2Zuu3EoSoDv4C53Fc0YhVACNug~VEEL-ZBGcCBcpVNud8dOMq-CbavkD5yKqHlvq~uzRi6BY5ajHI77uepJygkHcsm-8T0PXWXdc5ib4TtUI03tPkTar4Y2iVocY~oLk2jh7pQKZNioHJT4StWv9Pj8EWaVX4-emQB5kZOBwZItjo~EAGEoBT14NSM7CmKClgc6sg7fpvWF~-cNHkZsurBndni~~FKmUeWoO0FRQRF9Ao~C1DOt2V9oBbEW1~n6anjL5V~IyAAAA +fcp.entropy.i2p=jy0D13oJVmxSa1MltstV3FOfA5e2WAEEZJiYOJIZSUOcNnAkaR3ghE-AX4vuqyQPyOEUydpauD6cS8vfx4iZkb2U3ddlLcOU3YrFKdLrySpGtD~V126VO-9nOJFwDQOOaKAsiVGRKtMPLC64GkpU6TWSIhiVYWb7WmeAHXLLlR71DtgamAxEIlP3VhytxlS3vuvAoEH9ItsBwkv4N~7jec60WMQINl~c7uDDsuzKFY8wQlkHnLFQJCQ0VExfNYqK9nZ3x8TXNPmNKTMMQ1CUCowgwR783U7UAYqsxNrpkuWvTleadn7QcR9i2v4~L9zOeHd4nHBy8PAjO29g6nf6DIsYhg4c2HYnPYzktQ1NIElytmW83BhbXJXLgNBs1eI9gDaQmOiXi74FMgfg63IcXCYWeqCdwEzSouSphaXEHDcZZVTx7DE9R-1Bi4Dt~KvPOFsAoOqsjHCpHq1gS0u5HiL0hkSm1I1EMk4JBY0j4rM1nAt7e0ix~WiOz5jXlTVSAAAA +http.entropy.i2p=ON2Ud-B0-pJKbTR0Obpjp9wEG8grUpu55gEn5Mz3-dkVkPhHvHK6iLasr~P~Rf4kPPZvn-eK7z6rAVfsAytAJ9pcTH3lXERTjkd9FzVJJ0twbZSQ~XzX5d-24IPIMf00KegjnDkRJ82cRMKa-u4H-ayei~Y7xsSx64zC1eHv6qFxavtql3zRrS~du41~EHtpjqOtOo9Ea3lfFjhm2jUIJpYyVHqve3WbTfMBlguVALwGZIfenph7oQ1Hx~OnEtaviWuOEpupjm11LS9xqCNsccaEpJGvGt6ijxd9hrEuQZ5Ja~C0fNxf3xNtgRaUhakA8Xoo~jz8rCkV2vYQo58kj0E5xYrUQczomj8y-eDBZyq29BP8pfe2G1u3hpHA1z470LUeMPk8qVx8Cx8ZKmSK9XCvOl7WCnFS2~UUfzxbxSxPn9LfzxDZp05AVi9t~hJg-zkrL2n1wfEnScuUFapxarwK90rlAxNSnau-K61WfcXqyVMwDxl3leJOeVdHqhpbAAAA +www.mail.i2p=Y~V8YK2M-my6-Gw0lkrkJouxeqPuB03idp-4uT9pkIXCA5nki9m4YFfPObSPv0E7c2shBxwlUo-6beaRQ-7tCawJssDRc0C0PhRj12QUYYdtZP7JS8SQXy68gZIylY-wfyEXleIC4mYY5mSthhdUUfyo1lqzrdHc1NpjPBxRJcyMBFBGUeM7Of9E9M518jXpVl0bAmxSnr5dy7sgKAVNufzfqIBfEHnmL2ZYH78FoGnPybsV0F9~154emkmt89ZUbx0BuYvH3kT1zin8pSxKw1NqxvqYt7p8CElq1--U38rO9U5Y~kLB9f6F3RYJdkl28ANkvdgJUgqiHLVI5oPWATrJLAOokyGKhK4Xl4Bjp4SCuemxHwTOGyd-4Kl8cO41u3w1LksndX9stkV6U1X0gL9BeSIoa1997IgMLVbUiDMyCz7-cA0y2tc0EdQdlpc2y77nTdo7z23dMSJzWDXsrfmLhX7M24D70htLLc1dpwZ1BUEvM1uPqGfsBSrHdl-sAAAA +smtp.mail.i2p=gfSAYcvEsuU3oNGqeMpq1wZqH-whE1i~YCXEDwYzp8LrmukWsndvPER1~gF5QFrIp2RMXiietF3zEPtAJgevSG4ULxRU0s9MSAMXXlCACVhlf0m493J6kIYnkypOPC-Z8sulyF2kXM8BURLfSH57SS2uxLbx0hkc8j0kR9iys3ksxm5dgW-Rs7clAvLmmfASJkXZiU6DRhWbW84GbpAi9McE3ORhNLrWV5t1W4DXqzT0tzF2W0i8BEGns8XdOBQei9RAewzo5NRGPBmUl6ZKjEJ-2UtX189HPs7FcLknfsRxXhRcPQ1RombPezYCgcNhOgWY9owHq64mwGaDCnnpDSM01sdAuMlFfs1JJMoYNrILckjiHUNzV2XS8A0PIWdO4W0cT1EUs-V2a7Ocvg397HpR6Z4k-7fOrjs9yvpFsCPIEKYUD0mjr44N5pJIc61GGuNE~2ihQZGA3ju0OnUKTRZel3nK0rxl-qfFXsBsEB5vt-MTvKS73ZJdxUKWWzbWAAAA +pop.mail.i2p=aG6owmzirq7QZKYovpSVa4-WBLfI1uJ38cNmb6kkSkcS8A~JdoLWPj6eXieN8r3m7YJLxxyZhf3urmK9qJbiIPBp53M8bOSSkldpFz0NkQPWUWmYXiOrEsOBlugbJ8nNelDcOebqoieKBOTaF-WPJQil5C6RYdUy~PL50O6Qp-Hog1868zP26leYBBFiyzzWI3bOpOsgV~4bNXnqKQZeHXz1Ua2DkV-vDBpeamPzvNWQI6cNodf04PBKXK~TlduLZDK7v1yTt2LhPSBM5nE7ZRtS8KQIdh4o-nyYRmHjA~OQ70gowGpmsRqXHQxOpPro~C7w3gSe2N0zhqSHKstwoFJD-NmsBQ5opyOiKccATpWEQdAwmoICD6rw7TGG4XYXCtyTD2xxLffER0SEsJ-BJesKKhdm-qyEMAOQq00jatoEs9jBYoujFLFQMUgaDejRJdEHWkiGT~x4auosHGYavmrcm-0mdX0CWYgfjwVb~PORhBqHJ1G5IgRPjoZLuxiJAAAA +nm.i2p=UhUVbM972VwQgqS28SkGPtghCb~IpdpeMW7O9E7I3HtlB2I3XGbMeUAoya5RHsoG3TYxf~P6lA5IM5Z~mDZlcbZ~AG7255FE6Z3Jl9kfMArneou6AYaCfNBqNTRS-P7yX9s0Kss-vM63yBulxhS7CqmBKTZsXR27PNjJS0PMYsWBzciuy8UqUkE0YEhSLWSUYAXLP9FKs065CNjxsLumkkoF~MNUaBNEmbCrjpv5Ih9vrwz4XAJSE~S61qSMj6O-nvEPDVhTJJ5ymeoZnYMpIRt4r7FkCTH8vYSkXZkhqXUkLC11WPC33lw2wzh-irmIb5GQeab9o0-DuNQcvUnbK13Jxkq5XiilfK6kgKiPcEniqxMb-4paZAl8dj9Zp01LvhfjlS0c459Jv-gr7ZkjkX7hhTaEVvqwPyFgoVKnxQCitoZrK98WRKJwu7EQb-Kin2vzUsYfpGGI0aT68~gdr23oom2FsoZ~owVuur1h0bJr9mnCaMf6jaioQE7wezxgAAAA +mp3.tc.i2p=LRCWTiJouI6ohqtaVBNHWZe2ymjhtyj3z9KdeI2G2D9l0cYFsG0CRUVT5VPYOg~WykALRVBiL-2U24fbiQ28hhPdQgBMBDl9aQiZJM2hv~di0uVOdARhRSgCDgRQAWioAfpXeg6pyklzXU3TNLY4c2CRLe~9Y7wuLbK3lONsAApcxxKeHLrfGNkZZwJTKd7PcG78KAHU07E-TVNf4tQrOh8tSrHaMB1r7cQQv1Jl8mNUZWz4fGeNYEZ1wr04w74Em2~Z5K0VZ4mH5DhFGXc9ALYzZf6uOVzZKiUC0eOcdfGNdVUbIog2CYJGH69TgAX69d5vF~kEzvHSzX7RxUTt0y25Rlbi6rHSDF36xOfBrOUVnSPn5X~TdKLygz~zusYpRdGZwlsyOTKVTzJHKlU6Vp5Dofijj1bUwXDQ59XCpFUqEDA17nETiOO0H5jfieYBPS6Ke2cFTAhutSvaw~albCd1eV7RPqeGdw-vFfKoucDIEVUT40B5qalyFRKIxx3lAAAA mesh.firerabbit.i2p=BLt3pciNQcVIU-IGnTfSsrputh~b6drZpc1vH8qeA745XoE~nMCGtw8S7HGYX4uEbwk876nQRPV4qRwGtWWkWBs8BX9qX8NABoXFk4G-6NifB4TxEizC~ZAnXZ2uFs~nrqodhyCR8bBHJL0tzBYK3E36zc~SA-DKqQ9XSHWp7ScW7Z3cdQnYKma~x4u5eZmcS23uie3OIfOCk6pJOabtaE-YWRa1eUizhucI-ysm789GumjTo858vHR1mTQfrsPTqNri3yz0aIe~w06ifciq~UlNjVfx89lLEso1vmg8rfTQ-hwxS-qz-u3K5x5vtqdGp673vCvmEnQpU6GEycmkqoLCho9pNQzGbka-OVHg8fZqlFMeBfj2iLz~zlv170jvX6HTlMCNfBYnFqavs2RQJj7--dJ0g7JHReGMKL~TciQjxljrV5AoN-0afRzTZqtDg13PL4tltJm5U1~f-GcxlsjKLZAlv26LlZXsvTDU5plldsernv3fDcBev9UaKYCwAAAA chess.fillament.i2p=8xvXLwcBYu2MxqMAoG6DIahvkNAwBQs43kdZTg9SlouzC3JSQ25RHvspbrxWoGIVAlB~XCxVmBB6EolYEHoSZv2YCziOj4UVxEbVP7nHkh32-7Uc5T7xlcjk8-rsmZzdgz9NhxKVn2Uhp3xtcdVAiyG4mpXisvK-7NgHY-mXPNvj6goaH58roeDUZ5otJN7nCFmcRAUpaUk-nlQs8YfgSCCEsWKOWhsVnAaHwtqtDlpdTo1YKooACMRSa-DcV5W75Il80JEWBD79qpSAeONGAOMMPT~VEMwNNg001VG-UZbvyspZdxHaETw2yd7N9l3-mwI-sYcveTTnNXLWO8IjdgDLdUIP5qrIW6WS9XZIHRKqT2kpwEw7xsEgCA~qSNiZWeai8n6Zs0rLmdyeZeafVEEW9vr6CKcLZ5W7i1SMDqCKnzSbZdd2Hvpb9aWFf5foEjLt33n8w2KSaCUe4zmN~xuQMq2yiB-vQ9~5fgPmphlMxo3ca5MTCfbhshw5137YAAAA kaji.i2p=i-nivH~36AgabgzvS06oKHR~MLoy6NA0oSv8JuNJDLZB8FXEDzIiyzWISmw9XJm8I7QqZ1yFH8~xe84STCHHvMMIQQrfBmOUODLWbKZor~~KhiHdNLtfVZC5BpnXkBCJkklj~fMYSpWa0C~pVRrZl75RoGtBjDVP9P8hioTv5B6RC86g2ypBH5r093rY0wnzxSL8-ZuV3F~H48VYbqro8cRlbMmjx2oEsSHkDpQyjCMVkIYKaCijkSArqZTG~zX6op6Ng9CJwdrkjKSsbzjV6MLnE4aNv-jm2WaGGD5pR24h7e3ImDOGAr17tXRtmNX5ZEQ1udQp8qIhd8UMUumrnm962r8KJWK~9WNzcVeqDrIxaaxC7vcQmXxoPeEW2efbH0yKhVZ7OFu~I9cAapSe~aNWp9UK4URSpuJvOedt0axp3ORaaM-a5U7noW3Ao-HB83qfFEPU-6uUu16HNiPqCFMJiA0qODTOwHiyyx4HKQvbhjujh4mmknSbsuapdgR1AAAA From 33f4fac48f0d4afc53307c3c3926c2177810f735 Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 14 Mar 2009 21:42:50 +0000 Subject: [PATCH 175/191] summary bar help --- apps/routerconsole/jsp/help.jsp | 116 +++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/apps/routerconsole/jsp/help.jsp b/apps/routerconsole/jsp/help.jsp index d53f93a88..279df95dd 100644 --- a/apps/routerconsole/jsp/help.jsp +++ b/apps/routerconsole/jsp/help.jsp @@ -12,12 +12,126 @@

    Help

    -Sorry, there's no help text here yet, so check out the +Sorry, there's not much help text here yet, so also check out the FAQ on www.i2p2.i2p or the Deutsch FAQ. +You may also try the +forum +or IRC.
    +

    Summary Bar Information

    +

    General

    +
      +
    • Ident: +The first four characters (24 bits) of your 44-character (256-bit) Base64 router hash. +The full hash is shown on your router info page. +Never reveal this to anyone, as your router info contains your IP. +
    • Version: +The version of the I2P software you are running. +
    • Now: +The current time (UTC) and the skew, if any. I2P requires your computer's time be accurate. +If the skew is more than a few seconds, please correct the problem by adjusting +your computer's time. +
    • Reachability: +The router's view of whether it can be contacted by other routers. +Further information is on the configuration page. +
    + +

    Peers

    +
      +
    • Active: +The first number is the number of peers you've sent or received a message from in the last few minutes. +This may range from 8-10 to several hundred, depending on your total bandwidth, +shared bandwidth, and locally-generated traffic. +The second number is the number of peers seen in the last hour or so. +Do not be concerned if these numbers vary widely. +
    • Fast: +This is the number of peers you use for building client tunnels. It is generally in the +range 8-15. Your fast peers are shown on the profiles page. +
    • High Capacity: +This is the number of peers you use for building some of your exploratory tunnels. It is generally in the +range 8-25. The fast peers are included in the high capacity tier. +Your high capacity peers are shown on the profiles page. +
    • Well Integrated: +This is the number of peers you use for network database inquiries. +These are usually the "floodfill" peers. +Your well integrated peers are shown on the bottom of the profiles page. +
    • Known: +This is the total number of routers you know about. +They are listed on the network database page. +This may range from under 100 to 1000 or more. +This number is not the total size of the network; +it may vary widely depending on your total bandwidth, +shared bandwidth, and locally-generated traffic. +I2P does not require a router to know every other router. +
    + +

    Bandwidth in/out

    +Should be self-explanatory. All values are in bytes per second, not bits per second. +Change your bandwidth limits on the configuration page. + +

    Local destinations

    +The local applications connecting through your router. +These may be clients started through I2PTunnel +or external programs connecting through SAM, BOB, or directly to I2CP. + +

    Tunnels in/out

    +The actual tunnels are shown on the the tunnels page. +
      +
    • Exploratory: +Tunnels built by your router and used for communication with the floodfill peers, +building new tunnels, and testing existing tunnels. +
    • Client: +Tunnels built by your router for each client's use. +
    • Participating: +Tunnels built by other routers through your router. +This may vary widely depending on network demand, your +shared bandwidth, and amount of locally-generated traffic. +The recommended method for limiting participating tunnels is +to change your share percentage on the configuration page. +You may also limit the total number by setting router.maxParticipatingTunnels=nnn on +the advanced configuration page. +
    + +

    Congestion

    +Some basic indications of router overload. +
  • Job lag: +How long jobs are waiting before execution. The job queue is listed on the jobs page. +Unfortunately, there are several other job queues in the router that may be congested, +and their status is not available in the router console. +The job lag should generally be zero. +If it is consistently higher than 500ms, your computer is very slow, or the +router has serious problems. +
  • Message delay: +How long an outbound message waits in the queue. +This should generally be a few hundred milliseconds or less. +If it is consistently higher than 1000ms, your computer is very slow, +or you should adjust your bandwidth limits, or your (bittorrent?) clients +may be sending too much data and should have their transmit bandwidth limit reduced. +
  • Tunnel lag: +This is the round trip time for a tunnel test, which sends a single message +out a client tunnel and in an exploratory tunnel, or vice versa. +It should usually be less than 5 seconds. +If it is consistently higher than that, your computer is very slow, +or you should adjust your bandwidth limits, or there are network problems. +
  • Handle backlog: +This is the number of pending requests from other routers to build a +participating tunnel through your router. +It should usually be close to zero. +If it is consistently high, your computer is too slow, +and you should reduce your share bandwidth limits. +
  • Accepting/Rejecting: +Your routers' status on accepting or rejecting +requests from other routers to build a +participating tunnel through your router. +Your router may accept all requests, accept or reject a percentage of requests, +or reject all requests for a number of reasons, to control +the bandwidth and CPU demands and maintain capacity for +local clients. + +

    Legal stuff

    The I2P router (router.jar) and SDK (i2p.jar) are almost entirely public domain, with a few notable exceptions:
      From 91b3889cbcba549891faeba9cad4bc90250175fe Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 15 Mar 2009 00:16:38 +0000 Subject: [PATCH 176/191] catch a rare AIOOB --- router/java/src/net/i2p/router/tunnel/FragmentHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/router/java/src/net/i2p/router/tunnel/FragmentHandler.java b/router/java/src/net/i2p/router/tunnel/FragmentHandler.java index 5a97956b9..dbe256ebd 100644 --- a/router/java/src/net/i2p/router/tunnel/FragmentHandler.java +++ b/router/java/src/net/i2p/router/tunnel/FragmentHandler.java @@ -74,6 +74,12 @@ public class FragmentHandler { int padding = 0; while (preprocessed[offset] != (byte)0x00) { offset++; // skip the padding + // AIOOBE http://forum.i2p/viewtopic.php?t=3187 + if (offset >= TrivialPreprocessor.PREPROCESSED_SIZE) { + _cache.release(new ByteArray(preprocessed)); + _context.statManager().addRateData("tunnel.corruptMessage", 1, 1); + return; + } padding++; } offset++; // skip the final 0x00, terminating the padding From d0a969ca33d78c1eb2e0794ab5c7e4351c7bb10b Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 16 Mar 2009 19:31:29 +0000 Subject: [PATCH 177/191] fix NPE on delayed open http://forum.i2p/viewtopic.php?t=3189 --- .../java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index b42376dbf..8e5ef9f4d 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -185,7 +185,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable /** * create the default options (using the default timeout, etc) - * + * unused? */ protected I2PSocketOptions getDefaultOptions() { Properties defaultOpts = getTunnel().getClientOptions(); @@ -210,6 +210,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable defaultOpts.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, ""+DEFAULT_READ_TIMEOUT); if (!defaultOpts.contains("i2p.streaming.inactivityTimeout")) defaultOpts.setProperty("i2p.streaming.inactivityTimeout", ""+DEFAULT_READ_TIMEOUT); + // delayed start + if (sockMgr == null) + sockMgr = getSocketManager(); I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts); if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT)) opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT); From 0da964e47f620311b0721e7976ac69a971100370 Mon Sep 17 00:00:00 2001 From: zzz Date: Mon, 16 Mar 2009 19:34:59 +0000 Subject: [PATCH 178/191] -9 --- history.txt | 8 ++++++++ router/java/src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index 1388353e4..e5bfe6478 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,11 @@ +2009-03-16 zzz + * help.jsp: Add some + * I2PTunnel: Cleanup + * I2PTunnelHTTPClient: Fix NPE on delayed open + * I2PTunnelHTTPServer: Maybe catch an NPE + * SOCKS: Allow .onion addresses for onioncat testing + * Tunnel: Catch a rare AIOOB + 2009-03-09 zzz * Client: - Clean up retry code diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index eeea3faa9..6ade2ee81 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 8; + public final static long BUILD = 9; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From e5f19c98a8bde3ee448d46f369a35b8bd787f28d Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Mar 2009 15:21:34 +0000 Subject: [PATCH 179/191] change common corrupt errors to warns --- .../net/i2p/router/tunnel/FragmentHandler.java | 4 ++-- .../net/i2p/router/tunnel/FragmentedMessage.java | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/router/java/src/net/i2p/router/tunnel/FragmentHandler.java b/router/java/src/net/i2p/router/tunnel/FragmentHandler.java index dbe256ebd..99b66c0c8 100644 --- a/router/java/src/net/i2p/router/tunnel/FragmentHandler.java +++ b/router/java/src/net/i2p/router/tunnel/FragmentHandler.java @@ -393,8 +393,8 @@ public class FragmentHandler { _log.error("Error receiving fragmented message (corrupt?): " + stringified, ioe); } catch (I2NPMessageException ime) { if (stringified == null) stringified = msg.toString(); - if (_log.shouldLog(Log.ERROR)) - _log.error("Error receiving fragmented message (corrupt?): " + stringified, ime); + if (_log.shouldLog(Log.WARN)) + _log.warn("Error receiving fragmented message (corrupt?): " + stringified, ime); } } diff --git a/router/java/src/net/i2p/router/tunnel/FragmentedMessage.java b/router/java/src/net/i2p/router/tunnel/FragmentedMessage.java index d26c691b7..b0203f540 100644 --- a/router/java/src/net/i2p/router/tunnel/FragmentedMessage.java +++ b/router/java/src/net/i2p/router/tunnel/FragmentedMessage.java @@ -78,13 +78,13 @@ public class FragmentedMessage { return false; } if (length <= 0) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Length is impossible (" + length + ") for messageId " + messageId); + if (_log.shouldLog(Log.WARN)) + _log.warn("Length is impossible (" + length + ") for messageId " + messageId); return false; } if (offset + length > payload.length) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Length is impossible (" + length + "/" + offset + " out of " + payload.length + ") for messageId " + messageId); + if (_log.shouldLog(Log.WARN)) + _log.warn("Length is impossible (" + length + "/" + offset + " out of " + payload.length + ") for messageId " + messageId); return false; } if (_log.shouldLog(Log.DEBUG)) @@ -131,13 +131,13 @@ public class FragmentedMessage { return false; } if (length <= 0) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Length is impossible (" + length + ") for messageId " + messageId); + if (_log.shouldLog(Log.WARN)) + _log.warn("Length is impossible (" + length + ") for messageId " + messageId); return false; } if (offset + length > payload.length) { - if (_log.shouldLog(Log.ERROR)) - _log.error("Length is impossible (" + length + "/" + offset + " out of " + payload.length + ") for messageId " + messageId); + if (_log.shouldLog(Log.WARN)) + _log.warn("Length is impossible (" + length + "/" + offset + " out of " + payload.length + ") for messageId " + messageId); return false; } if (_log.shouldLog(Log.DEBUG)) From 09d700e1d6b6777d5fc2509dcc2b521855cdbe9d Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Mar 2009 18:19:47 +0000 Subject: [PATCH 180/191] fix encrypted leasesets --- core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java index 7a8bd200e..e662f6572 100644 --- a/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java +++ b/core/java/src/net/i2p/client/RequestLeaseSetMessageHandler.java @@ -79,7 +79,7 @@ class RequestLeaseSetMessageHandler extends HandlerImpl { leaseSet.setEncryptionKey(li.getPublicKey()); leaseSet.setSigningKey(li.getSigningPublicKey()); - boolean encrypt = Boolean.valueOf(session.getOptions().getProperty("i2cp.encryptLeaseset")).booleanValue(); + boolean encrypt = Boolean.valueOf(session.getOptions().getProperty("i2cp.encryptLeaseSet")).booleanValue(); String sk = session.getOptions().getProperty("i2cp.leaseSetKey"); if (encrypt && sk != null) { SessionKey key = new SessionKey(); From 47edc3c8537f079ed49c97051f8f539fd3b31168 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Mar 2009 18:21:28 +0000 Subject: [PATCH 181/191] add warnings for some new features --- apps/i2ptunnel/jsp/editClient.jsp | 4 ++-- apps/i2ptunnel/jsp/editServer.jsp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index 6dd839d97..915da5db9 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -318,7 +318,7 @@
      @@ -353,7 +353,7 @@
      diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index b3917cced..59a7b9cf7 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -286,7 +286,7 @@
      From e9063a22d565963db8435666cf6ebbaa7396eb9c Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Mar 2009 18:58:08 +0000 Subject: [PATCH 182/191] add anchors --- apps/routerconsole/jsp/configstats.jsp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/routerconsole/jsp/configstats.jsp b/apps/routerconsole/jsp/configstats.jsp index 651636176..c7ec3090c 100644 --- a/apps/routerconsole/jsp/configstats.jsp +++ b/apps/routerconsole/jsp/configstats.jsp @@ -88,6 +88,7 @@ function toggleAll(category)
  • LogGraph
    + checked="true" <% } %>/> From 6c365bef85b63c746cd09149c05a7d0510f8189c Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Mar 2009 19:52:06 +0000 Subject: [PATCH 183/191] add links to enable graphing --- apps/routerconsole/jsp/help.jsp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/routerconsole/jsp/help.jsp b/apps/routerconsole/jsp/help.jsp index 279df95dd..68f1b6245 100644 --- a/apps/routerconsole/jsp/help.jsp +++ b/apps/routerconsole/jsp/help.jsp @@ -22,6 +22,10 @@ or IRC.

    Summary Bar Information

    +Many of the stats on the summary bar may be +configured to be +graphed for further analysis. +

    General

    • Ident: @@ -47,13 +51,16 @@ This may range from 8-10 to several hundred, depending on your total bandwidth, shared bandwidth, and locally-generated traffic. The second number is the number of peers seen in the last hour or so. Do not be concerned if these numbers vary widely. +Enable graphing
    • Fast: This is the number of peers you use for building client tunnels. It is generally in the range 8-15. Your fast peers are shown on the profiles page. +Enable graphing
    • High Capacity: This is the number of peers you use for building some of your exploratory tunnels. It is generally in the range 8-25. The fast peers are included in the high capacity tier. Your high capacity peers are shown on the profiles page. +Enable graphing
    • Well Integrated: This is the number of peers you use for network database inquiries. These are usually the "floodfill" peers. @@ -71,6 +78,7 @@ I2P does not require a router to know every other router.

      Bandwidth in/out

      Should be self-explanatory. All values are in bytes per second, not bits per second. Change your bandwidth limits on the configuration page. +Bandwidth is graphed by default.

      Local destinations

      The local applications connecting through your router. @@ -93,10 +101,12 @@ The recommended method for limiting participating tunnels is to change your share percentage on the configuration page. You may also limit the total number by setting router.maxParticipatingTunnels=nnn on the advanced configuration page. +Enable graphing

    Congestion

    Some basic indications of router overload. +
    • Job lag: How long jobs are waiting before execution. The job queue is listed on the jobs page. Unfortunately, there are several other job queues in the router that may be congested, @@ -104,18 +114,21 @@ and their status is not available in the router console. The job lag should generally be zero. If it is consistently higher than 500ms, your computer is very slow, or the router has serious problems. +Enable graphing
    • Message delay: How long an outbound message waits in the queue. This should generally be a few hundred milliseconds or less. If it is consistently higher than 1000ms, your computer is very slow, or you should adjust your bandwidth limits, or your (bittorrent?) clients may be sending too much data and should have their transmit bandwidth limit reduced. +Enable graphing (transport.sendProcessingTime)
    • Tunnel lag: This is the round trip time for a tunnel test, which sends a single message out a client tunnel and in an exploratory tunnel, or vice versa. It should usually be less than 5 seconds. If it is consistently higher than that, your computer is very slow, or you should adjust your bandwidth limits, or there are network problems. +Enable graphing (tunnel.testSuccessTime)
    • Handle backlog: This is the number of pending requests from other routers to build a participating tunnel through your router. From bb51bf49b03d35836630923ddee0c1e69c1059d4 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Mar 2009 20:24:20 +0000 Subject: [PATCH 184/191] - Suppress log error on manual stop - Prevent NPE when closing a delayed-open tunnel --- .../i2p/i2ptunnel/I2PTunnelClientBase.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java index 94bc959c0..b6eb39224 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClientBase.java @@ -375,7 +375,7 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna // This will build a new socket manager and a new dest if the session is closed. sockMgr = getSocketManager(); if (oldSockMgr != sockMgr) { - _log.error("Built a new destination on resume"); + _log.warn("Built a new destination on resume"); } } } // else the old socket manager will reconnect the old session if necessary @@ -431,8 +431,10 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna _context.statManager().addRateData("i2ptunnel.client.manageTime", total, total); } } catch (IOException ex) { - _log.error("Error listening for connections on " + localPort, ex); - notifyEvent("openBaseClientResult", "error"); + if (open) { + _log.error("Error listening for connections on " + localPort, ex); + notifyEvent("openBaseClientResult", "error"); + } synchronized (sockLock) { mySockets.clear(); } @@ -513,20 +515,23 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna // might risk to create an orphan socket. Would be better // to return with an error in that situation quickly. synchronized (sockLock) { - mySockets.retainAll(sockMgr.listSockets()); - if (!forced && mySockets.size() != 0) { - l.log("There are still active connections!"); - _log.debug("can't close: there are still active connections!"); - for (Iterator it = mySockets.iterator(); it.hasNext();) { - l.log("->" + it.next()); + if (sockMgr != null) { + mySockets.retainAll(sockMgr.listSockets()); + if (!forced && mySockets.size() != 0) { + l.log("There are still active connections!"); + _log.debug("can't close: there are still active connections!"); + for (Iterator it = mySockets.iterator(); it.hasNext();) { + l.log("->" + it.next()); + } + return false; + } + I2PSession session = sockMgr.getSession(); + if (session != null) { + getTunnel().removeSession(session); } - return false; - } - I2PSession session = sockMgr.getSession(); - if (session != null) { - getTunnel().removeSession(session); } l.log("Closing client " + toString()); + open = false; try { if (ss != null) ss.close(); } catch (IOException ex) { @@ -534,7 +539,6 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna return false; } l.log("Client closed."); - open = false; } synchronized (_waitingSockets) { _waitingSockets.notifyAll(); } From 41718b47c16c0d51769a49645e850c75aa252b50 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Mar 2009 21:29:15 +0000 Subject: [PATCH 185/191] increase default bw to 64/32 --- .../i2p/router/transport/FIFOBandwidthRefiller.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java b/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java index d4bdfea85..693df45b1 100644 --- a/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java +++ b/router/java/src/net/i2p/router/transport/FIFOBandwidthRefiller.java @@ -33,11 +33,11 @@ public class FIFOBandwidthRefiller implements Runnable { public static final String PROP_OUTBOUND_BANDWIDTH_PEAK = "i2np.bandwidth.outboundBurstKBytes"; //public static final String PROP_REPLENISH_FREQUENCY = "i2np.bandwidth.replenishFrequencyMs"; - // no longer allow unlimited bandwidth - the user must specify a value, and if they do not, it is 32/16KBps - public static final int DEFAULT_INBOUND_BANDWIDTH = 48; - public static final int DEFAULT_OUTBOUND_BANDWIDTH = 24; - public static final int DEFAULT_INBOUND_BURST_BANDWIDTH = 64; - public static final int DEFAULT_OUTBOUND_BURST_BANDWIDTH = 32; + // no longer allow unlimited bandwidth - the user must specify a value, else use defaults below (KBps) + public static final int DEFAULT_INBOUND_BANDWIDTH = 64; + public static final int DEFAULT_OUTBOUND_BANDWIDTH = 32; + public static final int DEFAULT_INBOUND_BURST_BANDWIDTH = 80; + public static final int DEFAULT_OUTBOUND_BURST_BANDWIDTH = 40; public static final int DEFAULT_BURST_SECONDS = 60; From 2695461bd48a0da1ca056e9eda4dc803201cb0b1 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 24 Mar 2009 21:31:55 +0000 Subject: [PATCH 186/191] -10 --- history.txt | 10 ++++++++++ router/java/src/net/i2p/router/RouterVersion.java | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index e5bfe6478..d3fcf2f4c 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,13 @@ +2009-03-24 zzz + * I2PTunnel: + - Add some warnings about new features + - Fix encrypted leasesets broken in about -4 + - Suppress log error on manual stop + - Fix NPE on close of a tunnel not open yet + * Transport: + - Increase default bw to 64/32, burst 80/40 + * Tunnels: Change some fragmentation errors to warns + 2009-03-16 zzz * help.jsp: Add some * I2PTunnel: Cleanup diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 6ade2ee81..c1d6bdf7c 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 9; + public final static long BUILD = 10; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From 29df534161b1f914e9abcf7f6012232282314835 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 26 Mar 2009 00:02:29 +0000 Subject: [PATCH 187/191] update license splash text --- installer/resources/readme.license.txt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/installer/resources/readme.license.txt b/installer/resources/readme.license.txt index d707e557c..9e40f595c 100644 --- a/installer/resources/readme.license.txt +++ b/installer/resources/readme.license.txt @@ -16,12 +16,13 @@ following non-public domain code: * Bouncycastle's hash routines (MIT license) * Cryptix's AES routines (Cryptix license) * Adam Buckley's SNTP routines (BSD) +* FSF's PRNG and GMP (LGPL) Also included in this distribution are a bunch of third party client applications, all with their own dependencies. Please see our license policy page for details: - http://www.i2p.net/licenses + http://www.i2p2.de/licenses One of the bundled client apps (routerconsole) requires us to say: @@ -29,8 +30,11 @@ requires us to say: the Apache Software Foundation (http://www.apache.org/) -Another (I2PTunnel) is GPL licensed. +I2PTunnel, I2PSnark, SusiDNS, and SusiMail +are GPL licensed. + +For more information see LICENSE.txt +in the install directory. For source, please see: - http://www.i2p.net/download -or http://www.i2p.net/cvs \ No newline at end of file + http://www.i2p2.de/monotone From 6a6cd14398d790d32ad8b56c53f2495c38b298f1 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 26 Mar 2009 18:28:27 +0000 Subject: [PATCH 188/191] checklist update --- checklist.txt | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/checklist.txt b/checklist.txt index b649bef59..fcbe65028 100644 --- a/checklist.txt +++ b/checklist.txt @@ -15,45 +15,45 @@ Change revision in: core/java/src/net/i2p/CoreVersion.java Review the complete diff from the last release: - mtn diff -r t:i2p-0.6.(xx-1) > out.diff + mtn diff -r t:i2p-0.7.(xx-1) > out.diff vi out.diff Build and tag: ant pkg mtn ci - mtn tag h: i2p-0.6.xx + mtn tag h: i2p-0.7.xx Sync with mtn.i2p2.i2p Create a signed update file with: export I2P=~/i2p - java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate sign i2pupdate.zip i2pupdate.sud /path/to/private.key 0.6.xx + java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate sign i2pupdate.zip i2pupdate.sud /path/to/private.key 0.7.xx Verify signed update file with: java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate showversion i2pupdate.sud java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate verifysig i2pupdate.sud Make the source tarball: - Start with a clean checkout mtn -d i2p.mtn co --branch=i2p.i2p i2p-0.6.xx + Start with a clean checkout mtn -d i2p.mtn co --branch=i2p.i2p i2p-0.7.xx Double-check trust list - tar cjf i2psource-0.6.xx.tar.bz2 --exclude i2p-0.6.xx/_MTN i2p-0.6.xx - mv i2p-0.6.xx.tar.bz2 i2p.i2p + tar cjf i2psource-0.7.xx.tar.bz2 --exclude i2p-0.7.xx/_MTN i2p-0.7.xx + mv i2p-0.7.xx.tar.bz2 i2p.i2p Until the build script gets this ability, you need to rename some files: - mv i2pinstall.exe i2pinstall-0.6.xx.exe - mv i2p.tar.bz2 i2pheadless-0.6.xx.tar.bz2 - mv i2pupdate.zip i2pupdate-0.6.xx.zip + mv i2pinstall.exe i2pinstall-0.7.xx.exe + mv i2p.tar.bz2 i2pheadless-0.7.xx.tar.bz2 + mv i2pupdate.zip i2pupdate-0.7.xx.zip you probably don't need to rename i2pupdate.sud Generate hashes: - sha1sum i2p*0.6.xx.* + sha1sum i2p*0.7.xx.* sha1sum i2pupdate.sud now GPG-sign an announcement with the hashes Generate PGP signatures: - gpg -b i2pinstall-0.6.xx.exe - gpg -b i2pheadless-0.6.xx.tar.bz2 - gpg -b i2psource-0.6.xx.tar.bz2 - gpg -b i2pupdate-0.6.xx.zip + gpg -b i2pinstall-0.7.xx.exe + gpg -b i2pheadless-0.7.xx.tar.bz2 + gpg -b i2psource-0.7.xx.tar.bz2 + gpg -b i2pupdate-0.7.xx.zip gpg -b i2pupdate.sud Distribute files to download locations and to www.i2p2.i2p From 0343e8ffcd7ba0465aabdd3e1ca20472ac3d7676 Mon Sep 17 00:00:00 2001 From: zzz Date: Thu, 26 Mar 2009 18:51:43 +0000 Subject: [PATCH 189/191] readme_fr - thanks Narya and Mathiasdm --- build.xml | 3 ++- readme.html | 2 +- readme_de.html | 2 +- readme_fr.html | 37 +++++++++++++++++++++++++++++++++++++ readme_nl.html | 2 +- readme_sv.html | 4 ++-- 6 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 readme_fr.html diff --git a/build.xml b/build.xml index a9f58ef20..ef84b2aab 100644 --- a/build.xml +++ b/build.xml @@ -276,6 +276,7 @@ + - + diff --git a/readme.html b/readme.html index 9ba805a72..0e4891b5d 100644 --- a/readme.html +++ b/readme.html @@ -1,4 +1,4 @@ -

      English | Deutsch | Nederlands | Svenska

      +

      English | Deutsch | Français | Nederlands | Svenska

      If you've just started I2P, the Active: numbers on the left should start to grow over the next few minutes and you'll see a "shared clients" local destination listed on the left (if not, see below). Once those show up, diff --git a/readme_de.html b/readme_de.html index a6d233d6a..1534585da 100644 --- a/readme_de.html +++ b/readme_de.html @@ -1,4 +1,4 @@ -

      English | Deutsch | Nederlands | Svenska

      +

      English | Deutsch | Français | Nederlands | Svenska

      Wenn Du gerade I2P gestartet hast, sollten die "Active:" Zahlen links in den nächsten paar Minuten anwachsen und Du siehst dann dort ein "shared clients" lokales Ziel gelistet (falls nicht, siehe Unten). Sobald das erscheint, kannst Du:

      • "Eepsites" besuchen - In I2P sind anonym gehostete Websites - diff --git a/readme_fr.html b/readme_fr.html new file mode 100644 index 000000000..5e619cff2 --- /dev/null +++ b/readme_fr.html @@ -0,0 +1,37 @@ +

        Deutsch | English | Français | Nederlands | Svenska

        +

        Si vous venez juste de lancer I2P, les chiffres sur la gauche à coté de Active devraient commencer à augmenter dans les prochaines minutes et vous verrez un "Shared client" en destination locale listés sur la gauche (si non, voir plus bas). Une fois qu'ils apparaissent, vous pouvez:

        +
          +
        • parcourir les "eepsites" - sur I2P il y a des sites web anonymes hébergés - dites à votre navigateur d'utiliser le HTTP proxy a l'adresse localhost port 4444, ensuite vous pouvez naviguer sur les eepsites. + + Il y a bien plus d'eepsites - suivez juste les liens au départ de ceux sur lesquels vous êtes, mettez-les dans vos favoris et visitez-les souvent!
        • +
        • Parcourez le web - Il y a pour l'instant un outproxy HTTP sur I2P attaché à votre propre proxy HTTP sur le port 4444 - vous devez simplement configurer le proxy de votre navigateur pour l'utiliser (comme expliqué ci-dessus) et aller sur n'importe quel URL normale - vos requêtes seront relayées par le réseau i2p.
        • +
        • Transfer de fichiers - Il y a un port intégré de Snark le client BitTorrent.
        • +
        • Utiliser le service de mail anonyme - Postman a créé un sytème de mails compatible avec un client de messagerie normal (POP3 / SMTP) qui permet d'envoyer des emails autant au sein d'i2p que vers et à partir de l'internet normal! Créez-vous un compte à hq.postman.i2p. + Nous fournissons dans la version de base de i2p susimail, + un client web pop3/smtp orienté sur l'anonymat qui est configuré pour accéder aux services email de postman.
        • +
        • Chatter de manière anonyme - Activez votre client IRC et connectez-le sur le serveur localhost port 6668. Ceci pointe vers l'un des deux serveur IRC anonyme, mais ni vous ni eux ne savent qui est l'autre
        • +
        • Créez-vous un blog anonyme - Renseignez-vous chez Syndie
        • +
        • Et bien d'autres
        • +
        + +

        Vous voulez votre propre eepsite?

        + +

        Nous fournissons de base quelques logiciels pour vous permettre de créer votre propre eepsite - une instance +Jetty, qui écoute sur +http://localhost:7658/. Placer simplement vos fichiers dans le répertoire eepsite/docroot/ (ou placez n'importe quel fichier JSP/Servlet standard .war) dans eepsite/webapps, ou script CGI standard dans eepsite/cgi-bin) et ils apparaitront. Après avoir démarré un tunnel pour votre eepsite (le tunnel doit pointer sur l'adresse locale du eepsite), votre eepsite sera visible pour les autes. Des instructions plus détaillées pour créer un eepsite se trouvent sur Votre eepsite temporaire. +

        + +

        Dépannage

        + +

        Soyez patient - i2p peut s'avérer lent à démarrer la première fois car il recherche des pairs. Si, après 30 minutes, votre Actives: connecté/récent compte moins de 10 pairs connectés, vous devez ouvrir le port 8887 sur votre pare-feu pour avoir une meilleure connection. Si vous ne pouvez accéder à aucun eepsite (même www.i2p2.i2p), soyez sûr que votre navigateur utilise bien le proxy localhost sur le port 4444. Vous pouvez aussi faire part de votre démarche sur le site web I2P, poster des message sur le forum de discussion, ou passer par #i2p ou #i2p-chat sur IRC sur le serveur irc.freenode.net, irc.postman.i2p ou irc.freshcoffee.i2p (ils sont liés).

        + +

        Comme vous pouvez le remarquer, il suffit d'éditer la page "docs/readme.html" pour changer la page d'acceuil

        diff --git a/readme_nl.html b/readme_nl.html index 3203c79b5..427f75f91 100644 --- a/readme_nl.html +++ b/readme_nl.html @@ -1,4 +1,4 @@ -

        English | Deutsch | Nederlands | Svenska

        +

        English | Deutsch | Français | Nederlands | Svenska

        Als je net I2P opgestart hebt, zullen de 'Active:' (Actieve) getallen aan de linkerkant in de komende minuten stijgen, en je zal een "Shared clients" (Gedeelde clients) lokale bestemming zien staan aan de linkerkant (indien niet, zie hieronder). Eenmaal je deze bestemming ziet, kan je:

        • surfen naar "eepsites" - op I2P zijn er anonieme websites - stel je browser in om de HTTP proxy op localhost, poort 4444 te gebruiken, en surf vervolgens naar een eepsite - diff --git a/readme_sv.html b/readme_sv.html index e368fbc53..b1f59fa04 100644 --- a/readme_sv.html +++ b/readme_sv.html @@ -1,5 +1,5 @@

          English -| Deutsch | Deutsch | Français | Nederlands | Svenska

          Om du just har startat I2P kommer de "Aktiva: #/#" börja öka inom några få minuter och du kommer se en destination kallad "delade @@ -98,4 +98,4 @@ href="irc://irc.freenode.net/#i2p">irc.freenode.net, irc.postman.i2p eller irc.freshcoffee.i2p (de är alla sammankopplade).

          Du kan förändra denhär sidan genom att ändra i filen -"docs/readme_sv.html"

          \ No newline at end of file +"docs/readme_sv.html"

          From fe0d0d6737a8a5f3ae8342a1cac73ec9122b34cf Mon Sep 17 00:00:00 2001 From: zzz Date: Fri, 27 Mar 2009 18:09:46 +0000 Subject: [PATCH 190/191] -11, catch rare AIOOB --- history.txt | 5 +++++ router/java/src/net/i2p/data/i2np/TunnelGatewayMessage.java | 5 +++++ router/java/src/net/i2p/router/RouterVersion.java | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/history.txt b/history.txt index d3fcf2f4c..1495d99de 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,8 @@ +2009-03-27 zzz + * Add readme_fr.html + * License splash update + * Catch rare TunnelGatewayMessage AIOOB, root cause unknown + 2009-03-24 zzz * I2PTunnel: - Add some warnings about new features diff --git a/router/java/src/net/i2p/data/i2np/TunnelGatewayMessage.java b/router/java/src/net/i2p/data/i2np/TunnelGatewayMessage.java index f611c3213..8fc7c9fd9 100644 --- a/router/java/src/net/i2p/data/i2np/TunnelGatewayMessage.java +++ b/router/java/src/net/i2p/data/i2np/TunnelGatewayMessage.java @@ -75,6 +75,11 @@ public class TunnelGatewayMessage extends I2NPMessageImpl { } DataHelper.toLong(out, curIndex, 2, _msgData.length); curIndex += 2; + // where is this coming from? + if (curIndex + _msgData.length > out.length) { + _log.log(Log.ERROR, "output buffer too small idx: " + curIndex + " len: " + _msgData.length + " outlen: " + out.length); + throw new I2NPMessageException("Too much data to write out (id=" + _tunnelId + " data=" + _msg + ")"); + } System.arraycopy(_msgData, 0, out, curIndex, _msgData.length); curIndex += _msgData.length; return curIndex; diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index c1d6bdf7c..510adb6de 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 10; + public final static long BUILD = 11; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID); From 4a9543be78e3ada39c9572680068133096b9f4e5 Mon Sep 17 00:00:00 2001 From: complication Date: Sun, 29 Mar 2009 19:47:46 +0000 Subject: [PATCH 191/191] * Update versions, package release --- core/java/src/net/i2p/CoreVersion.java | 2 +- history.txt | 5 +++ initialNews.xml | 4 +- installer/install.xml | 2 +- news.xml | 42 +++++++++---------- .../src/net/i2p/router/RouterVersion.java | 2 +- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/core/java/src/net/i2p/CoreVersion.java b/core/java/src/net/i2p/CoreVersion.java index c24542b00..6c924fe10 100644 --- a/core/java/src/net/i2p/CoreVersion.java +++ b/core/java/src/net/i2p/CoreVersion.java @@ -15,7 +15,7 @@ package net.i2p; */ public class CoreVersion { public final static String ID = "$Revision: 1.72 $ $Date: 2008-08-24 12:00:00 $"; - public final static String VERSION = "0.7"; + public final static String VERSION = "0.7.1"; public static void main(String args[]) { System.out.println("I2P Core version: " + VERSION); diff --git a/history.txt b/history.txt index 1495d99de..e76d41055 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,8 @@ +* 2009-03-29 0.7.1 released + +2009-03-29 Complication + * Update versions, package release + 2009-03-27 zzz * Add readme_fr.html * License splash update diff --git a/initialNews.xml b/initialNews.xml index 6a9c4b4ca..0306dcd7b 100644 --- a/initialNews.xml +++ b/initialNews.xml @@ -1,5 +1,5 @@ - - +

          Congratulations on getting I2P installed!

            diff --git a/installer/install.xml b/installer/install.xml index 8c7ff1bf4..125e92231 100644 --- a/installer/install.xml +++ b/installer/install.xml @@ -4,7 +4,7 @@ i2p - 0.7 + 0.7.1 diff --git a/news.xml b/news.xml index 63adee7fa..b85e0f130 100644 --- a/news.xml +++ b/news.xml @@ -1,5 +1,5 @@ - - +

            • -2009-01-24: 0.7 Released +2009-03-29: 0.7.1 Released

            -The 0.7 release adds stability and flexibility to I2PSnark, -which can hopefully be used to distribute I2P updates in future. +The 0.7.1 release optimizes I2P towards better performance +and introduces new features.

            -The I2P router gets fixes and optimizations to various -transport-level and streaming issues, network exploration, -NetDB performance and the UDP introducer system. -Among other features, the new release offers -better connection limiting, higher tolerance to "out of memory" exceptions -in helper applications, and an experimental new address system -using Base32 hashes of destination keys (".b32.i2p" URLs). +Multiple bugs are fixed, replacements to the SimpleTimer class +should waste less time on object locking. Some old components +are dropped and several classes refactored to avoid repeating code.

            -Both the BOB and SAM protocols are improved upon, -more old components dropped, Router Console features added -and a possible latency measurement attack mitigated. -From this release onwards, block lists for misbehaving peers -are activated by default. +Support for encrypted LeaseSets (for creation of links over I2P +which an adversary cannot obstruct by attacking its gateways) +becomes more complete. New tunnel types like IRC server tunnels +and new options like delayed start and idling of tunnels +also gain support, along with improved usability of the I2P +Socks proxy mechanism.

            -It seems worthwhile to remind that already since -the last release, I2P requires Java 1.5 or higher. -If you are uncertain about your Java version, you can verify -by opening a terminal window or command prompt, -and entering the command "java -version". -If you have an older Java installed, please update it first!

            +Work continues on streamlining and expanding the Router Console, +on the BOB protocol, on I2P ports for Debian and Slackware Linux, +on the I2PSnark client, on TCP connection properties +and multiple other fronts. Updating is highly recommended. +

            diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 510adb6de..450adcf28 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -17,7 +17,7 @@ import net.i2p.CoreVersion; public class RouterVersion { public final static String ID = "$Revision: 1.548 $ $Date: 2008-06-07 23:00:00 $"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 11; + public final static long BUILD = 0; public static void main(String args[]) { System.out.println("I2P Router version: " + VERSION + "-" + BUILD); System.out.println("Router ID: " + RouterVersion.ID);