commit 35961ea6424cc0bf16f62ffb8b88668f9e4f6186 Author: zzz Date: Sun Mar 21 15:08:07 2010 +0000 b31 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c271639 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,29 @@ + 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. + + ======================================================================== + Includes code from Jetty 5.1.15: + + Copyright 199-2004 Mort Bay Consulting Pty. Ltd. + ------------------------------------------------------------------------ + 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/README.txt b/README.txt new file mode 100644 index 0000000..c4b7e90 --- /dev/null +++ b/README.txt @@ -0,0 +1,42 @@ +This is a very simple in-memory open tracker, wrapped into an I2P plugin. + +The plugin starts a new http serer tunnel, eepsite, and Jetty server running at port 7662. +The tracker status is available at http://127.0.0.1:7661/tracker/ . +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 zzzot and Jetty licenses. + +I2P source must be installed and built in ../i2p.i2p to compile this package. + +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. + +Valid announce URLs: + /a + /announce + /announce.jsp + /announce.php + /tracker/a + /tracker/announce + /tracker/announce.jsp + /tracker/announce.php + +Valid scrape URLs: + /scrape + /scrape.jsp + /scrape.php + /tracker/scrape + /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. diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..4c191c3 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,19 @@ +Configuration file: + - interval + - clean time + - max peers in response + - disable full scrapes + - disable all scrapes + - disable seedless + +Stop the cleaner + +Throttles: + - full scrapes + - per-requestor + +Bans: + - refuse non-GETs + +Verifier: + - Check dest vs. b32 in header diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..cf7bbf7 --- /dev/null +++ b/build.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin/clients.config b/plugin/clients.config new file mode 100644 index 0000000..90d73d1 --- /dev/null +++ b/plugin/clients.config @@ -0,0 +1,8 @@ +clientApp.0.main=net.i2p.zzzot.ZzzOTController +clientApp.0.name=ZzzOT +clientApp.0.args=-d $PLUGIN start +clientApp.0.stopargs=-d $PLUGIN stop +clientApp.0.delay=15 +clientApp.0.startOnLoad=true +# we also use i2p.jar and i2ptunnel.jar, they are in the standard router classpath +clientApp.0.classpath=$PLUGIN/lib/zzzot.jar,$I2P/lib/i2psnark.jar diff --git a/plugin/eepsite/docroot/index.html b/plugin/eepsite/docroot/index.html new file mode 100644 index 0000000..83716b9 --- /dev/null +++ b/plugin/eepsite/docroot/index.html @@ -0,0 +1,6 @@ + + +zzzot + +
zzzot
+ diff --git a/plugin/eepsite/docroot/robots.txt b/plugin/eepsite/docroot/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/plugin/eepsite/docroot/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/plugin/templates/help.html b/plugin/templates/help.html new file mode 100644 index 0000000..6e7ad0c --- /dev/null +++ b/plugin/templates/help.html @@ -0,0 +1,56 @@ +ZzzOT Plugin Help + +

Welcome to the ZzzOT I2P Plugin!

+ +A new eepsite tunnel and Jetty server have been started for your open tracker. + +

Click here to see the current stats. +This link is also at the top of your router console when ZzzOT is running. + +

Report bugs or add comments on +the plugin forum on zzz.i2p. + +

Eepsite Key and Helpful Hints for I2P

+ +

Your Base 32 address is $B32. + Others may access your eepsite using this address, even if you do not publish a hostname. +

Once you decide on a host name, you may + add the key to your local addressbook here. +

Your Base 64 key is:     +
You will need this key to register a hostname at stats.i2p. +

Your private key file is $PLUGIN/eepPriv.dat - back it up!!! +

Your eepsite document root is $PLUGIN/eepsite/docroot, + you may put other files there is you wish to have additional content on your eepsite. +

The supported announce URLs are: +

+

The supported scrape URLs are: +

+

Your eepsite tunnel is configured for 2 inbound and 2 outbound tunnels, 3 hops each. + You may change tunnel settings by editing $PLUGIN/i2ptunnel.config and restarting the plugin. + The tunnel will not appear in i2ptunnel. + If your tracker gets over 1000 peers, you will probably want to increase the number of tunnels. +

The Jetty webserver port is 7662. If you must change it, edit jetty.xml, i2ptunnel.config, and plugins.config + in the directory $PLUGIN. Then stop and restart the plugin. +

This help file is $PLUGIN/eepsite/docroot/help.html, you should probably move it + outside of the document root before you announce your eepsite as it may contain your user name. +

As you probably know, an open tracker does not require torrents to be registered, + and it does not host torrent files. You can, however, host torrent files elsewhere on + the eepsite, for example at /torrents. + + diff --git a/plugin/templates/jetty.xml b/plugin/templates/jetty.xml new file mode 100644 index 0000000..2b34d2b --- /dev/null +++ b/plugin/templates/jetty.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 127.0.0.1 + 7662 + + + 3 + 10 + 60000 + 1000 + 8443 + 8443 + main + + + + + + + + + + + + + + + + + + root + + + $PLUGIN/eepsite/webapps/ + + true + + + + + + / + $PLUGIN/eepsite/docroot + + + + FALSE + + + + + + + + + true + + + + /a + /tracker/announce.jsp + + + /announce + /tracker/announce.jsp + + + /announce.jsp + /tracker/announce.jsp + + + /announce.php + /tracker/announce.jsp + + + + /scrape + /tracker/scrape.jsp + + + /scrape.jsp + /tracker/scrape.jsp + + + /scrape.php + /tracker/scrape.jsp + + + + /Seedless + /tracker/seedless.jsp + + + /Seedless/ + /tracker/seedless.jsp + + + /Seedless/index.jsp + /tracker/seedless.jsp + + + + + + + + + + /cgi-bin/* + $PLUGIN/eepsite/cgi-bin + + Common Gateway Interface + / + org.mortbay.servlet.CGI + /usr/local/bin:/usr/ucb:/bin:/usr/bin + + + + + + + + + $PLUGIN/eepsite/logs/yyyy_mm_dd.request.log + 30 + true + false + false + GMT + + + + + + + 2000 + false + + diff --git a/scripts/i2ptunnel.config b/scripts/i2ptunnel.config new file mode 100644 index 0000000..5c2ebd0 --- /dev/null +++ b/scripts/i2ptunnel.config @@ -0,0 +1,25 @@ +tunnel.0.description=ZzzOT +tunnel.0.i2cpHost=127.0.0.1 +tunnel.0.i2cpPort=7654 +tunnel.0.name=zzzot +tunnel.0.option.i2cp.enableAccessList=false +tunnel.0.option.i2cp.encryptLeaseSet=false +tunnel.0.option.i2cp.reduceIdleTime=1200000 +tunnel.0.option.i2cp.reduceOnIdle=true +tunnel.0.option.i2cp.reduceQuantity=1 +tunnel.0.option.i2p.streaming.connectDelay=0 +tunnel.0.option.inbound.backupQuantity=0 +tunnel.0.option.inbound.length=3 +tunnel.0.option.inbound.lengthVariance=0 +tunnel.0.option.inbound.nickname=ZzzOT +tunnel.0.option.inbound.quantity=2 +tunnel.0.option.outbound.backupQuantity=0 +tunnel.0.option.outbound.length=3 +tunnel.0.option.outbound.lengthVariance=0 +tunnel.0.option.outbound.nickname=ZzzOT +tunnel.0.option.outbound.quantity=2 +tunnel.0.privKeyFile=plugins/zzzot/eepPriv.dat +tunnel.0.startOnLoad=true +tunnel.0.targetHost=127.0.0.1 +tunnel.0.targetPort=7662 +tunnel.0.type=httpserver diff --git a/scripts/makeplugin.sh b/scripts/makeplugin.sh new file mode 100755 index 0000000..71b77c2 --- /dev/null +++ b/scripts/makeplugin.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# +# basic packaging up of a plugin +# +# usage: makeplugin.sh plugindir +# +# zzz 2010-02 +# +PUBKEYDIR=$HOME/.i2p-plugin-keys +PUBKEYFILE=$PUBKEYDIR/plugin-public-signing.key +PRIVKEYFILE=$PUBKEYDIR/plugin-private-signing.key +B64KEYFILE=$PUBKEYDIR/plugin-public-signing.txt +export I2P=../i2p/pkg-temp + +PLUGINDIR=${1:-plugin} + +PC=plugin.config +PCT=${PC}.tmp + +if [ ! -f $PRIVKEYFILE ] +then + mkdir -p $PUBKEYDIR + java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate keygen $PUBKEYFILE $PRIVKEYFILE || exit 1 + java -cp $I2P/lib/i2p.jar net.i2p.data.Base64 encode $PUBKEYFILE $B64KEYFILE || exit 1 + rm -rf logs/ + chmod 444 $PUBKEYFILE $B64KEYFILE + chmod 400 $PRIVKEYFILE + echo "Created new keys: $PUBKEYFILE $PRIVKEYFILE" +fi + +rm -f plugin.zip +if [ ! -d $PLUGINDIR ] +then + echo "You must have a $PLUGINDIR directory" + exit 1 +fi + +OPWD=$PWD +cd $PLUGINDIR + +if [ ! -f $PC ] +then + echo "You must have a $PC file" + exit 1 +fi + +grep -q '^signer=' $PC +if [ "$?" -ne "0" ] +then + echo "You must have a signer in $PC" + echo 'For example signer=joe@mail.i2p' + exit 1 +fi + +grep -q '^name=' $PC +if [ "$?" -ne "0" ] +then + echo "You must have a plugin name in $PC" + echo 'For example name=foo' + exit 1 +fi + +grep -q '^version=' $PC +if [ "$?" -ne "0" ] +then + echo "You must have a version in $PC" + echo 'For example version=0.1.2' + exit 1 +fi + +# update the date +grep -v '^date=' $PC > $PCT +DATE=`date '+%s000'` +echo "date=$DATE" >> $PCT +mv $PCT $PC + +# add our Base64 key +grep -v '^key=' $PC > $PCT +B64KEY=`cat $B64KEYFILE` +echo "key=$B64KEY" >> $PCT || exit 1 +mv $PCT $PC + +# zip it +zip -r $OPWD/plugin.zip * -x \*.jar || exit 1 + +# get the version and use it for the sud header +VERSION=`grep '^version=' $PC | cut -f 2 -d '='` +# get the name and use it for the file name +NAME=`grep '^name=' $PC | cut -f 2 -d '='` +XPI2P=${NAME}.xpi2p +cd $OPWD + +# sign it +java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate sign plugin.zip $XPI2P $PRIVKEYFILE $VERSION || exit 1 +rm -f plugin.zip + +# verify +echo 'Verifying. ...' +java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate showversion $XPI2P || exit 1 +java -cp $I2P/lib/i2p.jar -Drouter.trustedUpdateKeys=$B64KEY net.i2p.crypto.TrustedUpdate verifysig $XPI2P || exit 1 +rm -rf logs/ + +echo -n 'Plugin created: ' +wc -c $XPI2P diff --git a/scripts/plugin.config b/scripts/plugin.config new file mode 100644 index 0000000..c3c8ea9 --- /dev/null +++ b/scripts/plugin.config @@ -0,0 +1,8 @@ +name=zzzot +signer=zzz-plugin@mail.i2p +consoleLinkName=ZzzOT +consoleLinkURL=http://127.0.0.1:7662/tracker/index.jsp +description=Open tracker +author=zzz +updateURL=http://stats.i2p/i2p/plugins/zzzot-update.xpi2p +license=Apache 2.0 diff --git a/src/build.xml b/src/build.xml new file mode 100644 index 0000000..c6f1b8f --- /dev/null +++ b/src/build.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/java/net/i2p/zzzot/InfoHash.java b/src/java/net/i2p/zzzot/InfoHash.java new file mode 100644 index 0000000..a8636ee --- /dev/null +++ b/src/java/net/i2p/zzzot/InfoHash.java @@ -0,0 +1,37 @@ +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.UnsupportedEncodingException; + +import net.i2p.data.ByteArray; + +/** + * A 20-byte SHA1 info hash + */ +public class InfoHash extends ByteArray { + + public InfoHash(String data) throws UnsupportedEncodingException { + this(data.getBytes("ISO-8859-1")); + } + + public InfoHash(byte[] data) { + super(data); + if (data.length != 20) + throw new IllegalArgumentException("Bad infohash length: " + data.length); + } +} diff --git a/src/java/net/i2p/zzzot/PID.java b/src/java/net/i2p/zzzot/PID.java new file mode 100644 index 0000000..142ddee --- /dev/null +++ b/src/java/net/i2p/zzzot/PID.java @@ -0,0 +1,37 @@ +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.UnsupportedEncodingException; + +import net.i2p.data.ByteArray; + +/** + * A 20-byte peer ID + */ +public class PID extends ByteArray { + + public PID(String data) throws UnsupportedEncodingException { + this(data.getBytes("ISO-8859-1")); + } + + public PID(byte[] data) { + super(data); + if (data.length != 20) + throw new IllegalArgumentException("Bad peer ID length: " + data.length); + } +} diff --git a/src/java/net/i2p/zzzot/Peer.java b/src/java/net/i2p/zzzot/Peer.java new file mode 100644 index 0000000..8b8fa88 --- /dev/null +++ b/src/java/net/i2p/zzzot/Peer.java @@ -0,0 +1,58 @@ +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.UnsupportedEncodingException; +import java.util.HashMap; + +import net.i2p.data.Destination; + +/* + * 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. + */ +public class Peer extends HashMap { + + private long lastSeen; + private long bytesLeft; + private static final Integer PORT = Integer.valueOf(6881); + + public Peer(byte[] id, Destination address) { + super(3); + if (id.length != 20) + throw new IllegalArgumentException("Bad peer ID length: " + id.length); + put("peer id", id); + put("ip", address.toBase64() + ".i2p"); + put("port", PORT); + } + + public void setLeft(long l) { + bytesLeft = l; + lastSeen = System.currentTimeMillis(); + } + + public boolean isSeed() { + return bytesLeft <= 0; + } + + public long lastSeen() { + return lastSeen; + } +} diff --git a/src/java/net/i2p/zzzot/Peers.java b/src/java/net/i2p/zzzot/Peers.java new file mode 100644 index 0000000..e3373ec --- /dev/null +++ b/src/java/net/i2p/zzzot/Peers.java @@ -0,0 +1,42 @@ +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.util.concurrent.ConcurrentHashMap; + +/** + * All the peers for a single torrent + */ +public class Peers extends ConcurrentHashMap { + + public Peers() { + super(); + } + + public int countSeeds() { + int rv = 0; + for (Peer p : values()) { + if (p.isSeed()) + rv++; + } + return rv; + } + + public int countLeeches() { + return size() - countSeeds(); + } +} diff --git a/src/java/net/i2p/zzzot/QForwardHandler.java b/src/java/net/i2p/zzzot/QForwardHandler.java new file mode 100644 index 0000000..95006d5 --- /dev/null +++ b/src/java/net/i2p/zzzot/QForwardHandler.java @@ -0,0 +1,158 @@ +// ======================================================================== +// $Id: ForwardHandler.java,v 1.16 2005/08/13 00:01:26 gregwilkins Exp $ +// Copyright 199-2004 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// 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. +// ======================================================================== + +package net.i2p.zzzot; + +import java.io.IOException; +import java.util.Map; + +import org.mortbay.http.HttpException; +import org.mortbay.http.HttpMessage; +import org.mortbay.http.HttpRequest; +import org.mortbay.http.HttpResponse; +import org.mortbay.http.PathMap; +import org.mortbay.http.handler.AbstractHttpHandler; +import org.mortbay.util.URI; +import org.mortbay.util.UrlEncoded; + + +/* ------------------------------------------------------------ */ +/** Forward Request Handler. + * Forwards a request to a new URI. Experimental - use with caution. + * @version $Revision: 1.16 $ + * @author Greg Wilkins (gregw) + * + * Just like ForwardHandler but forwards query parameters too + * And took out the dependency on apache logging + * @author zzz + */ +public class QForwardHandler extends AbstractHttpHandler +{ + PathMap _forward = new PathMap(); + String _root; + boolean _handleQueries = false; + + /* ------------------------------------------------------------ */ + /** Constructor. + */ + public QForwardHandler() + {} + + /* ------------------------------------------------------------ */ + /** Constructor. + * @param rootForward + */ + public QForwardHandler(String rootForward) + { + _root=rootForward; + } + + /* ------------------------------------------------------------ */ + /** Add a forward mapping. + * @param pathSpecInContext The path to forward from + * @param newPath The path to forward to. + */ + public void addForward(String pathSpecInContext, + String newPath) + { + _forward.put(pathSpecInContext,newPath); + } + + /* ------------------------------------------------------------ */ + /** Add a forward mapping for root path. + * This allows a forward for exactly / which is the default + * path in a pathSpec. + * @param newPath The path to forward to. + */ + public void setRootForward(String newPath) + { + _root=newPath; + } + + /* ------------------------------------------------------------ */ + /** Set the Handler up to cope with forwards to paths that contain query + * elements (e.g. "/blah"->"/foo?a=b"). + * AND (I2P) pass params through (e.g. "/blah?c=d? -> "/foo?c=d? ). + * @param b + */ + public void setHandleQueries(boolean b) + { + _handleQueries = b; + } + + /* ------------------------------------------------------------ */ + public void handle(String pathInContext, + String pathParams, + HttpRequest request, + HttpResponse response) + throws HttpException, IOException + { + String newPath=null; + String query=null; + if (_root!=null && ("/".equals(pathInContext) || pathInContext.startsWith("/;"))) + newPath=_root; + else + { + Map.Entry entry = _forward.getMatch(pathInContext); + if (entry!=null) + { + String match = (String)entry.getValue(); + if (_handleQueries) + { + int hook = match.indexOf('?'); + if (hook != -1){ + query = match.substring(hook+1); + match = match.substring(0, hook); + } + } + String info=PathMap.pathInfo((String)entry.getKey(),pathInContext); + newPath=info==null?match:(URI.addPaths(match,info)); + } + } + + if (newPath!=null) + { + // this is the new part for i2p + // setPath() changes the request URI and loses the parameters + // so save them and add them back + Map saved = null; + if (_handleQueries){ + request.setCharacterEncoding("ISO-8859-1", false); + saved = request.getParameters(); + } + + int last=request.setState(HttpMessage.__MSG_EDITABLE); + String context=getHttpContext().getContextPath(); + if (context.length()==1) + request.setPath(newPath); + else + request.setPath(URI.addPaths(context,newPath)); + if (_handleQueries && query != null){ + // add forwarded to query string to parameters + UrlEncoded.decodeTo(query, request.getParameters()); + } + + // this is the new part for i2p + if (_handleQueries){ + // add them back + request.getParameters().putAll(saved); + } + + request.setState(last); + getHttpContext().getHttpServer().service(request,response); + return; + } + } +} diff --git a/src/java/net/i2p/zzzot/Torrents.java b/src/java/net/i2p/zzzot/Torrents.java new file mode 100644 index 0000000..a01dc73 --- /dev/null +++ b/src/java/net/i2p/zzzot/Torrents.java @@ -0,0 +1,37 @@ +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.util.concurrent.ConcurrentHashMap; + +/** + * All the torrents + */ +public class Torrents extends ConcurrentHashMap { + + public Torrents() { + super(); + } + + public int countPeers() { + int rv = 0; + for (Peers p : values()) { + rv += p.size(); + } + return rv; + } +} diff --git a/src/java/net/i2p/zzzot/ZzzOT.java b/src/java/net/i2p/zzzot/ZzzOT.java new file mode 100644 index 0000000..fc4dc54 --- /dev/null +++ b/src/java/net/i2p/zzzot/ZzzOT.java @@ -0,0 +1,66 @@ +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.util.Iterator; + +import net.i2p.util.SimpleScheduler; +import net.i2p.util.SimpleTimer; + +/** + * Instantiate this to fire it up + */ +class ZzzOT { + + private Torrents _torrents; + private static final long CLEAN_TIME = 4*60*1000; + private static final long EXPIRE_TIME = 60*60*1000; + + ZzzOT() { + _torrents = new Torrents(); + SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); + } + + Torrents getTorrents() { + return _torrents; + } + + void stop() { + _torrents.clear(); + // no way to stop the cleaner + } + + private class Cleaner implements SimpleTimer.TimedEvent { + + public void timeReached() { + long now = System.currentTimeMillis(); + for (Iterator iter = _torrents.values().iterator(); iter.hasNext(); ) { + Peers p = iter.next(); + int recent = 0; + for (Iterator iterp = p.values().iterator(); iterp.hasNext(); ) { + Peer peer = iterp.next(); + if (peer.lastSeen() < now - EXPIRE_TIME) + iterp.remove(); + else + recent++; + } + if (recent <= 0) + iter.remove(); + } + } + } +} diff --git a/src/java/net/i2p/zzzot/ZzzOTController.java b/src/java/net/i2p/zzzot/ZzzOTController.java new file mode 100644 index 0000000..2e7445b --- /dev/null +++ b/src/java/net/i2p/zzzot/ZzzOTController.java @@ -0,0 +1,229 @@ +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.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.data.Base32; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKeyFile; +import net.i2p.util.FileUtil; +import net.i2p.util.I2PAppThread; +import net.i2p.util.Log; +import net.i2p.i2ptunnel.TunnelController; +import net.i2p.apps.systray.UrlLauncher; + +import org.mortbay.http.HttpContext; +import org.mortbay.jetty.Server; + +/** + * This handles the starting and stopping of an eepsite tunnel and jetty + * from a single static class so it can be called via clients.config. + * + * This makes installation of a new eepsite a turnkey operation - + * the user is not required to configure a new tunnel in i2ptunnel manually. + * + * Usage: ZzzOTController -d $PLUGIN [start|stop] + * + * @author zzz + */ +public class ZzzOTController { + private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(ZzzOTController.class); + private static Server _server; + private static TunnelController _tunnel; + private static ZzzOT _zzzot; + private static Object _lock = new Object(); + + public static void main(String args[]) { + if (args.length != 3 || (!"-d".equals(args[0]))) + throw new IllegalArgumentException("Usage: PluginController -d $PLUGIN [start|stop]"); + if ("start".equals(args[2])) + start(args); + else if ("stop".equals(args[2])) + stop(); + else + throw new IllegalArgumentException("Usage: PluginController -d $PLUGIN [start|stop]"); + } + + public static Torrents getTorrents() { + synchronized(_lock) { + if (_zzzot == null) + _zzzot = new ZzzOT(); + } + return _zzzot.getTorrents(); + } + + private static void start(String args[]) { + File pluginDir = new File(args[1]); + if (!pluginDir.exists()) + throw new IllegalArgumentException("Plugin directory " + pluginDir.getAbsolutePath() + " does not exist"); + + // We create the private key file in advance, so that we can + // create the help.html file from the templates + // without waiting for i2ptunnel to create it AND build the tunnels before returning. + Destination dest = null; + File key = new File(pluginDir, "eepPriv.dat"); + if (!key.exists()) { + PrivateKeyFile pkf = new PrivateKeyFile(new File(pluginDir, "eepPriv.dat")); + try { + dest = pkf.createIfAbsent(); + } catch (Exception e) { + _log.error("Unable to create " + key.getAbsolutePath() + ' ' + e); + throw new IllegalArgumentException("Unable to create " + key.getAbsolutePath() + ' ' + e); + } + _log.error("NOTICE: ZzzOT: New eepsite keys created in " + key.getAbsolutePath()); + _log.error("NOTICE: ZzzOT: You should back up this file!"); + String b32 = Base32.encode(dest.calculateHash().getData()) + ".b32.i2p"; + String b64 = dest.toBase64(); + _log.error("NOTICE: ZzzOT: Your base 32 address is " + b32); + _log.error("NOTICE: ZzzOT: Your base 64 address is " + b64); + } + startJetty(pluginDir, dest); + startI2PTunnel(pluginDir, dest); + } + + + private static void startI2PTunnel(File pluginDir, Destination dest) { + File i2ptunnelConfig = new File(pluginDir, "i2ptunnel.config"); + Properties i2ptunnelProps = new Properties(); + try { + DataHelper.loadProps(i2ptunnelProps, i2ptunnelConfig); + } catch (IOException ioe) { + _log.error("Cannot open " + i2ptunnelConfig.getAbsolutePath() + ' ' + ioe); + throw new IllegalArgumentException("Cannot open " + i2ptunnelConfig.getAbsolutePath() + ' ' + ioe); + } + TunnelController tun = new TunnelController(i2ptunnelProps, "tunnel.0."); + // start in foreground so we can get the destination + //tun.startTunnelBackground(); + tun.startTunnel(); + if (dest != null) { + List msgs = tun.clearMessages(); + for (Object s : msgs) { + _log.error("NOTICE: ZzzOT Tunnel message: " + s); + } + } + _tunnel = tun; + } + + private static void startJetty(File pluginDir, Destination dest) { + if (_server != null) + throw new IllegalArgumentException("Jetty already running!"); + migrateJettyXML(pluginDir); + I2PAppContext context = I2PAppContext.getGlobalContext(); + File tmpdir = new File(context.getTempDir().getAbsolutePath(), "/zzzot-work"); + tmpdir.mkdir(); + File jettyXml = new File(pluginDir, "jetty.xml"); + try { + Server serv = new Server(jettyXml.getAbsolutePath()); + HttpContext[] hcs = serv.getContexts(); + for (int i = 0; i < hcs.length; i++) + hcs[i].setTempDirectory(tmpdir); + serv.start(); + _server = serv; + } catch (Throwable t) { + _log.error("ZzzOT jetty start failed", t); + throw new IllegalArgumentException("Jetty start failed " + t); + } + if (dest != null) + launchHelp(pluginDir, dest); + } + + private static void stop() { + stopI2PTunnel(); + stopJetty(); + if (_zzzot != null) + _zzzot.stop(); + } + + private static void stopI2PTunnel() { + if (_tunnel == null) + return; + try { + _tunnel.stopTunnel(); + } catch (Throwable t) { + _log.error("ZzzOT tunnel stop failed", t); + throw new IllegalArgumentException("Tunnel stop failed " + t); + } + _tunnel = null; + } + + private static void stopJetty() { + if (_server == null) + return; + try { + _server.stop(); + } catch (Throwable t) { + _log.error("ZzzOT jetty stop failed", t); + throw new IllegalArgumentException("Jetty stop failed " + t); + } + _server = null; + } + + /** put the directory in the jetty.xml file */ + private static void migrateJettyXML(File pluginDir) { + File outFile = new File(pluginDir, "jetty.xml"); + if (outFile.exists()) + return; + File fileTmpl = new File(pluginDir, "templates/jetty.xml"); + try { + String props = FileUtil.readTextFile(fileTmpl.getAbsolutePath(), 250, true); + if (props == null) + throw new IOException(fileTmpl.getAbsolutePath() + " open failed"); + props = props.replace("$PLUGIN", pluginDir.getAbsolutePath()); + FileOutputStream os = new FileOutputStream(outFile); + os.write(props.getBytes("UTF-8")); + os.close(); + } catch (IOException ioe) { + _log.error("jetty.xml migrate failed", ioe); + } + } + + /** put the directory, base32, and base64 info in the help.html file and launch a browser window to display it */ + private static void launchHelp(File pluginDir, Destination dest) { + File fileTmpl = new File(pluginDir, "templates/help.html"); + File outFile = new File(pluginDir, "eepsite/docroot/help.html"); + String b32 = Base32.encode(dest.calculateHash().getData()) + ".b32.i2p"; + String b64 = dest.toBase64(); + try { + String html = FileUtil.readTextFile(fileTmpl.getAbsolutePath(), 100, true); + if (html == null) + throw new IOException(fileTmpl.getAbsolutePath() + " open failed"); + html = html.replace("$PLUGIN", pluginDir.getAbsolutePath()); + html = html.replace("$B32", b32); + html = html.replace("$B64", b64); + FileOutputStream os = new FileOutputStream(outFile); + os.write(html.getBytes("UTF-8")); + os.close(); + Thread t = new I2PAppThread(new Launcher(), "ZzzOTHelp", true); + t.start(); + } catch (IOException ioe) { + _log.error("ZzzOT help launch failed", ioe); + } + } + + private static class Launcher implements Runnable { + public void run() { + UrlLauncher.main(new String[] { "http://127.0.0.1:7662/help.html" } ); + } + } +} diff --git a/src/jsp/WEB-INF/web.xml b/src/jsp/WEB-INF/web.xml new file mode 100644 index 0000000..b2c3ad4 --- /dev/null +++ b/src/jsp/WEB-INF/web.xml @@ -0,0 +1,36 @@ + + + + + + + index.html + index.jsp + + + + net.i2p.zzzot.announce_jsp + /announce.php + + + + net.i2p.zzzot.announce_jsp + /announce + + + + net.i2p.zzzot.announce_jsp + /a + + + + net.i2p.zzzot.scrape_jsp + /scrape + + + + net.i2p.zzzot.scrape_jsp + /scrape.php + + + diff --git a/src/jsp/announce.jsp b/src/jsp/announce.jsp new file mode 100644 index 0000000..fb7fe1a --- /dev/null +++ b/src/jsp/announce.jsp @@ -0,0 +1,200 @@ +<%@page import="java.util.ArrayList" %><%@page import="java.util.Collections" %><%@page import="java.util.List" %><%@page import="java.util.Map" %><%@page import="java.util.HashMap" %><%@page import="net.i2p.data.Base64" %><%@page import="net.i2p.data.Destination" %><%@page import="net.i2p.zzzot.*" %><%@page import="org.klomp.snark.bencode.BEncoder" %><% + +/* + * Above one-liner is so there is no whitespace -> IllegalStateException + * + * 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. + * + */ + +/* + * USE CAUTION WHEN EDITING + * Trailing whitespace OR NEWLINE on the last line will cause + * IllegalStateExceptions !!! + * + */ + // would be nice to make these configurable + final int MAX_RESPONSES = 25; + final int INTERVAL = 27*60; + + // so the chars will turn into bytes correctly + request.setCharacterEncoding("ISO-8859-1"); + java.io.OutputStream cout = response.getOutputStream(); + response.setCharacterEncoding("ISO-8859-1"); + response.setContentType("text/plain"); + response.setHeader("Pragma", "no-cache"); + String info_hash = request.getParameter("info_hash"); + String peer_id = request.getParameter("peer_id"); + // ignored + String port = request.getParameter("port"); + // ignored + String uploaded = request.getParameter("uploaded"); + // ignored + String downloaded = request.getParameter("downloaded"); + String sleft = request.getParameter("left"); + String event = request.getParameter("event"); + String ip = request.getParameter("ip"); + String numwant = request.getParameter("numwant"); + // ignored, use someday to enforce destination + String him = request.getHeader("X-I2P-DestB32"); + String xff = request.getHeader("X-Forwarded-For"); + + boolean fail = false; + String msg = "bad announce"; + + if (xff != null) { + fail = true; + msg = "Non-I2P access denied"; + response.setStatus(403, msg); + } + + if (info_hash == null && !fail) { + fail = true; + msg = "no info hash"; + } + + if (ip == null && !fail) { + fail = true; + msg = "no ip (dest)"; + } + + if (peer_id == null && !fail) { + fail = true; + msg = "no peer id"; + } + + InfoHash ih = null; + if (!fail) { + try { + ih = new InfoHash(info_hash); + } catch (Exception e) { + fail = true; + msg = "bad infohash " + e; + } + } + + Destination d = null; + if (!fail) { + try { + if (ip.endsWith(".i2p")) + ip = ip.substring(0, ip.length() - 4); + d = new Destination(ip); // from b64 string + } catch (Exception e) { + fail = true; + msg = "bad dest " + e; + } + } + + PID pid = null; + if (!fail) { + try { + pid = new PID(peer_id); + } catch (Exception e) { + fail = true; + msg = "bad peer id " + e; + } + } + + // int params + + // ignored + long up = 0; + try { + up = Long.parseLong(uploaded); + if (up < 0) + up = 0; + } catch (NumberFormatException nfe) {}; + + // ignored + long down = 0; + try { + down = Long.parseLong(downloaded); + if (down < 0) + down = 0; + } catch (NumberFormatException nfe) {}; + + int want = MAX_RESPONSES; + try { + want = Integer.parseInt(numwant); + if (want > MAX_RESPONSES) + want = MAX_RESPONSES; + else if (want < 0) + want = 0; + } catch (NumberFormatException nfe) {}; + + long left = 0; + if (!"completed".equals(event)) { + try { + left = Long.parseLong(sleft); + if (left < 0) + left = 0; + } catch (NumberFormatException nfe) {}; + } + + Torrents torrents = ZzzOTController.getTorrents(); + Map m = new HashMap(); + if (fail) { + m.put("failure reason", msg); + } else if ("stopped".equals(event)) { + Peers peers = torrents.get(ih); + if (peers != null) + peers.remove(pid); + m.put("interval", Integer.valueOf(INTERVAL)); + } else { + Peers peers = torrents.get(ih); + if (peers == null) { + peers = new Peers(); + Peers p2 = torrents.putIfAbsent(ih, peers); + if (p2 != null) + peers = p2; + } + + // fixme same peer id, different dest + Peer p = peers.get(pid); + if (p == null) { + p = new Peer(pid.getData(), d); + Peer p2 = peers.putIfAbsent(pid, p); + if (p2 != null) + p = p2; + } + p.setLeft(left); + + m.put("interval", Integer.valueOf(INTERVAL)); + int size = peers.size(); + int seeds = peers.countSeeds(); + m.put("complete", Integer.valueOf(seeds)); + m.put("incomplete", Integer.valueOf(size - seeds)); + if (want <= 0) { + // snark < 0.7.13 always wants a list + m.put("peers", java.util.Collections.EMPTY_LIST); + } else { + List peerlist = new ArrayList(peers.values()); + peerlist.remove(p); // them + if (want < size - 1) { + Collections.shuffle(peerlist); + m.put("peers", peerlist.subList(0, want)); + } else { + m.put("peers", peerlist); + } + } + } + BEncoder.bencode(m, cout); + +/* + * Remove the newline on the last line or + * it will generate an IllegalStateException + * + */ +%> \ No newline at end of file diff --git a/src/jsp/index.html b/src/jsp/index.html new file mode 100644 index 0000000..a5267bf --- /dev/null +++ b/src/jsp/index.html @@ -0,0 +1,10 @@ + + + +zzzot + + +Enter + + + diff --git a/src/jsp/index.jsp b/src/jsp/index.jsp new file mode 100644 index 0000000..73e0be0 --- /dev/null +++ b/src/jsp/index.jsp @@ -0,0 +1,14 @@ +<%@page import="net.i2p.zzzot.ZzzOTController" %> + + +ZzzOT + +

+zzzot +

+ +
Torrents:<%=ZzzOTController.getTorrents().size()%> +
Peers:<%=ZzzOTController.getTorrents().countPeers()%> +
+ + diff --git a/src/jsp/scrape.jsp b/src/jsp/scrape.jsp new file mode 100644 index 0000000..279a3ef --- /dev/null +++ b/src/jsp/scrape.jsp @@ -0,0 +1,92 @@ +<%@page import="java.util.ArrayList" %><%@page import="java.util.List" %><%@page import="java.util.Map" %><%@page import="java.util.HashMap" %><%@page import="net.i2p.zzzot.*" %><%@page import="org.klomp.snark.bencode.BEncoder" %><% + +/* + * Above one-liner is so there is no whitespace -> IllegalStateException + * + * 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. + * + */ + +/* + * USE CAUTION WHEN EDITING + * Trailing whitespace OR NEWLINE on the last line will cause + * IllegalStateExceptions !!! + * + */ + // so the chars will turn into bytes correctly + request.setCharacterEncoding("ISO-8859-1"); + java.io.OutputStream cout = response.getOutputStream(); + response.setCharacterEncoding("ISO-8859-1"); + response.setContentType("text/plain"); + response.setHeader("Pragma", "no-cache"); + String info_hash = request.getParameter("info_hash"); + String xff = request.getHeader("X-Forwarded-For"); + + boolean fail = false; + String msg = "bad"; + + if (xff != null) { + fail = true; + msg = "Non-I2P access denied"; + response.setStatus(403, msg); + } + + boolean all = info_hash == null; + + InfoHash ih = null; + if ((!all) && !fail) { + try { + ih = new InfoHash(info_hash); + } catch (Exception e) { + fail = true; + msg = "bad infohash " + e; + } + } + + Torrents torrents = ZzzOTController.getTorrents(); + + // build 3-level dictionary + Map m = new HashMap(); + if (fail) { + m.put("failure reason", msg); + } else { + List ihList = new ArrayList(); + if (all) + ihList.addAll(torrents.keySet()); + else + ihList.add(ih); + Map files = new HashMap(); + for (InfoHash ihash : ihList) { + Peers peers = torrents.get(ihash); + if (peers == null) + continue; + Map dict = new HashMap(); + int size = peers.size(); + int seeds = peers.countSeeds(); + dict.put("complete", Integer.valueOf(seeds)); + dict.put("incomplete", Integer.valueOf(size - seeds)); + dict.put("downloaded", Integer.valueOf(0)); + files.put(new String(ihash.getData(), "ISO-8859-1"), dict); + } + m.put("files", files); + } + BEncoder.bencode(m, cout); + +/* + * Remove the newline on the last line or + * it will generate an IllegalStateException + * + */ +%> \ No newline at end of file diff --git a/src/jsp/seedless.jsp b/src/jsp/seedless.jsp new file mode 100644 index 0000000..05a5f6b --- /dev/null +++ b/src/jsp/seedless.jsp @@ -0,0 +1,74 @@ +<%@page import="net.i2p.crypto.SHA256Generator" %><%@page import="net.i2p.data.Base32" %><%@page import="net.i2p.data.Base64" %><%@page import="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. + * + */ + + String req = request.getHeader("X-Seedless"); + String me = request.getHeader("Host"); + // unused, we don't accept announces + String him = request.getHeader("X-I2P-DestB32"); + String xff = request.getHeader("X-Forwarded-For"); + + response.setContentType("text/plain"); + response.setHeader("X-Seedless", me); + + final int US_MINUTES = 360; + final int PEER_MINUTES = 60; + + if (xff != null) { + String msg = "Non-I2P access denied"; + response.setStatus(403, msg); + out.println(msg); + } else if (req == null) { + out.println("seedless server"); + } else if (req.startsWith("announce ")) { + out.println(""); + } else if (req.startsWith("locate ") && me != null) { + // ignore the search string, if any, in the request + // us + out.println(Base64.encode(me + ' ' + US_MINUTES + " bt-tracker")); + // all the peers + Torrents torrents = ZzzOTController.getTorrents(); + 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 = " bt-seed"; + else + role = " bt-leech"; + // spg wants UTF-8 but all we have is binary data, sorry + String ihs = new String(ihash.getData(), "ISO-8859-1"); + String ids = new String((byte[])p.get("peer id"), "ISO-8859-1"); + out.println(Base64.encode(b32 + PEER_MINUTES + role + + " info_hash=" + ihs + + " peer_id=" + ids)); + } + } + } else { + out.println("2"); + } + +%> \ No newline at end of file