34 Commits

Author SHA1 Message Date
zzz
5c08658360 Thread I2CP lookups
until we have a nonblocking I2CP lookup API.
Most of the time, this won't be needed, as our cache
will have it, but if additional announces come in over
the lifetime, we'll have to do a I2CP lookup.
Even then, the I2CP cache may have it,
or the router should have the LS, so it should be quick.
2025-05-01 18:40:29 -04:00
zzz
02328bd5d4 Add UDP announce rate stat 2025-04-29 08:59:30 -04:00
zzz
6bc7821f1b Reduce clean interval
To reduce memory usage and get at least two stat updates per 5 minutes
2025-04-28 09:24:00 -04:00
zzz
cf48fcd901 Change min i2p version to 2.8.2 for now
for testing, but requires the DG2 PR #500 to be merged
will change back to 2.9.0 for the 0.20.0 release.
2025-04-28 08:57:20 -04:00
zzz
8ff48800d6 README updates 2025-04-28 08:44:40 -04:00
zzz
1a90be6701 Disable returning non-compact peer list
We can no longer do it in a standard way, as we don't store the full destination.
Since we can't bencode Peer correctly any more, Peer no longer extends HashMap,
and we don't need the String destcache either.
This saves lots more space.
2025-04-28 07:44:37 -04:00
zzz
a4729aba16 remove seedless 2025-04-28 07:32:52 -04:00
zzz
cd2441ca4a Add dest cache
mitigates but doesn't fully solve the lookup blocking issue
2025-04-28 06:55:24 -04:00
zzz
d9d3283b3e Fix rate format 2025-04-27 17:31:39 -04:00
zzz
1acaef9514 Big refactor to reduce memory usage
- Remove support for non-compact responses
- Store peers as hashes, not dests
- Remove UDP connection table
- Generate and validate UDP connection IDs cryptographically
- Add support for sending UDP errors
2025-04-27 17:04:46 -04:00
zzz
0b6d22da3b reorder things on status page 2025-04-27 13:45:00 -04:00
zzz
38c47f9993 Fix UDP announce URL again 2025-04-27 13:42:21 -04:00
zzz
6e7406567b Get b32 from the session, not the request
consolidate controller code to get instance
2025-04-27 13:40:54 -04:00
zzz
b970d198ca log tweaks, notes on error responses 2025-04-27 12:57:07 -04:00
zzz
241b95bc65 Fix UDP announce URL 2025-04-27 11:24:06 -04:00
zzz
ee39fa92ae Avoid redirect to stats page 2025-04-27 10:54:25 -04:00
zzz
26ad8bfd08 Remove ElGamal support 2025-04-27 10:47:48 -04:00
zzz
8d82d13d1c Switch to DG2 (Proposals 160, 163), requires router 2.9.0 2025-04-27 10:15:15 -04:00
zzz
d9b4a5e1c4 fix displayed version 2025-04-26 12:41:31 -04:00
zzz
886a05730c stats page fixes 2025-04-26 12:36:40 -04:00
zzz
65dd1161ed Add announce rate to stats page 2025-04-26 12:09:48 -04:00
zzz
0edade465c Add tracker stats to the stats subsystem for graphing or export to prometheus 2025-04-26 11:53:17 -04:00
zzz
1e94a95a7a Put announce URLs on status page 2025-04-25 13:20:07 -04:00
zzz
b7009e3a20 TODO update 2025-04-25 13:02:25 -04:00
zzz
9eece7ab8b Fix note about scrapes 2025-04-25 12:59:12 -04:00
zzz
f5ed7ec874 update help page 2025-04-25 12:49:46 -04:00
zzz
071a36c348 Make UDP port configurable 2025-04-25 12:25:05 -04:00
zzz
d07780615c Show UDP status on status page 2025-04-25 12:07:10 -04:00
zzz
e157028db4 Implement UDP start/stop
Add UDP config, default false
Change UDP port
2025-04-25 11:57:51 -04:00
zzz
8ff2c93b0a Updates for proposal 160 changes (WIP)
- Remove fast mode support
- Add configurable lifetime

TODO: Switch to Datagram2
2025-04-25 11:14:13 -04:00
zzz
20245a3e8e Add interval to stats page 2025-04-22 10:23:12 -04:00
zzz
edbb803095 changelog update 2025-04-18 13:35:47 -04:00
zzz
b2432833e3 Fix dup ids in jetty.xml, fix DTD 2025-04-18 13:31:16 -04:00
zzz
6073e61ba4 README typo 2024-05-14 18:57:43 -04:00
19 changed files with 606 additions and 320 deletions

View File

@ -1,3 +1,15 @@
2025-xx-xx [0.20.0] (Requires I2P 2.9.0 or higher)
- Support UDP announces
- Fix dup ids in jetty.xml, existing installs must fix manually,
s/<Ref id=/<Ref refid=/g
- Add interval to stats page
- Add stats to I2P stats subsystem
- Show announce URLs on stats page
- Remove ElGamal support
- Remove support for non-compact announce replies
- Reduce memory usage
- Remove seedless support
2024-04-07 [0.19.0]
- Disable full scrape by default
- Handle BiglyBT scrape URLs

View File

@ -2,7 +2,7 @@ ZzzOT I2P Open Tracker Plugin
-----------------------------
This is a very simple in-memory open tracker, wrapped into an I2P plugin.
A compiled version of the plugin is available at http://stats.i2p/i2p/plugins/
Plugin su3 binaries are available at http://stats.i2p/i2p/plugins/
The plugin starts a new http server tunnel, eepsite, and Jetty server running at
port 7662. The tracker status is available at http://127.0.0.1:7662/tracker/
@ -11,16 +11,20 @@ If other files are desired on the eepsite, they can be added at eepsite/docroot
The open tracker code and jsps were written from scratch, but depend on some
code in i2psnark.jar from the I2P installation for bencoding, and of course on
other i2p libraries. See the license files in I2P for i2p and i2psnark licenses.
There is also some code modified from Jetty 5.1.15. See LICENSES.txt for the
There is also some code modified from Jetty. See LICENSES.txt for the
zzzot and Jetty licenses.
I2P source must be installed and built in ../i2p.i2p to compile this package.
I2P 1.7.0 or higher required to build and run.
Sure, as a standalone program in its own JVM with Jetty, this would be a pig -
you should use the C opentracker instead. But since you're already running the
JVM and Jetty, running this in the same JVM probably doesn't hog to much more
memory.
As of release 0.19.0:
- Full scrape is disabled by default
As of release 0.20.0:
- I2P 2.9.0 or higher required to build and run
- UDP announces are supported, see http://i2p-projekt.i2p/spec/proposals/160
- Non-compact responses are no longer supported
- Seedless support is removed
- Memory usage greatly reduced
Valid announce URLs:
/a
@ -32,6 +36,8 @@ Valid announce URLs:
/tracker/announce.jsp
/tracker/announce.php
UDP announce URLs (default port is 6969):
udp://yourb32string.b32.i2p:6969/
Valid scrape URLs:
/scrape
@ -41,10 +47,5 @@ Valid scrape URLs:
/tracker/scrape.jsp
/tracker/scrape.php
The tracker also responds to seedless queries at:
/Seedless/index.jsp
You may use the rest of the eepsite for other purposes; for example you
may place torrent files in: eepsite/docroot/torrents/

View File

@ -1,11 +1,9 @@
Configuration file:
- clean time
- max peers in response
- disable full scrapes
- disable all scrapes
- disable seedless
Stop the cleaner
Remove seedless
Throttles:
- full scrapes

View File

@ -16,6 +16,7 @@
<delete dir="plugin/eepsite/docroot/torrents/" />
<!-- get version number -->
<buildnumber file="scripts/build.number" />
<!-- NOTE: Change VERSION in ZzzOTController when you change this -->
<property name="release.number" value="0.19.0" />
<!-- make the update xpi2p -->

View File

@ -357,8 +357,8 @@
<h2>Welcome to the ZzzOT I2P Plugin!</h2>
<hr class="heading">
<p class="warn" id="docroot">This help file is located at: <code>$PLUGIN/eepsite/docroot/help.html</code><br><span class="emphasis"><b>You should probably move it outside of the document root before you announce your eepsite as it may contain your username.</b></span></p>
<p>ZzzOT is a simple in-memory BitTorrent open <a href="https://en.wikipedia.org/wiki/BitTorrent_tracker" class="external" target="_blank">tracker</a>, wrapped into an I2P plugin. The software depends on several I2P libraries, in addition to <code>i2psnark.jar</code> from the I2P installation for <a href="https://en.wikipedia.org/wiki/Bencode" class="external" target="_blank">bencoding</a>. Please report bugs on <a href="http://trac.i2p2.i2p/" target="_blank">trac.i2p2.i2p</a> or add comments and feature requests on <a href="http://zzz.i2p/forums/16" target="_blank">the plugin forum</a> on zzz.i2p.</p>
<p>Source code is available (under the <a href="https://www.apache.org/licenses/LICENSE-2.0.html" class="external" target="_blank">Apache 2.0 license</a>) via the <code>i2p.plugins.zzzot</code> branch in the <a href="https://geti2p.net/en/get-involved/guides/monotone" class="external" target="_blank">I2P monotone</a> database, or on <a href="https://github.com/i2p/i2p.plugins.zzzot" class="external" target="_blank">github</a>. Note that the I2P source code must be available (and compiled) in <code>../i2p.i2p</code> in order to build ZzzOT.</p>
<p>ZzzOT is a simple in-memory BitTorrent open <a href="https://en.wikipedia.org/wiki/BitTorrent_tracker" class="external" target="_blank">tracker</a>, wrapped into an I2P plugin. The software depends on several I2P libraries, in addition to <code>i2psnark.jar</code> from the I2P installation for <a href="https://en.wikipedia.org/wiki/Bencode" class="external" target="_blank">bencoding</a>. Please report bugs on <a href="http://i2pforum.i2p/" target="_blank">i2pforum.i2p</a>. New releases will be announced on <a href="http://zzz.i2p/forums/16" target="_blank">the plugin forum</a> on zzz.i2p.</p>
<p>Source code is available (under the <a href="https://www.apache.org/licenses/LICENSE-2.0.html" class="external" target="_blank">Apache 2.0 license</a>) at <a href="http://git.idk.i2p/I2P_Developers/i2p.plugins.zzzot" target="_blank">our Gitea site</a> and on <a href="https://github.com/i2p/i2p.plugins.zzzot" class="external" target="_blank">github</a>. Note that the I2P source code must be available (and compiled) in <code>../i2p.i2p</code> in order to build ZzzOT.</p>
<h3>Configuration &amp; Customization</h3>
<hr class="heading">
<ul id="config">
@ -366,8 +366,10 @@
<li>To change the tunnel settings, edit: <code>$PLUGIN/i2ptunnel.config</code> (the tunnel will not appear in the <a href="http://127.0.0.1:7657/i2ptunnelmgr" target="_blank">Tunnel Manager</a>), then stop and restart the plugin from the <a href="http://127.0.0.1:7657/configplugins" target="_blank">Plugin Manager</a>.</li>
<li>The Jetty webserver is running on port <code>7662</code>. If you must change it, edit: <code>jetty.xml</code>, <code>i2ptunnel.config</code>, and <code>plugins.config</code> in: <code>$PLUGIN/</code>, then stop and restart the plugin.</li>
<li>The configuration file for ZzzOT is located at: <code>$PLUGIN/zzzot.config</code>
<li>The only configuration for ZzzOT itself is the <i>interval</i>, which tells clients how long to wait between announces. The default is 1620 seconds (27 minutes).</li>
<li>The easiest way to reduce the load on your tracker is to increase the interval. To do so, edit the configuration file, then stop and restart the plugin.</li>
<li>The most important configuration for ZzzOT is the <code>interval</code>, which tells clients how long to wait between announces. The default is 1620 seconds (27 minutes).</li>
<li>The easiest way to reduce the load on your tracker is to increase the <code>interval</code>. To do so, edit the configuration file, then stop and restart the plugin.</li>
<li>UDP may also be enabled or disabled, and the connection lifetime changed, in the configuration file.</li>
<li>All conguration changes require the plugin to be stoped and restarted.</li>
<li>Live stats are available on the <a href="http://127.0.0.1:7662/tracker" target="_blank">tracker page</a> (this link is also on your router console sidebar when ZzzOT is running).</li>
<li>To change the display name for the site (for the logo and page titles), add the line: <code>sitename=<i>mytracker</i></code> to the configuration file (substituting <i>mytracker</i> with your desired name) and then stop and restart the plugin.</li>
<li>To change the footer text displayed on the tracker stats page, add the line: <code>footertext=<i>alternative text</i></code> to the configuration file, and then stop and restart the plugin.</li>
@ -404,7 +406,11 @@
<li><a href="http://$B32/tracker/announce.jsp">http://$B32/tracker/announce.jsp</a></li>
<li><a href="http://$B32/tracker/announce.php">http://$B32/tracker/announce.php</a></li>
</ul>
<p>Supported scrape URLs:</p>
<p>UDP announce URL, if enabled (enabled by default):</p>
<ul class="urls">
<li><a href="udp://$B32:6969/">udp://$B32:6969/</a></li>
</ul>
<p>Supported scrape URLs (note that full scrapes are disabled by default):</p>
<ul class="urls">
<li><a href="http://$B32/scrape">http://$B32/scrape</a></li>
<li><a href="http://$B32/scrape.jsp">http://$B32/scrape.jsp</a></li>

View File

@ -16,7 +16,7 @@
<body>
<div id="container">
<div id="panel">
<a href="/tracker" title="View OpenTracker stats"><span id="sitename">zzzot</span></a>
<a href="/tracker/" title="View OpenTracker stats"><span id="sitename">zzzot</span></a>
<span id="footer" class="b32">$B32</span>
</div>
</div>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<!-- ========================================================================= -->
<!-- This file configures the Jetty server. -->
@ -85,7 +85,7 @@
<Call name="addConnector">
<Arg>
<New class="org.eclipse.jetty.server.ServerConnector">
<Arg><Ref id="Server" /></Arg>
<Arg><Ref refid="Server" /></Arg>
<Arg type="int">1</Arg> <!-- number of acceptors -->
<Arg type="int">0</Arg> <!-- default number of selectors -->
<Arg>
@ -257,7 +257,7 @@
<Arg>
<New id="DeploymentManager" class="org.eclipse.jetty.deploy.DeploymentManager">
<Set name="contexts">
<Ref id="Contexts" />
<Ref refid="Contexts" />
</Set>
<Call name="setContextAttribute">
<Arg>org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern</Arg>
@ -278,7 +278,7 @@
<!-- in the $JETTY_HOME/contexts directory -->
<!-- -->
<!-- =========================================================== -->
<Ref id="DeploymentManager">
<Ref refid="DeploymentManager">
<Call name="addAppProvider">
<Arg>
<New class="org.eclipse.jetty.deploy.providers.WebAppProvider">
@ -302,7 +302,7 @@
<!-- Normally only one type of deployer need be used. -->
<!-- -->
<!-- =========================================================== -->
<Ref id="DeploymentManager">
<Ref refid="DeploymentManager">
<Call id="webappprovider" name="addAppProvider">
<Arg>
<New class="org.eclipse.jetty.deploy.providers.WebAppProvider">
@ -343,7 +343,7 @@
<!-- contexts configuration (see $(jetty.home)/contexts/test.xml -->
<!-- for an example). -->
<!-- =========================================================== -->
<Ref id="RequestLog">
<Ref refid="RequestLog">
<Set name="requestLog">
<New id="RequestLogImpl" class="net.i2p.jetty.I2PRequestLog">
<Set name="filename">$PLUGIN/eepsite/logs/yyyy_mm_dd.request.log</Set>

View File

@ -7,7 +7,7 @@ tunnel.0.option.crypto.tagsToSend=10
tunnel.0.option.i2cp.destination.sigType=7
tunnel.0.option.i2cp.enableAccessList=false
tunnel.0.option.i2cp.encryptLeaseSet=false
tunnel.0.option.i2cp.leaseSetEncType=4,0
tunnel.0.option.i2cp.leaseSetEncType=4
tunnel.0.option.i2cp.reduceIdleTime=1200000
tunnel.0.option.i2cp.reduceOnIdle=true
tunnel.0.option.i2cp.reduceQuantity=1

View File

@ -11,4 +11,4 @@ updateURL.su3=http://stats.i2p/i2p/plugins/zzzot-update.su3
websiteURL=http://zzz.i2p/forums/16
license=Apache 2.0
min-jetty-version=9
min-i2p-version=0.9.31
min-i2p-version=2.8.2

View File

@ -5,6 +5,19 @@
# minimum 900 (15 minutes), maximum 21600 (6 hours)
interval=1620
#
# Enable UDP announces
# default false
udp=false
#
# UDP connection lifetime in seconds
# minimum 60 (1 minute), maximum 21600 (6 hours)
lifetime=1200
#
#
# UDP announce port
# default 6969
port=6969
#
showfoooter=true
#footerText=your html text here
#

View File

@ -16,40 +16,31 @@ package net.i2p.zzzot;
*
*/
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.concurrent.ConcurrentMap;
import net.i2p.crypto.SHA256Generator;
import net.i2p.data.Base64;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
/*
* A single peer for a single torrent.
* Save a couple stats, and implements
* a Map so we can BEncode it
* So it's like PeerID but in reverse - we make a Map from the
* data. PeerID makes the data from a Map.
* Save a couple stats. We no longer support non-compact
* announces, so this is no longer a Map that can be BEncoded.
* See announce.jsp.
*/
public class Peer extends HashMap<String, Object> {
public class Peer {
private final Hash hash;
private long lastSeen;
private long bytesLeft;
private static final Integer PORT = Integer.valueOf(6881);
public Peer(byte[] id, Destination address, ConcurrentMap<String, String> destCache) {
super(3);
if (id.length != 20)
throw new IllegalArgumentException("Bad peer ID length: " + id.length);
put("peer id", id);
put("port", PORT);
// cache the 520-byte address strings
String dest = address.toBase64() + ".i2p";
String oldDest = destCache.putIfAbsent(dest, dest);
if (oldDest != null)
dest = oldDest;
put("ip", dest);
public Peer(byte[] id, Destination address) {
hash = address.calculateHash();
}
/**
* @since 0.20.0
*/
public Peer(byte[] id, Hash h) {
hash = h;
}
public void setLeft(long l) {
@ -65,20 +56,10 @@ public class Peer extends HashMap<String, Object> {
return lastSeen;
}
/** convert b64.i2p to a Hash, then to a binary string */
/* or should we just store it in the constructor? cache it? */
public String getHash() {
try {
return new String(getHashObject().getData(), "ISO-8859-1");
} catch (UnsupportedEncodingException uee) { return null; }
}
/**
* @since 0.19
* @since 0.20
*/
public Hash getHashObject() {
String ip = (String) get("ip");
byte[] b = Base64.decode(ip.substring(0, ip.length() - 4));
return SHA256Generator.getInstance().calculateHash(b);
public byte[] getHashBytes() {
return hash.getData();
}
}

View File

@ -1,60 +0,0 @@
package net.i2p.zzzot;
/*
* Copyright 2010 zzz (zzz@mail.i2p)
*
* 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.
*
*/
import java.io.IOException;
import java.io.OutputStream;
import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PSocketEepGet;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.data.Base64;
import net.i2p.i2ptunnel.TunnelController;
import net.i2p.util.EepGet;
/**
* Announce to seedless
* @since 0.6
*/
public class SeedlessAnnouncer {
private static final String SPONGE =
"VG4Bd~q1RA3BdoF3z5fSR7p0xe1CTVgDMWVGyFchA9Wm2iXUkIR35G45XE31Uc9~IOt-ktNLL2~TYQZ13Vl8udosngDn8RJG1NtVASH4khsbgkkoFLWd6UuvuOjQKBFKjaEPJgxOzh0kxolRPPNHhFuuAGzNLKvz~LI2MTf0P6nwmRg1lBoRIUpSVocEHY4X306nT2VtY07FixbJcPCU~EeRin24yNoiZop-C3Wi1SGwJJK-NS7mnkNzd8ngDJXDJtR-wLP1vNyyBY6NySgqPiIhENHoVeXd5krlR42HORCxEDb4jhoqlbyJq-PrhTJ5HdH4-~gEq09B~~NIHzy7X02XgmBXhTYRtl6HbLMXs6SI5fq9OFgVp5YZWYUklJjMDI7jOrGrEZGSHhnJK9kT6D3CqVIM0cYEhe4ttmTegbZvC~J6DrRTIAX422qRQJBPsTUnv4iFyuJE-8SodP6ikTjRH21Qx73SxqOvmrOiu7Bsp0lvVDa84aoaYLdiGv87AAAA";
private static final String ANNOUNCE = "announce " + Base64.encode("seedless,eepsite,torrent");
public void announce(TunnelController controller) {
// get the I2PTunnel from the controller (no method now)
// get the I2PTunnelTask from I2PTunnel
// cast to an I2PTunnelServer
// get the SocketManager from the server (no method now)
I2PSocketManager mgr = null;
I2PAppContext ctx = I2PAppContext.getGlobalContext();
String url = "http://" + SPONGE + "/Seedless/seedless";
EepGet get = new I2PSocketEepGet(ctx, mgr, 1, -1, 1024, null, new DummyOutputStream(), url);
get.addHeader("X-Seedless", ANNOUNCE);
get.fetch();
}
private static class DummyOutputStream extends OutputStream {
public void write(int b) {}
}
}

View File

@ -17,6 +17,7 @@ package net.i2p.zzzot;
*/
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.CoreVersion;
import net.i2p.data.DataHelper;
@ -33,15 +34,19 @@ public class Torrents extends ConcurrentHashMap<InfoHash, Peers> {
private final SDSCache<InfoHash> _hashCache;
private final SDSCache<PID> _pidCache;
private final Integer _interval;
private final int _udpLifetime;
private final AtomicInteger _announces = new AtomicInteger();
/**
* @param interval in seconds
* @param udpInterval in seconds
*/
public Torrents(int interval) {
public Torrents(int interval, int udpLifetime) {
super();
_hashCache = new SDSCache<InfoHash>(InfoHash.class, InfoHash.LENGTH, CACHE_SIZE);
_pidCache = new SDSCache<PID>(PID.class, PID.LENGTH, CACHE_SIZE);
_interval = Integer.valueOf(interval);
_udpLifetime = udpLifetime;
}
public int countPeers() {
@ -60,6 +65,14 @@ public class Torrents extends ConcurrentHashMap<InfoHash, Peers> {
return _interval;
}
/**
* @return in seconds
* @since 0.20.0
*/
public int getUDPLifetime() {
return _udpLifetime;
}
/**
* Pull from cache or return new
*
@ -86,6 +99,28 @@ public class Torrents extends ConcurrentHashMap<InfoHash, Peers> {
return _pidCache.get(d);
}
/**
* This is called for every announce except for event = STOPPED.
* Hook it here to keep an announce counter.
*
* @since 0.20.0
*/
@Override
public Peers putIfAbsent(InfoHash ih, Peers p) {
_announces.incrementAndGet();
return super.putIfAbsent(ih, p);
}
/**
* Return the number of announces since the last call.
* Resets the counter to zero.
*
* @since 0.20.0
*/
public int getAnnounces() {
return _announces.getAndSet(0);
}
/**
* @since 0.12.0
*/
@ -93,6 +128,7 @@ public class Torrents extends ConcurrentHashMap<InfoHash, Peers> {
public void clear() {
super.clear();
clearCaches();
_announces.set(0);
}
/**

View File

@ -21,18 +21,28 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PSessionMuxedListener;
import net.i2p.client.datagram.I2PDatagramDissector;
import net.i2p.client.datagram.Datagram2;
import net.i2p.client.datagram.Datagram3;
import net.i2p.crypto.SipHashInline;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.i2ptunnel.I2PTunnel;
import net.i2p.util.I2PAppThread;
import net.i2p.util.LHMCache;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;
@ -48,15 +58,17 @@ public class UDPHandler implements I2PSessionMuxedListener {
private final Log _log;
private final I2PTunnel _tunnel;
private final ZzzOT _zzzot;
private final I2PDatagramDissector _diss;
// conn ID to dest and time added
private final Map<Long, DestAndTime> _connectCache;
private final Cleaner _cleaner;
private final long sipk0, sipk1;
private final Map<Hash, Destination> _destCache;
private final AtomicInteger _announces = new AtomicInteger();
private volatile boolean _running;
private ThreadPoolExecutor _executor;
/** how long to wait before dropping an idle thread */
private static final long HANDLER_KEEPALIVE_MS = 2*60*1000;
// The listen port.
// We listen on all ports, so the announce URL
// doesn't need a port.
public static final int PORT = I2PSession.PORT_ANY;
public final int PORT;
private static final long MAGIC = 0x41727101980L;
private static final int ACTION_CONNECT = 0;
private static final int ACTION_ANNOUNCE = 1;
@ -67,26 +79,55 @@ public class UDPHandler implements I2PSessionMuxedListener {
private static final int EVENT_COMPLETED = 1;
private static final int EVENT_STARTED = 2;
private static final int EVENT_STOPPED = 3;
private static final long CLEAN_TIME = 2*60*1000;
// keep it short, we should have the leaseset,
// if a new ratchet session was created
private final long LOOKUP_TIMEOUT = 2000;
private final long CLEAN_TIME;
private final long STAT_TIME = 2*60*1000;
private static final byte[] INVALID = DataHelper.getUTF8("Invalid connection ID");
private static final byte[] PROTOCOL = DataHelper.getUTF8("Bad protocol");
private static final byte[] SCRAPE = DataHelper.getUTF8("Scrape unsupported");
public UDPHandler(I2PAppContext ctx, I2PTunnel tunnel, ZzzOT zzzot) {
public UDPHandler(I2PAppContext ctx, I2PTunnel tunnel, ZzzOT zzzot, int port) {
_context = ctx;
_log = ctx.logManager().getLog(UDPHandler.class);
_tunnel = tunnel;
_zzzot = zzzot;
_diss = new I2PDatagramDissector();
_connectCache = new ConcurrentHashMap<Long, DestAndTime>();
CLEAN_TIME = (zzzot.getTorrents().getUDPLifetime() + 60) * 1000;
PORT = port;
_cleaner = new Cleaner();
sipk0 = ctx.random().nextLong();
sipk1 = ctx.random().nextLong();
// the highest-traffic zzzot is running about 3000 announces/minute,
// give us enough to respond to the first announce after the connection
_destCache = new LHMCache<Hash, Destination>(1024);
}
public void start() {
public synchronized void start() {
_running = true;
_executor = new CustomThreadPoolExecutor();
_executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
(new I2PAppThread(new Waiter(), "ZzzOT UDP startup", true)).start();
long[] r = new long[] { 5*60*1000 };
_context.statManager().createRequiredRateStat("plugin.zzzot.announces.udp", "UDP announces per minute", "Plugins", r);
}
/**
* @since 0.20.0
*/
public synchronized void stop() {
_running = false;
_executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
_executor.shutdownNow();
_executor = null;
_cleaner.cancel();
_context.statManager().removeRateStat("plugin.zzzot.announces.udp");
_announces.set(0);
}
private class Waiter implements Runnable {
public void run() {
while (true) {
while (_running) {
// requires I2P 0.9.53 (1.7.0)
List<I2PSession> sessions = _tunnel.getSessions();
if (sessions.isEmpty()) {
@ -94,9 +135,9 @@ public class UDPHandler implements I2PSessionMuxedListener {
continue;
}
I2PSession session = sessions.get(0);
session.addMuxedSessionListener(UDPHandler.this, I2PSession.PROTO_DATAGRAM, PORT);
session.addMuxedSessionListener(UDPHandler.this, I2PSession.PROTO_DATAGRAM_RAW, PORT);
_cleaner.schedule(CLEAN_TIME);
session.addMuxedSessionListener(UDPHandler.this, I2PSession.PROTO_DATAGRAM2, PORT);
session.addMuxedSessionListener(UDPHandler.this, I2PSession.PROTO_DATAGRAM3, PORT);
_cleaner.schedule(STAT_TIME);
if (_log.shouldInfo())
_log.info("got session");
break;
@ -119,12 +160,13 @@ public class UDPHandler implements I2PSessionMuxedListener {
try {
// receive message
byte[] msg = session.receiveMessage(id);
if (proto == I2PSession.PROTO_DATAGRAM) {
if (proto == I2PSession.PROTO_DATAGRAM2) {
// load datagram into it
_diss.loadI2PDatagram(msg);
handle(session, _diss.getSender(), fromPort, _diss.getPayload());
} else if (proto == I2PSession.PROTO_DATAGRAM_RAW) {
handle(session, null, fromPort, _diss.getPayload());
Datagram2 dg = Datagram2.load(_context, session, msg);
handle(session, dg.getSender(), null, fromPort, dg.getPayload());
} else if (proto == I2PSession.PROTO_DATAGRAM3) {
Datagram3 dg = Datagram3.load(_context, session, msg);
handle(session, null, dg.getSender(), fromPort, dg.getPayload());
} else {
if (_log.shouldWarn())
_log.warn("dropping message with unknown protocol " + proto);
@ -139,7 +181,6 @@ public class UDPHandler implements I2PSessionMuxedListener {
public void disconnected(I2PSession arg0) {
_cleaner.cancel();
_connectCache.clear();
}
public void errorOccurred(I2PSession arg0, String arg1, Throwable arg2) {
@ -148,7 +189,12 @@ public class UDPHandler implements I2PSessionMuxedListener {
/// end listener methods ///
private void handle(I2PSession session, Destination from, int fromPort, byte[] data) {
/**
* One of from or fromHash non-null
* @param from non-null for connect request
* @param fromHash non-null for announce request
*/
private void handle(I2PSession session, Destination from, Hash fromHash, int fromPort, byte[] data) {
int sz = data.length;
if (sz < 16) {
if (_log.shouldWarn())
@ -165,18 +211,33 @@ public class UDPHandler implements I2PSessionMuxedListener {
}
if (from == null) {
if (_log.shouldWarn())
_log.warn("dropping raw connect");
_log.warn("dropping dg3 connect");
int transID = (int) DataHelper.fromLong(data, 12, 4);
sendError(session, fromHash, fromPort, transID, PROTOCOL);
return;
}
handleConnect(session, from, fromPort, data);
} else if (action == ACTION_ANNOUNCE) {
handleAnnounce(session, connID, from, fromPort, data);
if (fromHash == null) {
if (_log.shouldWarn())
_log.warn("dropping dg2 announce");
int transID = (int) DataHelper.fromLong(data, 12, 4);
sendError(session, from, fromPort, transID, PROTOCOL);
return;
}
handleAnnounce(session, connID, fromHash, fromPort, data);
} else if (action == ACTION_SCRAPE) {
if (_log.shouldWarn())
_log.warn("got unsupported scrape");
int transID = (int) DataHelper.fromLong(data, 12, 4);
if (from != null)
sendError(session, from, fromPort, transID, SCRAPE);
else
sendError(session, fromHash, fromPort, transID, SCRAPE);
} else {
if (_log.shouldWarn())
_log.warn("dropping bad action " + action);
// TODO send error?
}
}
@ -185,15 +246,19 @@ public class UDPHandler implements I2PSessionMuxedListener {
*/
private void handleConnect(I2PSession session, Destination from, int fromPort, byte[] data) {
int transID = (int) DataHelper.fromLong(data, 12, 4);
long connID = _context.random().nextLong();
byte[] resp = new byte[16];
long connID = generateCID(from.calculateHash());
byte[] resp = new byte[18];
DataHelper.toLong(resp, 4, 4, transID);
DataHelper.toLong8(resp, 8, connID);
// Addition to BEP 15
DataHelper.toLong(resp, 16, 2, _zzzot.getTorrents().getUDPLifetime());
try {
session.sendMessage(from, resp, I2PSession.PROTO_DATAGRAM_RAW, PORT, fromPort);
if (_log.shouldDebug())
_log.debug("sent connect reply to " + from);
_connectCache.put(Long.valueOf(connID), new DestAndTime(from, _context.clock().now()));
_log.debug("sent connect reply with conn ID " + connID + " to " + from.toBase32());
synchronized(_destCache) {
_destCache.put(from.calculateHash(), from);
}
} catch (I2PSessionException ise) {
if (_log.shouldWarn())
_log.warn("error sending connect reply", ise);
@ -203,25 +268,23 @@ public class UDPHandler implements I2PSessionMuxedListener {
/**
* @param from may be null
*/
private void handleAnnounce(I2PSession session, long connID, Destination from, int fromPort, byte[] data) {
private void handleAnnounce(I2PSession session, long connID, Hash fromHash, int fromPort, byte[] data) {
int sz = data.length;
if (sz < 96) {
if (_log.shouldWarn())
_log.warn("dropping short announce length " + sz);
return;
}
if (from == null) {
DestAndTime dat = _connectCache.get(Long.valueOf(connID));
if (dat == null) {
if (_log.shouldWarn())
_log.warn("no connID found " + connID);
return;
}
from = dat.dest;
int transID = (int) DataHelper.fromLong(data, 12, 4);
boolean ok = validateCID(fromHash, connID);
if (!ok) {
if (_log.shouldWarn())
_log.warn("conn ID invalid: " + connID);
sendError(session, fromHash, fromPort, transID, INVALID);
return;
}
// parse packet
int transID = (int) DataHelper.fromLong(data, 12, 4);
byte[] bih = new byte[InfoHash.LENGTH];
System.arraycopy(data, 16, bih, 0, InfoHash.LENGTH);
InfoHash ih = new InfoHash(bih);
@ -245,6 +308,7 @@ public class UDPHandler implements I2PSessionMuxedListener {
Torrents torrents = _zzzot.getTorrents();
Peers peers = torrents.get(ih);
if (peers == null && event != EVENT_STOPPED) {
_announces.incrementAndGet();
peers = new Peers();
Peers p2 = torrents.putIfAbsent(ih, peers);
if (p2 != null)
@ -262,8 +326,7 @@ public class UDPHandler implements I2PSessionMuxedListener {
} else {
Peer p = peers.get(pid);
if (p == null) {
ConcurrentMap<String, String> destCache = _zzzot.getDestCache();
p = new Peer(pid.getData(), from, destCache);
p = new Peer(pid.getData(), fromHash);
Peer p2 = peers.putIfAbsent(pid, p);
if (p2 != null)
p = p2;
@ -303,10 +366,21 @@ public class UDPHandler implements I2PSessionMuxedListener {
DataHelper.toLong(resp, 20, 2, count);
if (peerlist != null) {
for (int i = 0; i < count; i++) {
System.arraycopy(peerlist.get(i).getHashObject().getData(), 0, resp, 22 + (i * 32), 32);
System.arraycopy(peerlist.get(i).getHashBytes(), 0, resp, 22 + (i * 32), 32);
}
}
Destination from = lookupCache(fromHash);
if (from == null) {
try {
_executor.execute(new Lookup(session, fromHash, fromPort, resp));
} catch (RejectedExecutionException ree) {
if (_log.shouldWarn())
_log.warn("error sending announce reply - thread pool full");
}
return;
}
try {
session.sendMessage(from, resp, I2PSession.PROTO_DATAGRAM_RAW, PORT, fromPort);
if (_log.shouldDebug())
@ -317,30 +391,181 @@ public class UDPHandler implements I2PSessionMuxedListener {
}
}
private static class DestAndTime {
public final Destination dest;
public final long time;
/**
* @param from non-null
* @param msg non-null
*/
private void sendError(I2PSession session, Hash toHash, int toPort, long transID, byte[] msg) {
Destination to = lookupCache(toHash);
if (to == null) {
if (_log.shouldInfo())
_log.info("don't have cached dest to send error to " + toHash.toBase32());
return;
}
// don't bother looking up via I2CP
sendError(session, to, toPort, transID, msg);
}
public DestAndTime(Destination d, long t) {
dest = d;
time = t;
/**
* @param from non-null
* @param msg non-null
*/
private void sendError(I2PSession session, Destination to, int toPort, long transID, byte[] msg) {
byte[] resp = new byte[8 + msg.length];
DataHelper.toLong(resp, 0, 4, ACTION_ERROR);
DataHelper.toLong(resp, 4, 4, transID);
System.arraycopy(msg, 0, resp, 8, msg.length);
try {
session.sendMessage(to, resp, I2PSession.PROTO_DATAGRAM_RAW, PORT, toPort);
if (_log.shouldDebug())
_log.debug("sent error to " + to.toBase32());
} catch (I2PSessionException ise) {
if (_log.shouldWarn())
_log.warn("error sending connect reply", ise);
}
}
/**
* Blocking.
* @return null on failure
*/
private Destination lookup(I2PSession session, Hash hash) {
Destination rv = lookupCache(hash);
if (rv != null)
return rv;
return lookupI2CP(session, hash);
}
/**
* Nonblocking.
* @return null on failure
*/
private Destination lookupCache(Hash hash) {
// Test deferred
//if (true) return null;
synchronized(_destCache) {
return _destCache.get(hash);
}
}
/**
* Blocking.
* @return null on failure
*/
private Destination lookupI2CP(I2PSession session, Hash hash) {
Destination rv;
try {
rv = session.lookupDest(hash, LOOKUP_TIMEOUT);
} catch (I2PSessionException ise) {
if (_log.shouldWarn())
_log.warn("lookup error", ise);
return null;
}
if (rv == null) {
if (_log.shouldWarn())
_log.warn("lookup failed for response to " + hash.toBase32());
}
return rv;
}
private long generateCID(Hash hash) {
byte[] buf = new byte[40];
System.arraycopy(hash.getData(), 0, buf, 0, 32);
long time = _context.clock().now() / CLEAN_TIME;
DataHelper.toLong8(buf, 32, time);
return SipHashInline.hash24(sipk0, sipk1, buf);
}
private boolean validateCID(Hash hash, long cid) {
byte[] buf = new byte[40];
System.arraycopy(hash.getData(), 0, buf, 0, 32);
// current epoch
long time = _context.clock().now() / CLEAN_TIME;
DataHelper.toLong8(buf, 32, time);
long c = SipHashInline.hash24(sipk0, sipk1, buf);
if (cid == c)
return true;
// previous epoch
time--;
DataHelper.toLong8(buf, 32, time);
c = SipHashInline.hash24(sipk0, sipk1, buf);
return cid == c;
}
/**
* Update the announce stat and set the announce count to 0
*/
private class Cleaner extends SimpleTimer2.TimedEvent {
public Cleaner() { super(_context.simpleTimer2()); }
public void timeReached() {
if (!_connectCache.isEmpty()) {
long exp = _context.clock().now() - CLEAN_TIME;
for (Iterator<DestAndTime> iter = _connectCache.values().iterator(); iter.hasNext(); ) {
DestAndTime dat = iter.next();
if (dat.time < exp)
iter.remove();
}
long count = _announces.getAndSet(0);
_context.statManager().addRateData("plugin.zzzot.announces.udp", count / (STAT_TIME / (60*1000L)));
schedule(STAT_TIME);
}
}
/**
* Until we have a nonblocking lookup API in I2CP
*
* @since 0.20.0
*/
private class Lookup implements Runnable {
private final I2PSession _session;
private final Hash _hash;
private final int _port;
private final byte[] _msg;
public Lookup(I2PSession sess, Hash h, int port, byte[] msg) {
_session = sess;
_hash = h;
_port = port;
_msg = msg;
}
public void run() {
// blocking
Destination d = lookupI2CP(_session, _hash);
if (d == null) {
if (_log.shouldWarn())
_log.warn("deferred lookup failed for " + _hash.toBase32());
return;
}
schedule(CLEAN_TIME);
try {
_session.sendMessage(d, _msg, I2PSession.PROTO_DATAGRAM_RAW, PORT, _port);
if (_log.shouldDebug())
_log.debug("sent deferred reply to " + _hash.toBase32());
} catch (I2PSessionException ise) {
if (_log.shouldWarn())
_log.warn("error sending deferred reply", ise);
}
}
}
/**
* Until we have a nonblocking lookup API in I2CP
*
* @since 0.20.0
*/
private static class CustomThreadPoolExecutor extends ThreadPoolExecutor {
public CustomThreadPoolExecutor() {
super(0, 25, HANDLER_KEEPALIVE_MS, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(), new CustomThreadFactory());
}
}
/**
* Just to set the name and set Daemon
*
* @since 0.20.0
*/
private static class CustomThreadFactory implements ThreadFactory {
private final AtomicInteger _executorThreadCount = new AtomicInteger();
public Thread newThread(Runnable r) {
Thread rv = Executors.defaultThreadFactory().newThread(r);
rv.setName("ZzzOT lookup " + _executorThreadCount.incrementAndGet());
rv.setDaemon(true);
return rv;
}
}
}

View File

@ -29,17 +29,20 @@ import net.i2p.util.SimpleTimer2;
*/
class ZzzOT {
private final I2PAppContext _context;
private final Torrents _torrents;
private final Cleaner _cleaner;
private final ConcurrentHashMap<String, String> _destCache = new ConcurrentHashMap<String, String>();
private final long EXPIRE_TIME;
private static final String PROP_INTERVAL = "interval";
private static final long CLEAN_TIME = 4*60*1000;
private static final long DEST_CACHE_CLEAN_TIME = 3*60*60*1000;
private static final String PROP_UDP_LIFETIME = "lifetime";
private static final long CLEAN_TIME = 2*60*1000;
private static final int DEFAULT_INTERVAL = 27*60;
private static final int DEFAULT_UDP_LIFETIME = 20*60;
private static final int MIN_INTERVAL = 15*60;
private static final int MAX_INTERVAL = 6*60*60;
private static final int MIN_UDP_LIFETIME = 60;
private static final int MAX_UDP_LIFETIME = 6*60*60;
ZzzOT(I2PAppContext ctx, Properties p) {
String intv = p.getProperty(PROP_INTERVAL);
@ -53,28 +56,41 @@ class ZzzOT {
interval = MAX_INTERVAL;
} catch (NumberFormatException nfe) {}
}
_torrents = new Torrents(interval);
intv = p.getProperty(PROP_UDP_LIFETIME);
int lifetime = DEFAULT_UDP_LIFETIME;
if (intv != null) {
try {
lifetime = Integer.parseInt(intv);
if (lifetime < MIN_UDP_LIFETIME)
interval = MIN_UDP_LIFETIME;
else if (interval > MAX_UDP_LIFETIME)
interval = MAX_UDP_LIFETIME;
} catch (NumberFormatException nfe) {}
}
_torrents = new Torrents(interval, lifetime);
EXPIRE_TIME = 1000 * (interval + interval / 2);
_cleaner = new Cleaner(ctx);
_context = ctx;
}
Torrents getTorrents() {
return _torrents;
}
/** @since 0.9.14 */
ConcurrentHashMap<String, String> getDestCache() {
return _destCache;
}
void start() {
_cleaner.forceReschedule(CLEAN_TIME);
long[] r = new long[] { 5*60*1000 };
_context.statManager().createRequiredRateStat("plugin.zzzot.announces", "Total announces per minute", "Plugins", r);
_context.statManager().createRequiredRateStat("plugin.zzzot.peers", "Number of peers", "Plugins", r);
_context.statManager().createRequiredRateStat("plugin.zzzot.torrents", "Number of torrents", "Plugins", r);
}
void stop() {
_cleaner.cancel();
_torrents.clear();
_destCache.clear();
_context.statManager().removeRateStat("plugin.zzzot.announces");
_context.statManager().removeRateStat("plugin.zzzot.peers");
_context.statManager().removeRateStat("plugin.zzzot.torrents");
}
private class Cleaner extends SimpleTimer2.TimedEvent {
@ -88,6 +104,7 @@ class ZzzOT {
public void timeReached() {
long now = System.currentTimeMillis();
int peers = 0;
for (Iterator<Peers> iter = _torrents.values().iterator(); iter.hasNext(); ) {
Peers p = iter.next();
int recent = 0;
@ -100,10 +117,12 @@ class ZzzOT {
}
if (recent <= 0)
iter.remove();
else
peers += recent;
}
if (_runCount.incrementAndGet() % (DEST_CACHE_CLEAN_TIME / CLEAN_TIME) == 0) {
_destCache.clear();
}
_context.statManager().addRateData("plugin.zzzot.announces", _torrents.getAnnounces() / (CLEAN_TIME / (60*1000L)));
_context.statManager().addRateData("plugin.zzzot.peers", peers);
_context.statManager().addRateData("plugin.zzzot.torrents", _torrents.size());
schedule(CLEAN_TIME);
}
}

View File

@ -35,6 +35,8 @@ import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.PrivateKeyFile;
import net.i2p.i2ptunnel.TunnelController;
import net.i2p.stat.Rate;
import net.i2p.stat.RateStat;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
@ -70,19 +72,27 @@ public class ZzzOTController implements ClientApp {
private static boolean _showfooter;
private static String _footertext;
private static boolean _fullScrape;
private final boolean _enableUDP;
private final int _udpPort;
private UDPHandler _udp;
private String _b32;
private ClientAppState _state = UNINITIALIZED;
private static final String NAME = "ZzzOT";
private static final String DEFAULT_SITENAME = "ZZZOT";
private static final String PROP_SITENAME = "sitename";
private static final String VERSION = "0.18.0";
private static final String VERSION = "0.20.0-beta2";
private static final String DEFAULT_SHOWFOOTER = "true";
private static final String PROP_SHOWFOOTER = "showfooter";
private static final String DEFAULT_FOOTERTEXT = "Running <a href=\"http://git.idk.i2p/i2p-hackers/i2p.plugins.zzzot\" target=\"_blank\">ZZZOT</a> " + VERSION;
private static final String PROP_FOOTERTEXT = "footertext";
private static final String PROP_FULLSCRAPE = "allowFullScrape";
private static final String DEFAULT_FULLSCRAPE = "false";
private static final String PROP_UDP = "udp";
private static final String DEFAULT_UDP = "false";
private static final String PROP_UDP_PORT = "udp";
private static final int DEFAULT_UDP_PORT = 6969;
private static final String CONFIG_FILE = "zzzot.config";
private static final String BACKUP_SUFFIX = ".jetty8";
private static final String[] xmlFiles = {
@ -114,6 +124,15 @@ public class ZzzOTController implements ClientApp {
_showfooter = Boolean.parseBoolean(props.getProperty(PROP_SHOWFOOTER, DEFAULT_SHOWFOOTER));
_footertext = props.getProperty(PROP_FOOTERTEXT, DEFAULT_FOOTERTEXT);
_fullScrape = Boolean.parseBoolean(props.getProperty(PROP_FULLSCRAPE, DEFAULT_FULLSCRAPE));
_enableUDP = Boolean.parseBoolean(props.getProperty(PROP_UDP, DEFAULT_UDP));
int p = DEFAULT_UDP_PORT;
String port = props.getProperty(PROP_UDP_PORT);
if (port != null) {
try {
p = Integer.parseInt(port);
} catch (NumberFormatException nfe) {}
}
_udpPort = p;
_state = INITIALIZED;
}
@ -127,31 +146,106 @@ public class ZzzOTController implements ClientApp {
/**
* @return null if not running
*/
public static Torrents getTorrents() {
private static ZzzOTController getThis() {
ClientAppManager mgr = I2PAppContext.getGlobalContext().clientAppManager();
if (mgr == null)
return null;
ClientApp z = mgr.getRegisteredApp(NAME);
if (z == null)
return null;
ZzzOTController ctrlr = (ZzzOTController) z;
return ctrlr._zzzot.getTorrents();
return (ZzzOTController) z;
}
/**
* @return null if not running
* @since 0.9.14
*/
public static ConcurrentMap<String, String> getDestCache() {
ClientAppManager mgr = I2PAppContext.getGlobalContext().clientAppManager();
if (mgr == null)
public static Torrents getTorrents() {
ZzzOTController ctrlr = getThis();
if (ctrlr == null)
return null;
ClientApp z = mgr.getRegisteredApp(NAME);
if (z == null)
return ctrlr._zzzot.getTorrents();
}
/**
* @return announces per minute, 0 if not running
* @since 0.20.0
*/
public static double getAnnounceRate() {
RateStat rs = I2PAppContext.getGlobalContext().statManager().getRate("plugin.zzzot.announces");
if (rs == null)
return 0;
Rate r = rs.getRate(5*60*1000);
if (r == null)
return 0;
return r.getAvgOrLifetimeAvg();
}
/**
* @return announces per minute, 0 if not running or UDP not enabled
* @since 0.20.0
*/
public static double getUDPAnnounceRate() {
RateStat rs = I2PAppContext.getGlobalContext().statManager().getRate("plugin.zzzot.announces.udp");
if (rs == null)
return 0;
Rate r = rs.getRate(5*60*1000);
if (r == null)
return 0;
return r.getAvgOrLifetimeAvg();
}
/**
* @return false if not running
* @since 0.20.0
*/
public static boolean isUDPEnabled() {
ZzzOTController ctrlr = getThis();
if (ctrlr == null)
return false;
return ctrlr.getUDPEnabled();
}
/**
* @return false if not running
* @since 0.20.0
*/
private boolean getUDPEnabled() {
return _enableUDP;
}
/**
* @return 0 if not running
* @since 0.20.0
*/
public static int udpPort() {
ZzzOTController ctrlr = getThis();
if (ctrlr == null)
return 0;
return ctrlr.getUDPPort();
}
/**
* @since 0.20.0
*/
public int getUDPPort() {
return _udpPort;
}
/**
* @return null if not running
* @since 0.20.0
*/
public static String b32() {
ZzzOTController ctrlr = getThis();
if (ctrlr == null)
return null;
ZzzOTController ctrlr = (ZzzOTController) z;
return ctrlr._zzzot.getDestCache();
return ctrlr.getB32();
}
/**
* @since 0.20.0
*/
public String getB32() {
return _b32;
}
/**
@ -186,12 +280,11 @@ public class ZzzOTController implements ClientApp {
startJetty(pluginDir, dest);
startI2PTunnel(pluginDir, dest);
_zzzot.start();
/*
// requires I2P 0.9.53 (1.7.0)
UDPHandler udp = new UDPHandler(_context, _tunnel.getTunnel(), _zzzot);
udp.start();
*/
// SeedlessAnnouncer.announce(_tunnel);
// requires I2P 0.9.66 (2.9.0)
if (_enableUDP) {
_udp = new UDPHandler(_context, _tunnel.getTunnel(), _zzzot, _udpPort);
_udp.start();
}
}
@ -204,17 +297,20 @@ public class ZzzOTController implements ClientApp {
_log.error("Cannot open " + i2ptunnelConfig.getAbsolutePath() + ' ' + ioe);
throw new IllegalArgumentException("Cannot open " + i2ptunnelConfig.getAbsolutePath() + ' ' + ioe);
}
if (i2ptunnelProps.getProperty("tunnel.0.option.i2cp.leaseSetEncType") == null)
i2ptunnelProps.setProperty("tunnel.0.option.i2cp.leaseSetEncType", "4,0");
String p = i2ptunnelProps.getProperty("tunnel.0.option.i2cp.leaseSetEncType");
if (p == null || p.equals("4,0"))
i2ptunnelProps.setProperty("tunnel.0.option.i2cp.leaseSetEncType", "4");
TunnelController tun = new TunnelController(i2ptunnelProps, "tunnel.0.");
if (dest != null) {
if (dest == null) {
// start in foreground so we can get the destination
tun.startTunnel();
_b32 = tun.getMyDestHashBase32();
List msgs = tun.clearMessages();
for (Object s : msgs) {
_log.logAlways(Log.INFO, "NOTICE: ZzzOT Tunnel message: " + s);
}
} else {
_b32 = dest.calculateHash().toBase32();
tun.startTunnelBackground();
}
_tunnel = tun;
@ -247,6 +343,8 @@ public class ZzzOTController implements ClientApp {
private void stop() {
stopI2PTunnel();
stopJetty();
if (_udp != null)
_udp.stop();
_zzzot.stop();
}

View File

@ -29,6 +29,7 @@
final int MAX_RESPONSES = 25;
final boolean ALLOW_IP_MISMATCH = false;
final boolean ALLOW_COMPACT_RESPONSE = true;
final boolean ALLOW_NONCOMPACT_RESPONSE = false;
// so the chars will turn into bytes correctly
request.setCharacterEncoding("ISO-8859-1");
@ -68,6 +69,11 @@
response.setStatus(403);
}
if (!compact && !ALLOW_NONCOMPACT_RESPONSE && !fail) {
fail = true;
msg = "non-compact responses unsupported";
}
if (info_hash == null && !fail) {
fail = true;
msg = "no info hash";
@ -198,8 +204,7 @@
// fixme same peer id, different dest
Peer p = peers.get(pid);
if (p == null) {
ConcurrentMap<String, String> destCache = ZzzOTController.getDestCache();
p = new Peer(pid.getData(), d, destCache);
p = new Peer(pid.getData(), d);
// don't add if spoofed
if (matchIP) {
Peer p2 = peers.putIfAbsent(pid, p);
@ -236,18 +241,23 @@
}
}
if (compact) {
// old experimental way - list of hashes
//List<String> peerhashes = new ArrayList(peerlist.size());
//for (Peer pe : peerlist) {
// peerhashes.add(pe.getHash());
//}
// new way - one big string
// one big string
byte[] peerhashes = new byte[32 * peerlist.size()];
for (int i = 0; i < peerlist.size(); i++)
System.arraycopy(peerlist.get(i).getHash().getBytes("ISO-8859-1"), 0, peerhashes, i * 32, 32);
System.arraycopy(peerlist.get(i).getHashBytes(), 0, peerhashes, i * 32, 32);
m.put("peers", peerhashes);
} else if (ALLOW_NONCOMPACT_RESPONSE) {
// This requires the Peer entries to be Maps
// so they can be bencoded, but we don't save
// the full Destination any more, and Peer does not.
// extend HashMap, to greatly reduce memory usage.
// We could create a Map here with the b32 as the IP,
// but that's nonstandard. So if non-compact is enabled,
// don't return any peers.
//m.put("peers", peerlist);
m.put("peers", java.util.Collections.EMPTY_LIST);
} else {
m.put("peers", peerlist);
// won't get here
}
}
}

View File

@ -20,18 +20,43 @@
<p id="totals">
<b>Torrents:</b> <%=torrents.size()%><br>
<b>Peers:</b> <%=torrents.countPeers()%><br>
<%
boolean udp = ZzzOTController.isUDPEnabled();
if (udp) {
%>
<b>Total Announce Rate:</b>
<%
} else {
%>
<b>Announce Rate:</b>
<%
}
%>
<%=String.format(java.util.Locale.US, "%.1f", ZzzOTController.getAnnounceRate())%> / minute<br>
<b>Announce Interval:</b> <%=torrents.getInterval() / 60%> minutes<br>
<%
String host = ZzzOTController.b32();
if (host != null) {
%><b>Announce URL:</b> <a href="http://<%=host%>/a">http://<%=host%>/a</a><br><%
}
%>
<b>UDP Announce Support:</b> <%=udp ? "yes" : "no"%><br>
<%
if (udp) {
%>
<b>UDP Announce Rate:</b> <%=String.format(java.util.Locale.US, "%.1f", ZzzOTController.getUDPAnnounceRate())%> / minute<br>
<b>UDP Connection Lifetime:</b> <%=torrents.getUDPLifetime() / 60%> minutes<br>
<%
if (host != null) {
int port = ZzzOTController.udpPort();
%>
<b>UDP Announce URL:</b> <a href="udp://<%=host%>:<%=port%>/"</a>udp://<%=host%>:<%=port%>/</a><br>
<%
}
}
%>
</p>
<%
/*
String host = request.getHeader("Host");
if (host != null) {
int colon = host.indexOf(":");
if (colon > 0)
host = host.substring(0, colon);
host = net.i2p.data.DataHelper.escapeHTML(host);
%><p><b>Now with UDP announce support!</b><br>udp://<%=host%>/</p><%
}
*/
} else {
%>
<p id="initializing"><b><i>Initializing OpenTracker&hellip;</i></b></p>

View File

@ -17,86 +17,7 @@
*
*/
String req = request.getHeader("X-Seedless");
// extension for ease of eepget and browser
if (req == null)
req = request.getParameter("X-Seedless");
// we should really put in our own b32
String me = request.getHeader("Host");
if (me == null)
me = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.b32.i2p";
// unused, we don't accept announces
String him = request.getHeader("X-I2P-DestB32");
if (him == null)
him = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.b32.i2p";
String xff = request.getHeader("X-Forwarded-For");
String xfs = request.getHeader("X-Forwarded-Server");
response.setContentType("text/plain");
response.setHeader("X-Seedless", him);
final int US_MINUTES = 360;
final int PEER_MINUTES = 60;
if (xff != null || xfs != null) {
String msg = "Non-I2P access denied";
//response.setStatus(403, msg);
response.setStatus(403);
out.println(msg);
} else if (req == null) {
// probe
out.println("tracker " + US_MINUTES);
out.println("eepsite " + US_MINUTES);
out.println("seedless " + US_MINUTES);
} else if (req.startsWith("announce")) {
out.println("thanks");
} else if (req.startsWith("locate c2VlZGxlc")) { // locate b64(seedless)
// ignore the search string, if any, in the request
// us
out.println(Base64.encode(me + ' ' + US_MINUTES + " tracker"));
out.println(Base64.encode(me + ' ' + US_MINUTES + " seedless"));
out.println(Base64.encode(me + ' ' + US_MINUTES + " eepsite"));
} else if (req.startsWith("locate ZWVwc2l0Z")) { // locate b64(eepsite)
// ignore the search string, if any, in the request
// us
out.println(Base64.encode(me + ' ' + US_MINUTES + " zzzot"));
} else if (req.startsWith("locate dG9ycmVud")) { // locate b64(torrent)
// all the peers
Torrents torrents = ZzzOTController.getTorrents();
if (torrents == null) {
//response.setStatus(503, "Down");
response.setStatus(503);
return;
}
for (InfoHash ihash : torrents.keySet()) {
Peers peers = torrents.get(ihash);
if (peers == null)
continue;
for (Peer p : peers.values()) {
// dest to b32
String ip = (String) p.get("ip");
if (ip.endsWith(".i2p"))
ip = ip.substring(0, ip.length() - 4);
String b32 = Base32.encode(SHA256Generator.getInstance().calculateHash(Base64.decode(ip)).getData()) + ".b32.i2p ";
// service type
String role;
if (p.isSeed())
role = "seed";
else
role = "leech";
// spg wants UTF-8 but all we have is binary data, so hex it
String ihs = DataHelper.toHexString(ihash.getData());
String ids = DataHelper.toHexString((byte[])p.get("peer id"));
out.println(Base64.encode(b32 + PEER_MINUTES + ihs + '\n' +
ids + '\n' +
role));
}
}
} else {
// error code
//response.setStatus(406, "Bad request");
response.setStatus(406);
out.println("SC_NOT_ACCEPTABLE");
}
%>