I2P Address: [http://git.idk.i2p]

Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • equincey/i2p.i2p
  • marek/i2p.i2p
  • kytv/i2p.i2p
  • agentoocat/i2p.i2p
  • aargh/i2p.i2p
  • Kalhintz/i2p.i2p
  • longyap/i2p.i2p
  • kelare/i2p.i2p
  • apsoyka/i2p.i2p
  • mesh/i2p.i2p
  • ashtod/i2p.i2p
  • y2kboy23/i2p.i2p
  • Lfrr/i2p.i2p
  • anonymousmaybe/i2p.i2p
  • obscuratus/i2p.i2p
  • zzz/i2p.i2p
  • lbt/i2p.i2p
  • 31337/i2p.i2p
  • DuncanIdaho/i2p.i2p
  • loveisgrief/i2p.i2p
  • i2p-hackers/i2p.i2p
  • thebland/i2p.i2p
  • elde/i2p.i2p
  • echelon/i2p.i2p
  • welshlyluvah1967/i2p.i2p
  • zlatinb/i2p.i2p
  • sadie/i2p.i2p
  • pVT0/i2p.i2p
  • idk/i2p.i2p
29 results
Show changes
Showing
with 3397 additions and 266 deletions
#
# This is for app context configuration of standalone i2psnark.
# Almost all configuration settings are in i2psnark.config.d/i2psnark.config
#
# disable browser launch on startup
#routerconsole.browser=/bin/false
# disable browser launch on startup (Windows)
#routerconsole.browser=NUL
# change browser
#routerconsole.browser=firefox
# disable system tray
#desktopgui.enabled=false
# disable system tray notification popups
#desktopgui.showNotifications=false
# NOTE: This I2P config file must use UTF-8 encoding
#
# If you have a 'split' directory installation, with configuration
# files in ~/.i2p (Linux), %LOCALAPPDATA%\I2P (Windows),
# or /Users/(user)/Library/Application Support/i2p (Mac), be sure to
# edit the file in the configuration directory, NOT the install directory.
# When running as a Linux daemon, the configuration directory is /var/lib/i2p
# and the install directory is /usr/share/i2p .
# When running as a Windows service, the configuration directory is \ProgramData\i2p
# and the install directory is \Program Files\i2p .
#
i2psnark.dir=i2psnark
...@@ -3,91 +3,372 @@ ...@@ -3,91 +3,372 @@
<target name="all" depends="clean, build" /> <target name="all" depends="clean, build" />
<target name="build" depends="builddep, jar, war" /> <target name="build" depends="builddep, jar, war" />
<target name="builddep"> <target name="builddep">
<ant dir="../../jetty/" target="build" /> <!-- run from top level build.xml to get dependencies built -->
<ant dir="../../streaming/java/" target="build" />
<!-- streaming will build ministreaming and core -->
</target> </target>
<target name="compile"> <condition property="depend.available">
<typefound name="depend" />
</condition>
<target name="depend" if="depend.available">
<depend
cache="../../../build"
srcdir="./src"
destdir="./build/obj" >
<!-- Depend on classes instead of jars where available -->
<classpath>
<pathelement location="../../../core/java/build/obj" />
<pathelement location="../../../core/java/build/gnu-getopt.jar" />
<pathelement location="../../ministreaming/java/build/obj" />
<pathelement location="../../jetty/jettylib/javax.servlet.jar" />
<!-- jsp-api.jar only present for debian builds -->
<pathelement location="../../jetty/jettylib/jsp-api.jar" />
<!-- following jars only for standalone builds -->
<pathelement location="../../desktopgui/dist/desktopgui.jar" />
</classpath>
</depend>
</target>
<!-- only used if not set by a higher build.xml -->
<property name="javac.compilerargs" value="" />
<property name="javac.version" value="1.8" />
<property name="javac.release" value="8" />
<property name="require.gettext" value="true" />
<property name="manifest.classpath.name" value="Class-Path" />
<condition property="no.bundle">
<isfalse value="${require.gettext}" />
</condition>
<target name="compile" depends="depend">
<mkdir dir="./build" /> <mkdir dir="./build" />
<mkdir dir="./build/obj" /> <mkdir dir="./build/obj" />
<javac <javac
srcdir="./src" srcdir="./src"
debug="true" deprecation="on" source="1.3" target="1.3" debug="true" deprecation="on" source="${javac.version}" target="${javac.version}"
release="${javac.release}"
destdir="./build/obj" destdir="./build/obj"
classpath="../../../core/java/build/i2p.jar:../../jetty/jettylib/org.mortbay.jetty.jar:../../jetty/jettylib/javax.servlet.jar:../../ministreaming/java/build/mstreaming.jar" /> encoding="UTF-8"
includeAntRuntime="false" >
<compilerarg line="${javac.compilerargs}" />
<classpath>
<pathelement location="../../../core/java/build/i2p.jar" />
<!-- gnu-getopt.jar only present for debian builds -->
<pathelement location="../../../core/java/build/gnu-getopt.jar" />
<pathelement location="../../ministreaming/java/build/mstreaming.jar" />
<pathelement location="../../jetty/jettylib/javax.servlet.jar" />
<!-- jsp-api.jar only present for debian builds -->
<pathelement location="../../jetty/jettylib/jsp-api.jar" />
<!-- following jars only for standalone builds -->
<pathelement location="../../jetty/jettylib/jetty-i2p.jar" />
<pathelement location="../../systray/java/build/systray.jar" />
<pathelement location="../../jetty/jettylib/org.mortbay.jetty.jar" />
<pathelement location="../../jetty/jettylib/jetty-util.jar" />
<pathelement location="../../desktopgui/dist/desktopgui.jar" />
</classpath>
</javac>
</target>
<target name="listChangedFiles" depends="jarUpToDate" if="shouldListChanges" >
<exec executable="git" outputproperty="workspace.changes" errorproperty="mtn.error2" failifexecutionfails="false" >
<arg value="status" />
<arg value="-s" />
<arg value="--porcelain" />
<arg value="-uno" />
<arg value=".." />
</exec>
<!-- trim flags -->
<exec executable="sed" inputstring="${workspace.changes}" outputproperty="workspace.changes.sed" errorproperty="mtn.error2" failifexecutionfails="false" >
<arg value="-e" />
<arg value="s/^[MTADRCU ]*//" />
</exec>
<!-- \n in an attribute value generates an invalid manifest -->
<exec executable="tr" inputstring="${workspace.changes.sed}" outputproperty="workspace.changes.tr" errorproperty="mtn.error2" failifexecutionfails="false" >
<arg value="-s" />
<arg value="[:space:]" />
<arg value="," />
</exec>
</target> </target>
<target name="jar" depends="builddep, compile">
<jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class" excludes="**/*Servlet.class"> <target name="jar" depends="builddep, compile, jarUpToDate, listChangedFiles" unless="jar.uptodate" >
<!-- set if unset -->
<property name="workspace.changes.tr" value="" />
<jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class" excludes="**/web/* **/messages_*.class, **/standalone/*">
<manifest> <manifest>
<attribute name="Main-Class" value="org.klomp.snark.Snark" /> <attribute name="Main-Class" value="org.klomp.snark.CommandLine" />
<attribute name="Class-Path" value="i2p.jar mstreaming.jar streaming.jar" /> <attribute name="${manifest.classpath.name}" value="i2p.jar mstreaming.jar streaming.jar" />
<attribute name="Implementation-Version" value="${full.version}" />
<attribute name="Built-By" value="${build.built-by}" />
<attribute name="Build-Date" value="${build.timestamp}" />
<attribute name="Base-Revision" value="${workspace.version}" />
<attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
<attribute name="X-Compile-Source-JDK" value="${javac.version}" />
<attribute name="X-Compile-Target-JDK" value="${javac.version}" />
</manifest> </manifest>
</jar> </jar>
</target>
<target name="jarUpToDate">
<uptodate property="jar.uptodate" targetfile="build/i2psnark.jar" >
<srcfiles dir= "build/obj" includes="**/*.class" excludes="**/web/* **/messages_*.class" />
</uptodate>
<condition property="shouldListChanges" >
<and>
<not>
<isset property="jar.uptodate" />
</not>
<not>
<isset property="war.uptodate" />
</not>
<isset property="git.available" />
</and>
</condition>
</target> </target>
<target name="war" depends="jar">
<war destfile="../i2psnark.war" webxml="../web.xml"> <!-- Ideally we would include
<classes dir="./build/obj" includes="**/*" /> - only include the servlet, everything else is in the jar.
- However, the wrapper.config classpath in existing installs
- does not include i2psnark.jar.
- So we must continue to duplicate everything in the war.
<classes dir="./build/obj" includes="**/I2PSnarkServlet*.class" />
-->
<target name="war" depends="jar, bundle, warUpToDate, listChangedFiles" unless="war.uptodate" >
<!-- set if unset -->
<property name="workspace.changes.tr" value="" />
<copy todir="build/resources/.resources" >
<fileset dir="../resources/" />
</copy>
<!-- mime.properties must be in with the classes -->
<copy file="../mime.properties" todir="build/obj/org/klomp/snark/web" />
<war destfile="../i2psnark.war" webxml="../web.xml" >
<!-- include only the web stuff, as of 0.7.12 the router will add i2psnark.jar to the classpath for the war -->
<classes dir="./build/obj" includes="**/web/*" />
<fileset dir="build/resources/" />
<manifest>
<attribute name="Implementation-Version" value="${full.version}" />
<attribute name="Built-By" value="${build.built-by}" />
<attribute name="Build-Date" value="${build.timestamp}" />
<attribute name="Base-Revision" value="${workspace.version}" />
<attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
<attribute name="X-Compile-Source-JDK" value="${javac.version}" />
<attribute name="X-Compile-Target-JDK" value="${javac.version}" />
</manifest>
</war> </war>
</target> </target>
<target name="warUpToDate">
<uptodate property="war.uptodate" targetfile="../i2psnark.war" >
<srcfiles dir= "." includes="build/obj/org/klomp/snark/web/*.class ../resources/**/* ../web.xml" />
<srcfiles dir= "../resources" />
</uptodate>
</target>
<target name="bundle" depends="compile" unless="no.bundle">
<mkdir dir="build/messages-src" />
<!-- Update the messages_*.po files.
We need to supply the bat file for windows, and then change the fail property to true -->
<exec executable="sh" osfamily="unix" failifexecutionfails="true" failonerror="${require.gettext}" >
<env key="JAVA_HOME" value="${java.home}" />
<arg value="./bundle-messages.sh" />
</exec>
<exec executable="sh" osfamily="mac" failifexecutionfails="true" failonerror="${require.gettext}" >
<arg value="./bundle-messages.sh" />
</exec>
<!-- multi-lang is optional -->
<exec executable="sh" osfamily="windows" failifexecutionfails="false" >
<arg value="./bundle-messages.sh" />
</exec>
<javac source="${javac.version}" target="${javac.version}"
release="${javac.release}"
includeAntRuntime="false"
encoding="UTF-8"
srcdir="build/messages-src" destdir="build/obj">
<compilerarg line="${javac.compilerargs}" />
</javac>
</target>
<target name="poupdate" depends="builddep, compile">
<!-- Update the messages_*.po files. -->
<!-- set if unset -->
<property name="lg2" value="" />
<exec executable="sh" osfamily="unix" failifexecutionfails="true" failonerror="true" >
<env key="LG2" value="${lg2}" />
<arg value="./bundle-messages.sh" />
<arg value="-p" />
</exec>
<exec executable="sh" osfamily="mac" failifexecutionfails="true" failonerror="true" >
<env key="LG2" value="${lg2}" />
<arg value="./bundle-messages.sh" />
<arg value="-p" />
</exec>
<exec executable="sh" osfamily="windows" failifexecutionfails="true" failonerror="true" >
<env key="LG2" value="${lg2}" />
<arg value="./bundle-messages.sh" />
<arg value="-p" />
</exec>
</target>
<target name="standalone" depends="standalone_prep"> <target name="standalone" depends="standalone_prep">
<zip destfile="i2psnark-standalone.zip"> <!-- doesn't support file permissions
<zipfileset dir="./dist/" prefix="i2psnark/" /> <zip destfile="build/i2psnark-standalone.zip">
<zipfileset dir="./build/i2psnark/" fullpath="i2psnark" />
</zip> </zip>
-->
<exec executable="zip" dir="build" failifexecutionfails="true" failonerror="true" >
<arg value="-r" />
<arg value="i2psnark-standalone.zip" />
<arg value="i2psnark" />
</exec>
</target> </target>
<target name="standalone_prep" depends="war">
<javac debug="true" deprecation="on" source="1.3" target="1.3"
destdir="./build" srcdir="src/" includes="org/klomp/snark/web/RunStandalone.java" >
<classpath>
<pathelement location="../../jetty/jettylib/commons-logging.jar" />
<pathelement location="../../jetty/jettylib/commons-el.jar" />
<pathelement location="../../jetty/jettylib/org.mortbay.jetty.jar" />
<pathelement location="../../jetty/jettylib/javax.servlet.jar" />
<pathelement location="../../../core/java/build/i2p.jar" />
</classpath>
</javac>
<jar destfile="./build/launch-i2psnark.jar" basedir="./build/" includes="org/klomp/snark/web/RunStandalone.class">
<manifest>
<attribute name="Main-Class" value="org.klomp.snark.web.RunStandalone" />
<attribute name="Class-Path" value="lib/i2p.jar lib/mstreaming.jar lib/streaming.jar lib/commons-el.jar lib/commons-logging.jar lib/jasper-compiler.jar lib/jasper-runtime.jar lib/javax.servlet.jar lib/org.mortbay.jetty.jar" />
</manifest>
</jar>
<delete dir="./dist" /> <!-- make a fat jar for standalone -->
<mkdir dir="./dist" /> <target name="standalone_jar" depends="war">
<copy file="./build/launch-i2psnark.jar" tofile="./dist/launch-i2psnark.jar" /> <!-- set if unset -->
<mkdir dir="./dist/webapps" /> <property name="workspace.changes.tr" value="" />
<copy file="../i2psnark.war" tofile="./dist/webapps/i2psnark.war" /> <jar destfile="build/i2psnark-standalone.jar">
<mkdir dir="./dist/lib" /> <fileset dir="build/obj" includes="**/standalone/*.class" />
<copy file="../../../core/java/build/i2p.jar" tofile="./dist/lib/i2p.jar" /> <zipfileset src="build/i2psnark.jar" />
<copy file="../../jetty/jettylib/commons-el.jar" tofile="./dist/lib/commons-el.jar" /> <zipfileset src="../../../core/java/build/i2p.jar" />
<copy file="../../jetty/jettylib/commons-logging.jar" tofile="./dist/lib/commons-logging.jar" /> <!-- without this we get a warning about 'no JSP support' but that's it
<copy file="../../jetty/jettylib/javax.servlet.jar" tofile="./dist/lib/javax.servlet.jar" /> <zipfileset src="../../jetty/jettylib/jasper-runtime.jar" />
<copy file="../../jetty/jettylib/org.mortbay.jetty.jar" tofile="./dist/lib/org.mortbay.jetty.jar" /> -->
<copy file="../../jetty/jettylib/jasper-compiler.jar" tofile="./dist/lib/jasper-compiler.jar" /> <zipfileset src="../../jetty/jettylib/javax.servlet.jar" />
<copy file="../../jetty/jettylib/jasper-runtime.jar" tofile="./dist/lib/jasper-runtime.jar" /> <zipfileset src="../../jetty/jettylib/jetty-continuation.jar" />
<copy file="../../ministreaming/java/build/mstreaming.jar" tofile="./dist/lib/mstreaming.jar" /> <zipfileset src="../../jetty/jettylib/jetty-deploy.jar" />
<copy file="../../streaming/java/build/streaming.jar" tofile="./dist/lib/streaming.jar" /> <zipfileset src="../../jetty/jettylib/jetty-http.jar" />
<copy file="../jetty-i2psnark.xml" tofile="./dist/jetty-i2psnark.xml" /> <zipfileset src="../../jetty/jettylib/jetty-i2p.jar" />
<copy file="../readme-standalone.txt" tofile="./dist/readme.txt" /> <zipfileset src="../../jetty/jettylib/jetty-io.jar" />
<mkdir dir="./dist/work" /> <zipfileset src="../../jetty/jettylib/jetty-security.jar" />
<mkdir dir="./dist/logs" /> <zipfileset src="../../jetty/jettylib/jetty-servlet.jar" />
<zipfileset src="../../jetty/jettylib/jetty-util.jar" />
<zip destfile="i2psnark-standalone.zip"> <zipfileset src="../../jetty/jettylib/jetty-webapp.jar" />
<zipfileset dir="./dist/" prefix="i2psnark/" /> <zipfileset src="../../jetty/jettylib/jetty-xml.jar" />
<zipfileset src="../../jetty/jettylib/org.mortbay.jetty.jar" />
<zipfileset src="../../ministreaming/java/build/mstreaming.jar" />
<zipfileset src="../../streaming/java/build/streaming.jar" />
<zipfileset src="../../systray/java/build/systray.jar" />
<zipfileset src="../../../build/jbigi.jar" />
<zipfileset src="../../desktopgui/dist/desktopgui.jar" />
<!-- Countries translations. The i2psnark translations are in the war but it's easier to put these here -->
<!-- 300KB just to translate "Brazil", but why not... -->
<!--
<fileset dir="../../routerconsole/java/build/obj" includes="net/i2p/router/countries/*.class" />
-->
<manifest>
<attribute name="Main-Class" value="org.klomp.snark.standalone.RunStandalone"/>
<attribute name="Implementation-Version" value="${full.version}" />
<attribute name="Built-By" value="${build.built-by}" />
<attribute name="Build-Date" value="${build.timestamp}" />
<attribute name="Base-Revision" value="${workspace.version}" />
<attribute name="Workspace-Changes" value="${workspace.changes.tr}" />
<attribute name="X-Compile-Source-JDK" value="${javac.version}" />
<attribute name="X-Compile-Target-JDK" value="${javac.version}" />
<!--
Suppress JNI warning in JRE 24+, and eventual restriction
See https://openjdk.org/jeps/472
-->
<attribute name="Enable-Native-Access" value="ALL-UNNAMED" />
<!-- this is so Jetty will report its version correctly -->
<section name="org/eclipse/jetty/server/" >
<attribute name="Implementation-Vendor" value="Eclipse.org - Jetty" />
<attribute name="Implementation-Version" value="${jetty.ver}" />
</section>
</manifest>
</jar>
</target>
<!-- add css, image, and js files for standalone snark to the war -->
<target name="standalone_war" depends="war">
<mkdir dir="build/standalone-resources/.resources/themes/" />
<copy todir="build/standalone-resources/.resources/themes/" >
<fileset dir="../resources/themes/" />
</copy>
<replace dir="build/standalone-resources/.resources/themes"
summary="true"
token="url(/themes/console/dark/images/"
value="url(/i2psnark/.resources/themes/dark/images/" >
<include name="**/*.css" />
</replace>
<replace dir="build/standalone-resources/.resources/themes"
summary="true"
token="url(../../console/light/images/"
value="url(/i2psnark/.resources/themes/light/images/" >
<include name="**/*.css" />
</replace>
<replace dir="build/standalone-resources/.resources/themes"
summary="true"
token="url(/themes/console/light/images/"
value="url(/i2psnark/.resources/themes/light/images/" >
<include name="**/*.css" />
</replace>
<replace dir="build/standalone-resources/.resources/themes"
summary="true"
token="url(/themes/console/images/transparent.gif"
value="url(/i2psnark/.resources/themes/ubergine/images/transparent.gif" >
<include name="**/*.css" />
</replace>
<replace dir="build/standalone-resources/.resources/themes"
summary="true"
token="url(/themes/console/images/info/"
value="url(/i2psnark/.resources/themes/ubergine/images/" >
<include name="**/*.css" />
</replace>
<replace dir="build/standalone-resources/.resources/themes"
summary="true"
token="url(/themes/console/images/buttons/"
value="url(/i2psnark/.resources/icons/" >
<include name="**/*.css" />
</replace>
<!-- Rather than pulling in all the console theme images, let's just specify the ones we need -->
<copy file="../../routerconsole/jsp/themes/console/images/transparent.gif"
todir="build/standalone-resources/.resources/themes/ubergine/images" />
<copy file="../../routerconsole/jsp/themes/console/dark/images/header.png"
todir="build/standalone-resources/.resources/themes/dark/images" />
<copy file="../../routerconsole/jsp/themes/console/light/images/header.png"
todir="build/standalone-resources/.resources/themes/light/images" />
<copy file="../../routerconsole/jsp/themes/console/images/info/errortriangle.png"
todir="build/standalone-resources/.resources/themes/ubergine/images" />
<copy file="../../routerconsole/jsp/themes/console/images/buttons/search.png"
todir="build/standalone-resources/.resources/icons" />
<mkdir dir="build/standalone-resources/.resources/js" />
<copy file="../../routerconsole/jsp/js/ajax.js" todir="build/standalone-resources/.resources/js" />
<zip destfile="../i2psnark.war" update="true" duplicate="preserve" >
<fileset dir="build/standalone-resources" />
</zip> </zip>
</target> </target>
<target name="standalone_prep" depends="standalone_jar, standalone_war">
<delete dir="./build/i2psnark" />
<mkdir dir="./build/i2psnark" />
<copy file="../launch-i2psnark" todir="./build/i2psnark/" />
<chmod type="file" file="./build/i2psnark/launch-i2psnark" perm="+x" />
<copy file="../launch-i2psnark.bat" todir="./build/i2psnark/" />
<mkdir dir="./build/i2psnark/contexts" />
<copy file="../standalone-context.xml" tofile="./build/i2psnark/contexts/context.xml" />
<mkdir dir="./build/i2psnark/docroot" />
<copy file="../standalone-index.html" tofile="./build/i2psnark/docroot/index.html" />
<mkdir dir="./build/i2psnark/webapps" />
<copy file="../i2psnark.war" tofile="./build/i2psnark/webapps/i2psnark.war" />
<copy file="../jetty-i2psnark.xml" tofile="./build/i2psnark/jetty-i2psnark.xml" />
<copy file="../i2psnark-appctx.config" tofile="./build/i2psnark/i2psnark-appctx.config" />
<copy file="./build/i2psnark-standalone.jar" tofile="./build/i2psnark/i2psnark.jar" />
<copy file="../readme-standalone.txt" tofile="./build/i2psnark/readme.txt" />
<!-- temp so announces work -->
<copy file="../../../installer/resources/hosts.txt" tofile="./build/i2psnark/hosts.txt" />
<copy todir="./build/i2psnark/licenses" >
<fileset dir="../../../licenses" includes="LICENSE-GPLv2.txt, ABOUT-Jetty.html" />
</copy>
<mkdir dir="./build/i2psnark/logs" />
</target>
<target name="clean"> <target name="clean">
<delete dir="./build" /> <delete dir="./build" />
<delete file="../i2psnark.war" /> <delete file="../i2psnark.war" />
<delete file="./i2psnark-standalone.zip" />
</target> </target>
<target name="cleandep" depends="clean"> <target name="cleandep" depends="clean">
<ant dir="../../ministreaming/java/" target="distclean" />
</target> </target>
<target name="distclean" depends="clean"> <target name="distclean" depends="clean">
<ant dir="../../ministreaming/java/" target="distclean" />
</target> </target>
</project> </project>
#!/bin/sh
#
# Update messages_xx.po and messages_xx.class files,
# from both java and jsp sources.
# Requires installed programs xgettext, msgfmt, msgmerge, and find.
#
# usage:
# bundle-messages.sh (generates the resource bundle from the .po file)
# bundle-messages.sh -p (updates the .po file from the source tags, then generates the resource bundle)
#
# zzz - public domain
#
cd `dirname $0`
CLASS=org.klomp.snark.web.messages
TMPFILE=build/javafiles.txt
export TZ=UTC
RC=0
if ! $(which javac > /dev/null 2>&1); then
export JAVAC=${JAVA_HOME}/../bin/javac
fi
if [ "$1" = "-p" ]
then
POUPDATE=1
fi
# on windows, one must specify the path of commnad find
# since windows has its own version of find.
if which find|grep -q -i windows ; then
export PATH=.:/bin:/usr/local/bin:$PATH
fi
# Fast mode - update ondemond
# set LG2 to the language you need in environment variables to enable this
# add ../java/ so the refs will work in the po file
JPATHS="../java/src"
for i in ../locale/messages_*.po
do
# get language
LG=${i#../locale/messages_}
LG=${LG%.po}
# skip, if specified
if [ $LG2 ]; then
[ $LG != $LG2 ] && continue || echo INFO: Language update is set to [$LG2] only.
fi
if [ "$POUPDATE" = "1" ]
then
# make list of java files newer than the .po file
find $JPATHS -name *.java -newer $i > $TMPFILE
fi
if [ -s build/obj/org/klomp/snark/web/messages_$LG.class -a \
build/obj/org/klomp/snark/web/messages_$LG.class -nt $i -a \
! -s $TMPFILE ]
then
continue
fi
if [ "$POUPDATE" = "1" ]
then
echo "Updating the $i file from the tags..."
# extract strings from java and jsp files, and update messages.po files
# translate calls must be one of the forms:
# _t("foo")
# _x("foo")
# To start a new translation, copy the header from an old translation to the new .po file,
# then ant distclean poupdate.
find $JPATHS -name *.java > $TMPFILE
xgettext -f $TMPFILE -F -L java --from-code=UTF-8 --add-comments\
--keyword=_t --keyword=_x \
-o ${i}t
if [ $? -ne 0 ]
then
echo "ERROR - xgettext failed on ${i}, not updating translations"
rm -f ${i}t
RC=1
break
fi
msgmerge -U --backup=none $i ${i}t
if [ $? -ne 0 ]
then
echo "ERROR - msgmerge failed on ${i}, not updating translations"
rm -f ${i}t
RC=1
break
fi
rm -f ${i}t
# so we don't do this again
touch $i
fi
if [ "$LG" != "en" ]
then
# only generate for non-source language
echo "Generating ${CLASS}_$LG ResourceBundle..."
msgfmt -V | grep -q -E ' 0\.((19)|[2-9])'
if [ $? -ne 0 ]
then
# slow way
# convert to class files in build/obj
msgfmt --java2 --statistics -r $CLASS -l $LG -d build/obj $i
if [ $? -ne 0 ]
then
echo "ERROR - msgfmt failed on ${i}, not updating translations"
# msgfmt leaves the class file there so the build would work the next time
find build -name messages_${LG}.class -exec rm -f {} \;
RC=1
break
fi
else
# fast way
# convert to java files in build/messages-src
TD=build/messages-src-tmp
TDX=$TD/org/klomp/snark/web
TD2=build/messages-src
TDY=$TD2/org/klomp/snark/web
rm -rf $TD
mkdir -p $TD $TDY
msgfmt --java2 --statistics --source -r $CLASS -l $LG -d $TD $i
if [ $? -ne 0 ]
then
echo "ERROR - msgfmt failed on ${i}, not updating translations"
# msgfmt leaves the class file there so the build would work the next time
find build/obj -name messages_${LG}.class -exec rm -f {} \;
RC=1
break
fi
mv $TDX/messages_$LG.java $TDY
rm -rf $TD
fi
fi
done
rm -f $TMPFILE
exit $RC
/*
* Released into the public domain
* with no warranty of any kind, either expressed or implied.
*/
package org.klomp.snark;
import java.util.Properties;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSimpleClient;
/**
* Connect via I2CP and ask the router the bandwidth limits.
*
* The call is blocking and returns null on failure.
* Timeout is set to 5 seconds in I2PSimpleSession but it should be much faster.
*
* @author zzz
*/
class BWLimits {
public static int[] getBWLimits(String host, int port) {
int[] rv = null;
try {
I2PClient client = new I2PSimpleClient();
Properties opts = new Properties();
opts.put(I2PClient.PROP_TCP_HOST, host);
opts.put(I2PClient.PROP_TCP_PORT, "" + port);
I2PSession session = client.createSession(null, opts);
session.connect();
rv = session.bandwidthLimits();
session.destroySession();
} catch (I2PSessionException ise) {
I2PAppContext.getGlobalContext().logManager().getLog(BWLimits.class).warn("BWL fail", ise);
}
return rv;
}
/****
public static void main(String args[]) {
System.out.println(Arrays.toString(getBWLimits("127.0.0.1", 7654)));
}
****/
}
package org.klomp.snark;
/**
* Bandwidth and bandwidth limits
*
* Maintain three bandwidth estimators:
* Sent, received, and requested.
*
* @since 0.9.62
*/
public interface BandwidthListener {
/**
* The average rate in Bps
*/
public long getUploadRate();
/**
* The average rate in Bps
*/
public long getDownloadRate();
/**
* We unconditionally sent this many bytes
*/
public void uploaded(int size);
/**
* We unconditionally received this many bytes
*/
public void downloaded(int size);
/**
* Should we send this many bytes?
* Do NOT call uploaded() if this returns true.
*/
public boolean shouldSend(int size);
/**
* Should we request this many bytes?
*/
public boolean shouldRequest(Peer peer, int size);
/**
* Current limit in BPS
*/
public long getUpBWLimit();
/**
* Current limit in BPS
*/
public long getDownBWLimit();
/**
* Are we currently over the limit?
*/
public boolean overUpBWLimit();
/**
* Are we currently over the limit?
*/
public boolean overDownBWLimit();
}
package org.klomp.snark;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.Log;
import net.i2p.util.SyntheticREDQueue;
/**
* Bandwidth and bandwidth limits
*
* Maintain three bandwidth estimators:
* Sent, received, and requested.
*
* There are three layers of BandwidthListeners:
*<pre>
* BandwidthManager (total)
* PeerCoordinator (per-torrent)
* Peer/WebPeer (per-connection)
*</pre>
*
* Here at the top, we use SyntheticRedQueues for accurate
* and current moving averages of up, down, and requested bandwidth.
*
* At the lower layers, simple weighted moving averages of
* three buckets of time PeerCoordinator.CHECK_PERIOD each are used
* for up and down, and requested is delegated here.
*
* The lower layers must report to the next-higher layer.
*
* At the Peer layer, we report inbound piece data per-read,
* not per-piece, to get a smoother inbound estimate.
*
* Only the following data are counted by the BandwidthListeners:
*<ul><li>Pieces (both Peer and WebPeer)
*<li>ut_metadata
*</ul>
*
* No overhead at any layer is accounted for.
*
* @since 0.9.62
*/
public class BandwidthManager implements BandwidthListener {
private final I2PAppContext _context;
private final Log _log;
private SyntheticREDQueue _up, _down, _req;
BandwidthManager(I2PAppContext ctx, int upLimit, int downLimit) {
_context = ctx;
_log = ctx.logManager().getLog(BandwidthManager.class);
_up = new SyntheticREDQueue(ctx, upLimit);
_down = new SyntheticREDQueue(ctx, downLimit);
// Allow down limit a little higher based on testing
// Allow req limit a little higher still because it uses RED
// so it actually kicks in sooner.
_req = new SyntheticREDQueue(ctx, downLimit * 110 / 100);
}
/**
* Current limit in Bps
*/
void setUpBWLimit(long upLimit) {
int limit = (int) Math.min(upLimit, Integer.MAX_VALUE);
if (limit != getUpBWLimit())
_up = new SyntheticREDQueue(_context, limit);
}
/**
* Current limit in Bps
*/
void setDownBWLimit(long downLimit) {
int limit = (int) Math.min(downLimit, Integer.MAX_VALUE);
if (limit != getDownBWLimit()) {
_down = new SyntheticREDQueue(_context, limit);
_req = new SyntheticREDQueue(_context, limit * 110 / 100);
}
}
/**
* The average rate in Bps
*/
long getRequestRate() {
return (long) (1000f * _req.getBandwidthEstimate());
}
// begin BandwidthListener interface
/**
* The average rate in Bps
*/
public long getUploadRate() {
return (long) (1000f * _up.getBandwidthEstimate());
}
/**
* The average rate in Bps
*/
public long getDownloadRate() {
return (long) (1000f * _down.getBandwidthEstimate());
}
/**
* We unconditionally sent this many bytes
*/
public void uploaded(int size) {
_up.addSample(size);
}
/**
* We received this many bytes
*/
public void downloaded(int size) {
_down.addSample(size);
}
/**
* Should we send this many bytes?
* Do NOT call uploaded() if this returns true.
*/
public boolean shouldSend(int size) {
boolean rv = _up.offer(size, 1.0f);
if (!rv && _log.shouldWarn())
_log.warn("Deny sending " + size + " bytes, upload rate " + DataHelper.formatSize(getUploadRate()) + "Bps");
return rv;
}
/**
* Should we request this many bytes?
*
* @param peer ignored
*/
public boolean shouldRequest(Peer peer, int size) {
boolean rv = !overDownBWLimit() && _req.offer(size, 1.0f);
if (!rv && _log.shouldWarn())
_log.warn("Deny requesting " + size + " bytes, download rate " + DataHelper.formatSize(getDownloadRate()) + "Bps" +
", request rate " + DataHelper.formatSize(getRequestRate()) + "Bps");
return rv;
}
/**
* Current limit in BPS
*/
public long getUpBWLimit() {
return _up.getMaxBandwidth();
}
/**
* Current limit in BPS
*/
public long getDownBWLimit() {
return _down.getMaxBandwidth();
}
/**
* Are we currently over the limit?
*/
public boolean overUpBWLimit() {
return getUploadRate() > getUpBWLimit();
}
/**
* Are we currently over the limit?
*/
public boolean overDownBWLimit() {
return getDownloadRate() > getDownBWLimit();
}
/**
* In HTML for debug page
*/
@Override
public String toString() {
return "<br><b>Bandwidth Limiters</b><br><b>Up:</b> " + _up +
"<br><b>Down:</b> " + _down +
"<br><b>Req:</b> " + _req +
"<br>";
}
}
...@@ -20,9 +20,8 @@ ...@@ -20,9 +20,8 @@
package org.klomp.snark; package org.klomp.snark;
import java.util.Iterator; import java.util.Arrays;
import java.util.Set;
import java.util.HashSet;
/** /**
* Container of a byte array representing set and unset bits. * Container of a byte array representing set and unset bits.
...@@ -32,6 +31,7 @@ public class BitField ...@@ -32,6 +31,7 @@ public class BitField
private final byte[] bitfield; private final byte[] bitfield;
private final int size; private final int size;
private int count;
/** /**
* Creates a new BitField that represents <code>size</code> unset bits. * Creates a new BitField that represents <code>size</code> unset bits.
...@@ -48,7 +48,7 @@ public class BitField ...@@ -48,7 +48,7 @@ public class BitField
* as set by the given byte array. This will make a copy of the array. * as set by the given byte array. This will make a copy of the array.
* Extra bytes will be ignored. * Extra bytes will be ignored.
* *
* @exception ArrayOutOfBoundsException if give byte array is not large * @throws IndexOutOfBoundsException if give byte array is not large
* enough. * enough.
*/ */
public BitField(byte[] bitfield, int size) public BitField(byte[] bitfield, int size)
...@@ -60,13 +60,19 @@ public class BitField ...@@ -60,13 +60,19 @@ public class BitField
// XXX - More correct would be to check that unused bits are // XXX - More correct would be to check that unused bits are
// cleared or clear them explicitly ourselves. // cleared or clear them explicitly ourselves.
System.arraycopy(bitfield, 0, this.bitfield, 0, arraysize); System.arraycopy(bitfield, 0, this.bitfield, 0, arraysize);
for (int i = 0; i < size; i++)
if (get(i))
this.count++;
} }
/** /**
* This returns the actual byte array used. Changes to this array * This returns the actual byte array used. Changes to this array
* effect this BitField. Note that some bits at the end of the byte * affect this BitField. Note that some bits at the end of the byte
* array are supposed to be always unset if they represent bits * array are supposed to be always unset if they represent bits
* bigger then the size of the bitfield. * bigger then the size of the bitfield.
*
* Caller should synch on this and copy!
*/ */
public byte[] getFieldBytes() public byte[] getFieldBytes()
{ {
...@@ -86,7 +92,7 @@ public class BitField ...@@ -86,7 +92,7 @@ public class BitField
/** /**
* Sets the given bit to true. * Sets the given bit to true.
* *
* @exception IndexOutOfBoundsException if bit is smaller then zero * @throws IndexOutOfBoundsException if bit is smaller then zero
* bigger then size (inclusive). * bigger then size (inclusive).
*/ */
public void set(int bit) public void set(int bit)
...@@ -95,13 +101,49 @@ public class BitField ...@@ -95,13 +101,49 @@ public class BitField
throw new IndexOutOfBoundsException(Integer.toString(bit)); throw new IndexOutOfBoundsException(Integer.toString(bit));
int index = bit/8; int index = bit/8;
int mask = 128 >> (bit % 8); int mask = 128 >> (bit % 8);
bitfield[index] |= mask; synchronized(this) {
if ((bitfield[index] & mask) == 0) {
count++;
bitfield[index] |= mask;
}
}
}
/**
* Sets the given bit to false.
*
* @throws IndexOutOfBoundsException if bit is smaller then zero
* bigger then size (inclusive).
* @since 0.9.22
*/
public void clear(int bit)
{
if (bit < 0 || bit >= size)
throw new IndexOutOfBoundsException(Integer.toString(bit));
int index = bit/8;
int mask = 128 >> (bit % 8);
synchronized(this) {
if ((bitfield[index] & mask) != 0) {
count--;
bitfield[index] &= ~mask;
}
}
}
/**
* Sets all bits to true.
*
* @since 0.9.21
*/
public void setAll() {
Arrays.fill(bitfield, (byte) 0xff);
count = size;
} }
/** /**
* Return true if the bit is set or false if it is not. * Return true if the bit is set or false if it is not.
* *
* @exception IndexOutOfBoundsException if bit is smaller then zero * @throws IndexOutOfBoundsException if bit is smaller then zero
* bigger then size (inclusive). * bigger then size (inclusive).
*/ */
public boolean get(int bit) public boolean get(int bit)
...@@ -114,10 +156,44 @@ public class BitField ...@@ -114,10 +156,44 @@ public class BitField
return (bitfield[index] & mask) != 0; return (bitfield[index] & mask) != 0;
} }
/**
* Return the number of set bits.
*/
public int count()
{
return count;
}
/**
* Return true if all bits are set.
*/
public boolean complete()
{
return count >= size;
}
/** @since 0.9.33 */
@Override
public int hashCode() {
return (count << 16) ^ size;
}
/** @since 0.9.33 */
@Override
public boolean equals(Object o) {
if (o == null || !(o instanceof BitField))
return false;
BitField bf = (BitField) o;
return count == bf.count() &&
size == bf.size() &&
Arrays.equals(bitfield, bf.getFieldBytes());
}
@Override
public String toString() public String toString()
{ {
// Not very efficient // Not very efficient
StringBuffer sb = new StringBuffer("BitField("); StringBuilder sb = new StringBuilder("BitField(");
sb.append(size).append(")["); sb.append(size).append(")[");
for (int i = 0; i < size; i++) for (int i = 0; i < size; i++)
if (get(i)) if (get(i))
...@@ -129,4 +205,5 @@ public class BitField ...@@ -129,4 +205,5 @@ public class BitField
return sb.toString(); return sb.toString();
} }
} }
package org.klomp.snark;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Simple command line access to various utilities.
* Not a public API. Subject to change.
* Apps and plugins should use specific classes.
*
* @since 0.9.26
*/
public class CommandLine extends net.i2p.util.CommandLine {
protected static final List<String> SCLASSES = Arrays.asList(new String[] {
"org.klomp.snark.MetaInfo",
//"org.klomp.snark.Snark",
//"org.klomp.snark.StaticSnark",
"org.klomp.snark.Storage",
"org.klomp.snark.bencode.BDecoder",
//"org.klomp.snark.web.RunStandalone",
});
protected CommandLine() {}
public static void main(String args[]) {
List<String> classes = new ArrayList<String>(SCLASSES.size() + CLASSES.size());
classes.addAll(SCLASSES);
classes.addAll(CLASSES);
if (args.length > 0) {
exec(args, classes);
}
usage(classes);
System.exit(1);
}
private static void usage(List<String> classes) {
System.err.println("I2PSnark version " + SnarkManager.FULL_VERSION + '\n' +
"USAGE: java -jar /path/to/i2psnark.jar command [args]");
printCommands(classes);
}
}
/* CompleteListener - Callback for Snark events
Copyright (C) 2003 Mark J. Wielaard
This file is part of Snark.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.klomp.snark;
import org.klomp.snark.comments.CommentSet;
/**
* Callback for Snark events.
* @since 0.9.4 moved from Snark.java
*/
public interface CompleteListener {
public void torrentComplete(Snark snark);
public void updateStatus(Snark snark);
/**
* We transitioned from magnet mode, we have now initialized our
* metainfo and storage. The listener should now call getMetaInfo()
* and save the data to disk.
*
* @return the new name for the torrent or null on error
* @since 0.8.4
*/
public String gotMetaInfo(Snark snark);
/**
* @since 0.9
*/
public void fatal(Snark snark, String error);
/**
* @since 0.9.2
*/
public void addMessage(Snark snark, String message);
/**
* @since 0.9.4
*/
public void gotPiece(Snark snark);
/** not really listeners but the easiest way to get back to an optional SnarkManager */
public long getSavedTorrentTime(Snark snark);
public BitField getSavedTorrentBitField(Snark snark);
/**
* @since 0.9.15
*/
public boolean getSavedPreserveNamesSetting(Snark snark);
/**
* @since 0.9.15
*/
public long getSavedUploaded(Snark snark);
/**
* @since 0.9.31
*/
public CommentSet getSavedComments(Snark snark);
/**
* @since 0.9.31
*/
public void locked_saveComments(Snark snark, CommentSet comments);
/**
* @since 0.9.42
*/
public boolean shouldAutoStart();
/**
* @since 0.9.62
*/
public BandwidthListener getBandwidthListener();
}
...@@ -20,168 +20,294 @@ ...@@ -20,168 +20,294 @@
package org.klomp.snark; package org.klomp.snark;
import java.io.*; import java.io.BufferedInputStream;
import java.net.*; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import net.i2p.I2PAppContext;
import net.i2p.I2PException; import net.i2p.I2PException;
import net.i2p.client.streaming.I2PServerSocket; import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocket;
import net.i2p.util.I2PThread; import net.i2p.client.streaming.RouterRestartException;
import net.i2p.data.Hash;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log; import net.i2p.util.Log;
import net.i2p.util.ObjectCounter;
import net.i2p.util.SimpleTimer2;
/** /**
* Accepts connections on a TCP port and routes them to sub-acceptors. * Accepts connections on a I2PServerSocket and routes them to PeerAcceptors.
*/ */
public class ConnectionAcceptor implements Runnable class ConnectionAcceptor implements Runnable
{ {
private static final ConnectionAcceptor _instance = new ConnectionAcceptor(); private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(ConnectionAcceptor.class);
public static final ConnectionAcceptor instance() { return _instance; } private final PeerAcceptor peeracceptor;
private Log _log = new Log(ConnectionAcceptor.class);
private I2PServerSocket serverSocket;
private PeerAcceptor peeracceptor;
private Thread thread; private Thread thread;
private final I2PSnarkUtil _util;
private final ObjectCounter<Hash> _badCounter = new ObjectCounter<Hash>();
private final SimpleTimer2.TimedEvent _cleaner;
private boolean stop; private volatile boolean stop;
private boolean socketChanged;
private ConnectionAcceptor() {} // protocol errors before blacklisting.
private static final int MAX_BAD = 1;
private static final long BAD_CLEAN_INTERVAL = 30*60*1000;
/**
* Multitorrent. Caller MUST call startAccepting()
*/
public ConnectionAcceptor(I2PSnarkUtil util, PeerCoordinatorSet set) {
_util = util;
_cleaner = new Cleaner();
peeracceptor = new PeerAcceptor(set);
}
public synchronized void startAccepting(PeerCoordinatorSet set, I2PServerSocket socket) { /**
if (serverSocket != socket) { * May be called even when already running. May be called to start up again after halt().
if ( (peeracceptor == null) || (peeracceptor.coordinators != set) ) */
peeracceptor = new PeerAcceptor(set); public synchronized void startAccepting() {
serverSocket = socket;
stop = false; stop = false;
socketChanged = true; if (_log.shouldLog(Log.WARN))
_log.warn("ConnectionAcceptor startAccepting new thread? " + (thread == null));
if (thread == null) { if (thread == null) {
thread = new I2PThread(this, "I2PSnark acceptor"); thread = new I2PAppThread(this, "I2PSnark acceptor");
thread.setDaemon(true); thread.setDaemon(true);
thread.start(); thread.start();
_cleaner.reschedule(BAD_CLEAN_INTERVAL, false);
} }
}
} }
public ConnectionAcceptor(I2PServerSocket serverSocket, /**
* Unused (single torrent).
* Do NOT call startAccepting().
*/
public ConnectionAcceptor(I2PSnarkUtil util,
PeerAcceptor peeracceptor) PeerAcceptor peeracceptor)
{ {
this.serverSocket = serverSocket;
this.peeracceptor = peeracceptor; this.peeracceptor = peeracceptor;
_util = util;
socketChanged = false; thread = new I2PAppThread(this, "I2PSnark acceptor");
stop = false;
thread = new I2PThread(this, "I2PSnark acceptor");
thread.setDaemon(true); thread.setDaemon(true);
thread.start(); thread.start();
_cleaner = new Cleaner();
} }
public void halt() /**
* May be restarted later with startAccepting().
*/
public synchronized void halt()
{ {
if (true) throw new RuntimeException("wtf"); if (stop) return;
stop = true; stop = true;
locked_halt();
Thread t = thread;
if (t != null) {
t.interrupt();
thread = null;
}
}
I2PServerSocket ss = serverSocket;
if (ss != null) /**
* Caller must synch
* @since 0.9.9
*/
private void locked_halt()
{
I2PServerSocket ss = _util.getServerSocket();
if (ss != null) {
try try
{ {
ss.close(); ss.close();
} }
catch(I2PException ioe) { } catch(I2PException ioe) { }
}
Thread t = thread; _badCounter.clear();
if (t != null) _cleaner.cancel();
t.interrupt();
} }
public void restart() { /**
serverSocket = I2PSnarkUtil.instance().getServerSocket(); * Effectively unused, would only be called if we changed
socketChanged = true; * I2CP host/port, which is hidden in the gui if in router context
*/
public synchronized void restart() {
Thread t = thread; Thread t = thread;
if (t != null) if (t != null)
t.interrupt(); t.interrupt();
else
startAccepting();
} }
public int getPort() public int getPort()
{ {
return 6881; // serverSocket.getLocalPort(); return TrackerClient.PORT; // serverSocket.getLocalPort();
} }
public void run() public void run()
{
try {
run2();
} finally {
synchronized(this) {
thread = null;
}
}
}
private void run2()
{ {
while(!stop) while(!stop)
{ {
if (socketChanged) { I2PServerSocket serverSocket = _util.getServerSocket();
// ok, already updated
socketChanged = false;
}
while ( (serverSocket == null) && (!stop)) { while ( (serverSocket == null) && (!stop)) {
serverSocket = I2PSnarkUtil.instance().getServerSocket(); if (!(_util.isConnecting() || _util.connected())) {
if (serverSocket == null) stop = true;
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {} break;
}
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
serverSocket = _util.getServerSocket();
} }
if(stop)
break;
try try
{ {
I2PSocket socket = serverSocket.accept(); I2PSocket socket = serverSocket.accept();
if (socket == null) { if (socket == null) {
if (socketChanged) {
continue; continue;
} else {
I2PServerSocket ss = I2PSnarkUtil.instance().getServerSocket();
if (ss != serverSocket) {
serverSocket = ss;
socketChanged = true;
}
}
} else { } else {
Thread t = new I2PThread(new Handler(socket), "Connection-" + socket); if (socket.getPeerDestination().equals(_util.getMyDestination())) {
_log.error("Incoming connection from myself");
try { socket.close(); } catch (IOException ioe) {}
continue;
}
Hash h = socket.getPeerDestination().calculateHash();
if (socket.getLocalPort() == 80) {
_badCounter.increment(h);
if (_log.shouldLog(Log.WARN))
_log.error("Dropping incoming HTTP from " + h);
try { socket.close(); } catch (IOException ioe) {}
continue;
}
int bad = _badCounter.count(h);
if (bad >= MAX_BAD) {
if (_log.shouldLog(Log.WARN))
_log.warn("Rejecting connection from " + h +
" after " + bad + " failures, max is " + MAX_BAD);
try { socket.close(); } catch (IOException ioe) {}
continue;
}
Thread t = new I2PAppThread(new Handler(socket), "I2PSnark incoming connection");
t.start(); t.start();
} }
} }
catch (RouterRestartException rre) {
if (_log.shouldWarn())
_log.warn("Waiting for router restart", rre);
try {
Thread.sleep(2*60*1000);
} catch (InterruptedException ie) {}
while (true) {
if (_util.connected())
break;
if (_util.connect())
break;
try {
Thread.sleep(60*1000);
} catch (InterruptedException ie) { break; }
}
if (_log.shouldWarn())
_log.warn("Router restarted");
}
catch (I2PException ioe) catch (I2PException ioe)
{ {
if (!socketChanged) { int level = stop ? Log.WARN : Log.ERROR;
Snark.debug("Error while accepting: " + ioe, Snark.ERROR); if (_log.shouldLog(level))
stop = true; _log.log(level, "Error while accepting", ioe);
synchronized(this) {
if (!stop) {
locked_halt();
thread = null;
stop = true;
}
}
}
catch (ConnectException ioe)
{
// This is presumed to be due to socket closing by I2PSnarkUtil.disconnect(),
// which does not currently call our halt(), although it should
if (_log.shouldWarn())
_log.warn("Error while accepting", ioe);
synchronized(this) {
if (!stop) {
locked_halt();
thread = null;
stop = true;
}
} }
} }
catch (IOException ioe) catch (IOException ioe)
{ {
Snark.debug("Error while accepting: " + ioe, Snark.ERROR); int level = stop ? Log.WARN : Log.ERROR;
stop = true; if (_log.shouldLog(level))
_log.log(level, "Error while accepting", ioe);
synchronized(this) {
if (!stop) {
locked_halt();
thread = null;
stop = true;
}
}
} }
// catch oom?
} }
if (_log.shouldLog(Log.WARN))
try _log.warn("ConnectionAcceptor closed");
{
if (serverSocket != null)
serverSocket.close();
}
catch (I2PException ignored) { }
throw new RuntimeException("wtf");
} }
private class Handler implements Runnable { private class Handler implements Runnable {
private I2PSocket _socket; private final I2PSocket _socket;
public Handler(I2PSocket socket) { public Handler(I2PSocket socket) {
_socket = socket; _socket = socket;
} }
public void run() { public void run() {
try { try {
InputStream in = _socket.getInputStream(); InputStream in = _socket.getInputStream();
OutputStream out = _socket.getOutputStream(); OutputStream out = _socket.getOutputStream();
// this is for the readahead in PeerAcceptor.connection()
if (true) { in = new BufferedInputStream(in);
in = new BufferedInputStream(in);
//out = new BufferedOutputStream(out);
}
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Handling socket from " + _socket.getPeerDestination().calculateHash().toBase64()); _log.debug("Handling socket from " + _socket.getPeerDestination().calculateHash());
peeracceptor.connection(_socket, in, out); peeracceptor.connection(_socket, in, out);
} catch (PeerAcceptor.ProtocolException ihe) {
_badCounter.increment(_socket.getPeerDestination().calculateHash());
if (_log.shouldLog(Log.INFO))
_log.info("Protocol error from " + _socket.getPeerDestination().calculateHash(), ihe);
try { _socket.close(); } catch (IOException ignored) { }
} catch (IOException ioe) { } catch (IOException ioe) {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Error handling connection from " + _socket.getPeerDestination().calculateHash().toBase64(), ioe); _log.debug("Error handling connection from " + _socket.getPeerDestination().calculateHash(), ioe);
try { _socket.close(); } catch (IOException ignored) { } try { _socket.close(); } catch (IOException ignored) { }
} }
} }
} }
/** @since 0.9.1 */
private class Cleaner extends SimpleTimer2.TimedEvent {
public Cleaner() {
super(_util.getContext().simpleTimer2());
}
public void timeReached() {
if (stop)
return;
_badCounter.clear();
schedule(BAD_CLEAN_INTERVAL);
}
}
} }
...@@ -24,10 +24,23 @@ package org.klomp.snark; ...@@ -24,10 +24,23 @@ package org.klomp.snark;
/** /**
* Callback used when some peer changes state. * Callback used when some peer changes state.
*/ */
public interface CoordinatorListener interface CoordinatorListener
{ {
/** /**
* Called when the PeerCoordinator notices a change in the state of a peer. * Called when the PeerCoordinator notices a change in the state of a peer.
*/ */
void peerChange(PeerCoordinator coordinator, Peer peer); void peerChange(PeerCoordinator coordinator, Peer peer);
/**
* Called when the PeerCoordinator got the MetaInfo via magnet.
* @since 0.8.4
*/
void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo);
/**
* Is this number of uploaders over the per-torrent limit?
*/
public boolean overUploadLimit(int uploaders);
public void addMessage(String message);
} }
package org.klomp.snark;
import net.i2p.data.ByteArray;
/**
* Callback used to fetch data
* @since 0.8.2
*/
interface DataLoader
{
/**
* This is the callback that PeerConnectionOut calls to get the data from disk
* @return bytes or null for errors
*/
public ByteArray loadData(int piece, int begin, int length);
}
package org.klomp.snark;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.Log;
import org.klomp.snark.bencode.BDecoder;
import org.klomp.snark.bencode.BEncoder;
import org.klomp.snark.bencode.BEValue;
import org.klomp.snark.comments.Comment;
import org.klomp.snark.comments.CommentSet;
/**
* REF: BEP 10 Extension Protocol
* @since 0.8.2
* @author zzz
*/
abstract class ExtensionHandler {
public static final int ID_HANDSHAKE = 0;
public static final int ID_METADATA = 1;
public static final String TYPE_METADATA = "ut_metadata";
public static final int ID_PEX = 2;
/** not ut_pex since the compact format is different */
public static final String TYPE_PEX = "i2p_pex";
public static final int ID_DHT = 3;
/** not using the option bit since the compact format is different */
public static final String TYPE_DHT = "i2p_dht";
/** @since 0.9.31 */
public static final int ID_COMMENT = 4;
/** @since 0.9.31 */
public static final String TYPE_COMMENT = "ut_comment";
/** Pieces * SHA1 Hash length, + 25% extra for file names, bencoding overhead, etc */
private static final int MAX_METADATA_SIZE = Storage.MAX_PIECES * 20 * 5 / 4;
private static final int PARALLEL_REQUESTS = 3;
/**
* @param metasize -1 if unknown
* @param pexAndMetadata advertise these capabilities
* @param dht advertise DHT capability
* @param comment advertise ut_comment capability
* @return bencoded outgoing handshake message
*/
public static byte[] getHandshake(int metasize, boolean pexAndMetadata, boolean dht, boolean uploadOnly, boolean comment) {
Map<String, Object> handshake = new HashMap<String, Object>();
Map<String, Integer> m = new HashMap<String, Integer>();
if (pexAndMetadata) {
m.put(TYPE_METADATA, Integer.valueOf(ID_METADATA));
m.put(TYPE_PEX, Integer.valueOf(ID_PEX));
if (metasize >= 0)
handshake.put("metadata_size", Integer.valueOf(metasize));
}
if (dht) {
m.put(TYPE_DHT, Integer.valueOf(ID_DHT));
}
if (comment) {
m.put(TYPE_COMMENT, Integer.valueOf(ID_COMMENT));
}
// include the map even if empty so the far-end doesn't NPE
handshake.put("m", m);
handshake.put("p", Integer.valueOf(TrackerClient.PORT));
handshake.put("v", "I2PSnark");
handshake.put("reqq", Integer.valueOf(PeerState.MAX_PIPELINE));
// BEP 21
if (uploadOnly)
handshake.put("upload_only", Integer.valueOf(1));
return BEncoder.bencode(handshake);
}
public static void handleMessage(Peer peer, PeerListener listener, int id, byte[] bs) {
Log log = I2PAppContext.getGlobalContext().logManager().getLog(ExtensionHandler.class);
if (log.shouldLog(Log.INFO))
log.info("Got extension msg " + id + " length " + bs.length + " from " + peer);
if (id == ID_HANDSHAKE)
handleHandshake(peer, listener, bs, log);
else if (id == ID_METADATA)
handleMetadata(peer, listener, bs, log);
else if (id == ID_PEX)
handlePEX(peer, listener, bs, log);
else if (id == ID_DHT)
handleDHT(peer, listener, bs, log);
else if (id == ID_COMMENT)
handleComment(peer, listener, bs, log);
else if (log.shouldLog(Log.INFO))
log.info("Unknown extension msg " + id + " from " + peer);
}
private static void handleHandshake(Peer peer, PeerListener listener, byte[] bs, Log log) {
if (log.shouldLog(Log.DEBUG))
log.debug("Got handshake msg from " + peer);
try {
// this throws NPE on missing keys
InputStream is = new ByteArrayInputStream(bs);
BDecoder dec = new BDecoder(is);
BEValue bev = dec.bdecodeMap();
Map<String, BEValue> map = bev.getMap();
peer.setHandshakeMap(map);
Map<String, BEValue> msgmap = map.get("m").getMap();
if (log.shouldLog(Log.DEBUG))
log.debug("Peer " + peer + " supports extensions: " + msgmap.keySet());
//if (msgmap.get(TYPE_PEX) != null) {
// if (log.shouldLog(Log.DEBUG))
// log.debug("Peer supports PEX extension: " + peer);
// // peer state calls peer listener calls sendPEX()
//}
//if (msgmap.get(TYPE_DHT) != null) {
// if (log.shouldLog(Log.DEBUG))
// log.debug("Peer supports DHT extension: " + peer);
// // peer state calls peer listener calls sendDHT()
//}
MagnetState state = peer.getMagnetState();
if (msgmap.get(TYPE_METADATA) == null) {
if (log.shouldLog(Log.DEBUG))
log.debug("Peer does not support metadata extension: " + peer);
// drop if we need metainfo and we haven't found anybody yet
synchronized(state) {
if (!state.isInitialized()) {
if (log.shouldLog(Log.DEBUG))
log.debug("Dropping peer, we need metadata! " + peer);
peer.disconnect();
}
}
return;
}
BEValue msize = map.get("metadata_size");
if (msize == null) {
if (log.shouldLog(Log.DEBUG))
log.debug("Peer does not have the metainfo size yet: " + peer);
// drop if we need metainfo and we haven't found anybody yet
synchronized(state) {
if (!state.isInitialized()) {
if (log.shouldLog(Log.DEBUG))
log.debug("Dropping peer, we need metadata! " + peer);
peer.disconnect();
}
}
return;
}
int metaSize = msize.getInt();
if (log.shouldLog(Log.DEBUG))
log.debug("Got the metainfo size: " + metaSize);
int remaining;
synchronized(state) {
if (state.isComplete())
return;
if (state.isInitialized()) {
if (state.getSize() != metaSize) {
if (log.shouldLog(Log.DEBUG))
log.debug("Wrong metainfo size " + metaSize + " from: " + peer);
peer.disconnect();
return;
}
} else {
// initialize it
if (metaSize > MAX_METADATA_SIZE) {
if (log.shouldLog(Log.DEBUG))
log.debug("Huge metainfo size " + metaSize + " from: " + peer);
peer.disconnect(false);
return;
}
if (log.shouldLog(Log.INFO))
log.info("Initialized state, metadata size = " + metaSize + " from " + peer);
state.initialize(metaSize);
}
remaining = state.chunksRemaining();
}
// send requests for chunks
int count = Math.min(remaining, PARALLEL_REQUESTS);
for (int i = 0; i < count; i++) {
int chk;
synchronized(state) {
chk = state.getNextRequest();
}
if (log.shouldLog(Log.INFO))
log.info("Request chunk " + chk + " from " + peer);
// ignore the rv, always request
peer.shouldRequest(state.chunkSize(chk));
sendRequest(peer, chk);
}
} catch (Exception e) {
if (log.shouldLog(Log.WARN))
log.warn("Handshake exception from " + peer, e);
}
}
private static final int TYPE_REQUEST = 0;
private static final int TYPE_DATA = 1;
private static final int TYPE_REJECT = 2;
/**
* REF: BEP 9
* @since 0.8.4
*/
private static void handleMetadata(Peer peer, PeerListener listener, byte[] bs, Log log) {
if (log.shouldLog(Log.DEBUG))
log.debug("Got metadata msg from " + peer);
try {
InputStream is = new ByteArrayInputStream(bs);
BDecoder dec = new BDecoder(is);
BEValue bev = dec.bdecodeMap();
Map<String, BEValue> map = bev.getMap();
int type = map.get("msg_type").getInt();
int piece = map.get("piece").getInt();
MagnetState state = peer.getMagnetState();
if (type == TYPE_REQUEST) {
if (log.shouldLog(Log.DEBUG))
log.debug("Got request for " + piece + " from: " + peer);
byte[] pc;
int totalSize;
synchronized(state) {
pc = state.getChunk(piece);
totalSize = state.getSize();
}
sendPiece(peer, piece, pc, totalSize);
// Do this here because PeerConnectionOut only reports for PIECE messages
peer.uploaded(pc.length);
} else if (type == TYPE_DATA) {
// On close reading of BEP 9, this is the total metadata size.
// Prior to 0.9.21, we sent the piece size, so we can't count on it.
// just ignore it. The actual length will be verified in saveChunk()
//int size = map.get("total_size").getInt();
//if (log.shouldLog(Log.DEBUG))
// log.debug("Got data for " + piece + " length " + size + " from: " + peer);
boolean done;
int chk = -1;
synchronized(state) {
if (state.isComplete())
return;
int len = is.available();
peer.downloaded(len);
// this checks the size
done = state.saveChunk(piece, bs, bs.length - len, len);
if (log.shouldLog(Log.INFO))
log.info("Got chunk " + piece + " from " + peer);
if (!done)
chk = state.getNextRequest();
}
// out of the lock
if (done) {
// Done!
// PeerState will call the listener (peer coord), who will
// check to see if the MagnetState has it
if (log.shouldLog(Log.WARN))
log.warn("Got last chunk from " + peer);
} else {
// get the next chunk
if (log.shouldLog(Log.INFO))
log.info("Request chunk " + chk + " from " + peer);
// ignore the rv, always request
peer.shouldRequest(state.chunkSize(chk));
sendRequest(peer, chk);
}
} else if (type == TYPE_REJECT) {
if (log.shouldLog(Log.WARN))
log.warn("Got reject msg from " + peer);
peer.disconnect(false);
} else {
if (log.shouldLog(Log.WARN))
log.warn("Got unknown metadata msg from " + peer);
peer.disconnect(false);
}
} catch (Exception e) {
if (log.shouldLog(Log.INFO))
log.info("Metadata ext. msg. exception from " + peer, e);
// fatal ?
peer.disconnect(false);
}
}
private static void sendRequest(Peer peer, int piece) {
sendMessage(peer, TYPE_REQUEST, piece);
}
/****
private static void sendReject(Peer peer, int piece) {
sendMessage(peer, TYPE_REJECT, piece);
}
****/
/** REQUEST and REJECT are the same except for message type */
private static void sendMessage(Peer peer, int type, int piece) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("msg_type", Integer.valueOf(type));
map.put("piece", Integer.valueOf(piece));
byte[] payload = BEncoder.bencode(map);
try {
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_METADATA).getInt();
peer.sendExtension(hisMsgCode, payload);
} catch (Exception e) {
// NPE, no metadata capability
//if (log.shouldLog(Log.INFO))
// log.info("Metadata send req msg exception to " + peer, e);
}
}
private static void sendPiece(Peer peer, int piece, byte[] data, int totalSize) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("msg_type", Integer.valueOf(TYPE_DATA));
map.put("piece", Integer.valueOf(piece));
// BEP 9
// "This key has the same semantics as the 'metadata_size' in the extension header"
// which apparently means the same value. Fixed in 0.9.21.
//map.put("total_size", Integer.valueOf(data.length));
map.put("total_size", Integer.valueOf(totalSize));
byte[] dict = BEncoder.bencode(map);
byte[] payload = new byte[dict.length + data.length];
System.arraycopy(dict, 0, payload, 0, dict.length);
System.arraycopy(data, 0, payload, dict.length, data.length);
try {
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_METADATA).getInt();
peer.sendExtension(hisMsgCode, payload);
} catch (Exception e) {
// NPE, no metadata caps
//if (log.shouldLog(Log.INFO))
// log.info("Metadata send piece msg exception to " + peer, e);
}
}
private static final int HASH_LENGTH = 32;
/**
* Can't find a published standard for this anywhere.
* See the libtorrent code.
* Here we use the "added" key as a single string of concatenated
* 32-byte peer hashes.
* added.f and dropped unsupported
* @since 0.8.4
*/
private static void handlePEX(Peer peer, PeerListener listener, byte[] bs, Log log) {
if (log.shouldLog(Log.DEBUG))
log.debug("Got PEX msg from " + peer);
try {
InputStream is = new ByteArrayInputStream(bs);
BDecoder dec = new BDecoder(is);
BEValue bev = dec.bdecodeMap();
Map<String, BEValue> map = bev.getMap();
bev = map.get("added");
if (bev == null)
return;
byte[] ids = bev.getBytes();
if (ids.length < HASH_LENGTH)
return;
int len = Math.min(ids.length, (I2PSnarkUtil.MAX_CONNECTIONS - 1) * HASH_LENGTH);
List<PeerID> peers = new ArrayList<PeerID>(len / HASH_LENGTH);
for (int off = 0; off < len; off += HASH_LENGTH) {
byte[] hash = new byte[HASH_LENGTH];
System.arraycopy(ids, off, hash, 0, HASH_LENGTH);
if (DataHelper.eq(hash, peer.getPeerID().getDestHash()))
continue;
PeerID pID = new PeerID(hash, listener.getUtil());
peers.add(pID);
}
// could include ourselves, listener must remove
listener.gotPeers(peer, peers);
} catch (Exception e) {
if (log.shouldLog(Log.INFO))
log.info("PEX msg exception from " + peer, e);
//peer.disconnect(false);
}
}
/**
* Receive the DHT port numbers
* @since DHT
*/
private static void handleDHT(Peer peer, PeerListener listener, byte[] bs, Log log) {
if (log.shouldLog(Log.DEBUG))
log.debug("Got DHT msg from " + peer);
try {
InputStream is = new ByteArrayInputStream(bs);
BDecoder dec = new BDecoder(is);
BEValue bev = dec.bdecodeMap();
Map<String, BEValue> map = bev.getMap();
int qport = map.get("port").getInt();
int rport = map.get("rport").getInt();
listener.gotPort(peer, qport, rport);
} catch (Exception e) {
if (log.shouldLog(Log.INFO))
log.info("DHT msg exception from " + peer, e);
//peer.disconnect(false);
}
}
/**
* added.f and dropped unsupported
* @param pList non-null
* @since 0.8.4
*/
public static void sendPEX(Peer peer, List<Peer> pList) {
if (pList.isEmpty())
return;
Map<String, Object> map = new HashMap<String, Object>();
byte[] peers = new byte[HASH_LENGTH * pList.size()];
int off = 0;
for (Peer p : pList) {
System.arraycopy(p.getPeerID().getDestHash(), 0, peers, off, HASH_LENGTH);
off += HASH_LENGTH;
}
map.put("added", peers);
byte[] payload = BEncoder.bencode(map);
try {
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_PEX).getInt();
peer.sendExtension(hisMsgCode, payload);
} catch (Exception e) {
// NPE, no PEX caps
//if (log.shouldLog(Log.INFO))
// log.info("PEX msg exception to " + peer, e);
}
}
/**
* Send the DHT port numbers
* @since DHT
*/
public static void sendDHT(Peer peer, int qport, int rport) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("port", Integer.valueOf(qport));
map.put("rport", Integer.valueOf(rport));
byte[] payload = BEncoder.bencode(map);
try {
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_DHT).getInt();
peer.sendExtension(hisMsgCode, payload);
} catch (Exception e) {
// NPE, no DHT caps
//if (log.shouldLog(Log.INFO))
// log.info("DHT msg exception to " + peer, e);
}
}
/**
* Handle comment request and response
*
* Ref: https://blinkenlights.ch/ccms/software/bittorrent.html
* Ref: https://github.com/adrian-bl/bitflu/blob/3cb7fe887dbdea8132e4fa36fbbf5f26cf992db3/plugins/Bitflu/20_DownloadBitTorrent.pm#L3403
* @since 0.9.31
*/
private static void handleComment(Peer peer, PeerListener listener, byte[] bs, Log log) {
if (log.shouldLog(Log.DEBUG))
log.debug("Got comment msg from " + peer);
try {
InputStream is = new ByteArrayInputStream(bs);
BDecoder dec = new BDecoder(is);
BEValue bev = dec.bdecodeMap();
Map<String, BEValue> map = bev.getMap();
int type = map.get("msg_type").getInt();
if (type == 0) {
// request
int num = 20;
BEValue b = map.get("num");
if (b != null)
num = b.getInt();
listener.gotCommentReq(peer, num);
} else if (type == 1) {
// response
List<BEValue> list = map.get("comments").getList();
if (list.isEmpty())
return;
List<Comment> comments = new ArrayList<Comment>(list.size());
long now = I2PAppContext.getGlobalContext().clock().now();
for (BEValue li : list) {
Map<String, BEValue> m = li.getMap();
String owner = m.get("owner").getString();
String text = m.get("text").getString();
// 0-5 range for rating is enforced by Comment constructor
int rating = m.get("like").getInt();
long time = now - (Math.max(0, m.get("timestamp").getInt()) * 1000L);
Comment c = new Comment(text, owner, rating, time, false);
comments.add(c);
}
listener.gotComments(peer, comments);
} else {
if (log.shouldLog(Log.INFO))
log.info("Unknown comment msg type " + type + " from " + peer);
}
} catch (Exception e) {
if (log.shouldLog(Log.INFO))
log.info("Comment msg exception from " + peer, e);
//peer.disconnect(false);
}
}
private static final byte[] COMMENTS_FILTER = new byte[64];
/**
* Send comment request
* @since 0.9.31
*/
public static void sendCommentReq(Peer peer, int num) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("msg_type", Integer.valueOf(0));
map.put("num", Integer.valueOf(num));
map.put("filter", COMMENTS_FILTER);
byte[] payload = BEncoder.bencode(map);
try {
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_COMMENT).getInt();
peer.sendExtension(hisMsgCode, payload);
} catch (Exception e) {
// NPE, no caps
}
}
/**
* Send comments
* Caller must sync on comments
* @param num max to send
* @param comments non-null
* @since 0.9.31
*/
public static void locked_sendComments(Peer peer, int num, CommentSet comments) {
int toSend = Math.min(num, comments.size());
if (toSend <= 0)
return;
Map<String, Object> map = new HashMap<String, Object>();
map.put("msg_type", Integer.valueOf(1));
List<Object> lc = new ArrayList<Object>(toSend);
long now = I2PAppContext.getGlobalContext().clock().now();
int i = 0;
for (Comment c : comments) {
if (i++ >= toSend)
break;
Map<String, Object> mc = new HashMap<String, Object>();
String s = c.getName();
mc.put("owner", s != null ? s : "");
s = c.getText();
mc.put("text", s != null ? s : "");
mc.put("like", Integer.valueOf(c.getRating()));
mc.put("timestamp", Long.valueOf((now - c.getTime()) / 1000L));
lc.add(mc);
}
map.put("comments", lc);
byte[] payload = BEncoder.bencode(map);
try {
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_COMMENT).getInt();
peer.sendExtension(hisMsgCode, payload);
} catch (Exception e) {
// NPE, no caps
}
}
}
package org.klomp.snark; package org.klomp.snark;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.I2PException; import net.i2p.I2PException;
import net.i2p.util.EepGet; import net.i2p.client.I2PClient;
import net.i2p.client.I2PSession; import net.i2p.client.I2PSession;
import net.i2p.data.*; import net.i2p.client.I2PSessionException;
import net.i2p.client.streaming.I2PServerSocket; import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketEepGet;
import net.i2p.client.streaming.I2PSocketManager; import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener;
import net.i2p.client.streaming.I2PSocketManagerFactory; import net.i2p.client.streaming.I2PSocketManagerFactory;
import net.i2p.client.streaming.I2PSocketOptions;
import net.i2p.data.Base32;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.EepGet;
import net.i2p.util.FileUtil;
import net.i2p.util.Log; import net.i2p.util.Log;
import net.i2p.util.SecureDirectory;
import net.i2p.util.SecureFile;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.SimpleTimer;
import net.i2p.util.SystemVersion;
import net.i2p.util.Translate;
import java.io.*; import org.klomp.snark.dht.DHT;
import java.util.*; import org.klomp.snark.dht.KRPC;
/** /**
* I2P specific helpers for I2PSnark * I2P specific helpers for I2PSnark
* We use this class as a sort of context for i2psnark
* so we can run multiple instances of single Snarks
* (but not multiple SnarkManagers, it is still static)
*/ */
public class I2PSnarkUtil { public class I2PSnarkUtil implements DisconnectListener {
private I2PAppContext _context; private final I2PAppContext _context;
private Log _log; private final Log _log;
private static I2PSnarkUtil _instance = new I2PSnarkUtil(); private final String _baseName;
public static I2PSnarkUtil instance() { return _instance; }
private boolean _shouldProxy; private boolean _shouldProxy;
private String _proxyHost; private String _proxyHost;
private int _proxyPort; private int _proxyPort;
private String _i2cpHost; private String _i2cpHost;
private int _i2cpPort; private int _i2cpPort;
private Map _opts; private final Map<String, String> _opts;
private I2PSocketManager _manager; private volatile I2PSocketManager _manager;
private boolean _configured; private volatile boolean _connecting;
private final Set<Hash> _banlist;
private I2PSnarkUtil() { private int _maxUploaders;
_context = I2PAppContext.getGlobalContext(); private int _maxUpBW;
_log = _context.logManager().getLog(Snark.class); private int _maxConnections;
_opts = new HashMap(); private final File _tmpDir;
setProxy("127.0.0.1", 4444); private int _startupDelay;
setI2CPConfig("127.0.0.1", 7654, null); private boolean _collapsePanels;
_configured = false; private boolean _shouldUseOT;
private boolean _shouldUseDHT;
private boolean _enableRatings, _enableComments;
private String _commentsName;
private boolean _areFilesPublic;
private List<String> _openTrackers;
private DHT _dht;
private long _startedTime;
private final DisconnectListener _discon;
private int _maxFilesPerTorrent = SnarkManager.DEFAULT_MAX_FILES_PER_TORRENT;
private static final int EEPGET_CONNECT_TIMEOUT = 45*1000;
private static final int EEPGET_CONNECT_TIMEOUT_SHORT = 5*1000;
public static final int DEFAULT_STARTUP_DELAY = 3;
public static final boolean DEFAULT_COLLAPSE_PANELS = true;
public static final boolean DEFAULT_USE_OPENTRACKERS = true;
public static final int MAX_CONNECTIONS = 24; // per torrent
public static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond";
public static final boolean DEFAULT_USE_DHT = true;
public static final String EEPGET_USER_AGENT = "I2PSnark";
private static final List<String> HIDDEN_I2CP_OPTS = Arrays.asList(new String[] {
PROP_MAX_BW, "inbound.length", "outbound.length", "inbound.quantity", "outbound.quantity"
});
public I2PSnarkUtil(I2PAppContext ctx) {
this(ctx, "i2psnark", null);
}
/**
* @param baseName generally "i2psnark"
* @since Jetty 7
*/
public I2PSnarkUtil(I2PAppContext ctx, String baseName, DisconnectListener discon) {
_context = ctx;
_log = _context.logManager().getLog(I2PSnarkUtil.class);
_baseName = baseName;
_discon = discon;
_opts = new HashMap<String, String>();
//setProxy("127.0.0.1", 4444);
setI2CPConfig("127.0.0.1", I2PClient.DEFAULT_LISTEN_PORT, null);
_banlist = new ConcurrentHashSet<Hash>();
_maxUploaders = Snark.MAX_TOTAL_UPLOADERS;
_maxUpBW = SnarkManager.DEFAULT_MAX_UP_BW;
_maxConnections = MAX_CONNECTIONS;
_startupDelay = DEFAULT_STARTUP_DELAY;
_shouldUseOT = DEFAULT_USE_OPENTRACKERS;
_openTrackers = Collections.emptyList();
_shouldUseDHT = DEFAULT_USE_DHT;
_collapsePanels = DEFAULT_COLLAPSE_PANELS;
_enableRatings = _enableComments = true;
_commentsName = "";
// This is used for both announce replies and .torrent file downloads,
// so it must be available even if not connected to I2CP.
// so much for multiple instances
_tmpDir = new SecureDirectory(ctx.getTempDir(), baseName + '-' + ctx.random().nextInt());
//FileUtil.rmdir(_tmpDir, false);
_tmpDir.mkdirs();
} }
/** /**
...@@ -46,6 +144,7 @@ public class I2PSnarkUtil { ...@@ -46,6 +144,7 @@ public class I2PSnarkUtil {
* host for no proxying) * host for no proxying)
* *
*/ */
/*****
public void setProxy(String host, int port) { public void setProxy(String host, int port) {
if ( (host != null) && (port > 0) ) { if ( (host != null) && (port > 0) ) {
_shouldProxy = true; _shouldProxy = true;
...@@ -58,99 +157,420 @@ public class I2PSnarkUtil { ...@@ -58,99 +157,420 @@ public class I2PSnarkUtil {
} }
_configured = true; _configured = true;
} }
******/
public boolean configured() { return _configured; } /** @since 0.9.1 */
public I2PAppContext getContext() { return _context; }
/**
* @param i2cpHost may be null for no change
* @param i2cpPort may be 0 for no change
* @param opts may be null for no change
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public void setI2CPConfig(String i2cpHost, int i2cpPort, Map opts) { public void setI2CPConfig(String i2cpHost, int i2cpPort, Map opts) {
_i2cpHost = i2cpHost; if (i2cpHost != null)
_i2cpPort = i2cpPort; _i2cpHost = i2cpHost;
if (opts != null) if (i2cpPort > 0)
_opts.putAll(opts); _i2cpPort = i2cpPort;
_configured = true; if (opts != null) {
synchronized(_opts) {
// removed options...
for (Iterator<String> iter = _opts.keySet().iterator(); iter.hasNext(); ) {
String k = iter.next();
if (!HIDDEN_I2CP_OPTS.contains(k) && !opts.containsKey(k))
iter.remove();
}
_opts.putAll(opts);
}
}
// this updates the session options and tells the router
setMaxUpBW(_maxUpBW);
}
public void setMaxUploaders(int limit) {
_maxUploaders = limit;
}
/**
* This updates ALL the session options (not just the bw) and tells the router
* @param limit KBps
*/
public void setMaxUpBW(int limit) {
_maxUpBW = limit;
synchronized(_opts) {
_opts.put(PROP_MAX_BW, Integer.toString(limit * (1024 * 6 / 5))); // add a little for overhead
}
if (_manager != null) {
I2PSession sess = _manager.getSession();
if (sess != null) {
Properties newProps = new Properties();
synchronized(_opts) {
newProps.putAll(_opts);
}
sess.updateOptions(newProps);
}
}
}
public void setMaxConnections(int limit) {
_maxConnections = limit;
}
public void setStartupDelay(int minutes) {
_startupDelay = minutes;
} }
public String getI2CPHost() { return _i2cpHost; } public String getI2CPHost() { return _i2cpHost; }
public int getI2CPPort() { return _i2cpPort; } public int getI2CPPort() { return _i2cpPort; }
public Map getI2CPOptions() { return _opts; }
/**
* @return a copy
*/
public Map<String, String> getI2CPOptions() {
synchronized(_opts) {
return new HashMap<String, String>(_opts);
}
}
public String getEepProxyHost() { return _proxyHost; } public String getEepProxyHost() { return _proxyHost; }
public int getEepProxyPort() { return _proxyPort; } public int getEepProxyPort() { return _proxyPort; }
public boolean getEepProxySet() { return _shouldProxy; } public boolean getEepProxySet() { return _shouldProxy; }
public int getMaxUploaders() { return _maxUploaders; }
/**
* @return KBps
*/
public int getMaxUpBW() { return _maxUpBW; }
public int getMaxConnections() { return _maxConnections; }
public int getStartupDelay() { return _startupDelay; }
/** @since 0.8.9 */
public boolean getFilesPublic() { return _areFilesPublic; }
/** @since 0.8.9 */
public void setFilesPublic(boolean yes) { _areFilesPublic = yes; }
/** @since 0.9.1 */
public File getTempDir() { return _tmpDir; }
/** @since 0.9.58 */
public int getMaxFilesPerTorrent() { return _maxFilesPerTorrent; }
/** @since 0.9.58 */
public void setMaxFilesPerTorrent(int max) { _maxFilesPerTorrent = Math.max(max, 1); }
/** /**
* Connect to the router, if we aren't already * Connect to the router, if we aren't already
*/ */
public boolean connect() { synchronized public boolean connect() {
if (_manager == null) { if (_manager == null) {
Properties opts = new Properties(); _connecting = true;
if (_opts != null) { // try to find why reconnecting after stop
for (Iterator iter = _opts.keySet().iterator(); iter.hasNext(); ) { if (_log.shouldLog(Log.DEBUG))
String key = (String)iter.next(); _log.debug("Connecting to I2P", new Exception("I did it"));
opts.setProperty(key, _opts.get(key).toString()); Properties opts = _context.getProperties();
synchronized(_opts) {
opts.putAll(_opts);
}
// override preference and start with two tunnels. IdleChecker will ramp up/down as necessary
String sin = opts.getProperty("inbound.quantity");
if (sin != null) {
int in;
try {
in = Integer.parseInt(sin);
} catch (NumberFormatException nfe) {
in = 3;
}
if (in > 2)
opts.setProperty("inbound.quantity", "2");
}
String sout = opts.getProperty("outbound.quantity");
if (sout != null) {
int out;
try {
out = Integer.parseInt(sout);
} catch (NumberFormatException nfe) {
out = 3;
} }
if (out > 2)
opts.setProperty("outbound.quantity", "2");
} }
if (opts.containsKey("inbound.backupQuantity"))
opts.setProperty("inbound.backupQuantity", "0");
if (opts.containsKey("outbound.backupQuantity"))
opts.setProperty("outbound.backupQuantity", "0");
if (opts.getProperty("inbound.nickname") == null) if (opts.getProperty("inbound.nickname") == null)
opts.setProperty("inbound.nickname", "I2PSnark"); opts.setProperty("inbound.nickname", _baseName.replace("i2psnark", "I2PSnark"));
if (opts.getProperty("outbound.nickname") == null)
opts.setProperty("outbound.nickname", _baseName.replace("i2psnark", "I2PSnark"));
if (opts.getProperty("outbound.priority") == null)
opts.setProperty("outbound.priority", "-10");
// Dont do this for now, it is set in I2PSocketEepGet for announces,
// we don't need fast handshake for peer connections.
//if (opts.getProperty("i2p.streaming.connectDelay") == null)
// opts.setProperty("i2p.streaming.connectDelay", "500");
if (opts.getProperty(I2PSocketOptions.PROP_CONNECT_TIMEOUT) == null)
opts.setProperty(I2PSocketOptions.PROP_CONNECT_TIMEOUT, "75000");
if (opts.getProperty("i2p.streaming.inactivityTimeout") == null) if (opts.getProperty("i2p.streaming.inactivityTimeout") == null)
opts.setProperty("i2p.streaming.inactivityTimeout", "90000"); opts.setProperty("i2p.streaming.inactivityTimeout", "240000");
if (opts.getProperty("i2p.streaming.inactivityAction") == null) if (opts.getProperty("i2p.streaming.inactivityAction") == null)
opts.setProperty("i2p.streaming.inactivityAction", "1"); opts.setProperty("i2p.streaming.inactivityAction", "1"); // 1 == disconnect, 2 == ping
if (opts.getProperty("i2p.streaming.writeTimeout") == null) if (opts.getProperty("i2p.streaming.initialWindowSize") == null)
opts.setProperty("i2p.streaming.writeTimeout", "90000"); opts.setProperty("i2p.streaming.initialWindowSize", "1");
if (opts.getProperty("i2p.streaming.slowStartGrowthRateFactor") == null)
opts.setProperty("i2p.streaming.slowStartGrowthRateFactor", "1");
//if (opts.getProperty("i2p.streaming.writeTimeout") == null)
// opts.setProperty("i2p.streaming.writeTimeout", "90000");
//if (opts.getProperty("i2p.streaming.readTimeout") == null) //if (opts.getProperty("i2p.streaming.readTimeout") == null)
// opts.setProperty("i2p.streaming.readTimeout", "120000"); // opts.setProperty("i2p.streaming.readTimeout", "120000");
if (opts.getProperty("i2p.streaming.maxConnsPerMinute") == null)
opts.setProperty("i2p.streaming.maxConnsPerMinute", "2");
if (opts.getProperty("i2p.streaming.maxTotalConnsPerMinute") == null)
opts.setProperty("i2p.streaming.maxTotalConnsPerMinute", "8");
if (opts.getProperty("i2p.streaming.maxConnsPerHour") == null)
opts.setProperty("i2p.streaming.maxConnsPerHour", "20");
if (opts.getProperty("i2p.streaming.enforceProtocol") == null)
opts.setProperty("i2p.streaming.enforceProtocol", "true");
if (opts.getProperty("i2p.streaming.disableRejectLogging") == null)
opts.setProperty("i2p.streaming.disableRejectLogging", "true");
if (opts.getProperty("i2p.streaming.answerPings") == null)
opts.setProperty("i2p.streaming.answerPings", "false");
if (opts.getProperty(I2PSocketOptions.PROP_PROFILE) == null)
opts.setProperty(I2PSocketOptions.PROP_PROFILE, Integer.toString(I2PSocketOptions.PROFILE_BULK));
if (opts.getProperty(I2PClient.PROP_SIGTYPE) == null)
opts.setProperty(I2PClient.PROP_SIGTYPE, "EdDSA_SHA512_Ed25519");
if (opts.getProperty("i2cp.leaseSetEncType") == null)
opts.setProperty("i2cp.leaseSetEncType", "4,0");
// assume compressed content
if (opts.getProperty(I2PClient.PROP_GZIP) == null)
opts.setProperty(I2PClient.PROP_GZIP, "false");
_manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts); _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts);
if (_manager != null) {
_startedTime = _context.clock().now();
if (_discon != null)
_manager.addDisconnectListener(this);
}
_connecting = false;
} }
if (_shouldUseDHT && _manager != null && _dht == null)
_dht = new KRPC(_context, _baseName, _manager.getSession());
return (_manager != null); return (_manager != null);
} }
/**
* DisconnectListener interface
* @since 0.9.53
*/
public void sessionDisconnected() {
synchronized(this) {
_manager = null;
_connecting = false;
if (_dht != null) {
_dht.stop();
_dht = null;
}
}
if (_discon != null)
_discon.sessionDisconnected();
}
/**
* @return null if disabled or not started
* @since 0.8.4
*/
public DHT getDHT() { return _dht; }
public boolean connected() { return _manager != null; } public boolean connected() { return _manager != null; }
/** @since 0.9.1 */
public boolean isConnecting() { return _manager == null && _connecting; }
/**
* For FetchAndAdd
* @return null if not connected
* @since 0.9.1
*/
public I2PSocketManager getSocketManager() {
return _manager;
}
/** /**
* Destroy the destination itself * Destroy the destination itself
*/ */
public void disconnect() { public synchronized void disconnect() {
if (_dht != null) {
_dht.stop();
_dht = null;
}
_startedTime = 0;
I2PSocketManager mgr = _manager; I2PSocketManager mgr = _manager;
// FIXME this can cause race NPEs elsewhere
_manager = null; _manager = null;
mgr.destroySocketManager(); _banlist.clear();
if (mgr != null) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Disconnecting from I2P", new Exception("I did it"));
mgr.destroySocketManager();
}
// this will delete a .torrent file d/l in progress so don't do that...
FileUtil.rmdir(_tmpDir, false);
// in case the user will d/l a .torrent file next...
_tmpDir.mkdirs();
} }
/**
* When did we connect to the network?
* For RPC
* @return 0 if not connected
* @since 0.9.30
*/
public long getStartedTime() {
return _startedTime;
}
/** connect to the given destination */ /** connect to the given destination */
I2PSocket connect(PeerID peer) throws IOException { I2PSocket connect(PeerID peer) throws IOException {
I2PSocketManager mgr = _manager;
if (mgr == null)
throw new IOException("No socket manager");
Destination addr = peer.getAddress();
if (addr == null)
throw new IOException("Null address");
if (addr.equals(getMyDestination()))
throw new IOException("Attempt to connect to myself");
Hash dest = addr.calculateHash();
if (_banlist.contains(dest))
throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are banlisted");
try { try {
return _manager.connect(peer.getAddress()); // TODO opts.setPort(xxx); connect(addr, opts)
// DHT moved above 6881 in 0.9.9
I2PSocket rv = _manager.connect(addr);
if (rv != null)
_banlist.remove(dest);
return rv;
} catch (I2PException ie) { } catch (I2PException ie) {
throw new IOException("Unable to reach the peer " + peer + ": " + ie.getMessage()); _banlist.add(dest);
_context.simpleTimer2().addEvent(new Unbanlist(dest), 10*60*1000);
IOException ioe = new IOException("Unable to reach the peer " + peer);
ioe.initCause(ie);
throw ioe;
} }
} }
private class Unbanlist implements SimpleTimer.TimedEvent {
private Hash _dest;
public Unbanlist(Hash dest) { _dest = dest; }
public void timeReached() { _banlist.remove(_dest); }
}
/**
* Fetch the given URL, returning the file it is stored in, or null on error.
* No retries.
*/
public File get(String url) { return get(url, true, 0); }
/**
* @param rewrite if true, convert http://KEY.i2p/foo/announce to http://i2p/KEY/foo/announce
*/
public File get(String url, boolean rewrite) { return get(url, rewrite, 0); }
/**
* @param retries if &lt; 0, set timeout to a few seconds
*/
public File get(String url, int retries) { return get(url, true, retries); }
/** /**
* fetch the given URL, returning the file it is stored in, or null on error * @param retries if &lt; 0, set timeout to a few seconds
*/ */
public File get(String url) { return get(url, true); } public File get(String url, boolean rewrite, int retries) {
public File get(String url, boolean rewrite) { if (_log.shouldLog(Log.DEBUG))
_log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy); _log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy);
File out = null; File out = null;
try { try {
out = File.createTempFile("i2psnark", "url", new File(".")); // we could use the system tmp dir but deleteOnExit() doesn't seem to work on all platforms...
out = SecureFile.createTempFile("i2psnark", null, _tmpDir);
} catch (IOException ioe) { } catch (IOException ioe) {
ioe.printStackTrace(); _log.error("temp file error", ioe);
out.delete(); if (out != null)
out.delete();
return null; return null;
} }
out.deleteOnExit();
String fetchURL = url; String fetchURL = url;
if (rewrite) if (rewrite)
fetchURL = rewriteAnnounce(url); fetchURL = rewriteAnnounce(url);
//_log.debug("Rewritten url [" + fetchURL + "]"); //_log.debug("Rewritten url [" + fetchURL + "]");
EepGet get = new EepGet(_context, _shouldProxy, _proxyHost, _proxyPort, 1, out.getAbsolutePath(), fetchURL); //EepGet get = new EepGet(_context, _shouldProxy, _proxyHost, _proxyPort, retries, out.getAbsolutePath(), fetchURL);
if (get.fetch()) { // Use our tunnel for announces and .torrent fetches too! Make sure we're connected first...
_log.debug("Fetch successful [" + url + "]: size=" + out.length()); int timeout;
if (retries < 0) {
if (!connected())
return null;
timeout = EEPGET_CONNECT_TIMEOUT_SHORT;
retries = 0;
} else {
timeout = EEPGET_CONNECT_TIMEOUT;
if (!connected()) {
if (!connect())
return null;
}
}
EepGet get = new I2PSocketEepGet(_context, _manager, retries, out.getAbsolutePath(), fetchURL);
get.addHeader("User-Agent", EEPGET_USER_AGENT);
if (get.fetch(timeout)) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Fetch successful [" + url + "]: size=" + out.length());
return out; return out;
} else { } else {
_log.warn("Fetch failed [" + url + "]"); if (_log.shouldLog(Log.WARN))
_log.warn("Fetch failed [" + url + "]");
out.delete(); out.delete();
return null; return null;
} }
} }
/**
* Fetch to memory
* @param retries if &lt; 0, set timeout to a few seconds
* @param initialSize buffer size
* @param maxSize fails if greater
* @return null on error
* @since 0.9.4
*/
public byte[] get(String url, boolean rewrite, int retries, int initialSize, int maxSize) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Fetching [" + url + "] to memory");
String fetchURL = url;
if (rewrite)
fetchURL = rewriteAnnounce(url);
int timeout;
if (retries < 0) {
if (!connected())
return null;
timeout = EEPGET_CONNECT_TIMEOUT_SHORT;
retries = 0;
} else {
timeout = EEPGET_CONNECT_TIMEOUT;
if (!connected()) {
if (!connect())
return null;
}
}
ByteArrayOutputStream out = new ByteArrayOutputStream(initialSize);
EepGet get = new I2PSocketEepGet(_context, _manager, retries, -1, maxSize, null, out, fetchURL);
get.addHeader("User-Agent", EEPGET_USER_AGENT);
if (get.fetch(timeout)) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Fetch successful [" + url + "]: size=" + out.size());
return out.toByteArray();
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Fetch failed [" + url + "]");
return null;
}
}
public I2PServerSocket getServerSocket() { public I2PServerSocket getServerSocket() {
I2PSocketManager mgr = _manager; I2PSocketManager mgr = _manager;
if (mgr != null) if (mgr != null)
...@@ -159,29 +579,88 @@ public class I2PSnarkUtil { ...@@ -159,29 +579,88 @@ public class I2PSnarkUtil {
return null; return null;
} }
String getOurIPString() { /**
* Full Base64 of Destination
*/
public String getOurIPString() {
Destination dest = getMyDestination();
if (dest != null)
return dest.toBase64();
return "unknown";
}
/**
* @return dest or null
* @since 0.8.4
*/
Destination getMyDestination() {
if (_manager == null)
return null;
I2PSession sess = _manager.getSession(); I2PSession sess = _manager.getSession();
if (sess != null) { if (sess != null)
Destination dest = sess.getMyDestination(); return sess.getMyDestination();
if (dest != null) return null;
return dest.toBase64(); }
/** Base64 only - static (no naming service) */
static Destination getDestinationFromBase64(String ip) {
if (ip == null) return null;
if (ip.endsWith(".i2p")) {
if (ip.length() < 520)
return null;
try {
return new Destination(ip.substring(0, ip.length()-4)); // sans .i2p
} catch (DataFormatException dfe) {
return null;
}
} else {
try {
return new Destination(ip);
} catch (DataFormatException dfe) {
return null;
}
} }
return "unknown";
} }
private static final int BASE32_HASH_LENGTH = 52; // 1 + Hash.HASH_LENGTH * 8 / 5
/** Base64 Hash or Hash.i2p or name.i2p using naming service */
Destination getDestination(String ip) { Destination getDestination(String ip) {
if (ip == null) return null; if (ip == null) return null;
if (ip.endsWith(".i2p")) { if (ip.endsWith(".i2p")) {
Destination dest = _context.namingService().lookup(ip); if (ip.length() < 520) { // key + ".i2p"
if (dest != null) { if (_manager != null && ip.length() == BASE32_HASH_LENGTH + 8 && ip.endsWith(".b32.i2p")) {
return dest; // Use existing I2PSession for b32 lookups if we have it
} else { // This is much more efficient than using the naming service
try { I2PSession sess = _manager.getSession();
return new Destination(ip.substring(0, ip.length()-4)); // sans .i2p if (sess != null) {
} catch (DataFormatException dfe) { byte[] b = Base32.decode(ip.substring(0, BASE32_HASH_LENGTH));
return null; if (b != null) {
//Hash h = new Hash(b);
Hash h = Hash.create(b);
if (_log.shouldLog(Log.INFO))
_log.info("Using existing session for lookup of " + ip);
try {
return sess.lookupDest(h, 15*1000);
} catch (I2PSessionException ise) {
}
}
}
} }
if (_log.shouldLog(Log.INFO))
_log.info("Using naming service for lookup of " + ip);
return _context.namingService().lookup(ip);
}
if (_log.shouldLog(Log.INFO))
_log.info("Creating Destination for " + ip);
try {
return new Destination(ip.substring(0, ip.length()-4)); // sans .i2p
} catch (DataFormatException dfe) {
return null;
} }
} else { } else {
if (_log.shouldLog(Log.INFO))
_log.info("Creating Destination for " + ip);
try { try {
return new Destination(ip); return new Destination(ip);
} catch (DataFormatException dfe) { } catch (DataFormatException dfe) {
...@@ -190,46 +669,269 @@ public class I2PSnarkUtil { ...@@ -190,46 +669,269 @@ public class I2PSnarkUtil {
} }
} }
public String lookup(String name) {
Destination dest = getDestination(name);
if (dest == null)
return null;
return dest.toBase64();
}
/** /**
* Given http://blah.i2p/foo/announce turn it into http://i2p/blah/foo/announce * Given http://KEY.i2p/foo/announce turn it into http://i2p/KEY/foo/announce
* Given http://tracker.blah.i2p/foo/announce leave it alone
*/ */
String rewriteAnnounce(String origAnnounce) { String rewriteAnnounce(String origAnnounce) {
int destStart = "http://".length(); int destStart = "http://".length();
int destEnd = origAnnounce.indexOf(".i2p"); int destEnd = origAnnounce.indexOf(".i2p");
if (destEnd < destStart + 516)
return origAnnounce;
int pathStart = origAnnounce.indexOf('/', destEnd); int pathStart = origAnnounce.indexOf('/', destEnd);
String rv = "http://i2p/" + origAnnounce.substring(destStart, destEnd) + origAnnounce.substring(pathStart); String rv = "http://i2p/" + origAnnounce.substring(destStart, destEnd) + origAnnounce.substring(pathStart);
//_log.debug("Rewriting [" + origAnnounce + "] as [" + rv + "]"); //_log.debug("Rewriting [" + origAnnounce + "] as [" + rv + "]");
return rv; return rv;
} }
/** hook between snark's logger and an i2p log */ /** @param ot non-null list of announce URLs */
void debug(String msg, int snarkDebugLevel, Throwable t) { public void setOpenTrackers(List<String> ot) {
if (t instanceof OutOfMemoryError) { _openTrackers = ot;
try { Thread.sleep(100); } catch (InterruptedException ie) {} }
try {
t.printStackTrace(); /** List of open tracker announce URLs to use as backups
} catch (Throwable tt) {} * @return non-null, possibly unmodifiable, empty if disabled
try { */
System.out.println("OOM thread: " + Thread.currentThread().getName()); public List<String> getOpenTrackers() {
} catch (Throwable tt) {} if (!shouldUseOpenTrackers())
} return Collections.emptyList();
switch (snarkDebugLevel) { return _openTrackers;
case 0: }
case 1:
_log.error(msg, t); /**
break; * Is this announce URL probably for an open tracker?
case 2: *
_log.warn(msg, t); * @since 0.9.17
break; */
case 3: public boolean isKnownOpenTracker(String url) {
case 4: try {
_log.info(msg, t); URI u = new URI(url);
break; String host = u.getHost();
case 5: return host != null && SnarkManager.KNOWN_OPENTRACKERS.contains(host);
case 6: } catch (URISyntaxException use) {
default: return false;
_log.debug(msg, t); }
break; }
/**
* List of open tracker announce URLs to use as backups even if disabled
* @return non-null
* @since 0.9.4
*/
public List<String> getBackupTrackers() {
return _openTrackers;
}
public void setUseOpenTrackers(boolean yes) {
_shouldUseOT = yes;
}
public boolean shouldUseOpenTrackers() {
return _shouldUseOT;
}
/** @since DHT */
public synchronized void setUseDHT(boolean yes) {
_shouldUseDHT = yes;
if (yes && _manager != null && _dht == null) {
_dht = new KRPC(_context, _baseName, _manager.getSession());
} else if (!yes && _dht != null) {
_dht.stop();
_dht = null;
}
}
/** @since DHT */
public boolean shouldUseDHT() {
return _shouldUseDHT;
}
/** @since 0.9.31 */
public void setRatingsEnabled(boolean yes) {
_enableRatings = yes;
}
/** @since 0.9.31 */
public boolean ratingsEnabled() {
return _enableRatings;
}
/** @since 0.9.31 */
public void setCommentsEnabled(boolean yes) {
_enableComments = yes;
}
/** @since 0.9.31 */
public boolean commentsEnabled() {
return _enableComments;
}
/** @since 0.9.31 */
public void setCommentsName(String name) {
_commentsName = name;
}
/**
* @return non-null, "" if none
* @since 0.9.31
*/
public String getCommentsName() {
return _commentsName == null ? "" : _commentsName;
}
/** @since 0.9.31 */
public boolean utCommentsEnabled() {
return _enableRatings || _enableComments;
}
/** @since 0.9.32 */
public boolean collapsePanels() {
return _collapsePanels;
}
/** @since 0.9.32 */
public void setCollapsePanels(boolean yes) {
_collapsePanels = yes;
}
/**
* Like DataHelper.toHexString but ensures no loss of leading zero bytes
* @since 0.8.4
*/
public static String toHex(byte[] b) {
StringBuilder buf = new StringBuilder(40);
for (int i = 0; i < b.length; i++) {
int bi = b[i] & 0xff;
if (bi < 16)
buf.append('0');
buf.append(Integer.toHexString(bi));
}
return buf.toString();
}
private static final String BUNDLE_NAME = "org.klomp.snark.web.messages";
/** lang in routerconsole.lang property, else current locale */
public String getString(String key) {
return Translate.getString(key, _context, BUNDLE_NAME);
}
/**
* translate a string with a parameter
* This is a lot more expensive than getString(s, ctx), so use sparingly.
*
* @param s string to be translated containing {0}
* The {0} will be replaced by the parameter.
* Single quotes must be doubled, i.e. ' -&gt; '' in the string.
* @param o parameter, not translated.
* To translate parameter also, use _t("foo {0} bar", _t("baz"))
* Do not double the single quotes in the parameter.
* Use autoboxing to call with ints, longs, floats, etc.
*/
public String getString(String s, Object o) {
return Translate.getString(s, o, _context, BUNDLE_NAME);
}
/** {0} and {1} */
public String getString(String s, Object o, Object o2) {
return Translate.getString(s, o, o2, _context, BUNDLE_NAME);
}
/** ngettext @since 0.7.14 */
public String getString(int n, String s, String p) {
return Translate.getString(n, s, p, _context, BUNDLE_NAME);
}
private static final boolean SHOULD_SYNC = !(SystemVersion.isAndroid() || SystemVersion.isARM());
private static final Pattern ILLEGAL_KEY = Pattern.compile("[#=\\r\\n;]");
private static final Pattern ILLEGAL_VALUE = Pattern.compile("[\\r\\n]");
/**
* Same as DataHelper.loadProps() but allows '#' in values,
* so we can have filenames with '#' in them in torrent config files.
* '#' must be in column 1 for a comment.
*
* @since 0.9.58
*/
static void loadProps(Properties props, File f) throws IOException {
BufferedReader in = null;
try {
in = new BufferedReader(new InputStreamReader(new FileInputStream(f), "UTF-8"), 1024);
String line = null;
while ( (line = in.readLine()) != null) {
if (line.trim().length() <= 0) continue;
if (line.charAt(0) == '#') continue;
if (line.charAt(0) == ';') continue;
int split = line.indexOf('=');
if (split <= 0) continue;
String key = line.substring(0, split);
String val = line.substring(split+1).trim();
props.setProperty(key, val);
}
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
/**
* Same as DataHelper.loadProps() but allows '#' in values,
* so we can have filenames with '#' in them in torrent config files.
* '#' must be in column 1 for a comment.
*
* @since 0.9.58
*/
static void storeProps(Properties props, File file) throws IOException {
FileOutputStream fos = null;
PrintWriter out = null;
IOException ioe = null;
File tmpFile = new File(file.getPath() + ".tmp");
try {
fos = new SecureFileOutputStream(tmpFile);
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")));
out.println("# NOTE: This I2P config file must use UTF-8 encoding");
out.println("# Last saved: " + DataHelper.formatTime(System.currentTimeMillis()));
for (Map.Entry<Object, Object> entry : props.entrySet()) {
String name = (String) entry.getKey();
String val = (String) entry.getValue();
if (ILLEGAL_KEY.matcher(name).find()) {
if (ioe == null)
ioe = new IOException("Invalid character (one of \"#;=\\r\\n\") in key: \"" +
name + "\" = \"" + val + '\"');
continue;
}
if (ILLEGAL_VALUE.matcher(val).find()) {
if (ioe == null)
ioe = new IOException("Invalid character (one of \"\\r\\n\") in value: \"" +
name + "\" = \"" + val + '\"');
continue;
}
out.println(name + "=" + val);
}
if (SHOULD_SYNC) {
out.flush();
fos.getFD().sync();
}
out.close();
if (out.checkError()) {
out = null;
tmpFile.delete();
throw new IOException("Failed to write properties to " + tmpFile);
}
out = null;
if (!FileUtil.rename(tmpFile, file))
throw new IOException("Failed rename from " + tmpFile + " to " + file);
} finally {
if (out != null) out.close();
if (fos != null) try { fos.close(); } catch (IOException e) {}
} }
if (ioe != null)
throw ioe;
} }
} }
/*
* Released into the public domain
* with no warranty of any kind, either expressed or implied.
*/
package org.klomp.snark;
import java.util.Map;
import java.util.Properties;
import net.i2p.client.I2PSession;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;
/**
* Periodically check for idle condition based on connected peers,
* and reduce/restore tunnel count as necessary.
* We can't use the I2CP idle detector because it's based on traffic,
* so DHT and announces would keep it non-idle.
*
* @since 0.9.7
*/
class IdleChecker extends SimpleTimer2.TimedEvent {
private final SnarkManager _mgr;
private final I2PSnarkUtil _util;
private final PeerCoordinatorSet _pcs;
private final Log _log;
private int _consec;
private int _consecNotRunning;
private boolean _isIdle;
private String _lastIn = DEFAULT_QTY;
private String _lastOut = DEFAULT_QTY;
private final Object _lock = new Object();
private static final long CHECK_TIME = 63*1000;
private static final int MAX_CONSEC_IDLE = 4;
private static final int MAX_CONSEC_NOT_RUNNING = 20;
private static final String DEFAULT_QTY = "2";
/**
* Caller must schedule
*/
public IdleChecker(SnarkManager mgr, PeerCoordinatorSet pcs) {
super(mgr.util().getContext().simpleTimer2());
_util = mgr.util();
_log = _util.getContext().logManager().getLog(IdleChecker.class);
_mgr = mgr;
_pcs = pcs;
}
public void timeReached() {
synchronized (_lock) {
locked_timeReached();
}
}
private void locked_timeReached() {
if (_util.connected()) {
boolean torrentRunning = false;
int peerCount = 0;
for (PeerCoordinator pc : _pcs) {
if (!pc.halted()) {
torrentRunning = true;
peerCount += pc.getPeers();
}
}
if (torrentRunning) {
_consecNotRunning = 0;
} else {
if (_consecNotRunning++ >= MAX_CONSEC_NOT_RUNNING) {
if (_log.shouldLog(Log.WARN))
_log.warn("Closing tunnels on idle");
_util.disconnect();
_mgr.addMessage(_util.getString("No more torrents running.") + ' ' +
_util.getString("I2P tunnel closed."));
schedule(3 * CHECK_TIME);
return;
}
}
if (peerCount > 0) {
restoreTunnels(peerCount);
} else {
if (!_isIdle) {
if (_consec++ >= MAX_CONSEC_IDLE)
reduceTunnels();
else
restoreTunnels(1); // pretend we have one peer for now
}
}
} else {
_isIdle = false;
_consec = 0;
_consecNotRunning = 0;
_lastIn = DEFAULT_QTY;
_lastOut = DEFAULT_QTY;
}
schedule(CHECK_TIME);
}
/**
* Reduce to 1 in / 1 out tunnel
*/
private void reduceTunnels() {
_isIdle = true;
if (_log.shouldLog(Log.INFO))
_log.info("Reducing tunnels on idle");
setTunnels("1", "1", "0", "0");
}
/**
* Restore or adjust tunnel count based on current peer count
* @param peerCount greater than zero
*/
private void restoreTunnels(int peerCount) {
if (_isIdle && _log.shouldLog(Log.INFO))
_log.info("Restoring tunnels on activity");
_isIdle = false;
Map<String, String> opts = _util.getI2CPOptions();
String i = opts.get("inbound.quantity");
if (i == null)
i = Integer.toString(SnarkManager.DEFAULT_TUNNEL_QUANTITY);
String o = opts.get("outbound.quantity");
if (o == null)
o = Integer.toString(SnarkManager.DEFAULT_TUNNEL_QUANTITY);
String ib = opts.get("inbound.backupQuantity");
if (ib == null)
ib = "0";
String ob= opts.get("outbound.backupQuantity");
if (ob == null)
ob = "0";
// we don't need more tunnels than we have peers, reduce if so
// reduce to max(peerCount / 2, 2)
int in, out;
try {
in = Integer.parseInt(i);
} catch (NumberFormatException nfe) {
in = 3;
}
try {
out = Integer.parseInt(o);
} catch (NumberFormatException nfe) {
out = 3;
}
int target = Math.max(peerCount / 2, 2);
if (target < in && in > 2) {
in = target;
i = Integer.toString(in);
}
if (target < out && out > 2) {
out = target;
o = Integer.toString(out);
}
if (!(_lastIn.equals(i) && _lastOut.equals(o)))
setTunnels(i, o, ib, ob);
}
/**
* Set in / out / in backup / out backup tunnel counts
*/
private void setTunnels(String i, String o, String ib, String ob) {
_consec = 0;
I2PSocketManager mgr = _util.getSocketManager();
if (mgr != null) {
I2PSession sess = mgr.getSession();
if (sess != null) {
if (_log.shouldLog(Log.INFO))
_log.info("New tunnel settings " + i + " / " + o + " / " + ib + " / " + ob);
Properties newProps = new Properties();
newProps.setProperty("inbound.quantity", i);
newProps.setProperty("outbound.quantity", o);
newProps.setProperty("inbound.backupQuantity", ib);
newProps.setProperty("outbound.backupQuantity", ob);
sess.updateOptions(newProps);
_lastIn = i;
_lastOut = o;
}
}
}
}
package org.klomp.snark;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import net.i2p.data.DataHelper;
import net.i2p.util.RandomSource;
import org.klomp.snark.bencode.BDecoder;
import org.klomp.snark.bencode.BEValue;
/**
* Simple state for the download of the metainfo, shared between
* Peer and ExtensionHandler.
*
* Nothing is synchronized here!
* Caller must synchronize on this for everything!
*
* Reference: BEP 9
*
* @since 0.8.4
* author zzz
*/
class MagnetState {
public static final int CHUNK_SIZE = 16*1024;
private final byte[] infohash;
private boolean complete;
/** if false, nothing below is valid */
private boolean isInitialized;
private int metaSize;
private int totalChunks;
/** bitfield for the metainfo chunks - will remain null if we start out complete */
private BitField requested;
private BitField have;
/** bitfield for the metainfo */
private byte[] metainfoBytes;
/** only valid when finished */
private MetaInfo metainfo;
/**
* @param meta null for new magnet
*/
public MagnetState(byte[] iHash, MetaInfo meta) {
infohash = iHash;
if (meta != null) {
metainfo = meta;
initialize(meta.getInfoBytesLength());
complete = true;
}
}
/**
* Call this for a new magnet when you have the size
* @throws IllegalArgumentException
*/
public void initialize(int size) {
if (isInitialized)
throw new IllegalArgumentException("already set");
isInitialized = true;
metaSize = size;
totalChunks = (size + (CHUNK_SIZE - 1)) / CHUNK_SIZE;
if (metainfo != null) {
metainfoBytes = metainfo.getInfoBytes();
} else {
// we don't need these if complete
have = new BitField(totalChunks);
requested = new BitField(totalChunks);
metainfoBytes = new byte[metaSize];
}
}
/**
* Call this for a new magnet when the download is complete.
* @throws IllegalArgumentException
*/
public void setMetaInfo(MetaInfo meta) {
metainfo = meta;
}
/**
* @throws IllegalArgumentException
*/
public MetaInfo getMetaInfo() {
if (!complete)
throw new IllegalArgumentException("not complete");
return metainfo;
}
/**
* @throws IllegalArgumentException
*/
public int getSize() {
if (!isInitialized)
throw new IllegalArgumentException("not initialized");
return metaSize;
}
public boolean isInitialized() {
return isInitialized;
}
public boolean isComplete() {
return complete;
}
public int chunkSize(int chunk) {
return Math.min(CHUNK_SIZE, metaSize - (chunk * CHUNK_SIZE));
}
/** @return chunk count */
public int chunksRemaining() {
if (!isInitialized)
throw new IllegalArgumentException("not initialized");
if (complete)
return 0;
return totalChunks - have.count();
}
/** @return chunk number */
public int getNextRequest() {
if (!isInitialized)
throw new IllegalArgumentException("not initialized");
if (complete)
throw new IllegalArgumentException("complete");
int rand = RandomSource.getInstance().nextInt(totalChunks);
for (int i = 0; i < totalChunks; i++) {
int chk = (i + rand) % totalChunks;
if (!(have.get(chk) || requested.get(chk))) {
requested.set(chk);
return chk;
}
}
// all requested - end game
for (int i = 0; i < totalChunks; i++) {
int chk = (i + rand) % totalChunks;
if (!have.get(chk))
return chk;
}
throw new IllegalArgumentException("complete");
}
/**
* @throws IllegalArgumentException
*/
public byte[] getChunk(int chunk) {
if (!complete)
throw new IllegalArgumentException("not complete");
if (chunk < 0 || chunk >= totalChunks)
throw new IllegalArgumentException("bad chunk number");
int size = chunkSize(chunk);
byte[] rv = new byte[size];
System.arraycopy(metainfoBytes, chunk * CHUNK_SIZE, rv, 0, size);
// use meta.getInfoBytes() so we don't save it in memory
return rv;
}
/**
* @return true if this was the last piece
* @throws NullPointerException IllegalArgumentException, IOException, ...
*/
public boolean saveChunk(int chunk, byte[] data, int off, int length) throws Exception {
if (!isInitialized)
throw new IllegalArgumentException("not initialized");
if (chunk < 0 || chunk >= totalChunks)
throw new IllegalArgumentException("bad chunk number");
if (have.get(chunk))
return false; // shouldn't happen if synced
int size = chunkSize(chunk);
if (size != length)
throw new IllegalArgumentException("bad chunk length");
System.arraycopy(data, off, metainfoBytes, chunk * CHUNK_SIZE, size);
have.set(chunk);
boolean done = have.complete();
if (done) {
metainfo = buildMetaInfo();
complete = true;
}
return done;
}
/**
* @return true if this was the last piece
* @throws NullPointerException IllegalArgumentException, IOException, ...
*/
private MetaInfo buildMetaInfo() throws Exception {
// top map has nothing in it but the info map (no announce)
Map<String, BEValue> map = new HashMap<String, BEValue>();
InputStream is = new ByteArrayInputStream(metainfoBytes);
BDecoder dec = new BDecoder(is);
BEValue bev = dec.bdecodeMap();
map.put("info", bev);
MetaInfo newmeta = new MetaInfo(map);
if (!DataHelper.eq(newmeta.getInfoHash(), infohash)) {
// Disaster. Start over. ExtensionHandler will catch
// the IOE and disconnect the peer, hopefully we will
// find a new peer.
// TODO: Count fails and give up eventually
have = new BitField(totalChunks);
requested = new BitField(totalChunks);
throw new IOException("info hash mismatch");
}
return newmeta;
}
}
package org.klomp.snark;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import net.i2p.data.Base32;
/**
*
* @since 0.9.4 moved from I2PSnarkServlet
*/
public class MagnetURI {
private final String _tracker;
private final String _name;
private final byte[] _ih;
/** BEP 9 */
public static final String MAGNET = "magnet:";
public static final String MAGNET_FULL = MAGNET + "?xt=urn:btih:";
/** http://sponge.i2p/files/maggotspec.txt */
public static final String MAGGOT = "maggot://";
/**
* https://blog.libtorrent.org/2020/09/bittorrent-v2/
* TODO, dup param parsing, as a dual v1/v2 link
* will contain two xt params
* @since 0.9.48
*/
public static final String MAGNET_FULL_V2 = MAGNET + "?xt=urn:btmh:";
/**
* @param url non-null
*/
public MagnetURI(I2PSnarkUtil util, String url) throws IllegalArgumentException {
String ihash;
String name;
String trackerURL = null;
if (url.startsWith(MAGNET)) {
// magnet:?xt=urn:btih:0691e40aae02e552cfcb57af1dca56214680c0c5&tr=http://tracker2.postman.i2p/announce.php
String xt = getParam("xt", url);
// TODO btmh
if (xt == null || !xt.startsWith("urn:btih:"))
throw new IllegalArgumentException();
ihash = xt.substring("urn:btih:".length());
trackerURL = getTrackerParam(url);
name = util.getString("Magnet") + ' ' + ihash;
String dn = getParam("dn", url);
if (dn != null)
name += " (" + dn + ')';
} else if (url.startsWith(MAGGOT)) {
// maggot://0691e40aae02e552cfcb57af1dca56214680c0c5:0b557bbdf8718e95d352fbe994dec3a383e2ede7
ihash = url.substring(MAGGOT.length()).trim();
int col = ihash.indexOf(':');
if (col >= 0)
ihash = ihash.substring(0, col);
name = util.getString("Magnet") + ' ' + ihash;
} else {
throw new IllegalArgumentException();
}
byte[] ih = null;
if (ihash.length() == 32) {
ih = Base32.decode(ihash);
} else if (ihash.length() == 40) {
// Like DataHelper.fromHexString() but ensures no loss of leading zero bytes
ih = new byte[20];
try {
for (int i = 0; i < 20; i++) {
ih[i] = (byte) (Integer.parseInt(ihash.substring(i*2, (i*2) + 2), 16) & 0xff);
}
} catch (NumberFormatException nfe) {
ih = null;
}
}
if (ih == null || ih.length != 20)
throw new IllegalArgumentException();
_ih = ih;
_name = name;
_tracker = trackerURL;
}
/**
* @return 20 bytes or null
*/
public byte[] getInfoHash() {
return _ih;
}
/**
* @return pretty name or null, NOT HTML escaped
*/
public String getName() {
return _name;
}
/**
* @return tracker url or null
*/
public String getTrackerURL() {
return _tracker;
}
/**
* @return first decoded parameter or null
*/
private static String getParam(String key, String uri) {
int idx = uri.indexOf('?' + key + '=');
if (idx >= 0) {
idx += key.length() + 2;
} else {
idx = uri.indexOf('&' + key + '=');
if (idx >= 0)
idx += key.length() + 2;
}
if (idx < 0 || idx > uri.length())
return null;
String rv = uri.substring(idx);
idx = rv.indexOf('&');
if (idx >= 0)
rv = rv.substring(0, idx);
else
rv = rv.trim();
return decode(rv);
}
/**
* @return all decoded parameters or null
* @since 0.9.1
*/
private static List<String> getMultiParam(String key, String uri) {
int idx = uri.indexOf('?' + key + '=');
if (idx >= 0) {
idx += key.length() + 2;
} else {
idx = uri.indexOf('&' + key + '=');
if (idx >= 0)
idx += key.length() + 2;
}
if (idx < 0 || idx > uri.length())
return null;
List<String> rv = new ArrayList<String>();
while (true) {
String p = uri.substring(idx);
uri = p;
idx = p.indexOf('&');
if (idx >= 0)
p = p.substring(0, idx);
else
p = p.trim();
rv.add(decode(p));
idx = uri.indexOf('&' + key + '=');
if (idx < 0)
break;
idx += key.length() + 2;
}
return rv;
}
/**
* @return first valid I2P tracker or null
* @since 0.9.1
*/
private static String getTrackerParam(String uri) {
List<String> trackers = getMultiParam("tr", uri);
if (trackers == null)
return null;
for (String t : trackers) {
try {
URI u = new URI(t);
String protocol = u.getScheme();
String host = u.getHost();
if (protocol == null || host == null ||
!protocol.toLowerCase(Locale.US).equals("http") ||
!host.toLowerCase(Locale.US).endsWith(".i2p"))
continue;
return t;
} catch(URISyntaxException use) {}
}
return null;
}
/**
* Decode %xx encoding, convert to UTF-8 if necessary.
* Copied from i2ptunnel LocalHTTPServer.
* Also converts '+' to ' ' so the dn parameter comes out right
* These are coming in via a application/x-www-form-urlencoded form so
* the pluses are in there...
* hopefully any real + is encoded as %2B.
*
* @since 0.9.1
*/
private static String decode(String s) {
if (!(s.contains("%") || s.contains("+")))
return s;
StringBuilder buf = new StringBuilder(s.length());
boolean utf8 = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '+') {
buf.append(' ');
} else if (c != '%') {
buf.append(c);
} else {
try {
int val = Integer.parseInt(s.substring(++i, (++i) + 1), 16);
if ((val & 0x80) != 0)
utf8 = true;
buf.append((char) val);
} catch (IndexOutOfBoundsException ioobe) {
break;
} catch (NumberFormatException nfe) {
break;
}
}
}
if (utf8) {
try {
return new String(buf.toString().getBytes("ISO-8859-1"), "UTF-8");
} catch (UnsupportedEncodingException uee) {}
}
return buf.toString();
}
}
...@@ -23,10 +23,13 @@ package org.klomp.snark; ...@@ -23,10 +23,13 @@ package org.klomp.snark;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import net.i2p.util.SimpleTimer; import net.i2p.data.ByteArray;
import net.i2p.util.ByteCache;
// Used to queue outgoing connections /**
// sendMessage() should be used to translate them to wire format. * Used to queue outgoing connections
* sendMessage() should be used to translate them to wire format.
*/
class Message class Message
{ {
final static byte KEEP_ALIVE = -1; final static byte KEEP_ALIVE = -1;
...@@ -39,25 +42,119 @@ class Message ...@@ -39,25 +42,119 @@ class Message
final static byte REQUEST = 6; final static byte REQUEST = 6;
final static byte PIECE = 7; final static byte PIECE = 7;
final static byte CANCEL = 8; final static byte CANCEL = 8;
final static byte PORT = 9; // DHT (BEP 5)
final static byte SUGGEST = 13; // Fast (BEP 6)
final static byte HAVE_ALL = 14; // Fast (BEP 6)
final static byte HAVE_NONE = 15; // Fast (BEP 6)
final static byte REJECT = 16; // Fast (BEP 6)
final static byte ALLOWED_FAST = 17; // Fast (BEP 6)
final static byte EXTENSION = 20; // BEP 10
final static byte HASH_REQUEST = 21; // BEP 52
final static byte HASHES = 22; // BEP 52
final static byte HASH_REJECT = 23; // BEP 52
// Not all fields are used for every message. // Not all fields are used for every message.
// KEEP_ALIVE doesn't have a real wire representation // KEEP_ALIVE doesn't have a real wire representation
byte type; final byte type;
// Used for HAVE, REQUEST, PIECE and CANCEL messages. // Used for HAVE, REQUEST, PIECE and CANCEL messages.
int piece; // Also SUGGEST, REJECT, ALLOWED_FAST
// low byte used for EXTENSION message
// low two bytes used for PORT message
final int piece;
// Used for REQUEST, PIECE and CANCEL messages. // Used for REQUEST, PIECE and CANCEL messages.
int begin; // Also REJECT
int length; final int begin;
final int length;
// Used for PIECE and BITFIELD messages // Used for PIECE and BITFIELD and EXTENSION messages
byte[] data; byte[] data;
int off; final int off;
int len; final int len;
SimpleTimer.TimedEvent expireEvent; // Used to do deferred fetch of data
private final DataLoader dataLoader;
// now unused
//SimpleTimer.TimedEvent expireEvent;
private static final int BUFSIZE = PeerState.PARTSIZE;
private static final ByteCache _cache = ByteCache.getInstance(16, BUFSIZE);
/**
* For types KEEP_ALIVE, CHOKE, UNCHOKE, INTERESTED, UNINTERESTED, HAVE_ALL, HAVE_NONE
* @since 0.9.32
*/
Message(byte type) {
this(type, 0, 0, 0, null, 0, 0, null);
}
/**
* For types HAVE, PORT, SUGGEST, ALLOWED_FAST
* @since 0.9.32
*/
Message(byte type, int piece) {
this(type, piece, 0, 0, null, 0, 0, null);
}
/**
* For types REQUEST, REJECT, CANCEL
* @since 0.9.32
*/
Message(byte type, int piece, int begin, int length) {
this(type, piece, begin, length, null, 0, 0, null);
}
/**
* For type BITFIELD
* @since 0.9.32
*/
Message(byte[] data) {
this(BITFIELD, 0, 0, 0, data, 0, data.length, null);
}
/**
* For type EXTENSION
* @since 0.9.32
*/
Message(int id, byte[] data) {
this(EXTENSION, id, 0, 0, data, 0, data.length, null);
}
/**
* For type PIECE with deferred data
* @since 0.9.32
*/
Message(int piece, int begin, int length, DataLoader loader) {
this(PIECE, piece, begin, length, null, 0, length, loader);
}
/**
* For type PIECE with data
* We don't use this anymore.
* @since 0.9.32
*/
/****
Message(int piece, int begin, int length, byte[] data) {
this(PIECE, piece, begin, length, data, 0, length, null);
}
****/
/**
* @since 0.9.32
*/
private Message(byte type, int piece, int begin, int length, byte[] data, int off, int len, DataLoader loader) {
this.type = type;
this.piece = piece;
this.begin = begin;
this.length = length;
this.data = data;
this.off = off;
this.len = len;
dataLoader = loader;
}
/** Utility method for sending a message through a DataStream. */ /** Utility method for sending a message through a DataStream. */
void sendMessage(DataOutputStream dos) throws IOException void sendMessage(DataOutputStream dos) throws IOException
{ {
...@@ -68,25 +165,46 @@ class Message ...@@ -68,25 +165,46 @@ class Message
return; return;
} }
ByteArray ba;
// Get deferred data
if (data == null && dataLoader != null) {
ba = dataLoader.loadData(piece, begin, length);
if (ba == null)
return; // hmm will get retried, but shouldn't happen
data = ba.getData();
} else {
ba = null;
}
// Calculate the total length in bytes // Calculate the total length in bytes
// Type is one byte. // Type is one byte.
int datalen = 1; int datalen = 1;
// piece is 4 bytes. // piece is 4 bytes.
if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL) if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL ||
type == SUGGEST || type == REJECT || type == ALLOWED_FAST)
datalen += 4; datalen += 4;
// begin/offset is 4 bytes // begin/offset is 4 bytes
if (type == REQUEST || type == PIECE || type == CANCEL) if (type == REQUEST || type == PIECE || type == CANCEL ||
type == REJECT)
datalen += 4; datalen += 4;
// length is 4 bytes // length is 4 bytes
if (type == REQUEST || type == CANCEL) if (type == REQUEST || type == CANCEL ||
type == REJECT)
datalen += 4; datalen += 4;
// msg type is 1 byte
else if (type == EXTENSION)
datalen += 1;
else if (type == PORT)
datalen += 2;
// add length of data for piece or bitfield array. // add length of data for piece or bitfield array.
if (type == BITFIELD || type == PIECE) if (type == BITFIELD || type == PIECE || type == EXTENSION)
datalen += len; datalen += len;
// Send length // Send length
...@@ -94,22 +212,36 @@ class Message ...@@ -94,22 +212,36 @@ class Message
dos.writeByte(type & 0xFF); dos.writeByte(type & 0xFF);
// Send additional info (piece number) // Send additional info (piece number)
if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL) if (type == HAVE || type == REQUEST || type == PIECE || type == CANCEL ||
type == SUGGEST || type == REJECT || type == ALLOWED_FAST)
dos.writeInt(piece); dos.writeInt(piece);
// Send additional info (begin/offset) // Send additional info (begin/offset)
if (type == REQUEST || type == PIECE || type == CANCEL) if (type == REQUEST || type == PIECE || type == CANCEL ||
type == REJECT)
dos.writeInt(begin); dos.writeInt(begin);
// Send additional info (length); for PIECE this is implicit. // Send additional info (length); for PIECE this is implicit.
if (type == REQUEST || type == CANCEL) if (type == REQUEST || type == CANCEL ||
type == REJECT)
dos.writeInt(length); dos.writeInt(length);
else if (type == EXTENSION)
dos.writeByte((byte) piece & 0xff);
else if (type == PORT)
dos.writeShort(piece & 0xffff);
// Send actual data // Send actual data
if (type == BITFIELD || type == PIECE) if (type == BITFIELD || type == PIECE || type == EXTENSION)
dos.write(data, off, len); dos.write(data, off, len);
// Was pulled from cache in Storage.getPiece() via dataLoader
if (ba != null && ba.getData().length == BUFSIZE)
_cache.release(ba, false);
} }
@Override
public String toString() public String toString()
{ {
switch (type) switch (type)
...@@ -125,17 +257,39 @@ class Message ...@@ -125,17 +257,39 @@ class Message
case UNINTERESTED: case UNINTERESTED:
return "UNINTERESTED"; return "UNINTERESTED";
case HAVE: case HAVE:
return "HAVE(" + piece + ")"; return "HAVE(" + piece + ')';
case BITFIELD: case BITFIELD:
return "BITFIELD"; return "BITFIELD";
case REQUEST: case REQUEST:
return "REQUEST(" + piece + "," + begin + "," + length + ")"; return "REQUEST(" + piece + ',' + begin + ',' + length + ')';
case PIECE: case PIECE:
return "PIECE(" + piece + "," + begin + "," + length + ")"; return "PIECE(" + piece + ',' + begin + ',' + length + ')';
case CANCEL: case CANCEL:
return "CANCEL(" + piece + "," + begin + "," + length + ")"; return "CANCEL(" + piece + ',' + begin + ',' + length + ')';
case PORT:
return "PORT(" + piece + ')';
case EXTENSION:
return "EXTENSION(" + piece + ',' + data.length + ')';
// fast extensions below here
case SUGGEST:
return "SUGGEST(" + piece + ')';
case HAVE_ALL:
return "HAVE_ALL";
case HAVE_NONE:
return "HAVE_NONE";
case REJECT:
return "REJECT(" + piece + ',' + begin + ',' + length + ')';
case ALLOWED_FAST:
return "ALLOWED_FAST(" + piece + ')';
// BEP 52 below here
case HASH_REQUEST:
return "HASH_REQUEST";
case HASHES:
return "HASHES";
case HASH_REJECT:
return "HASH_REJECT";
default: default:
return "<UNKNOWN>"; return "UNKNOWN (" + type + ')';
} }
} }
} }