Compare commits

...

64 Commits

Author SHA1 Message Date
jrandom
784d465d17 * 2005-12-22 0.6.1.8 released
2005-12-22  jrandom
    * Bundle the standalone I2PSnark launcher in the installer and update
      process (launch as "java -jar launch-i2psnark.jar", viewing the
      interface on http://localhost:8002/)
    * Don't autostart swarming torrents by default so that you can run a
      standalone I2PSnark from the I2P install dir and not have the embedded
      I2PSnark autolaunch the torrents that the standalone instance is running
    * Fixed a rare streaming lib bug that could let a blocking call wait
      forever.
2005-12-22 12:49:09 +00:00
jrandom
327089c9d1 4matting 2005-12-22 10:10:39 +00:00
jrandom
148dd99c86 2005-12-22 jrandom
* Cleaned up some buffer synchronization issues in I2PSnark that could
       cause blockage.
2005-12-22 10:04:12 +00:00
jrandom
98277d3b64 2005-12-21 jrandom
* Adjusted I2PSnark's usage of the streaming lib (tweaking it for BT's
      behavior)
    * Fixed the I2PSnark bug that would lose track of live peers
2005-12-21 12:04:54 +00:00
cervantes
702e5a5eab 2005-12-19 cervantes
* Silly RDF syntax
    * Oops...
2005-12-21 04:15:36 +00:00
jrandom
54c91731c0 rdf updates for easier fire2pe/xul handling 2005-12-20 10:22:24 +00:00
jrandom
3bfc109476 (cough. not often a problem though) 2005-12-20 02:29:09 +00:00
jrandom
3989638f2d 2005-12-19 jrandom
* Fix for old Syndie blog bookmarks (thanks Complication!)
    * Fix for I2PSnark to accept incoming connections again (oops)
    * Randomize the order that peers from the tracker are contacted
2005-12-20 02:01:37 +00:00
jrandom
4a65fd4f46 2005-12-19 jrandom
* I2PSnark logging, disconnect old inactive peers rather than new ones,
      memory usage reduction, better OOM handling, and a shared connection
      acceptor.
    * Cleaned up the Syndie blog page and the resulting filters (viewing a
      blog from the blog page shows threads started by the selected author,
      not those that they merely participate in)
2005-12-19 13:34:52 +00:00
jrandom
d525c49d45 2005-12-18 jrandom
* Added a standalone runner for the I2PSnark web ui (build with the
      command "ant i2psnark", unzip i2psnark-standalone.zip somewhere, run
      with "java -jar launch-i2psnark.jar", and go to http://localhost:8002/).
    * Further I2PSnark error handling
2005-12-17  jrandom
    * Let multiuser accounts authorize themselves to access the remote
      functionality again (thanks Ch0Hag!)
    * Adjust the JVM heap size to 128MB for new installs (existing users can
      accomplish this by editing wrapper.config, adding the line
      "wrapper.java.maxmemory=128", and then doing a full shutdown and startup
      of the router).  This is relevent for heavy usage of I2PSnark in the
      router console.
2005-12-18 05:52:19 +00:00
jrandom
c287bace0f 2005-12-18 jrandom
* Added a standalone runner for the I2PSnark web ui (build with the
      command "ant i2psnark", unzip i2psnark-standalone.zip somewhere, run
      with "java -jar launch-i2psnark.jar", and go to http://localhost:8002/).
    * Further I2PSnark error handling
2005-12-18 05:39:52 +00:00
jrandom
ee0951b5b2 2005-12-17 jrandom
* Use our faster SHA1, rather than the JVM's within I2PSnark, and let
      'piece' sizes grow larger than before.
2005-12-17 09:22:07 +00:00
jrandom
1eb3ae5e1b 2005-12-16 jrandom
* Added some I2PSnark sanity checks, an OOMListener when running
      standalone, and a guard against keeping memory tied up indefinitely.
    * Sanity check on the watchdog (thanks zzz!)
    * Handle invalid HTTP requests in I2PTunnel a little better
2005-12-17 03:47:02 +00:00
jrandom
7d234b1978 2005-12-16 jrandom
* Moved I2PSnark from using Threads to I2PThreads, so we handle OOMs
      properly (thanks Complication!)
    * More guards in I2PSnark for zany behavior (I2PSession recon w/ skew,
      b0rking in the DirMonitor, etc)
2005-12-16 23:18:56 +00:00
jrandom
6f424fa751 2005-12-16 jrandom
* Try to run a torrent in readonly mode if we can't write to the file, and
      handle failures a little more gracefully (thanks polecat!)
2005-12-16 11:01:20 +00:00
jrandom
7726bd1a5c 2005-12-16 jrandom
* Refuse torrents with too many files (128), avoiding ulimit errors.
    * Remove an fd leak in I2PSnark
    * Further I2PSnark web UI cleanup
2005-12-16 08:24:21 +00:00
jrandom
2a922098d6 refresh link (good idea Complication) 2005-12-16 04:01:05 +00:00
jrandom
3ec92c8b62 2005-12-15 jrandom
* Added a first pass to the I2PSnark web UI (see /i2psnark/)
2005-12-16 03:00:48 +00:00
jrandom
b37bb9372e 2005-12-15 jrandom
* Added multitorrent support to I2PSnark, accessible currently by running
      "i2psnark.jar --config i2psnark.config" (which may or may not exist).
      It then joins the swarm for any torrents in ./i2psnark/*.torrent, saving
      their data in that directory as well.  Removing the .torrent file stops
      participation, and it is currently set to seed indefinitely.  Completion
      is logged to the logger and standard output, with further UI interaction
      left to the (work in progress) web UI.
2005-12-15 08:58:30 +00:00
jrandom
369b6930e5 2005-12-14 jrandom
* Fix to drop peer references when we shitlist people again (thanks zzz!)
    * Further I2PSnark fixes to deal with arbitrary torrent info attributes
      (thanks Complication!)
2005-12-14 09:32:50 +00:00
jrandom
5033a22a9b 2005-12-13 zzz
* Don't test tunnels expiring within 90 seconds
    * Defer Test Tunnel jobs if job lag too large
    * Use JobQueue.getMaxLag() rather than the jobQueue.jobLag stat to measure
      job lag for tunnel build backoff, allowing for more agile handling
      (since the stat is only updated once a minute)
    * Use tunnel length override if all tunnels are expiring within one
      minute.
2005-12-13 21:56:41 +00:00
jrandom
6e5114a4c2 multihop load tests (testing everyone with a fixed set of fast other peers).
not for general purpose use, so you can probably safely ignore this code, but its
useful for testing
2005-12-13 21:32:50 +00:00
jrandom
77c818a0b1 toolbar.html isn't in cvs yet (polecat - please check this diff :) 2005-12-13 09:43:41 +00:00
jrandom
7ac673cea4 2005-12-13 jrandom
* Fixed I2PSnark's handling of some torrent files to deal with those
      created by Azureus and I2PRufus (it didn't know how to deal with
      additional meta info, such as path.utf-8 or name.utf-8).
2005-12-13 09:38:51 +00:00
polecat
e5fa7e0ae4 Navbar is now customizeable via docs/toolbar.html. There is a default should that file not be there. And... wtf, didn't my syndie thumbnail patch take? Well, no conflicts reported so, here it goes again. 2005-12-13 08:18:59 +00:00
jrandom
ab4f3008cb 2005-12-09 zzz
* Create different strategies for exploratory tunnels (which are difficult
      to create) and client tunnels (which are much easier)
    * Gradually increase number of parallel build attempts as tunnel expiry
      nears.
    * Temporarily shorten attempted build tunnel length if builds using
      configured tunnel length are unsuccessful
    * React more aggressively to tunnel failure than routine tunnel
      replacement
    * Make tunnel creation times randomized - there is existing code to
      randomize the tunnels but it isn't effective due to the tunnel creation
      strategy. Currently, most tunnels get built all at once, at about 2 1/2
      to 3 minutes before expiration. The patch fixes this by fixing the
      randomization, and by changing the overlap time (with old tunnels) to a
      range of 2 to 4 minutes.
    * Reduce number of excess tunnels. Lots of excess tunnels get created due
      to overlapping calls. Just about anything generated a call which could
      build many tunnels all at once, even if tunnel building was already in
      process.
    * Miscellaneous router console enhancements
2005-12-09 08:05:44 +00:00
jrandom
f738a02760 link to the filtered blog, not to the current viewBlogs page 2005-12-08 21:01:04 +00:00
jrandom
7d64ecb628 2005-12-08 jrandom
* Minor bugfix in SSU for dealing with corrupt packets
    * Added some hooks for load testing
2005-12-08 20:53:41 +00:00
jrandom
7beacff028 2005-12-07 jrandom
* Added a first pass at a blog view in Syndie
2005-12-08 00:50:32 +00:00
jrandom
952bcc696a 2005-12-07 jrandom
* Expand the thread we're viewing to its leaf
    * Bugfix on intraday ordering (children are always newer than parents)
2005-12-07 20:19:42 +00:00
jrandom
19bba048f2 2005-12-05 jrandom
* Added an RDF and XML thread export to Syndie, reachable at
      .../threadnav/rdf or .../threadnav/xml, accepting the parameters
      count=$numThreads and offset=$threadIndex.  If the $numThreads is -1, it
      displays all threads.
2005-12-05 06:14:15 +00:00
jrandom
5966fcf5ff 2005-12-04 TLorD
* Patch for the C SAM library to null terminate strings on copy (thanks!)
2005-12-04 20:12:19 +00:00
jrandom
fbd7feee61 2005-12-04 jrandom
* Bugfix in Syndie for a problem in the threaded indexer (thanks CofE!)
    * Always include ourselves in the favorite authors (since we don't
      bookmark ourselves)
2005-12-04 20:02:24 +00:00
polecat
5faca98176 I am such a useless bitch. 2005-12-04 17:04:10 +00:00
polecat
99951bf815 Adding a schema for [link] to handle if you want to display links directly to your attachments within the context of the blog itself. Some redundant code here (3 files modified with cut & paste) so we may want to further abstract the External links: HTML generation code. 2005-12-04 13:55:27 +00:00
jrandom
024b1a04e2 no message 2005-12-04 03:30:32 +00:00
jrandom
a4cc18df76 2005-12-03 jrandom
* Use newgroup-like tags by default in Syndie's interface
2005-12-04 03:18:08 +00:00
jrandom
fef578973a no message 2005-12-03 22:09:01 +00:00
jrandom
35b75a7300 2005-12-03 jrandom
* Added support for a 'most recent posts' view that CofE requested, which
      includes the ability to filter by age (e.g. posts by your favorite
      authors in the last 5 days).
2005-12-03 19:03:44 +00:00
jrandom
1c6c397913 2005-12-03 jrandom
* Adjusted Syndie to use the threaded view that cervantes suggested, which
      displays a a single thread path at a time - from root to leaf - rather
      than a depth first traversal.
2005-12-03 17:33:35 +00:00
jrandom
c96965d364 2005-12-03 jrandom
* Package up a standalone Syndie install into a "syndie-standalone.zip",
      buildable with "ant syndie".  It extracts into ./syndie/, launches with
      "java -jar launchsyndie.jar" (or javaw, on windows, to avoid a dos box),
      running a single user Syndie instance (by default).  It also creates a
      default subscription to syndiemedia without any anonymity (using no
      proxy).  Upgrades can be done by just replacing the syndie.war with the
      one from I2P.
2005-12-03 05:41:25 +00:00
jrandom
12900ca709 * 2005-12-01 0.6.1.7 released
2005-12-01  jrandom
    * Add a new criteria to the tunnel join throttle, backing off people if we
      are failing to talk to our peers more than usual.
2005-12-01 17:16:53 +00:00
jrandom
f5b829a124 2005-11-30 jrandom
* Cleaned up the build process to deal with Jetty 5.1.6 and rename the
      new commons-logging-api.jar to commons-logging.jar, which it replaces.
      Jetty 5.1.6 is pushed with all updates.  Also, no need to push a
      separate jdom or rome, as they're inside syndie.war.
2005-12-01 01:13:44 +00:00
jrandom
f62a6d3ce6 2005-11-30 jrandom
* Don't let the TCP transport alone shitlist a peer, since other
      transports may be working.  Also display whether TCP connections are
      inbound or outbound on the peers page.
    * Fixed some substantial bugs in the SSU introducers where we wouldn't
      talk to anyone who didn't expose an IP (even if they had introducers),
      among other goofy things.
    * When dealing with SSU introducers, send them all a packet at 3s/6s/9s,
      rather than sending one a packet at 3s, then another a packet at 6s,
      and a third a packet at 9s.
    * Fixed Syndie attachments (oops)
2005-11-30 20:48:25 +00:00
jrandom
3d18bf870b 2005-11-29 zzz
* Added a link to orion's jump page on the 'key not found' error page.
2005-11-29 17:56:02 +00:00
jrandom
d8071296eb 2005-11-29 jrandom
* Further Syndie UI cleanup
    * Bundled our patched MultiPartRequest code from jetty (APL2 licensed),
      since it hasn't been applied to the jetty CVS yet [1].  Its packaged
      into syndie.jar and renamed to net.i2p.syndie.web.MultiPartRequest, but
      will be removed as soon as its integrated into Jetty.  This patch allows
      posting content in various character sets.
      [1] http://article.gmane.org/gmane.comp.java.jetty.general/6031
    * Upgraded new installs to the latest stable jetty (5.1.6), though this
      isn't pushed as part of the update yet, as there aren't any critical
      bugs.
2005-11-29 16:58:01 +00:00
jrandom
c66e3256aa 2005-11-29 jrandom
* Added back in the OSX jbigi, which was accidentally removed a few revs
      back (thanks for the bug report stoerte!)  New installs will get the
      full jbigi, or you can pull the jbigi.jar from CVS by going to
      http://dev.i2p.net/cgi-bin/cvsweb.cgi/i2p/installer/lib/jbigi/jbigi.jar
      and clicking on the first "download" link, saving that jbigi.jar to
      lib/jbigi.jar in your I2P installation directory.  After restarting your
      router, it should load up fine.
2005-11-29 12:46:34 +00:00
jrandom
686742a67b 2005-11-27 jrandom
* Inlined the Syndie CSS to reduce the number of HTTP requests (and
      because firefox [and others?] delay rendering until they fetch the css).
    * Make sure we fire the shutdown tasks when regenerating a new identity
      (thanks picsou!)
    * Cleaned up some of the things I b0rked in the 'dynamic keys' mode
    * Don't drop SSU sessions if they're still transmitting data successfully,
      even if there are transmission failures
    * Adjusted the time summarization to display hours after 119m, not 90m
    * Further EepGet cleanup (grr)
2005-11-28 16:02:38 +00:00
jrandom
cdf94295f3 (1.185) added TheBreton.i2p adab.i2p awup.i2p china.i2p davidkra.i2p
comwiz.i2p dust.i2p eepsites.i2p jmg.i2p kuroneko.i2p
               mywastedlife.i2p site.games.i2p squid2.i2p striker.i2p
               tracker.awup.i2p zzz.i2p
2005-11-26 18:32:22 +00:00
jrandom
fbf1705c4e * 2005-11-26 0.6.1.6 released 2005-11-26 18:26:22 +00:00
jrandom
453ecc4208 Further improvements, and works fine for ubergeeks, but not yet for normal
geeks (aka no router console)
2005-11-26 17:19:29 +00:00
jrandom
d1f2b447ac 2005-11-26 jrandom
* Update the sorting in Syndie to consider children 'newer' than parents,
      even if they have the same message ID (duh)
    * Cleaned up some nav links in Syndie (good idea gloin, spaetz!)
    * Added a bunch of tooltips to Syndie's fields (thanks polecat!)
    * Force support for nonvalidating XML in Jetty (so we can handle GCJ/etc
      better)
2005-11-26 16:51:16 +00:00
jrandom
70c4560f02 2005-11-26 jrandom
* Be more explicit about what messages we will handle through a client
      tunnel, and how we will handle them.  This cuts off a set of attacks
      that an active adversary could mount, though they're probably nonobvious
      and would require at least some sophistication.
2005-11-26 11:39:32 +00:00
jrandom
9089fdd2d5 2005-11-26 Raccoon23
* Added support for 'dynamic keys' mode, where the router creates a new
      router identity whenever it detects a substantial change in its public
      address (read: SSU IP or port).  This only offers minimal additional
      protection against trivial attackers, but should provide functional
      improvement for people who have periodic IP changes, since their new
      router address would not be shitlisted while their old one would be.
    * Added further infrastructure for restricted route operation, but its use
      is not recommended.
2005-11-26 09:16:11 +00:00
jrandom
ef82cc4f20 2005-11-25 jrandom
* Further Syndie UI cleanups
    * Logging cleanup
    * Fixed link to fproxy.tino.i2p (thanks zzz!)
2005-11-26 05:05:52 +00:00
jrandom
f2c2a5b386 2005-11-25 jrandom
* Don't publish stats for periods we haven't reached yet (thanks zzz!)
    * Cleaned up the syndie threaded display to show the last updated date for
      a subthread, and to highlight threads updated in the last two days.
2005-11-25 11:06:00 +00:00
jrandom
fc858bc950 replaced the old nonfunctional perl SAM lib with postman's new
implementation (thanks postman!)
2005-11-24 09:41:01 +00:00
jrandom
dbb4b3d0c2 2005-11-24 jrandom
* Fix to save syndication settings in Syndie (thanks spaetz!)
2005-11-24 08:45:54 +00:00
jrandom
2b841ad667 2005-11-23 jrandom
* Removed spurious streaming lib RTO increase (it wasn't helpful)
    * Streamlined the tunnel batching to schedule batch transmissions more
      appropriately.
    * Default tunnel pool variance to 2 +0-1 hops
2005-11-23 16:04:52 +00:00
jrandom
5e094b43b3 2005-11-21 jrandom
* IE doesn't strip SPAN from <button> form fields, so add in a workaround
      within I2PTunnel.
    * Increase the maximum SSU retransmission timeout to accomodate slower or
      more congested links (though SSU's RTO calculation will usually use a
      much lower timeout)
    * Moved the streaming lib timed events off the main timer queues and onto
      a streaming lib specific set of timer queues.  Streaming lib timed
      events are more likely to have lock contention on the I2CP socket while
      other timed events in the router are (largely) independent.
    * Fixed a case sensitive lookup bug (thanks tino!)
    * Syndie cleanup - new edit form on the preview page, and fixed some blog
      links (thanks tino!)
2005-11-21 14:45:04 +00:00
jrandom
33d57dd545 2005-11-21 jrandom
* IE doesn't strip SPAN from <button> form fields, so add in a workaround
      within I2PTunnel.
    * Increase the maximum SSU retransmission timeout to accomodate slower or
      more congested links (though SSU's RTO calculation will usually use a
      much lower timeout)
    * Moved the streaming lib timed events off the main timer queues and onto
      a streaming lib specific set of timer queues.  Streaming lib timed
      events are more likely to have lock contention on the I2CP socket while
      other timed events in the router are (largely) independent.
    * Fixed a case sensitive lookup bug (thanks tino!)
    * Syndie cleanup - new edit form on the preview page, and fixed some blog
      links (thanks tino!)
2005-11-21 14:37:06 +00:00
jrandom
61f75b5f09 2005-11-19 jrandom
* Implemented a trivial pure java PMTU backoff strategy, switching between
      a 608 byte MTU and a 1350 byte MTU, depending upon retransmission rates.
    * Fixed new user registration in Syndie (thanks Complication!)
2005-11-20 04:42:16 +00:00
jrandom
3f65e53592 2005-11-17 jrandom
* More cautious file handling in Syndie
2005-11-17 08:23:45 +00:00
jrandom
99ae3ee459 2005-11-16 jrandom
* More aggressive I2PTunnel content encoding munging to work around some
      rare HTTP behavior (ignoring q values on Accept-encoding, using gzip
      even when only identity is specified, etc).  I2PTunnelHTTPServer now
      sends "Accept-encoding: \r\n" plus "X-Accept-encoding: x-i2p-gzip\r\n",
      and I2PTunnelHTTPServer handles x-i2p-gzip in either the Accept-encoding
      or X-Accept-encoding headers.  Eepsite operators who do not know to
      check for X-Accept-encoding will simply use the identity encoding.
2005-11-16 11:50:56 +00:00
176 changed files with 7887 additions and 1432 deletions

View File

@@ -24,10 +24,40 @@ JAR_BASE=i2p.jar mstreaming.jar streaming.jar
JAR_CLIENTS=i2ptunnel.jar sam.jar i2psnark.jar
JAR_ROUTER=router.jar
JAR_JBIGI=jbigi.jar
JAR_XML=xml-apis.jar resolver.jar xercesImpl.jar
JAR_CONSOLE=\
javax.servlet.jar \
commons-el.jar \
commons-logging.jar \
jasper-runtime.jar \
ant-apache-bcel.jar \
ant.jar \
jasper-compiler.jar \
org.mortbay.jetty.jar \
routerconsole.jar
JAR_SUCKER=jdom.jar rome-0.7.jar sucker.jar
LIBI2P_JARS=${JAR_BASE} ${JAR_CLIENTS} ${JAR_ROUTER} ${JAR_JBIGI}
# unfortunately, its not quite ready for most end users, as the
# ${JAR_CONSOLE} fails to compile with:
# org/apache/commons/logging/impl/LogKitLogger.java: In class 'org.apache.commons.logging.impl.LogKitLogger':
# .../LogKitLogger.java: In constructor '(java.lang.String)':
# .../LogKitLogger.java:91: error: cannot find file for class org.apache.log.Hierarchy
# .../LogKitLogger.java:91: error: cannot find file for class org.apache.log.Hierarchy
# .../LogKitLogger.java:104: error: cannot find file for class org.apache.log.Hierarchy
# .../LogKitLogger.java:104: confused by earlier errors, bailing out
SYSTEM_PROPS=
#${JAR_CONSOLE}\
#${JAR_XML} \
#${JAR_SUCKER}
#${JAR_CONSOLE}
SYSTEM_PROPS=-DloggerFilenameOverride=logs/log-router-@.txt \
-Dorg.mortbay.http.Version.paranoid=true \
-Dorg.mortbay.util.FileResource.checkAliases=false \
-Dorg.mortbay.xml.XmlParser.NotValidating=true
#SYSTEM_PROPS=-Di2p.weakPRNG=true
OPTIMIZE=-O2
#OPTIMIZE=-O3
LD_LIBRARY_PATH=${EXTRA_LD_PATH}:.
@@ -47,23 +77,24 @@ native_clean:
@mkdir ${NATIVE_DIR}
native_shared: libi2p.so
@cd build ; ${GCJ} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2p_dsa --main=net.i2p.crypto.DSAEngine
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2p_dsa --main=net.i2p.crypto.DSAEngine
@echo "* i2p_dsa is a simple test app with the DSA engine and Fortuna PRNG to make sure crypto is working"
@cd build ; ${GCJ} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2ptunnel --main=net.i2p.i2ptunnel.I2PTunnel
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/prng --main=gnu.crypto.prng.Fortuna
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2ptunnel --main=net.i2p.i2ptunnel.I2PTunnel
@echo "* i2ptunnel is mihi's I2PTunnel CLI"
@echo " run it as ./i2ptunnel -cli to avoid awt complaints"
@cd build ; ${GCJ} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2ptunnelctl --main=net.i2p.i2ptunnel.TunnelControllerGroup
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2ptunnelctl --main=net.i2p.i2ptunnel.TunnelControllerGroup
@echo "* i2ptunnelctl is a controller for I2PTunnel, reading i2ptunnel.config"
@echo " and launching the appropriate proxies"
@cd build ; ${GCJ} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2psnark --main=org.klomp.snark.Snark
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2psnark --main=org.klomp.snark.Snark
@echo "* i2psnark is an anonymous bittorrent client"
@cd build ; ${GCJ} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2prouter --main=net.i2p.router.Router
@cd build ; ${GCJ} ${OPTIMIZE} -fjni -L../${NATIVE_DIR} -li2p ${SYSTEM_PROPS} -o ../${NATIVE_DIR}/i2prouter --main=net.i2p.router.Router
@echo "* i2prouter is the main I2P router"
@echo " it can be used, and while the router console won't load,"
@echo " i2ptunnel will, so it will start all the proxies defined in i2ptunnel.config"
libi2p.so:
@echo "* Building libi2p.so"
@(cd build ; ${GCJ} -fPIC -fjni -shared -o ../${NATIVE_DIR}/libi2p.so ${LIBI2P_JARS} ; cd .. )
@(cd build ; ${GCJ} ${OPTIMIZE} -fPIC -fjni -shared -o ../${NATIVE_DIR}/libi2p.so ${LIBI2P_JARS} ; cd .. )
@ls -l ${NATIVE_DIR}/libi2p.so
@echo "* libi2p.so built"

View File

@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project basedir="." default="all" name="i2psnark">
<target name="all" depends="clean, build" />
<target name="build" depends="builddep, jar" />
<target name="build" depends="builddep, jar, war" />
<target name="builddep">
<ant dir="../../ministreaming/java/" target="build" />
<!-- ministreaming will build core -->
<ant dir="../../jetty/" target="build" />
<ant dir="../../streaming/java/" target="build" />
<!-- streaming will build ministreaming and core -->
</target>
<target name="compile">
<mkdir dir="./build" />
@@ -13,18 +14,75 @@
srcdir="./src"
debug="true" deprecation="on" source="1.3" target="1.3"
destdir="./build/obj"
classpath="../../../core/java/build/i2p.jar:../../ministreaming/java/build/mstreaming.jar" />
classpath="../../../core/java/build/i2p.jar:../../jetty/jettylib/org.mortbay.jetty.jar:../../jetty/jettylib/javax.servlet.jar:../../ministreaming/java/build/mstreaming.jar" />
</target>
<target name="jar" depends="builddep, compile">
<jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class">
<jar destfile="./build/i2psnark.jar" basedir="./build/obj" includes="**/*.class" excludes="**/*Servlet.class">
<manifest>
<attribute name="Main-Class" value="org.klomp.snark.Snark" />
<attribute name="Class-Path" value="i2p.jar mstreaming.jar streaming.jar" />
</manifest>
</jar>
</target>
<target name="war" depends="jar">
<war destfile="../i2psnark.war" webxml="../web.xml">
<classes dir="./build/obj" includes="**/*" />
</war>
</target>
<target name="standalone" depends="standalone_prep">
<zip destfile="i2psnark-standalone.zip">
<zipfileset dir="./dist/" prefix="i2psnark/" />
</zip>
</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" />
<mkdir dir="./dist" />
<copy file="./build/launch-i2psnark.jar" tofile="./dist/launch-i2psnark.jar" />
<mkdir dir="./dist/webapps" />
<copy file="../i2psnark.war" tofile="./dist/webapps/i2psnark.war" />
<mkdir dir="./dist/lib" />
<copy file="../../../core/java/build/i2p.jar" tofile="./dist/lib/i2p.jar" />
<copy file="../../jetty/jettylib/commons-el.jar" tofile="./dist/lib/commons-el.jar" />
<copy file="../../jetty/jettylib/commons-logging.jar" tofile="./dist/lib/commons-logging.jar" />
<copy file="../../jetty/jettylib/javax.servlet.jar" tofile="./dist/lib/javax.servlet.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" />
<copy file="../../jetty/jettylib/jasper-runtime.jar" tofile="./dist/lib/jasper-runtime.jar" />
<copy file="../../ministreaming/java/build/mstreaming.jar" tofile="./dist/lib/mstreaming.jar" />
<copy file="../../streaming/java/build/streaming.jar" tofile="./dist/lib/streaming.jar" />
<copy file="../jetty-i2psnark.xml" tofile="./dist/jetty-i2psnark.xml" />
<copy file="../readme-standalone.txt" tofile="./dist/readme.txt" />
<mkdir dir="./dist/work" />
<mkdir dir="./dist/logs" />
<zip destfile="i2psnark-standalone.zip">
<zipfileset dir="./dist/" prefix="i2psnark/" />
</zip>
</target>
<target name="clean">
<delete dir="./build" />
<delete file="../i2psnark.war" />
<delete file="./i2psnark-standalone.zip" />
</target>
<target name="cleandep" depends="clean">
<ant dir="../../ministreaming/java/" target="distclean" />

View File

@@ -26,31 +26,57 @@ import java.net.*;
import net.i2p.I2PException;
import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
/**
* Accepts connections on a TCP port and routes them to sub-acceptors.
*/
public class ConnectionAcceptor implements Runnable
{
private final I2PServerSocket serverSocket;
private final PeerAcceptor peeracceptor;
private static final ConnectionAcceptor _instance = new ConnectionAcceptor();
public static final ConnectionAcceptor instance() { return _instance; }
private Log _log = new Log(ConnectionAcceptor.class);
private I2PServerSocket serverSocket;
private PeerAcceptor peeracceptor;
private Thread thread;
private boolean stop;
private boolean socketChanged;
private ConnectionAcceptor() {}
public synchronized void startAccepting(PeerCoordinatorSet set, I2PServerSocket socket) {
if (serverSocket != socket) {
if ( (peeracceptor == null) || (peeracceptor.coordinators != set) )
peeracceptor = new PeerAcceptor(set);
serverSocket = socket;
stop = false;
socketChanged = true;
if (thread == null) {
thread = new I2PThread(this, "I2PSnark acceptor");
thread.setDaemon(true);
thread.start();
}
}
}
public ConnectionAcceptor(I2PServerSocket serverSocket,
PeerAcceptor peeracceptor)
{
this.serverSocket = serverSocket;
this.peeracceptor = peeracceptor;
socketChanged = false;
stop = false;
thread = new Thread(this);
thread = new I2PThread(this, "I2PSnark acceptor");
thread.setDaemon(true);
thread.start();
}
public void halt()
{
if (true) throw new RuntimeException("wtf");
stop = true;
I2PServerSocket ss = serverSocket;
@@ -65,6 +91,14 @@ public class ConnectionAcceptor implements Runnable
if (t != null)
t.interrupt();
}
public void restart() {
serverSocket = I2PSnarkUtil.instance().getServerSocket();
socketChanged = true;
Thread t = thread;
if (t != null)
t.interrupt();
}
public int getPort()
{
@@ -75,57 +109,35 @@ public class ConnectionAcceptor implements Runnable
{
while(!stop)
{
if (socketChanged) {
// ok, already updated
socketChanged = false;
}
if (serverSocket == null) {
Snark.debug("Server socket went away.. boo hiss", Snark.ERROR);
stop = true;
return;
}
try
{
final I2PSocket socket = serverSocket.accept();
Thread t = new Thread("Connection-" + socket)
{
public void run()
{
try
{
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
BufferedInputStream bis = new BufferedInputStream(in);
BufferedOutputStream bos = new BufferedOutputStream(out);
// See what kind of connection it is.
/*
if (httpacceptor != null)
{
byte[] scratch = new byte[4];
bis.mark(4);
int len = bis.read(scratch);
if (len != 4)
throw new IOException("Need at least 4 bytes");
bis.reset();
if (scratch[0] == 19 && scratch[1] == 'B'
&& scratch[2] == 'i' && scratch[3] == 't')
peeracceptor.connection(socket, bis, bos);
else if (scratch[0] == 'G' && scratch[1] == 'E'
&& scratch[2] == 'T' && scratch[3] == ' ')
httpacceptor.connection(socket, bis, bos);
}
else
*/
peeracceptor.connection(socket, bis, bos);
}
catch (IOException ioe)
{
try
{
socket.close();
}
catch (IOException ignored) { }
}
I2PSocket socket = serverSocket.accept();
if (socket == null) {
if (socketChanged) {
continue;
} else {
Snark.debug("Null socket accepted, but socket wasn't changed?", Snark.ERROR);
}
};
t.start();
} else {
Thread t = new I2PThread(new Handler(socket), "Connection-" + socket);
t.start();
}
}
catch (I2PException ioe)
{
Snark.debug("Error while accepting: " + ioe, Snark.ERROR);
stop = true;
if (!socketChanged) {
Snark.debug("Error while accepting: " + ioe, Snark.ERROR);
stop = true;
}
}
catch (IOException ioe)
{
@@ -139,5 +151,32 @@ public class ConnectionAcceptor implements Runnable
serverSocket.close();
}
catch (I2PException ignored) { }
throw new RuntimeException("wtf");
}
private class Handler implements Runnable {
private I2PSocket _socket;
public Handler(I2PSocket socket) {
_socket = socket;
}
public void run() {
try {
InputStream in = _socket.getInputStream();
OutputStream out = _socket.getOutputStream();
if (true) {
in = new BufferedInputStream(in);
//out = new BufferedOutputStream(out);
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Handling socket from " + _socket.getPeerDestination().calculateHash().toBase64());
peeracceptor.connection(_socket, in, out);
} catch (IOException ioe) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Error handling connection from " + _socket.getPeerDestination().calculateHash().toBase64(), ioe);
try { _socket.close(); } catch (IOException ignored) { }
}
}
}
}

View File

@@ -3,9 +3,8 @@ package org.klomp.snark;
import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.util.EepGet;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.client.I2PSession;
import net.i2p.data.*;
import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketManager;
@@ -13,7 +12,7 @@ import net.i2p.client.streaming.I2PSocketManagerFactory;
import net.i2p.util.Log;
import java.io.*;
import java.util.Properties;
import java.util.*;
/**
* I2P specific helpers for I2PSnark
@@ -29,14 +28,17 @@ public class I2PSnarkUtil {
private int _proxyPort;
private String _i2cpHost;
private int _i2cpPort;
private Properties _opts;
private Map _opts;
private I2PSocketManager _manager;
private boolean _configured;
private I2PSnarkUtil() {
_context = I2PAppContext.getGlobalContext();
_log = _context.logManager().getLog(Snark.class);
_opts = new HashMap();
setProxy("127.0.0.1", 4444);
setI2CPConfig("127.0.0.1", 7654, null);
_configured = false;
}
/**
@@ -54,25 +56,63 @@ public class I2PSnarkUtil {
_proxyHost = null;
_proxyPort = -1;
}
_configured = true;
}
public void setI2CPConfig(String i2cpHost, int i2cpPort, Properties opts) {
public boolean configured() { return _configured; }
public void setI2CPConfig(String i2cpHost, int i2cpPort, Map opts) {
_i2cpHost = i2cpHost;
_i2cpPort = i2cpPort;
if (opts != null)
_opts = opts;
_opts.putAll(opts);
_configured = true;
}
public String getI2CPHost() { return _i2cpHost; }
public int getI2CPPort() { return _i2cpPort; }
public Map getI2CPOptions() { return _opts; }
public String getEepProxyHost() { return _proxyHost; }
public int getEepProxyPort() { return _proxyPort; }
public boolean getEepProxySet() { return _shouldProxy; }
/**
* Connect to the router, if we aren't already
*/
boolean connect() {
public boolean connect() {
if (_manager == null) {
_manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, _opts);
Properties opts = new Properties();
if (_opts != null) {
for (Iterator iter = _opts.keySet().iterator(); iter.hasNext(); ) {
String key = (String)iter.next();
opts.setProperty(key, _opts.get(key).toString());
}
}
if (opts.getProperty("inbound.nickname") == null)
opts.setProperty("inbound.nickname", "I2PSnark");
if (opts.getProperty("i2p.streaming.inactivityTimeout") == null)
opts.setProperty("i2p.streaming.inactivityTimeout", "90000");
if (opts.getProperty("i2p.streaming.inactivityAction") == null)
opts.setProperty("i2p.streaming.inactivityAction", "1");
if (opts.getProperty("i2p.streaming.writeTimeout") == null)
opts.setProperty("i2p.streaming.writeTimeout", "90000");
if (opts.getProperty("i2p.streaming.readTimeout") == null)
opts.setProperty("i2p.streaming.readTimeout", "90000");
_manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts);
}
return (_manager != null);
}
public boolean connected() { return _manager != null; }
/**
* Destroy the destination itself
*/
public void disconnect() {
I2PSocketManager mgr = _manager;
_manager = null;
mgr.destroySocketManager();
}
/** connect to the given destination */
I2PSocket connect(PeerID peer) throws IOException {
try {
@@ -85,29 +125,44 @@ public class I2PSnarkUtil {
/**
* fetch the given URL, returning the file it is stored in, or null on error
*/
File get(String url) {
public File get(String url) { return get(url, true); }
public File get(String url, boolean rewrite) {
_log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy);
File out = null;
try {
out = File.createTempFile("i2psnark", "url");
} catch (IOException ioe) {
ioe.printStackTrace();
out.delete();
return null;
}
EepGet get = new EepGet(_context, _shouldProxy, _proxyHost, _proxyPort, 1, out.getAbsolutePath(), url);
String fetchURL = url;
if (rewrite)
fetchURL = rewriteAnnounce(url);
//_log.debug("Rewritten url [" + fetchURL + "]");
EepGet get = new EepGet(_context, _shouldProxy, _proxyHost, _proxyPort, 1, out.getAbsolutePath(), fetchURL);
if (get.fetch()) {
_log.debug("Fetch successful [" + url + "]: size=" + out.length());
return out;
} else {
_log.warn("Fetch failed [" + url + "]");
out.delete();
return null;
}
}
I2PServerSocket getServerSocket() {
public I2PServerSocket getServerSocket() {
return _manager.getServerSocket();
}
String getOurIPString() {
return _manager.getSession().getMyDestination().toBase64();
I2PSession sess = _manager.getSession();
if (sess != null) {
Destination dest = sess.getMyDestination();
if (dest != null)
return dest.toBase64();
}
return "unknown";
}
Destination getDestination(String ip) {
if (ip == null) return null;
@@ -138,11 +193,22 @@ public class I2PSnarkUtil {
int destStart = "http://".length();
int destEnd = origAnnounce.indexOf(".i2p");
int pathStart = origAnnounce.indexOf('/', destEnd);
return "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 + "]");
return rv;
}
/** hook between snark's logger and an i2p log */
void debug(String msg, int snarkDebugLevel, Throwable t) {
if (t instanceof OutOfMemoryError) {
try { Thread.sleep(100); } catch (InterruptedException ie) {}
try {
t.printStackTrace();
} catch (Throwable tt) {}
try {
System.out.println("OOM thread: " + Thread.currentThread().getName());
} catch (Throwable tt) {}
}
switch (snarkDebugLevel) {
case 0:
case 1:

View File

@@ -23,6 +23,8 @@ package org.klomp.snark;
import java.io.DataOutputStream;
import java.io.IOException;
import net.i2p.util.SimpleTimer;
// Used to queue outgoing connections
// sendMessage() should be used to translate them to wire format.
class Message
@@ -54,6 +56,8 @@ class Message
int off;
int len;
SimpleTimer.TimedEvent expireEvent;
/** Utility method for sending a message through a DataStream. */
void sendMessage(DataOutputStream dos) throws IOException
{

View File

@@ -33,32 +33,48 @@ import java.util.Map;
import java.util.HashMap;
import org.klomp.snark.bencode.*;
import net.i2p.data.Base64;
import net.i2p.util.Log;
import net.i2p.crypto.SHA1;
/**
* Note: this class is buggy, as it doesn't propogate custom meta fields into the bencoded
* info data, and from there to the info_hash. At the moment, though, it seems to work with
* torrents created by I2P-BT, I2PRufus and Azureus.
*
*/
public class MetaInfo
{
{
private static final Log _log = new Log(MetaInfo.class);
private final String announce;
private final byte[] info_hash;
private final String name;
private final String name_utf8;
private final List files;
private final List files_utf8;
private final List lengths;
private final int piece_length;
private final byte[] piece_hashes;
private final long length;
private final Map infoMap;
private byte[] torrentdata;
MetaInfo(String announce, String name, List files, List lengths,
MetaInfo(String announce, String name, String name_utf8, List files, List lengths,
int piece_length, byte[] piece_hashes, long length)
{
this.announce = announce;
this.name = name;
this.name_utf8 = name_utf8;
this.files = files;
this.files_utf8 = null;
this.lengths = lengths;
this.piece_length = piece_length;
this.piece_hashes = piece_hashes;
this.length = length;
this.info_hash = calculateInfoHash();
infoMap = null;
}
/**
@@ -90,6 +106,7 @@ public class MetaInfo
*/
public MetaInfo(Map m) throws InvalidBEncodingException
{
_log.debug("Creating a metaInfo: " + m, new Exception("source"));
BEValue val = (BEValue)m.get("announce");
if (val == null)
throw new InvalidBEncodingException("Missing announce string");
@@ -99,12 +116,19 @@ public class MetaInfo
if (val == null)
throw new InvalidBEncodingException("Missing info map");
Map info = val.getMap();
infoMap = info;
val = (BEValue)info.get("name");
if (val == null)
throw new InvalidBEncodingException("Missing name string");
name = val.getString();
val = (BEValue)info.get("name.utf-8");
if (val != null)
name_utf8 = val.getString();
else
name_utf8 = null;
val = (BEValue)info.get("piece length");
if (val == null)
throw new InvalidBEncodingException("Missing piece length number");
@@ -121,6 +145,7 @@ public class MetaInfo
// Single file case.
length = val.getLong();
files = null;
files_utf8 = null;
lengths = null;
}
else
@@ -137,6 +162,7 @@ public class MetaInfo
throw new InvalidBEncodingException("zero size files list");
files = new ArrayList(size);
files_utf8 = new ArrayList(size);
lengths = new ArrayList(size);
long l = 0;
for (int i = 0; i < list.size(); i++)
@@ -163,6 +189,19 @@ public class MetaInfo
file.add(((BEValue)it.next()).getString());
files.add(file);
val = (BEValue)desc.get("path.utf-8");
if (val != null) {
path_list = val.getList();
path_length = path_list.size();
if (path_length > 0) {
file = new ArrayList(path_length);
it = path_list.iterator();
while (it.hasNext())
file.add(((BEValue)it.next()).getString());
files_utf8.add(file);
}
}
}
length = l;
}
@@ -260,6 +299,12 @@ public class MetaInfo
*/
public boolean checkPiece(int piece, byte[] bs, int off, int length)
{
if (true)
return fast_checkPiece(piece, bs, off, length);
else
return orig_checkPiece(piece, bs, off, length);
}
private boolean orig_checkPiece(int piece, byte[] bs, int off, int length) {
// Check digest
MessageDigest sha1;
try
@@ -278,6 +323,17 @@ public class MetaInfo
return false;
return true;
}
private boolean fast_checkPiece(int piece, byte[] bs, int off, int length) {
SHA1 sha1 = new SHA1();
sha1.update(bs, off, length);
byte[] hash = sha1.digest();
for (int i = 0; i < 20; i++)
if (hash[i] != piece_hashes[20 * piece + i])
return false;
return true;
}
/**
* Returns the total length of the torrent in bytes.
@@ -322,7 +378,7 @@ public class MetaInfo
*/
public MetaInfo reannounce(String announce)
{
return new MetaInfo(announce, name, files,
return new MetaInfo(announce, name, name_utf8, files,
lengths, piece_length,
piece_hashes, length);
}
@@ -343,7 +399,13 @@ public class MetaInfo
private Map createInfoMap()
{
Map info = new HashMap();
if (infoMap != null) {
info.putAll(infoMap);
return info;
}
info.put("name", name);
if (name_utf8 != null)
info.put("name.utf-8", name_utf8);
info.put("piece length", new Integer(piece_length));
info.put("pieces", piece_hashes);
if (files == null)
@@ -355,6 +417,8 @@ public class MetaInfo
{
Map file = new HashMap();
file.put("path", files.get(i));
if ( (files_utf8 != null) && (files_utf8.size() > i) )
file.put("path.utf-8", files_utf8.get(i));
file.put("length", lengths.get(i));
l.add(file);
}
@@ -366,11 +430,26 @@ public class MetaInfo
private byte[] calculateInfoHash()
{
Map info = createInfoMap();
StringBuffer buf = new StringBuffer(128);
buf.append("info: ");
for (Iterator iter = info.keySet().iterator(); iter.hasNext(); ) {
String key = (String)iter.next();
Object val = info.get(key);
buf.append(key).append('=');
if (val instanceof byte[])
buf.append(Base64.encode((byte[])val, true));
else
buf.append(val.toString());
}
_log.debug(buf.toString());
byte[] infoBytes = BEncoder.bencode(info);
//_log.debug("info bencoded: [" + Base64.encode(infoBytes, true) + "]");
try
{
MessageDigest digest = MessageDigest.getInstance("SHA");
return digest.digest(infoBytes);
byte hash[] = digest.digest(infoBytes);
_log.debug("info hash: [" + net.i2p.data.Base64.encode(hash) + "]");
return hash;
}
catch(NoSuchAlgorithmException nsa)
{

View File

@@ -28,14 +28,17 @@ import java.util.Map;
import org.klomp.snark.bencode.*;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.data.DataHelper;
import net.i2p.util.Log;
public class Peer implements Comparable
{
private Log _log = new Log(Peer.class);
// Identifying property, the peer id of the other side.
private final PeerID peerID;
private final byte[] my_id;
private final MetaInfo metainfo;
final MetaInfo metainfo;
// The data in/output streams set during the handshake and used by
// the actual connections.
@@ -46,7 +49,11 @@ public class Peer implements Comparable
// was successful, the connection setup and runs
PeerState state;
private I2PSocket sock;
private boolean deregister = true;
private static long __id;
private long _id;
/**
* Creates a disconnected peer given a PeerID, your own id and the
@@ -58,6 +65,8 @@ public class Peer implements Comparable
this.peerID = peerID;
this.my_id = my_id;
this.metainfo = metainfo;
_id = ++__id;
//_log.debug("Creating a new peer with " + peerID.getAddress().calculateHash().toBase64(), new Exception("creating"));
}
/**
@@ -69,15 +78,17 @@ public class Peer implements Comparable
*
* @exception IOException when an error occurred during the handshake.
*/
public Peer(final I2PSocket sock, BufferedInputStream bis,
BufferedOutputStream bos, byte[] my_id, MetaInfo metainfo)
public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, MetaInfo metainfo)
throws IOException
{
this.my_id = my_id;
this.metainfo = metainfo;
this.sock = sock;
byte[] id = handshake(bis, bos);
byte[] id = handshake(in, out);
this.peerID = new PeerID(id, sock.getPeerDestination());
_id = ++__id;
_log.debug("Creating a new peer with " + peerID.getAddress().calculateHash().toBase64(), new Exception("creating " + _id));
}
/**
@@ -93,7 +104,10 @@ public class Peer implements Comparable
*/
public String toString()
{
return peerID.toString();
if (peerID != null)
return peerID.toString() + ' ' + _id;
else
return "[unknown id] " + _id;
}
/**
@@ -101,7 +115,7 @@ public class Peer implements Comparable
*/
public int hashCode()
{
return peerID.hashCode();
return peerID.hashCode() ^ (2 << _id);
}
/**
@@ -113,7 +127,7 @@ public class Peer implements Comparable
if (o instanceof Peer)
{
Peer p = (Peer)o;
return peerID.equals(p.peerID);
return _id == p._id && peerID.equals(p.peerID);
}
else
return false;
@@ -125,7 +139,14 @@ public class Peer implements Comparable
public int compareTo(Object o)
{
Peer p = (Peer)o;
return peerID.compareTo(p.peerID);
int rv = peerID.compareTo(p.peerID);
if (rv == 0) {
if (_id > p._id) return 1;
else if (_id < p._id) return -1;
else return 0;
} else {
return rv;
}
}
/**
@@ -145,23 +166,40 @@ public class Peer implements Comparable
if (state != null)
throw new IllegalStateException("Peer already started");
_log.debug("Running connection to " + peerID.getAddress().calculateHash().toBase64(), new Exception("connecting"));
try
{
// Do we need to handshake?
if (din == null)
{
I2PSocket sock = I2PSnarkUtil.instance().connect(peerID);
BufferedInputStream bis
= new BufferedInputStream(sock.getInputStream());
BufferedOutputStream bos
= new BufferedOutputStream(sock.getOutputStream());
byte [] id = handshake(bis, bos);
sock = I2PSnarkUtil.instance().connect(peerID);
_log.debug("Connected to " + peerID + ": " + sock);
if ((sock == null) || (sock.isClosed())) {
throw new IOException("Unable to reach " + peerID);
}
InputStream in = sock.getInputStream();
OutputStream out = sock.getOutputStream(); //new BufferedOutputStream(sock.getOutputStream());
if (true) {
// buffered output streams are internally synchronized, so we can't get through to the underlying
// I2PSocket's MessageOutputStream to close() it if we are blocking on a write(...). Oh, and the
// buffer is unnecessary anyway, as unbuffered access lets the streaming lib do the 'right thing'.
//out = new BufferedOutputStream(out);
in = new BufferedInputStream(sock.getInputStream());
}
//BufferedInputStream bis
// = new BufferedInputStream(sock.getInputStream());
//BufferedOutputStream bos
// = new BufferedOutputStream(sock.getOutputStream());
byte [] id = handshake(in, out); //handshake(bis, bos);
byte [] expected_id = peerID.getID();
if (!Arrays.equals(expected_id, id))
throw new IOException("Unexpected peerID '"
+ PeerID.idencode(id)
+ "' expected '"
+ PeerID.idencode(expected_id) + "'");
_log.debug("Handshake got matching IDs with " + toString());
} else {
_log.debug("Already have din [" + sock + "] with " + toString());
}
PeerConnectionIn in = new PeerConnectionIn(this, din);
@@ -176,23 +214,30 @@ public class Peer implements Comparable
state = s;
listener.connected(this);
_log.debug("Start running the reader with " + toString());
// Use this thread for running the incomming connection.
// The outgoing connection has created its own Thread.
// The outgoing connection creates its own Thread.
out.startup();
Thread.currentThread().setName("Snark reader from " + peerID);
s.in.run();
}
catch(IOException eofe)
{
// Ignore, probably just the other side closing the connection.
// Or refusing the connection, timing out, etc.
if (_log.shouldLog(Log.DEBUG))
_log.debug(this.toString(), eofe);
}
catch(Throwable t)
{
Snark.debug(this + ": " + t, Snark.ERROR);
t.printStackTrace();
_log.error(this + ": " + t.getMessage(), t);
if (t instanceof OutOfMemoryError)
throw (OutOfMemoryError)t;
}
finally
{
if (deregister) listener.disconnected(this);
disconnect();
}
}
@@ -200,11 +245,11 @@ public class Peer implements Comparable
* Sets DataIn/OutputStreams, does the handshake and returns the id
* reported by the other side.
*/
private byte[] handshake(BufferedInputStream bis, BufferedOutputStream bos)
private byte[] handshake(InputStream in, OutputStream out) //BufferedInputStream bis, BufferedOutputStream bos)
throws IOException
{
din = new DataInputStream(bis);
dout = new DataOutputStream(bos);
din = new DataInputStream(in);
dout = new DataOutputStream(out);
// Handshake write - header
dout.write(19);
@@ -219,11 +264,13 @@ public class Peer implements Comparable
dout.write(my_id);
dout.flush();
_log.debug("Wrote my shared hash and ID to " + toString());
// Handshake read - header
byte b = din.readByte();
if (b != 19)
throw new IOException("Handshake failure, expected 19, got "
+ (b & 0xff));
+ (b & 0xff) + " on " + sock);
byte[] bs = new byte[19];
din.readFully(bs);
@@ -244,6 +291,7 @@ public class Peer implements Comparable
// Handshake read - peer id
din.readFully(bs);
_log.debug("Read the remote side's hash and peerID fully from " + toString());
return bs;
}
@@ -278,7 +326,19 @@ public class Peer implements Comparable
PeerConnectionOut out = s.out;
if (out != null)
out.disconnect();
PeerListener pl = s.listener;
if (pl != null)
pl.disconnected(this);
}
I2PSocket csock = sock;
sock = null;
if ( (csock != null) && (!csock.isClosed()) ) {
try {
csock.close();
} catch (IOException ioe) {
_log.warn("Error disconnecting " + toString(), ioe);
}
}
}
/**
@@ -385,4 +445,17 @@ public class Peer implements Comparable
s.uploaded = 0;
}
}
public long getInactiveTime() {
PeerState s = state;
if (s != null) {
PeerConnectionOut out = s.out;
if (out != null)
return System.currentTimeMillis() - out.lastSent;
else
return -1; //"state, no out";
} else {
return -1; //"no state";
}
}
}

View File

@@ -22,8 +22,12 @@ package org.klomp.snark;
import java.io.*;
import java.net.*;
import java.util.Iterator;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.Log;
/**
* Accepts incomming connections from peers. The ConnectionAcceptor
@@ -33,30 +37,109 @@ import net.i2p.client.streaming.I2PSocket;
*/
public class PeerAcceptor
{
private static final Log _log = new Log(PeerAcceptor.class);
private final PeerCoordinator coordinator;
final PeerCoordinatorSet coordinators;
public PeerAcceptor(PeerCoordinator coordinator)
{
this.coordinator = coordinator;
this.coordinators = null;
}
public PeerAcceptor(PeerCoordinatorSet coordinators)
{
this.coordinators = coordinators;
this.coordinator = null;
}
public void connection(I2PSocket socket,
BufferedInputStream bis, BufferedOutputStream bos)
InputStream in, OutputStream out)
throws IOException
{
if (coordinator.needPeers())
{
// XXX: inside this Peer constructor's handshake is where you'd deal with the other
// side saying they want to communicate with another torrent - aka multitorrent
// support. you'd then want to grab the meta info /they/ want, look that up in
// our own list of active torrents, and put it on the right coordinator for it.
// this currently, however, throws an IOException if the metainfo doesn't match
// coodinator.getMetaInfo (Peer.java:242)
Peer peer = new Peer(socket, bis, bos, coordinator.getID(),
coordinator.getMetaInfo());
coordinator.addPeer(peer);
}
else
socket.close();
// inside this Peer constructor's handshake is where you'd deal with the other
// side saying they want to communicate with another torrent - aka multitorrent
// support, but because of how the protocol works, we can get away with just reading
// ahead the first $LOOKAHEAD_SIZE bytes to figure out which infohash they want to
// talk about, and we can just look for that in our list of active torrents.
byte peerInfoHash[] = null;
if (in instanceof BufferedInputStream) {
in.mark(LOOKAHEAD_SIZE);
peerInfoHash = readHash(in);
in.reset();
} else {
// is this working right?
try {
peerInfoHash = readHash(in);
_log.info("infohash read from " + socket.getPeerDestination().calculateHash().toBase64()
+ ": " + Base64.encode(peerInfoHash));
} catch (IOException ioe) {
_log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64());
throw ioe;
}
in = new SequenceInputStream(new ByteArrayInputStream(peerInfoHash), in);
}
if (coordinator != null) {
// single torrent capability
MetaInfo meta = coordinator.getMetaInfo();
if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) {
if (coordinator.needPeers())
{
Peer peer = new Peer(socket, in, out, coordinator.getID(),
coordinator.getMetaInfo());
coordinator.addPeer(peer);
}
else
socket.close();
} else {
// its for another infohash, but we are only single torrent capable. b0rk.
throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash)
+ ") while we only support (" + Base64.encode(meta.getInfoHash()) + ")");
}
} else {
// multitorrent capable, so lets see what we can handle
for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) {
PeerCoordinator cur = (PeerCoordinator)iter.next();
MetaInfo meta = cur.getMetaInfo();
if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) {
if (cur.needPeers())
{
Peer peer = new Peer(socket, in, out, cur.getID(),
cur.getMetaInfo());
cur.addPeer(peer);
return;
}
else
{
if (_log.shouldLog(Log.DEBUG))
_log.debug("Rejecting new peer for " + cur.snark.torrent);
socket.close();
return;
}
}
}
// this is only reached if none of the coordinators match the infohash
throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash)
+ ") while we don't support that hash");
}
}
private static final int LOOKAHEAD_SIZE = "19".length() +
"BitTorrent protocol".length() +
8 + // blank, reserved
20; // infohash
/**
* Read ahead to the infohash, throwing an exception if there isn't enough data
*/
private byte[] readHash(InputStream in) throws IOException {
byte buf[] = new byte[LOOKAHEAD_SIZE];
int read = DataHelper.read(in, buf);
if (read != buf.length)
throw new IOException("Unable to read the hash (read " + read + ")");
byte rv[] = new byte[20];
System.arraycopy(buf, buf.length-rv.length-1, rv, 0, rv.length);
return rv;
}
}

View File

@@ -70,6 +70,7 @@ class PeerCheckerTask extends TimerTask
{
it.remove();
coordinator.removePeerFromPieces(peer);
coordinator.peerCount = coordinator.peers.size();
continue;
}
@@ -185,6 +186,7 @@ class PeerCheckerTask extends TimerTask
// Put it at the back of the list
coordinator.peers.remove(worstDownloader);
coordinator.peerCount = coordinator.peers.size();
removed.add(worstDownloader);
}
@@ -193,6 +195,10 @@ class PeerCheckerTask extends TimerTask
// Put peers back at the end of the list that we removed earlier.
coordinator.peers.addAll(removed);
coordinator.peerCount = coordinator.peers.size();
}
if (coordinator.halted()) {
cancel();
}
}
}

View File

@@ -24,8 +24,11 @@ import java.io.*;
import java.net.*;
import java.util.*;
import net.i2p.util.Log;
class PeerConnectionIn implements Runnable
{
private Log _log = new Log(PeerConnectionIn.class);
private final Peer peer;
private final DataInputStream din;
@@ -72,6 +75,8 @@ class PeerConnectionIn implements Runnable
if (i == 0)
{
ps.keepAliveMessage();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received keepalive from " + peer + " on " + peer.metainfo.getName());
continue;
}
@@ -82,30 +87,44 @@ class PeerConnectionIn implements Runnable
{
case 0:
ps.chokeMessage(true);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received choke from " + peer + " on " + peer.metainfo.getName());
break;
case 1:
ps.chokeMessage(false);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received unchoke from " + peer + " on " + peer.metainfo.getName());
break;
case 2:
ps.interestedMessage(true);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received interested from " + peer + " on " + peer.metainfo.getName());
break;
case 3:
ps.interestedMessage(false);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received not interested from " + peer + " on " + peer.metainfo.getName());
break;
case 4:
piece = din.readInt();
ps.haveMessage(piece);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received havePiece(" + piece + ") from " + peer + " on " + peer.metainfo.getName());
break;
case 5:
byte[] bitmap = new byte[i-1];
din.readFully(bitmap);
ps.bitfieldMessage(bitmap);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received bitmap from " + peer + " on " + peer.metainfo.getName());
break;
case 6:
piece = din.readInt();
begin = din.readInt();
len = din.readInt();
ps.requestMessage(piece, begin, len);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received request(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
break;
case 7:
piece = din.readInt();
@@ -118,12 +137,16 @@ class PeerConnectionIn implements Runnable
piece_bytes = req.bs;
din.readFully(piece_bytes, begin, len);
ps.pieceMessage(req);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
}
else
{
// XXX - Consume but throw away afterwards.
piece_bytes = new byte[len];
din.readFully(piece_bytes);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received UNWANTED data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
}
break;
case 8:
@@ -131,22 +154,29 @@ class PeerConnectionIn implements Runnable
begin = din.readInt();
len = din.readInt();
ps.cancelMessage(piece, begin, len);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received cancel(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
break;
default:
byte[] bs = new byte[i-1];
din.readFully(bs);
ps.unknownMessage(b, bs);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received unknown message from " + peer + " on " + peer.metainfo.getName());
}
}
}
catch (IOException ioe)
{
// Ignore, probably the other side closed connection.
if (_log.shouldLog(Log.INFO))
_log.info("IOError talking with " + peer, ioe);
}
catch (Throwable t)
{
Snark.debug(peer + ": " + t, Snark.ERROR);
t.printStackTrace();
_log.error("Error talking with " + peer, t);
if (t instanceof OutOfMemoryError)
throw (OutOfMemoryError)t;
}
finally
{

View File

@@ -24,8 +24,13 @@ import java.io.*;
import java.net.*;
import java.util.*;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer;
class PeerConnectionOut implements Runnable
{
private Log _log = new Log(PeerConnectionOut.class);
private final Peer peer;
private final DataOutputStream dout;
@@ -34,14 +39,24 @@ class PeerConnectionOut implements Runnable
// Contains Messages.
private List sendQueue = new ArrayList();
private static long __id = 0;
private long _id;
long lastSent;
public PeerConnectionOut(Peer peer, DataOutputStream dout)
{
this.peer = peer;
this.dout = dout;
_id = ++__id;
lastSent = System.currentTimeMillis();
quit = false;
thread = new Thread(this);
}
public void startup() {
thread = new I2PThread(this, "Snark sender " + _id + ": " + peer);
thread.start();
}
@@ -53,13 +68,13 @@ class PeerConnectionOut implements Runnable
{
try
{
while (!quit)
while (!quit && peer.isConnected())
{
Message m = null;
PeerState state = null;
synchronized(sendQueue)
{
while (!quit && sendQueue.isEmpty())
while (!quit && peer.isConnected() && sendQueue.isEmpty())
{
try
{
@@ -67,7 +82,7 @@ class PeerConnectionOut implements Runnable
dout.flush();
// Wait till more data arrives.
sendQueue.wait();
sendQueue.wait(60*1000);
}
catch (InterruptedException ie)
{
@@ -75,7 +90,7 @@ class PeerConnectionOut implements Runnable
}
}
state = peer.state;
if (!quit && state != null)
if (!quit && state != null && peer.isConnected())
{
// Piece messages are big. So if there are other
// (control) messages make sure they are send first.
@@ -84,37 +99,46 @@ class PeerConnectionOut implements Runnable
// being send even if we get unchoked a little later.
// (Since we will resent them anyway in that case.)
// And remove piece messages if we are choking.
// this should get fixed for starvation
Iterator it = sendQueue.iterator();
while (m == null && it.hasNext())
{
Message nm = (Message)it.next();
if (nm.type == Message.PIECE)
{
if (state.choking)
if (state.choking) {
it.remove();
SimpleTimer.getInstance().removeEvent(nm.expireEvent);
}
nm = null;
}
else if (nm.type == Message.REQUEST && state.choked)
{
it.remove();
SimpleTimer.getInstance().removeEvent(nm.expireEvent);
nm = null;
}
if (m == null && nm != null)
{
m = nm;
SimpleTimer.getInstance().removeEvent(nm.expireEvent);
it.remove();
}
}
if (m == null && sendQueue.size() > 0)
if (m == null && sendQueue.size() > 0) {
m = (Message)sendQueue.remove(0);
SimpleTimer.getInstance().removeEvent(m.expireEvent);
}
}
}
if (m != null)
{
if (Snark.debug >= Snark.ALL)
Snark.debug("Send " + peer + ": " + m, Snark.ALL);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Send " + peer + ": " + m + " on " + peer.metainfo.getName());
m.sendMessage(dout);
lastSent = System.currentTimeMillis();
// Remove all piece messages after sending a choke message.
if (m.type == Message.CHOKE)
@@ -131,11 +155,14 @@ class PeerConnectionOut implements Runnable
catch (IOException ioe)
{
// Ignore, probably other side closed connection.
if (_log.shouldLog(Log.INFO))
_log.info("IOError sending to " + peer, ioe);
}
catch (Throwable t)
{
Snark.debug(peer + ": " + t, Snark.ERROR);
t.printStackTrace();
_log.error("Error sending to " + peer, t);
if (t instanceof OutOfMemoryError)
throw (OutOfMemoryError)t;
}
finally
{
@@ -148,15 +175,23 @@ class PeerConnectionOut implements Runnable
{
synchronized(sendQueue)
{
if (quit == true)
return;
//if (quit == true)
// return;
quit = true;
thread.interrupt();
if (thread != null)
thread.interrupt();
sendQueue.clear();
sendQueue.notify();
}
if (dout != null) {
try {
dout.close();
} catch (IOException ioe) {
_log.warn("Error closing the stream to " + peer, ioe);
}
}
}
/**
@@ -165,10 +200,31 @@ class PeerConnectionOut implements Runnable
*/
private void addMessage(Message m)
{
SimpleTimer.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT);
synchronized(sendQueue)
{
sendQueue.add(m);
sendQueue.notify();
sendQueue.notifyAll();
}
}
/** remove messages not sent in 3m */
private static final int SEND_TIMEOUT = 3*60*1000;
private class RemoveTooSlow implements SimpleTimer.TimedEvent {
private Message _m;
public RemoveTooSlow(Message m) {
_m = m;
m.expireEvent = RemoveTooSlow.this;
}
public void timeReached() {
boolean removed = false;
synchronized (sendQueue) {
removed = sendQueue.remove(_m);
sendQueue.notifyAll();
}
if (removed)
_log.info("Took too long to send " + _m + " to " + peer);
}
}
@@ -194,6 +250,7 @@ class PeerConnectionOut implements Runnable
removed = true;
}
}
sendQueue.notifyAll();
}
return removed;
}

View File

@@ -23,13 +23,18 @@ package org.klomp.snark;
import java.util.*;
import java.io.IOException;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
/**
* Coordinates what peer does what.
*/
public class PeerCoordinator implements PeerListener
{
private final Log _log = new Log(PeerCoordinator.class);
final MetaInfo metainfo;
final Storage storage;
final Snark snark;
// package local for access by CheckDownLoadersTask
final static long CHECK_PERIOD = 20*1000; // 20 seconds
@@ -48,6 +53,8 @@ public class PeerCoordinator implements PeerListener
// synchronize on this when changing peers or downloaders
final List peers = new ArrayList();
/** estimate of the peers, without requiring any synchronization */
volatile int peerCount;
/** Timer to handle all periodical tasks. */
private final Timer timer = new Timer(true);
@@ -60,14 +67,18 @@ public class PeerCoordinator implements PeerListener
private boolean halted = false;
private final CoordinatorListener listener;
public String trackerProblems = null;
public int trackerSeenPeers = 0;
public PeerCoordinator(byte[] id, MetaInfo metainfo, Storage storage,
CoordinatorListener listener)
CoordinatorListener listener, Snark torrent)
{
this.id = id;
this.metainfo = metainfo;
this.storage = storage;
this.listener = listener;
this.snark = torrent;
// Make a list of pieces
wantedPieces = new ArrayList();
@@ -80,6 +91,9 @@ public class PeerCoordinator implements PeerListener
// Install a timer to check the uploaders.
timer.schedule(new PeerCheckerTask(this), CHECK_PERIOD, CHECK_PERIOD);
}
public Storage getStorage() { return storage; }
public CoordinatorListener getListener() { return listener; }
public byte[] getID()
{
@@ -91,12 +105,15 @@ public class PeerCoordinator implements PeerListener
return storage.complete();
}
public int getPeerCount() { return peerCount; }
public int getPeers()
{
synchronized(peers)
{
return peers.size();
int rv = peers.size();
peerCount = rv;
return rv;
}
}
@@ -137,66 +154,85 @@ public class PeerCoordinator implements PeerListener
return !halted && peers.size() < MAX_CONNECTIONS;
}
}
public boolean halted() { return halted; }
public void halt()
{
halted = true;
List removed = new ArrayList();
synchronized(peers)
{
// Stop peer checker task.
timer.cancel();
// Stop peers.
Iterator it = peers.iterator();
while(it.hasNext())
{
Peer peer = (Peer)it.next();
peer.disconnect();
it.remove();
removePeerFromPieces(peer);
}
removed.addAll(peers);
peers.clear();
peerCount = 0;
}
while (removed.size() > 0) {
Peer peer = (Peer)removed.remove(0);
peer.disconnect();
removePeerFromPieces(peer);
}
}
public void connected(Peer peer)
{
{
if (halted)
{
peer.disconnect(false);
return;
}
Peer toDisconnect = null;
synchronized(peers)
{
if (peerIDInList(peer.getPeerID(), peers))
Peer old = peerIDInList(peer.getPeerID(), peers);
if ( (old != null) && (old.getInactiveTime() > 2*60*1000) ) {
// idle for 2 minutes, kill the old con
peers.remove(old);
toDisconnect = old;
old = null;
}
if (old != null)
{
if (Snark.debug >= Snark.INFO)
Snark.debug("Already connected to: " + peer, Snark.INFO);
if (_log.shouldLog(Log.WARN))
_log.warn("Already connected to: " + peer + ": " + old + ", inactive for " + old.getInactiveTime());
peer.disconnect(false); // Don't deregister this connection/peer.
}
else
{
if (Snark.debug >= Snark.INFO)
Snark.debug("New connection to peer: " + peer, Snark.INFO);
if (_log.shouldLog(Log.INFO))
_log.info("New connection to peer: " + peer + " for " + metainfo.getName());
// Add it to the beginning of the list.
// And try to optimistically make it a uploader.
peers.add(0, peer);
peerCount = peers.size();
unchokePeer();
if (listener != null)
listener.peerChange(this, peer);
}
}
if (toDisconnect != null) {
toDisconnect.disconnect(false);
removePeerFromPieces(toDisconnect);
}
}
private static boolean peerIDInList(PeerID pid, List peers)
private static Peer peerIDInList(PeerID pid, List peers)
{
Iterator it = peers.iterator();
while (it.hasNext())
if (pid.sameID(((Peer)it.next()).getPeerID()))
return true;
return false;
while (it.hasNext()) {
Peer cur = (Peer)it.next();
if (pid.sameID(cur.getPeerID()))
return cur;
}
return null;
}
public void addPeer(final Peer peer)
@@ -215,6 +251,8 @@ public class PeerCoordinator implements PeerListener
if (need_more)
{
_log.debug("Adding a peer " + peer.getPeerID().getAddress().calculateHash().toBase64() + " for " + metainfo.getName(), new Exception("add/run"));
// Run the peer with us as listener and the current bitfield.
final PeerListener listener = this;
final BitField bitfield = storage.getBitField();
@@ -226,15 +264,16 @@ public class PeerCoordinator implements PeerListener
}
};
String threadName = peer.toString();
new Thread(r, threadName).start();
new I2PThread(r, threadName).start();
}
else
if (Snark.debug >= Snark.INFO)
if (_log.shouldLog(Log.DEBUG)) {
if (peer.isConnected())
Snark.debug("Add peer already connected: " + peer, Snark.INFO);
_log.info("Add peer already connected: " + peer);
else
Snark.debug("MAX_CONNECTIONS = " + MAX_CONNECTIONS
+ " not accepting extra peer: " + peer, Snark.INFO);
_log.info("MAX_CONNECTIONS = " + MAX_CONNECTIONS
+ " not accepting extra peer: " + peer);
}
}
@@ -245,33 +284,36 @@ public class PeerCoordinator implements PeerListener
// At the start are the peers that have us unchoked at the end the
// other peer that are interested, but are choking us.
List interested = new LinkedList();
Iterator it = peers.iterator();
while (it.hasNext())
{
Peer peer = (Peer)it.next();
boolean remove = false;
if (uploaders < MAX_UPLOADERS
&& peer.isChoking()
&& peer.isInterested())
synchronized (peers) {
Iterator it = peers.iterator();
while (it.hasNext())
{
if (!peer.isChoked())
interested.add(0, peer);
else
interested.add(peer);
Peer peer = (Peer)it.next();
boolean remove = false;
if (uploaders < MAX_UPLOADERS
&& peer.isChoking()
&& peer.isInterested())
{
if (!peer.isChoked())
interested.add(0, peer);
else
interested.add(peer);
}
}
}
while (uploaders < MAX_UPLOADERS && interested.size() > 0)
{
Peer peer = (Peer)interested.remove(0);
if (Snark.debug >= Snark.INFO)
Snark.debug("Unchoke: " + peer, Snark.INFO);
peer.setChoking(false);
uploaders++;
// Put peer back at the end of the list.
peers.remove(peer);
peers.add(peer);
}
while (uploaders < MAX_UPLOADERS && interested.size() > 0)
{
Peer peer = (Peer)interested.remove(0);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Unchoke: " + peer);
peer.setChoking(false);
uploaders++;
// Put peer back at the end of the list.
peers.remove(peer);
peers.add(peer);
peerCount = peers.size();
}
}
}
public byte[] getBitMap()
@@ -378,8 +420,9 @@ public class PeerCoordinator implements PeerListener
}
catch (IOException ioe)
{
Snark.fatal("Error reading storage", ioe);
return null; // Never reached.
snark.stopTorrent();
_log.error("Error reading the storage for " + metainfo.getName(), ioe);
throw new RuntimeException("B0rked");
}
}
@@ -412,17 +455,17 @@ public class PeerCoordinator implements PeerListener
*/
public boolean gotPiece(Peer peer, int piece, byte[] bs)
{
if (halted)
if (halted) {
_log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
return true; // We don't actually care anymore.
}
synchronized(wantedPieces)
{
Piece p = new Piece(piece);
if (!wantedPieces.contains(p))
{
if (Snark.debug >= Snark.INFO)
Snark.debug(peer + " piece " + piece + " no longer needed",
Snark.INFO);
_log.info("Got unwanted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
// No need to announce have piece to peers.
// Assume we got a good piece, we don't really care anymore.
@@ -433,22 +476,21 @@ public class PeerCoordinator implements PeerListener
{
if (storage.putPiece(piece, bs))
{
if (Snark.debug >= Snark.INFO)
Snark.debug("Recv p" + piece + " " + peer, Snark.INFO);
_log.info("Got valid piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
}
else
{
// Oops. We didn't actually download this then... :(
downloaded -= metainfo.getPieceLength(piece);
if (Snark.debug >= Snark.NOTICE)
Snark.debug("Got BAD piece " + piece + " from " + peer,
Snark.NOTICE);
_log.warn("Got BAD piece " + piece + "/" + metainfo.getPieces() + " from " + peer + " for " + metainfo.getName());
return false; // No need to announce BAD piece to peers.
}
}
catch (IOException ioe)
{
Snark.fatal("Error writing storage", ioe);
snark.stopTorrent();
_log.error("Error writing storage for " + metainfo.getName(), ioe);
throw new RuntimeException("B0rked");
}
wantedPieces.remove(p);
}
@@ -470,8 +512,8 @@ public class PeerCoordinator implements PeerListener
public void gotChoke(Peer peer, boolean choke)
{
if (Snark.debug >= Snark.INFO)
Snark.debug("Got choke(" + choke + "): " + peer, Snark.INFO);
if (_log.shouldLog(Log.INFO))
_log.info("Got choke(" + choke + "): " + peer);
if (listener != null)
listener.peerChange(this, peer);
@@ -489,8 +531,8 @@ public class PeerCoordinator implements PeerListener
{
uploaders++;
peer.setChoking(false);
if (Snark.debug >= Snark.INFO)
Snark.debug("Unchoke: " + peer, Snark.INFO);
if (_log.shouldLog(Log.INFO))
_log.info("Unchoke: " + peer);
}
}
}
@@ -502,8 +544,8 @@ public class PeerCoordinator implements PeerListener
public void disconnected(Peer peer)
{
if (Snark.debug >= Snark.INFO)
Snark.debug("Disconnected " + peer, Snark.INFO);
if (_log.shouldLog(Log.INFO))
_log.info("Disconnected " + peer, new Exception("Disconnected by"));
synchronized(peers)
{
@@ -514,6 +556,7 @@ public class PeerCoordinator implements PeerListener
unchokePeer();
removePeerFromPieces(peer);
}
peerCount = peers.size();
}
if (listener != null)

View File

@@ -0,0 +1,36 @@
package org.klomp.snark;
import java.util.*;
/**
* Hmm, any guesses as to what this is? Used by the multitorrent functionality
* in the PeerAcceptor to pick the right PeerCoordinator to accept the con for.
* Each PeerCoordinator is added to the set from within the Snark (and removed
* from it there too)
*/
public class PeerCoordinatorSet {
private static final PeerCoordinatorSet _instance = new PeerCoordinatorSet();
public static final PeerCoordinatorSet instance() { return _instance; }
private Set _coordinators;
private PeerCoordinatorSet() {
_coordinators = new HashSet();
}
public Iterator iterator() {
synchronized (_coordinators) {
return new ArrayList(_coordinators).iterator();
}
}
public void add(PeerCoordinator coordinator) {
synchronized (_coordinators) {
_coordinators.add(coordinator);
}
}
public void remove(PeerCoordinator coordinator) {
synchronized (_coordinators) {
_coordinators.remove(coordinator);
}
}
}

View File

@@ -26,8 +26,11 @@ import java.util.List;
import java.util.Set;
import java.util.HashSet;
import net.i2p.util.Log;
class PeerState
{
private Log _log = new Log(PeerState.class);
final Peer peer;
final PeerListener listener;
final MetaInfo metainfo;
@@ -59,7 +62,7 @@ class PeerState
// If we have te resend outstanding requests (true after we got choked).
private boolean resend = false;
private final static int MAX_PIPELINE = 5;
private final static int MAX_PIPELINE = 1;
private final static int PARTSIZE = 64*1024; // default was 16K, i2p-bt uses 64KB
PeerState(Peer peer, PeerListener listener, MetaInfo metainfo,
@@ -77,16 +80,15 @@ class PeerState
void keepAliveMessage()
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " rcv alive", Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " rcv alive");
/* XXX - ignored */
}
void chokeMessage(boolean choke)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " rcv " + (choke ? "" : "un") + "choked",
Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " rcv " + (choke ? "" : "un") + "choked");
choked = choke;
if (choked)
@@ -100,24 +102,23 @@ class PeerState
void interestedMessage(boolean interest)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " rcv " + (interest ? "" : "un")
+ "interested", Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " rcv " + (interest ? "" : "un")
+ "interested");
interested = interest;
listener.gotInterest(peer, interest);
}
void haveMessage(int piece)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " rcv have(" + piece + ")", Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " rcv have(" + piece + ")");
// Sanity check
if (piece < 0 || piece >= metainfo.getPieces())
{
// XXX disconnect?
if (Snark.debug >= Snark.INFO)
Snark.debug("Got strange 'have: " + piece + "' message from " + peer,
+ Snark.INFO);
if (_log.shouldLog(Log.WARN))
_log.warn("Got strange 'have: " + piece + "' message from " + peer);
return;
}
@@ -138,14 +139,13 @@ class PeerState
{
synchronized(this)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " rcv bitfield", Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " rcv bitfield");
if (bitfield != null)
{
// XXX - Be liberal in what you except?
if (Snark.debug >= Snark.INFO)
Snark.debug("Got unexpected bitfield message from " + peer,
Snark.INFO);
if (_log.shouldLog(Log.WARN))
_log.warn("Got unexpected bitfield message from " + peer);
return;
}
@@ -157,14 +157,13 @@ class PeerState
void requestMessage(int piece, int begin, int length)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " rcv request("
+ piece + ", " + begin + ", " + length + ") ",
Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " rcv request("
+ piece + ", " + begin + ", " + length + ") ");
if (choking)
{
if (Snark.debug >= Snark.INFO)
Snark.debug("Request received, but choking " + peer, Snark.INFO);
if (_log.shouldLog(Log.INFO))
_log.info("Request received, but choking " + peer);
return;
}
@@ -177,12 +176,11 @@ class PeerState
|| length > 4*PARTSIZE)
{
// XXX - Protocol error -> disconnect?
if (Snark.debug >= Snark.INFO)
Snark.debug("Got strange 'request: " + piece
if (_log.shouldLog(Log.WARN))
_log.warn("Got strange 'request: " + piece
+ ", " + begin
+ ", " + length
+ "' message from " + peer,
Snark.INFO);
+ "' message from " + peer);
return;
}
@@ -190,8 +188,8 @@ class PeerState
if (pieceBytes == null)
{
// XXX - Protocol error-> diconnect?
if (Snark.debug >= Snark.INFO)
Snark.debug("Got request for unknown piece: " + piece, Snark.INFO);
if (_log.shouldLog(Log.WARN))
_log.warn("Got request for unknown piece: " + piece);
return;
}
@@ -199,25 +197,18 @@ class PeerState
if (begin >= pieceBytes.length || begin + length > pieceBytes.length)
{
// XXX - Protocol error-> disconnect?
if (Snark.debug >= Snark.INFO)
Snark.debug("Got out of range 'request: " + piece
if (_log.shouldLog(Log.WARN))
_log.warn("Got out of range 'request: " + piece
+ ", " + begin
+ ", " + length
+ "' message from " + peer,
Snark.INFO);
+ "' message from " + peer);
return;
}
if (Snark.debug >= Snark.DEBUG)
Snark.debug("Sending (" + piece + ", " + begin + ", "
+ length + ")" + " to " + peer, Snark.DEBUG);
if (_log.shouldLog(Log.INFO))
_log.info("Sending (" + piece + ", " + begin + ", "
+ length + ")" + " to " + peer);
out.sendPiece(piece, begin, length, pieceBytes);
// Tell about last subpiece delivery.
if (begin + length == pieceBytes.length)
if (Snark.debug >= Snark.DEBUG)
Snark.debug("Send p" + piece + " " + peer,
Snark.DEBUG);
}
/**
@@ -245,14 +236,13 @@ class PeerState
{
if (listener.gotPiece(peer, req.piece, req.bs))
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug("Got " + req.piece + ": " + peer, Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Got " + req.piece + ": " + peer);
}
else
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug("Got BAD " + req.piece + " from " + peer,
Snark.DEBUG);
if (_log.shouldLog(Log.WARN))
_log.warn("Got BAD " + req.piece + " from " + peer);
// XXX ARGH What now !?!
downloaded = 0;
}
@@ -275,21 +265,20 @@ class PeerState
*/
Request getOutstandingRequest(int piece, int begin, int length)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug("getChunk("
if (_log.shouldLog(Log.DEBUG))
_log.debug("getChunk("
+ piece + "," + begin + "," + length + ") "
+ peer, Snark.DEBUG);
+ peer);
int r = getFirstOutstandingRequest(piece);
// Unrequested piece number?
if (r == -1)
{
if (Snark.debug >= Snark.INFO)
Snark.debug("Unrequested 'piece: " + piece + ", "
if (_log.shouldLog(Log.INFO))
_log.info("Unrequested 'piece: " + piece + ", "
+ begin + ", " + length + "' received from "
+ peer,
Snark.INFO);
+ peer);
downloaded = 0; // XXX - punishment?
return null;
}
@@ -309,13 +298,12 @@ class PeerState
// Something wrong?
if (req.piece != piece || req.off != begin || req.len != length)
{
if (Snark.debug >= Snark.INFO)
Snark.debug("Unrequested or unneeded 'piece: "
if (_log.shouldLog(Log.INFO))
_log.info("Unrequested or unneeded 'piece: "
+ piece + ", "
+ begin + ", "
+ length + "' received from "
+ peer,
Snark.INFO);
+ peer);
downloaded = 0; // XXX - punishment?
return null;
}
@@ -323,9 +311,9 @@ class PeerState
// Report missing requests.
if (r != 0)
{
if (Snark.debug >= Snark.INFO)
System.err.print("Some requests dropped, got " + req
+ ", wanted:");
if (_log.shouldLog(Log.WARN))
_log.warn("Some requests dropped, got " + req
+ ", wanted for peer: " + peer);
for (int i = 0; i < r; i++)
{
Request dropReq = (Request)outstandingRequests.remove(0);
@@ -338,11 +326,9 @@ class PeerState
if (!choked)
out.sendRequest(dropReq);
*/
if (Snark.debug >= Snark.INFO)
System.err.print(" " + dropReq);
if (_log.shouldLog(Log.WARN))
_log.warn("dropped " + dropReq + " with peer " + peer);
}
if (Snark.debug >= Snark.INFO)
System.err.println(" " + peer);
}
outstandingRequests.remove(0);
}
@@ -356,24 +342,23 @@ class PeerState
void cancelMessage(int piece, int begin, int length)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug("Got cancel message ("
+ piece + ", " + begin + ", " + length + ")",
Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Got cancel message ("
+ piece + ", " + begin + ", " + length + ")");
out.cancelRequest(piece, begin, length);
}
void unknownMessage(int type, byte[] bs)
{
if (Snark.debug >= Snark.WARNING)
Snark.debug("Warning: Ignoring unknown message type: " + type
+ " length: " + bs.length, Snark.WARNING);
if (_log.shouldLog(Log.WARN))
_log.warn("Warning: Ignoring unknown message type: " + type
+ " length: " + bs.length);
}
void havePiece(int piece)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug("Tell " + peer + " havePiece(" + piece + ")", Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Tell " + peer + " havePiece(" + piece + ")");
synchronized(this)
{
@@ -474,8 +459,8 @@ class PeerState
}
}
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " requests " + outstandingRequests, Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " requests " + outstandingRequests);
}
// Starts requesting first chunk of next piece. Returns true if
@@ -486,8 +471,8 @@ class PeerState
if (bitfield != null)
{
int nextPiece = listener.wantPiece(peer, bitfield);
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " want piece " + nextPiece, Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " want piece " + nextPiece);
synchronized(this)
{
if (nextPiece != -1
@@ -512,8 +497,8 @@ class PeerState
synchronized void setInteresting(boolean interest)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " setInteresting(" + interest + ")", Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " setInteresting(" + interest + ")");
if (interest != interesting)
{
@@ -527,8 +512,8 @@ class PeerState
synchronized void setChoking(boolean choke)
{
if (Snark.debug >= Snark.DEBUG)
Snark.debug(peer + " setChoking(" + choke + ")", Snark.DEBUG);
if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " setChoking(" + choke + ")");
if (choking != choke)
{

View File

@@ -28,6 +28,7 @@ import org.klomp.snark.bencode.*;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.util.I2PThread;
/**
* Main Snark program startup class.
@@ -85,13 +86,41 @@ public class Snark
"Commands: 'info', 'list', 'quit'.";
// String indicating main activity
static String activity = "Not started";
String activity = "Not started";
private static class OOMListener implements I2PThread.OOMEventListener {
public void outOfMemory(OutOfMemoryError err) {
try {
err.printStackTrace();
I2PSnarkUtil.instance().debug("OOM in the snark", Snark.ERROR, err);
} catch (Throwable t) {
System.out.println("OOM in the OOM");
}
System.exit(0);
}
}
public static void main(String[] args)
{
System.out.println(copyright);
System.out.println();
if ( (args.length > 0) && ("--config".equals(args[0])) ) {
I2PThread.addOOMEventListener(new OOMListener());
SnarkManager sm = SnarkManager.instance();
if (args.length > 1)
sm.loadConfig(args[1]);
System.out.println("Running in multitorrent mode");
while (true) {
try {
synchronized (sm) {
sm.wait();
}
} catch (InterruptedException ie) {}
}
}
// Parse debug, share/ip and torrent file options.
Snark snark = parseArguments(args);
@@ -101,7 +130,7 @@ public class Snark
snark.acceptor,
snark.trackerclient,
snark);
Runtime.getRuntime().addShutdownHook(snarkhook);
//Runtime.getRuntime().addShutdownHook(snarkhook);
Timer timer = new Timer(true);
TimerTask monitor = new PeerMonitorTask(snark.coordinator);
@@ -130,15 +159,15 @@ public class Snark
quit = true;
else if ("list".equals(line))
{
synchronized(coordinator.peers)
synchronized(snark.coordinator.peers)
{
System.out.println(coordinator.peers.size()
System.out.println(snark.coordinator.peers.size()
+ " peers -"
+ " (i)nterested,"
+ " (I)nteresting,"
+ " (c)hoking,"
+ " (C)hoked:");
Iterator it = coordinator.peers.iterator();
Iterator it = snark.coordinator.peers.iterator();
while (it.hasNext())
{
Peer peer = (Peer)it.next();
@@ -152,18 +181,18 @@ public class Snark
}
else if ("info".equals(line))
{
System.out.println("Name: " + meta.getName());
System.out.println("Torrent: " + torrent);
System.out.println("Tracker: " + meta.getAnnounce());
List files = meta.getFiles();
System.out.println("Name: " + snark.meta.getName());
System.out.println("Torrent: " + snark.torrent);
System.out.println("Tracker: " + snark.meta.getAnnounce());
List files = snark.meta.getFiles();
System.out.println("Files: "
+ ((files == null) ? 1 : files.size()));
System.out.println("Pieces: " + meta.getPieces());
System.out.println("Pieces: " + snark.meta.getPieces());
System.out.println("Piece size: "
+ meta.getPieceLength(0) / 1024
+ snark.meta.getPieceLength(0) / 1024
+ " KB");
System.out.println("Total size: "
+ meta.getTotalLength() / (1024 * 1024)
+ snark.meta.getTotalLength() / (1024 * 1024)
+ " MB");
}
else if ("".equals(line) || "help".equals(line))
@@ -195,15 +224,22 @@ public class Snark
}
}
static String torrent;
static MetaInfo meta;
static Storage storage;
static PeerCoordinator coordinator;
static ConnectionAcceptor acceptor;
static TrackerClient trackerclient;
public String torrent;
public MetaInfo meta;
public Storage storage;
public PeerCoordinator coordinator;
public ConnectionAcceptor acceptor;
public TrackerClient trackerclient;
public String rootDataDir = ".";
public CompleteListener completeListener;
public boolean stopped;
private Snark(String torrent, String ip, int user_port,
StorageListener slistener, CoordinatorListener clistener)
Snark(String torrent, String ip, int user_port,
StorageListener slistener, CoordinatorListener clistener) {
this(torrent, ip, user_port, slistener, clistener, true, ".");
}
Snark(String torrent, String ip, int user_port,
StorageListener slistener, CoordinatorListener clistener, boolean start, String rootDir)
{
if (slistener == null)
slistener = this;
@@ -212,7 +248,9 @@ public class Snark
clistener = this;
this.torrent = torrent;
this.rootDataDir = rootDir;
stopped = true;
activity = "Network setup";
// "Taking Three as the subject to reason about--
@@ -310,7 +348,7 @@ public class Snark
}
debug(meta.toString(), INFO);
// When the metainfo torrent was created from an existing file/dir
// it already exists.
if (storage == null)
@@ -319,23 +357,69 @@ public class Snark
{
activity = "Checking storage";
storage = new Storage(meta, slistener);
storage.check();
storage.check(rootDataDir);
}
catch (IOException ioe)
{
try { storage.close(); } catch (IOException ioee) {
ioee.printStackTrace();
}
fatal("Could not create storage", ioe);
}
}
activity = "Collecting pieces";
coordinator = new PeerCoordinator(id, meta, storage, clistener);
PeerAcceptor peeracceptor = new PeerAcceptor(coordinator);
ConnectionAcceptor acceptor = new ConnectionAcceptor(serversocket,
peeracceptor);
coordinator = new PeerCoordinator(id, meta, storage, clistener, this);
PeerCoordinatorSet set = PeerCoordinatorSet.instance();
set.add(coordinator);
ConnectionAcceptor acceptor = ConnectionAcceptor.instance();
acceptor.startAccepting(set, serversocket);
trackerclient = new TrackerClient(meta, coordinator);
trackerclient.start();
if (start)
startTorrent();
}
/**
* Start up contacting peers and querying the tracker
*/
public void startTorrent() {
stopped = false;
boolean coordinatorChanged = false;
if (coordinator.halted()) {
// ok, we have already started and stopped, but the coordinator seems a bit annoying to
// restart safely, so lets build a new one to replace the old
PeerCoordinatorSet set = PeerCoordinatorSet.instance();
set.remove(coordinator);
PeerCoordinator newCoord = new PeerCoordinator(coordinator.getID(), coordinator.getMetaInfo(),
coordinator.getStorage(), coordinator.getListener(), this);
set.add(newCoord);
coordinator = newCoord;
coordinatorChanged = true;
}
if (!trackerclient.started() && !coordinatorChanged) {
trackerclient.start();
} else if (trackerclient.halted() || coordinatorChanged) {
TrackerClient newClient = new TrackerClient(coordinator.getMetaInfo(), coordinator);
if (!trackerclient.halted())
trackerclient.halt();
trackerclient = newClient;
trackerclient.start();
}
}
/**
* Stop contacting the tracker and talking with peers
*/
public void stopTorrent() {
stopped = true;
trackerclient.halt();
coordinator.halt();
try {
storage.close();
} catch (IOException ioe) {
System.out.println("Error closing " + torrent);
ioe.printStackTrace();
}
PeerCoordinatorSet.instance().remove(coordinator);
}
static Snark parseArguments(String[] args)
@@ -357,6 +441,8 @@ public class Snark
String ip = null;
String torrent = null;
boolean configured = I2PSnarkUtil.instance().configured();
int i = 0;
while (i < args.length)
{
@@ -403,7 +489,8 @@ public class Snark
{
String proxyHost = args[i+1];
String proxyPort = args[i+2];
I2PSnarkUtil.instance().setProxy(proxyHost, Integer.parseInt(proxyPort));
if (!configured)
I2PSnarkUtil.instance().setProxy(proxyHost, Integer.parseInt(proxyPort));
i += 3;
}
else if (args[i].equals("--i2cp"))
@@ -424,7 +511,8 @@ public class Snark
}
}
}
I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, Integer.parseInt(i2cpPort), opts);
if (!configured)
I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, Integer.parseInt(i2cpPort), opts);
i += 3 + (opts != null ? 1 : 0);
}
else
@@ -498,7 +586,7 @@ public class Snark
/**
* Aborts program abnormally.
*/
public static void fatal(String s)
public void fatal(String s)
{
fatal(s, null);
}
@@ -506,12 +594,13 @@ public class Snark
/**
* Aborts program abnormally.
*/
public static void fatal(String s, Throwable t)
public void fatal(String s, Throwable t)
{
I2PSnarkUtil.instance().debug(s, ERROR, t);
//System.err.println("snark: " + s + ((t == null) ? "" : (": " + t)));
//if (debug >= INFO && t != null)
// t.printStackTrace();
stopTorrent();
throw new RuntimeException("die bart die");
}
@@ -588,6 +677,15 @@ public class Snark
allChecked = true;
checking = false;
}
public void storageCompleted(Storage storage)
{
Snark.debug("Completely received " + torrent, Snark.INFO);
//storage.close();
System.out.println("Completely received: " + torrent);
if (completeListener != null)
completeListener.torrentComplete(this);
}
public void shutdown()
{
@@ -595,4 +693,8 @@ public class Snark
// have died. But in reality this does not always happen.
System.exit(0);
}
public interface CompleteListener {
public void torrentComplete(Snark snark);
}
}

View File

@@ -0,0 +1,470 @@
package org.klomp.snark;
import java.io.*;
import java.util.*;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
/**
* Manage multiple snarks
*/
public class SnarkManager implements Snark.CompleteListener {
private static SnarkManager _instance = new SnarkManager();
public static SnarkManager instance() { return _instance; }
/** map of (canonical) filename to Snark instance (unsynchronized) */
private Map _snarks;
private String _configFile;
private Properties _config;
private I2PAppContext _context;
private Log _log;
private List _messages;
public static final String PROP_I2CP_HOST = "i2psnark.i2cpHost";
public static final String PROP_I2CP_PORT = "i2psnark.i2cpPort";
public static final String PROP_I2CP_OPTS = "i2psnark.i2cpOptions";
public static final String PROP_EEP_HOST = "i2psnark.eepHost";
public static final String PROP_EEP_PORT = "i2psnark.eepPort";
public static final String PROP_DIR = "i2psnark.dir";
public static final String PROP_AUTO_START = "i2snark.autoStart";
public static final String DEFAULT_AUTO_START = "false";
private SnarkManager() {
_snarks = new HashMap();
_context = I2PAppContext.getGlobalContext();
_log = _context.logManager().getLog(SnarkManager.class);
_messages = new ArrayList(16);
loadConfig("i2psnark.config");
int minutes = getStartupDelayMinutes();
_messages.add("Starting up torrents in " + minutes + (minutes == 1 ? " minute" : " minutes"));
I2PThread monitor = new I2PThread(new DirMonitor(), "Snark DirMonitor");
monitor.setDaemon(true);
monitor.start();
}
private static final int MAX_MESSAGES = 5;
public void addMessage(String message) {
synchronized (_messages) {
_messages.add(message);
while (_messages.size() > MAX_MESSAGES)
_messages.remove(0);
}
_log.info("MSG: " + message);
}
/** newest last */
public List getMessages() {
synchronized (_messages) {
return new ArrayList(_messages);
}
}
public boolean shouldAutoStart() {
return Boolean.valueOf(_config.getProperty(PROP_AUTO_START, DEFAULT_AUTO_START+"")).booleanValue();
}
private int getStartupDelayMinutes() { return 1; }
public File getDataDir() {
String dir = _config.getProperty(PROP_DIR);
if ( (dir == null) || (dir.trim().length() <= 0) )
dir = "i2psnark";
return new File(dir);
}
public void loadConfig(String filename) {
_configFile = filename;
if (_config == null)
_config = new Properties();
File cfg = new File(filename);
if (cfg.exists()) {
try {
DataHelper.loadProps(_config, cfg);
} catch (IOException ioe) {
_log.error("Error loading I2PSnark config '" + filename + "'", ioe);
}
}
// now add sane defaults
if (!_config.containsKey(PROP_I2CP_HOST))
_config.setProperty(PROP_I2CP_HOST, "localhost");
if (!_config.containsKey(PROP_I2CP_PORT))
_config.setProperty(PROP_I2CP_PORT, "7654");
if (!_config.containsKey(PROP_EEP_HOST))
_config.setProperty(PROP_EEP_HOST, "localhost");
if (!_config.containsKey(PROP_EEP_PORT))
_config.setProperty(PROP_EEP_PORT, "4444");
if (!_config.containsKey(PROP_DIR))
_config.setProperty(PROP_DIR, "i2psnark");
if (!_config.containsKey(PROP_AUTO_START))
_config.setProperty(PROP_AUTO_START, DEFAULT_AUTO_START);
updateConfig();
}
private void updateConfig() {
String i2cpHost = _config.getProperty(PROP_I2CP_HOST);
int i2cpPort = getInt(PROP_I2CP_PORT, 7654);
String opts = _config.getProperty(PROP_I2CP_OPTS);
Map i2cpOpts = new HashMap();
if (opts != null) {
StringTokenizer tok = new StringTokenizer(opts, " ");
while (tok.hasMoreTokens()) {
String pair = tok.nextToken();
int split = pair.indexOf('=');
if (split > 0)
i2cpOpts.put(pair.substring(0, split), pair.substring(split+1));
}
}
if (i2cpHost != null) {
I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts);
_log.debug("Configuring with I2CP options " + i2cpOpts);
}
//I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties());
String eepHost = _config.getProperty(PROP_EEP_HOST);
int eepPort = getInt(PROP_EEP_PORT, 4444);
if (eepHost != null)
I2PSnarkUtil.instance().setProxy(eepHost, eepPort);
getDataDir().mkdirs();
}
private int getInt(String prop, int defaultVal) {
String p = _config.getProperty(prop);
try {
if ( (p != null) && (p.trim().length() > 0) )
return Integer.parseInt(p.trim());
} catch (NumberFormatException nfe) {
// ignore
}
return defaultVal;
}
public void updateConfig(String dataDir, boolean autoStart, String seedPct, String eepHost,
String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts) {
boolean changed = false;
if (eepHost != null) {
int port = I2PSnarkUtil.instance().getEepProxyPort();
try { port = Integer.parseInt(eepPort); } catch (NumberFormatException nfe) {}
String host = I2PSnarkUtil.instance().getEepProxyHost();
if ( (eepHost.trim().length() > 0) && (port > 0) &&
((!host.equals(eepHost) || (port != I2PSnarkUtil.instance().getEepProxyPort()) )) ) {
I2PSnarkUtil.instance().setProxy(eepHost, port);
changed = true;
_config.setProperty(PROP_EEP_HOST, eepHost);
_config.setProperty(PROP_EEP_PORT, eepPort+"");
addMessage("EepProxy location changed to " + eepHost + ":" + port);
}
}
if (i2cpHost != null) {
int oldI2CPPort = I2PSnarkUtil.instance().getI2CPPort();
String oldI2CPHost = I2PSnarkUtil.instance().getI2CPHost();
int port = oldI2CPPort;
try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {}
String host = oldI2CPHost;
Map opts = new HashMap();
if (i2cpOpts == null) i2cpOpts = "";
StringTokenizer tok = new StringTokenizer(i2cpOpts, " \t\n");
while (tok.hasMoreTokens()) {
String pair = tok.nextToken();
int split = pair.indexOf('=');
if (split > 0)
opts.put(pair.substring(0, split), pair.substring(split+1));
}
Map oldOpts = new HashMap();
String oldI2CPOpts = _config.getProperty(PROP_I2CP_OPTS);
if (oldI2CPOpts == null) oldI2CPOpts = "";
tok = new StringTokenizer(oldI2CPOpts, " \t\n");
while (tok.hasMoreTokens()) {
String pair = tok.nextToken();
int split = pair.indexOf('=');
if (split > 0)
oldOpts.put(pair.substring(0, split), pair.substring(split+1));
}
if ( (i2cpHost.trim().length() > 0) && (port > 0) &&
((!host.equals(i2cpHost) ||
(port != I2PSnarkUtil.instance().getI2CPPort()) ||
(!oldOpts.equals(opts)))) ) {
boolean snarksActive = false;
Set names = listTorrentFiles();
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
Snark snark = getTorrent((String)iter.next());
if ( (snark != null) && (!snark.stopped) ) {
snarksActive = true;
break;
}
}
if (snarksActive) {
addMessage("Cannot change the I2CP settings while torrents are active");
_log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts
+ "] oldOpts [" + oldOpts + "]");
} else {
if (I2PSnarkUtil.instance().connected()) {
I2PSnarkUtil.instance().disconnect();
addMessage("Disconnecting old I2CP destination");
}
Properties p = new Properties();
p.putAll(opts);
addMessage("I2CP settings changed to " + i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")");
I2PSnarkUtil.instance().setI2CPConfig(i2cpHost, port, p);
boolean ok = I2PSnarkUtil.instance().connect();
if (!ok) {
addMessage("Unable to connect with the new settings, reverting to the old I2CP settings");
I2PSnarkUtil.instance().setI2CPConfig(oldI2CPHost, oldI2CPPort, oldOpts);
ok = I2PSnarkUtil.instance().connect();
if (!ok)
addMessage("Unable to reconnect with the old settings!");
} else {
addMessage("Reconnected on the new I2CP destination");
_config.setProperty(PROP_I2CP_HOST, i2cpHost.trim());
_config.setProperty(PROP_I2CP_PORT, "" + port);
_config.setProperty(PROP_I2CP_OPTS, i2cpOpts.trim());
changed = true;
// no PeerAcceptors/I2PServerSockets to deal with, since all snarks are inactive
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
String name = (String)iter.next();
Snark snark = getTorrent(name);
if ( (snark != null) && (snark.acceptor != null) ) {
snark.acceptor.restart();
addMessage("I2CP listener restarted for " + snark.meta.getName());
}
}
}
}
changed = true;
}
}
if (shouldAutoStart() != autoStart) {
_config.setProperty(PROP_AUTO_START, autoStart + "");
addMessage("Adjusted autostart to " + autoStart);
changed = true;
}
if (changed) {
saveConfig();
} else {
addMessage("Configuration unchanged");
}
}
public void saveConfig() {
try {
DataHelper.storeProps(_config, new File(_configFile));
} catch (IOException ioe) {
addMessage("Unable to save the config to '" + _configFile + "'");
}
}
public Properties getConfig() { return _config; }
/** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */
private static final int MAX_FILES_PER_TORRENT = 128;
/** set of filenames that we are dealing with */
public Set listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } }
/**
* Grab the torrent given the (canonical) filename
*/
public Snark getTorrent(String filename) { synchronized (_snarks) { return (Snark)_snarks.get(filename); } }
public void addTorrent(String filename) {
if (!I2PSnarkUtil.instance().connected()) {
addMessage("Connecting to I2P");
boolean ok = I2PSnarkUtil.instance().connect();
if (!ok) {
addMessage("Error connecting to I2P - check your I2CP settings");
return;
}
}
File sfile = new File(filename);
try {
filename = sfile.getCanonicalPath();
} catch (IOException ioe) {
_log.error("Unable to add the torrent " + filename, ioe);
addMessage("ERR: Could not add the torrent '" + filename + "': " + ioe.getMessage());
return;
}
File dataDir = getDataDir();
Snark torrent = null;
synchronized (_snarks) {
torrent = (Snark)_snarks.get(filename);
if (torrent == null) {
FileInputStream fis = null;
try {
fis = new FileInputStream(sfile);
MetaInfo info = new MetaInfo(fis);
fis.close();
fis = null;
String rejectMessage = locked_validateTorrent(info);
if (rejectMessage != null) {
sfile.delete();
addMessage(rejectMessage);
return;
} else {
torrent = new Snark(filename, null, -1, null, null, false, dataDir.getPath());
torrent.completeListener = this;
_snarks.put(filename, torrent);
}
} catch (IOException ioe) {
addMessage("Torrent in " + sfile.getName() + " is invalid: " + ioe.getMessage());
if (sfile.exists())
sfile.delete();
return;
} finally {
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
}
} else {
return;
}
}
// ok, snark created, now lets start it up or configure it further
File f = new File(filename);
if (shouldAutoStart()) {
torrent.startTorrent();
addMessage("Torrent added and started: '" + f.getName() + "'");
} else {
addMessage("Torrent added: '" + f.getName() + "'");
}
}
private String locked_validateTorrent(MetaInfo info) throws IOException {
List files = info.getFiles();
if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
return "Too many files in " + info.getName() + " (" + files.size() + "), deleting it";
} else if (info.getPieces() <= 0) {
return "No pieces in " + info.getName() + "? deleting it";
} else if (info.getPieceLength(0) > 10*1024*1024) {
return "Pieces are too large in " + info.getName() + " (" + info.getPieceLength(0)/1024 + "KB, deleting it";
} else if (info.getTotalLength() > 10*1024*1024*1024l) {
System.out.println("torrent info: " + info.toString());
List lengths = info.getLengths();
if (lengths != null)
for (int i = 0; i < lengths.size(); i++)
System.out.println("File " + i + " is " + lengths.get(i) + " long");
return "Torrents larger than 10GB are not supported yet (because we're paranoid): " + info.getName() + ", deleting it";
} else {
// ok
return null;
}
}
/**
* Stop the torrent, leaving it on the list of torrents unless told to remove it
*/
public Snark stopTorrent(String filename, boolean shouldRemove) {
File sfile = new File(filename);
try {
filename = sfile.getCanonicalPath();
} catch (IOException ioe) {
_log.error("Unable to remove the torrent " + filename, ioe);
addMessage("ERR: Could not remove the torrent '" + filename + "': " + ioe.getMessage());
return null;
}
int remaining = 0;
Snark torrent = null;
synchronized (_snarks) {
if (shouldRemove)
torrent = (Snark)_snarks.remove(filename);
else
torrent = (Snark)_snarks.get(filename);
remaining = _snarks.size();
}
if (torrent != null) {
boolean wasStopped = torrent.stopped;
torrent.stopTorrent();
if (remaining == 0) {
// should we disconnect/reconnect here (taking care to deal with the other thread's
// I2PServerSocket.accept() call properly?)
////I2PSnarkUtil.instance().
}
if (!wasStopped)
addMessage("Torrent stopped: '" + sfile.getName() + "'");
}
return torrent;
}
/**
* Stop the torrent and delete the torrent file itself, but leaving the data
* behind.
*/
public void removeTorrent(String filename) {
Snark torrent = stopTorrent(filename, true);
if (torrent != null) {
File torrentFile = new File(filename);
torrentFile.delete();
addMessage("Torrent removed: '" + torrentFile.getName() + "'");
}
}
private class DirMonitor implements Runnable {
public void run() {
try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {}
// the first message was a "We are starting up in 1m"
synchronized (_messages) {
if (_messages.size() == 1)
_messages.remove(0);
}
while (true) {
File dir = getDataDir();
_log.debug("Directory Monitor loop over " + dir.getAbsolutePath());
try {
monitorTorrents(dir);
} catch (Exception e) {
_log.error("Error in the DirectoryMonitor", e);
}
try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
}
}
}
public void torrentComplete(Snark snark) {
File f = new File(snark.torrent);
long len = snark.meta.getTotalLength();
addMessage("Download complete of " + f.getName()
+ (len < 5*1024*1024 ? " (size: " + (len/1024) + "KB)" : " (size: " + (len/(1024*1024l)) + "MB)"));
}
private void monitorTorrents(File dir) {
String fileNames[] = dir.list(TorrentFilenameFilter.instance());
List foundNames = new ArrayList(0);
if (fileNames != null) {
for (int i = 0; i < fileNames.length; i++) {
try {
foundNames.add(new File(dir, fileNames[i]).getCanonicalPath());
} catch (IOException ioe) {
_log.error("Error resolving '" + fileNames[i] + "' in '" + dir, ioe);
}
}
}
Set existingNames = listTorrentFiles();
// lets find new ones first...
for (int i = 0; i < foundNames.size(); i++) {
if (existingNames.contains(foundNames.get(i))) {
// already known. noop
} else {
if (I2PSnarkUtil.instance().connect())
addTorrent((String)foundNames.get(i));
else
addMessage("Unable to connect to I2P");
}
}
// now lets see which ones have been removed...
for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) {
String name = (String)iter.next();
if (foundNames.contains(name)) {
// known and still there. noop
} else {
// known, but removed. drop it
stopTorrent(name, true);
}
}
}
private static class TorrentFilenameFilter implements FilenameFilter {
private static final TorrentFilenameFilter _filter = new TorrentFilenameFilter();
public static TorrentFilenameFilter instance() { return _filter; }
public boolean accept(File dir, String name) {
return (name != null) && (name.endsWith(".torrent"));
}
}
}

View File

@@ -22,10 +22,12 @@ package org.klomp.snark;
import java.io.IOException;
import net.i2p.util.I2PThread;
/**
* Makes sure everything ends correctly when shutting down.
*/
public class SnarkShutdown extends Thread
public class SnarkShutdown extends I2PThread
{
private final Storage storage;
private final PeerCoordinator coordinator;
@@ -72,7 +74,8 @@ public class SnarkShutdown extends Thread
}
catch(IOException ioe)
{
Snark.fatal("Couldn't properly close storage", ioe);
I2PSnarkUtil.instance().debug("Couldn't properly close storage", Snark.ERROR, ioe);
throw new RuntimeException("b0rking");
}
}

View File

@@ -25,6 +25,8 @@ import java.util.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import net.i2p.crypto.SHA1;
/**
* Maintains pieces on disk. Can be used to store and retrieve pieces.
*/
@@ -121,7 +123,7 @@ public class Storage
}
// Note that the piece_hashes are not correctly setup yet.
metainfo = new MetaInfo(announce, baseFile.getName(), files,
metainfo = new MetaInfo(announce, baseFile.getName(), null, files,
lengthsList, piece_size, piece_hashes, total);
}
@@ -129,6 +131,14 @@ public class Storage
// Creates piece hases for a new storage.
public void create() throws IOException
{
if (true) {
fast_digestCreate();
} else {
orig_digestCreate();
}
}
private void orig_digestCreate() throws IOException {
// Calculate piece_hashes
MessageDigest digest = null;
try
@@ -164,6 +174,34 @@ public class Storage
metainfo = metainfo.reannounce(metainfo.getAnnounce());
}
private void fast_digestCreate() throws IOException {
// Calculate piece_hashes
SHA1 digest = new SHA1();
byte[] piece_hashes = metainfo.getPieceHashes();
byte[] piece = new byte[piece_size];
for (int i = 0; i < pieces; i++)
{
int length = getUncheckedPiece(i, piece, 0);
digest.update(piece, 0, length);
byte[] hash = digest.digest();
for (int j = 0; j < 20; j++)
piece_hashes[20 * i + j] = hash[j];
bitfield.set(i);
if (listener != null)
listener.storageChecked(this, i, true);
}
if (listener != null)
listener.storageAllChecked(this);
// Reannounce to force recalculating the info_hash.
metainfo = metainfo.reannounce(metainfo.getAnnounce());
}
private void getFiles(File base) throws IOException
{
ArrayList files = new ArrayList();
@@ -240,9 +278,9 @@ public class Storage
/**
* Creates (and/or checks) all files from the metainfo file list.
*/
public void check() throws IOException
public void check(String rootDir) throws IOException
{
File base = new File(filterName(metainfo.getName()));
File base = new File(rootDir, filterName(metainfo.getName()));
List files = metainfo.getFiles();
if (files == null)
@@ -256,7 +294,10 @@ public class Storage
rafs = new RandomAccessFile[1];
names = new String[1];
lengths[0] = metainfo.getTotalLength();
rafs[0] = new RandomAccessFile(base, "rw");
if (base.exists() && !base.canWrite()) // hope we can get away with this, if we are only seeding...
rafs[0] = new RandomAccessFile(base, "r");
else
rafs[0] = new RandomAccessFile(base, "rw");
names[0] = base.getName();
}
else
@@ -277,7 +318,10 @@ public class Storage
File f = createFileFromNames(base, (List)files.get(i));
lengths[i] = ((Long)ls.get(i)).longValue();
total += lengths[i];
rafs[i] = new RandomAccessFile(f, "rw");
if (f.exists() && !f.canWrite()) // see above re: only seeding
rafs[i] = new RandomAccessFile(f, "r");
else
rafs[i] = new RandomAccessFile(f, "rw");
names[i] = f.getName();
}
@@ -368,8 +412,11 @@ public class Storage
}
}
if (listener != null)
if (listener != null) {
listener.storageAllChecked(this);
if (needed <= 0)
listener.storageCompleted(this);
}
}
private void allocateFile(int nr) throws IOException
@@ -399,12 +446,18 @@ public class Storage
*/
public void close() throws IOException
{
if (rafs == null) return;
for (int i = 0; i < rafs.length; i++)
{
synchronized(rafs[i])
{
rafs[i].close();
}
try {
synchronized(rafs[i])
{
rafs[i].close();
}
} catch (IOException ioe) {
I2PSnarkUtil.instance().debug("Error closing " + rafs[i], Snark.ERROR, ioe);
// gobble gobble
}
}
}
@@ -483,6 +536,12 @@ public class Storage
}
}
if (complete) {
listener.storageCompleted(this);
// do we also need to close all of the files and reopen
// them readonly?
}
return true;
}

View File

@@ -49,4 +49,10 @@ public interface StorageListener
* storage is known.
*/
void storageAllChecked(Storage storage);
/**
* Called the one time when the data is completely received and checked.
*
*/
void storageCompleted(Storage storage);
}

View File

@@ -25,6 +25,8 @@ import java.net.*;
import java.util.*;
import org.klomp.snark.bencode.*;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
/**
* Informs metainfo tracker of events and gets new peers for peer
@@ -32,8 +34,9 @@ import org.klomp.snark.bencode.*;
*
* @author Mark Wielaard (mark@klomp.org)
*/
public class TrackerClient extends Thread
public class TrackerClient extends I2PThread
{
private static final Log _log = new Log(TrackerClient.class);
private static final String NO_EVENT = "";
private static final String STARTED_EVENT = "started";
private static final String COMPLETED_EVENT = "completed";
@@ -46,6 +49,7 @@ public class TrackerClient extends Thread
private final int port;
private boolean stop;
private boolean started;
private long interval;
private long lastRequestTime;
@@ -60,8 +64,18 @@ public class TrackerClient extends Thread
this.port = 6881; //(port == -1) ? 9 : port;
stop = false;
started = false;
}
public void start() {
if (stop) throw new RuntimeException("Dont rerun me, create a copy");
super.start();
started = true;
}
public boolean halted() { return stop; }
public boolean started() { return started; }
/**
* Interrupts this Thread to stop it.
*/
@@ -71,13 +85,26 @@ public class TrackerClient extends Thread
this.interrupt();
}
private boolean verifyConnected() {
while (!stop && !I2PSnarkUtil.instance().connected()) {
boolean ok = I2PSnarkUtil.instance().connect();
if (!ok) {
try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
}
}
return !stop && I2PSnarkUtil.instance().connected();
}
public void run()
{
// XXX - Support other IPs
String announce = I2PSnarkUtil.instance().rewriteAnnounce(meta.getAnnounce());
String announce = meta.getAnnounce(); //I2PSnarkUtil.instance().rewriteAnnounce(meta.getAnnounce());
String infoHash = urlencode(meta.getInfoHash());
String peerID = urlencode(coordinator.getID());
_log.debug("Announce: [" + meta.getAnnounce() + "] infoHash: " + infoHash
+ " xmitAnnounce: [" + announce + "]");
long uploaded = coordinator.getUploaded();
long downloaded = coordinator.getDownloaded();
long left = coordinator.getLeft();
@@ -86,6 +113,7 @@ public class TrackerClient extends Thread
try
{
if (!verifyConnected()) return;
boolean started = false;
while (!started)
{
@@ -95,10 +123,20 @@ public class TrackerClient extends Thread
TrackerInfo info = doRequest(announce, infoHash, peerID,
uploaded, downloaded, left,
STARTED_EVENT);
Iterator it = info.getPeers().iterator();
while (it.hasNext())
coordinator.addPeer((Peer)it.next());
Set peers = info.getPeers();
coordinator.trackerSeenPeers = peers.size();
if (!completed) {
Iterator it = peers.iterator();
while (it.hasNext()) {
Peer cur = (Peer)it.next();
coordinator.addPeer(cur);
int delay = 3000;
int c = ((int)cur.getPeerID().getAddress().calculateHash().toBase64().charAt(0)) % 10;
try { Thread.sleep(delay * c); } catch (InterruptedException ie) {}
}
}
started = true;
coordinator.trackerProblems = null;
}
catch (IOException ioe)
{
@@ -106,6 +144,7 @@ public class TrackerClient extends Thread
Snark.debug
("WARNING: Could not contact tracker at '"
+ announce + "': " + ioe, Snark.WARNING);
coordinator.trackerProblems = ioe.getMessage();
}
if (!started && !stop)
@@ -123,12 +162,14 @@ public class TrackerClient extends Thread
}
}
Random r = new Random();
while(!stop)
{
try
{
// Sleep some minutes...
Thread.sleep(SLEEP*60*1000);
int delay = SLEEP*60*1000 + r.nextInt(120*1000);
Thread.sleep(delay);
}
catch(InterruptedException interrupt)
{
@@ -137,6 +178,8 @@ public class TrackerClient extends Thread
if (stop)
break;
if (!verifyConnected()) return;
uploaded = coordinator.getUploaded();
downloaded = coordinator.getDownloaded();
@@ -163,9 +206,22 @@ public class TrackerClient extends Thread
uploaded, downloaded, left,
event);
Iterator it = info.getPeers().iterator();
while (it.hasNext())
coordinator.addPeer((Peer)it.next());
Set peers = info.getPeers();
coordinator.trackerSeenPeers = peers.size();
if ( (left > 0) && (!completed) ) {
// we only want to talk to new people if we need things
// from them (duh)
List ordered = new ArrayList(peers);
Collections.shuffle(ordered);
Iterator it = ordered.iterator();
while (it.hasNext()) {
Peer cur = (Peer)it.next();
coordinator.addPeer(cur);
int delay = 3000;
int c = ((int)cur.getPeerID().getAddress().calculateHash().toBase64().charAt(0)) % 10;
try { Thread.sleep(delay * c); } catch (InterruptedException ie) {}
}
}
}
catch (IOException ioe)
{
@@ -179,13 +235,15 @@ public class TrackerClient extends Thread
}
catch (Throwable t)
{
Snark.debug("TrackerClient: " + t, Snark.ERROR);
t.printStackTrace();
I2PSnarkUtil.instance().debug("TrackerClient: " + t, Snark.ERROR, t);
if (t instanceof OutOfMemoryError)
throw (OutOfMemoryError)t;
}
finally
{
try
{
if (!verifyConnected()) return;
TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded,
downloaded, left, STOPPED_EVENT);
}
@@ -203,7 +261,7 @@ public class TrackerClient extends Thread
+ "?info_hash=" + infoHash
+ "&peer_id=" + peerID
+ "&port=" + port
+ "&ip=" + I2PSnarkUtil.instance().getOurIPString()
+ "&ip=" + I2PSnarkUtil.instance().getOurIPString() + ".i2p"
+ "&uploaded=" + uploaded
+ "&downloaded=" + downloaded
+ "&left=" + left
@@ -216,21 +274,26 @@ public class TrackerClient extends Thread
throw new IOException("Error fetching " + s);
}
fetched.deleteOnExit();
InputStream in = new FileInputStream(fetched);
InputStream in = null;
try {
in = new FileInputStream(fetched);
TrackerInfo info = new TrackerInfo(in, coordinator.getID(),
coordinator.getMetaInfo());
if (Snark.debug >= Snark.INFO)
Snark.debug("TrackerClient response: " + info, Snark.INFO);
lastRequestTime = System.currentTimeMillis();
String failure = info.getFailureReason();
if (failure != null)
throw new IOException(failure);
interval = info.getInterval() * 1000;
return info;
TrackerInfo info = new TrackerInfo(in, coordinator.getID(),
coordinator.getMetaInfo());
if (Snark.debug >= Snark.INFO)
Snark.debug("TrackerClient response: " + info, Snark.INFO);
lastRequestTime = System.currentTimeMillis();
String failure = info.getFailureReason();
if (failure != null)
throw new IOException(failure);
interval = info.getInterval() * 1000;
return info;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
fetched.delete();
}
}
/**

View File

@@ -170,6 +170,9 @@ public class BEValue
}
}
/** return the untyped value */
public Object getValue() { return value; }
public String toString()
{
String valueString;

View File

@@ -62,6 +62,8 @@ public class BEncoder
bencode((List)o, out);
else if (o instanceof Map)
bencode((Map)o, out);
else if (o instanceof BEValue)
bencode(((BEValue)o).getValue(), out);
else
throw new IllegalArgumentException("Cannot bencode: " + o.getClass());
}

View File

@@ -0,0 +1,620 @@
package org.klomp.snark.web;
import java.io.*;
import java.util.*;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServlet;
import net.i2p.I2PAppContext;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import org.klomp.snark.*;
/**
*
*/
public class I2PSnarkServlet extends HttpServlet {
private I2PAppContext _context;
private Log _log;
private SnarkManager _manager;
private static long _nonce;
public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
public void init(ServletConfig cfg) throws ServletException {
super.init(cfg);
_context = I2PAppContext.getGlobalContext();
_log = _context.logManager().getLog(I2PSnarkServlet.class);
_nonce = _context.random().nextLong();
_manager = SnarkManager.instance();
String configFile = _context.getProperty(PROP_CONFIG_FILE);
if ( (configFile == null) || (configFile.trim().length() <= 0) )
configFile = "i2psnark.config";
_manager.loadConfig(configFile);
}
public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html; charset=UTF-8");
String nonce = req.getParameter("nonce");
if ( (nonce != null) && (nonce.equals(String.valueOf(_nonce))) )
processRequest(req);
PrintWriter out = resp.getWriter();
out.write(HEADER_BEGIN);
// we want it to go to the base URI so we don't refresh with some funky action= value
out.write("<meta http-equiv=\"refresh\" content=\"60;" + req.getRequestURI() + "\">\n");
out.write(HEADER);
out.write("<table border=\"0\" width=\"100%\">\n");
out.write("<tr><td width=\"5%\" class=\"snarkTitle\" valign=\"top\" align=\"left\">");
out.write("I2PSnark<br />\n");
out.write("<a href=\"" + req.getRequestURI() + "\" class=\"snarkRefresh\">Refresh</a>\n");
out.write("</td><td width=\"95%\" class=\"snarkMessages\" valign=\"top\" align=\"left\"><pre>");
List msgs = _manager.getMessages();
for (int i = msgs.size()-1; i >= 0; i--) {
String msg = (String)msgs.get(i);
out.write(msg + "\n");
}
out.write("</pre></td></tr></table>\n");
out.write(TABLE_HEADER);
List snarks = getSortedSnarks(req);
String uri = req.getRequestURI();
for (int i = 0; i < snarks.size(); i++) {
Snark snark = (Snark)snarks.get(i);
displaySnark(out, snark, uri, i);
}
if (snarks.size() <= 0) {
out.write(TABLE_EMPTY);
}
out.write(TABLE_FOOTER);
writeAddForm(out, req);
if (false) // seeding needs to register the torrent first (boo, hiss)
writeSeedForm(out, req);
writeConfigForm(out, req);
out.write(FOOTER);
}
/**
* Do what they ask, adding messages to _manager.addMessage as necessary
*/
private void processRequest(HttpServletRequest req) {
String action = req.getParameter("action");
if (action == null) {
// noop
} else if ("Add torrent".equals(action)) {
String newFile = req.getParameter("newFile");
String newURL = req.getParameter("newURL");
File f = null;
if ( (newFile != null) && (newFile.trim().length() > 0) )
f = new File(newFile.trim());
if ( (f != null) && (!f.exists()) ) {
_manager.addMessage("Torrent file " + newFile +" does not exist");
}
if ( (f != null) && (f.exists()) ) {
File local = new File(_manager.getDataDir(), f.getName());
String canonical = null;
try {
canonical = local.getCanonicalPath();
if (local.exists()) {
if (_manager.getTorrent(canonical) != null)
_manager.addMessage("Torrent already running: " + newFile);
else
_manager.addMessage("Torrent already in the queue: " + newFile);
} else {
boolean ok = FileUtil.copy(f.getAbsolutePath(), local.getAbsolutePath(), true);
if (ok) {
_manager.addMessage("Copying torrent to " + local.getAbsolutePath());
_manager.addTorrent(canonical);
} else {
_manager.addMessage("Unable to copy the torrent to " + local.getAbsolutePath() + " from " + f.getAbsolutePath());
}
}
} catch (IOException ioe) {
_log.warn("hrm: " + local, ioe);
}
} else if ( (newURL != null) && (newURL.trim().length() > "http://.i2p/".length()) ) {
_manager.addMessage("Fetching " + newURL);
I2PThread fetch = new I2PThread(new FetchAndAdd(newURL), "Fetch and add");
fetch.start();
} else {
// no file or URL specified
}
} else if ("Stop".equals(action)) {
String torrent = req.getParameter("torrent");
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
String name = (String)iter.next();
Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
_manager.stopTorrent(name, false);
break;
}
}
}
}
} else if ("Start".equals(action)) {
String torrent = req.getParameter("torrent");
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
String name = (String)iter.next();
Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
snark.startTorrent();
_manager.addMessage("Starting up torrent " + name);
break;
}
}
}
}
} else if ("Remove".equals(action)) {
String torrent = req.getParameter("torrent");
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
String name = (String)iter.next();
Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
_manager.stopTorrent(name, true);
// should we delete the torrent file?
// yeah, need to, otherwise it'll get autoadded again (at the moment
File f = new File(name);
f.delete();
_manager.addMessage("Torrent file deleted: " + f.getAbsolutePath());
break;
}
}
}
}
} else if ("Delete".equals(action)) {
String torrent = req.getParameter("torrent");
if (torrent != null) {
byte infoHash[] = Base64.decode(torrent);
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
String name = (String)iter.next();
Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
_manager.stopTorrent(name, true);
File f = new File(name);
f.delete();
_manager.addMessage("Torrent file deleted: " + f.getAbsolutePath());
List files = snark.meta.getFiles();
String dataFile = snark.meta.getName();
for (int i = 0; files != null && i < files.size(); i++) {
File df = new File(_manager.getDataDir(), (String)files.get(i));
boolean deleted = FileUtil.rmdir(df, false);
if (deleted)
_manager.addMessage("Data dir deleted: " + df.getAbsolutePath());
else
_manager.addMessage("Data dir could not be deleted: " + df.getAbsolutePath());
}
if (dataFile != null) {
f = new File(_manager.getDataDir(), dataFile);
boolean deleted = f.delete();
if (deleted)
_manager.addMessage("Data file deleted: " + f.getAbsolutePath());
else
_manager.addMessage("Data file could not be deleted: " + f.getAbsolutePath());
}
break;
}
}
}
}
} else if ("Save configuration".equals(action)) {
String dataDir = req.getParameter("dataDir");
boolean autoStart = req.getParameter("autoStart") != null;
String seedPct = req.getParameter("seedPct");
String eepHost = req.getParameter("eepHost");
String eepPort = req.getParameter("eepPort");
String i2cpHost = req.getParameter("i2cpHost");
String i2cpPort = req.getParameter("i2cpPort");
String i2cpOpts = req.getParameter("i2cpOpts");
_manager.updateConfig(dataDir, autoStart, seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts);
} else if ("Create torrent".equals(action)) {
String baseData = req.getParameter("baseFile");
if (baseData != null) {
File baseFile = new File(_manager.getDataDir(), baseData);
String announceURL = req.getParameter("announceURL");
String announceURLOther = req.getParameter("announceURLOther");
if ( (announceURLOther != null) && (announceURLOther.trim().length() > "http://.i2p/announce".length()) )
announceURL = announceURLOther;
if (baseFile.exists()) {
try {
Storage s = new Storage(baseFile, announceURL, null);
s.create();
MetaInfo info = s.getMetaInfo();
File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent");
if (torrentFile.exists())
throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath());
FileOutputStream out = new FileOutputStream(torrentFile);
out.write(info.getTorrentData());
out.close();
_manager.addMessage("Torrent created for " + baseFile.getName() + ": " + torrentFile.getAbsolutePath());
// now fire it up and seed away!
_manager.addTorrent(torrentFile.getCanonicalPath());
} catch (IOException ioe) {
_manager.addMessage("Error creating a torrent for " + baseFile.getAbsolutePath() + ": " + ioe.getMessage());
}
} else {
_manager.addMessage("Cannot create a torrent for the nonexistant data: " + baseFile.getAbsolutePath());
}
}
}
}
private class FetchAndAdd implements Runnable {
private String _url;
public FetchAndAdd(String url) {
_url = url;
}
public void run() {
_url = _url.trim();
File file = I2PSnarkUtil.instance().get(_url, false);
try {
if ( (file != null) && (file.exists()) && (file.length() > 0) ) {
_manager.addMessage("Torrent fetched from " + _url);
FileInputStream in = null;
try {
in = new FileInputStream(file);
MetaInfo info = new MetaInfo(in);
String name = info.getName();
name = name.replace('/', '_');
name = name.replace('\\', '_');
name = name.replace('&', '+');
name = name.replace('\'', '_');
name = name.replace('"', '_');
name = name.replace('`', '_');
name = name + ".torrent";
File torrentFile = new File(_manager.getDataDir(), name);
String canonical = torrentFile.getCanonicalPath();
if (torrentFile.exists()) {
if (_manager.getTorrent(canonical) != null)
_manager.addMessage("Torrent already running: " + name);
else
_manager.addMessage("Torrent already in the queue: " + name);
} else {
FileUtil.copy(file.getAbsolutePath(), canonical, true);
_manager.addTorrent(canonical);
}
} catch (IOException ioe) {
_manager.addMessage("Torrent at " + _url + " was not valid: " + ioe.getMessage());
} finally {
try { in.close(); } catch (IOException ioe) {}
}
} else {
_manager.addMessage("Torrent was not retrieved from " + _url);
}
} finally {
if (file != null) file.delete();
}
}
}
private List getSortedSnarks(HttpServletRequest req) {
Set files = _manager.listTorrentFiles();
TreeSet fileNames = new TreeSet(files); // sorts it alphabetically
ArrayList rv = new ArrayList(fileNames.size());
for (Iterator iter = fileNames.iterator(); iter.hasNext(); ) {
String name = (String)iter.next();
Snark snark = _manager.getTorrent(name);
if (snark != null)
rv.add(snark);
}
return rv;
}
private static final int MAX_DISPLAYED_FILENAME_LENGTH = 60;
private void displaySnark(PrintWriter out, Snark snark, String uri, int row) throws IOException {
String filename = snark.torrent;
File f = new File(filename);
filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name
if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH)
filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "...";
long total = snark.meta.getTotalLength();
long remaining = snark.storage.needed() * snark.meta.getPieceLength(0);
if (remaining > total)
remaining = total;
int totalBps = 4096; // should probably grab this from the snark...
long remainingSeconds = remaining / totalBps;
long uploaded = snark.coordinator.getUploaded();
boolean isRunning = !snark.stopped;
boolean isValid = snark.meta != null;
boolean singleFile = snark.meta.getFiles() == null;
String err = snark.coordinator.trackerProblems;
int curPeers = snark.coordinator.getPeerCount();
int knownPeers = snark.coordinator.trackerSeenPeers;
String statusString = "Unknown";
if (err != null) {
if (isRunning)
statusString = "TrackerErr (" + curPeers + "/" + knownPeers + " peers)";
else
statusString = "TrackerErr (" + err + ")";
} else if (remaining <= 0) {
if (isRunning)
statusString = "Seeding (" + curPeers + "/" + knownPeers + " peers)";
else
statusString = "Complete";
} else {
if (isRunning)
statusString = "OK (" + curPeers + "/" + knownPeers + " peers)";
else
statusString = "Stopped";
}
String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
out.write("<tr class=\"" + rowClass + "\">");
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentStatus " + rowClass + "\">");
out.write(statusString + "</td>\n\t");
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentName " + rowClass + "\">");
if (remaining == 0)
out.write("<a href=\"file:///" + _manager.getDataDir().getAbsolutePath() + File.separatorChar + snark.meta.getName()
+ "\" title=\"Download the completed file\">");
out.write(filename);
if (remaining == 0)
out.write("</a>");
out.write("</td>\n\t");
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
if (remaining > 0) {
out.write(formatSize(total-remaining) + "/" + formatSize(total)); // 18MB/3GB
// lets hold off on the ETA until we have rates sorted...
//out.write(" (eta " + DataHelper.formatDuration(remainingSeconds*1000) + ")"); // (eta 6h)
} else {
out.write(formatSize(total)); // 3GB
}
out.write("</td>\n\t");
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentUploaded " + rowClass
+ "\">" + formatSize(uploaded) + "</td>\n\t");
//out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentRate\">");
//out.write("n/a"); //2KBps/12KBps/4KBps
//out.write("</td>\n\t");
out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentAction " + rowClass + "\">");
if (isRunning) {
out.write("<a href=\"" + uri + "?action=Stop&nonce=" + _nonce
+ "&torrent=" + Base64.encode(snark.meta.getInfoHash())
+ "\" title=\"Stop the torrent\">Stop</a>");
} else {
if (isValid)
out.write("<a href=\"" + uri + "?action=Start&nonce=" + _nonce
+ "&torrent=" + Base64.encode(snark.meta.getInfoHash())
+ "\" title=\"Start the torrent\">Start</a> ");
out.write("<a href=\"" + uri + "?action=Remove&nonce=" + _nonce
+ "&torrent=" + Base64.encode(snark.meta.getInfoHash())
+ "\" title=\"Remove the torrent from the active list, deleting the .torrent file\">Remove</a><br />");
out.write("<a href=\"" + uri + "?action=Delete&nonce=" + _nonce
+ "&torrent=" + Base64.encode(snark.meta.getInfoHash())
+ "\" title=\"Delete the .torrent file and the associated data file(s)\">Delete</a> ");
}
out.write("</td>\n</tr>\n");
}
private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
String uri = req.getRequestURI();
String newURL = req.getParameter("newURL");
if ( (newURL == null) || (newURL.trim().length() <= 0) ) newURL = "http://";
String newFile = req.getParameter("newFile");
if ( (newFile == null) || (newFile.trim().length() <= 0) ) newFile = "";
out.write("<span class=\"snarkNewTorrent\">\n");
// *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
out.write("<form action=\"" + uri + "\" method=\"POST\">\n");
out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n");
out.write("From URL&nbsp;: <input type=\"text\" name=\"newURL\" size=\"50\" value=\"" + newURL + "\" /> \n");
// not supporting from file at the moment, since the file name passed isn't always absolute (so it may not resolve)
//out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br />\n");
out.write("<input type=\"submit\" value=\"Add torrent\" name=\"action\" /><br />\n");
out.write("<span class=\"snarkAddInfo\">Alternately, you can copy .torrent files to " + _manager.getDataDir().getAbsolutePath() + "<br />\n");
out.write("Removing that .torrent file will cause the torrent to stop.<br /></span>\n");
out.write("</form>\n</span>\n");
}
private static final String DEFAULT_TRACKERS[] = {
"Postman's tracker", "http://YRgrgTLGnbTq2aZOZDJQ~o6Uk5k6TK-OZtx0St9pb0G-5EGYURZioxqYG8AQt~LgyyI~NCj6aYWpPO-150RcEvsfgXLR~CxkkZcVpgt6pns8SRc3Bi-QSAkXpJtloapRGcQfzTtwllokbdC-aMGpeDOjYLd8b5V9Im8wdCHYy7LRFxhEtGb~RL55DA8aYOgEXcTpr6RPPywbV~Qf3q5UK55el6Kex-6VCxreUnPEe4hmTAbqZNR7Fm0hpCiHKGoToRcygafpFqDw5frLXToYiqs9d4liyVB-BcOb0ihORbo0nS3CLmAwZGvdAP8BZ7cIYE3Z9IU9D1G8JCMxWarfKX1pix~6pIA-sp1gKlL1HhYhPMxwyxvuSqx34o3BqU7vdTYwWiLpGM~zU1~j9rHL7x60pVuYaXcFQDR4-QVy26b6Pt6BlAZoFmHhPcAuWfu-SFhjyZYsqzmEmHeYdAwa~HojSbofg0TMUgESRXMw6YThK1KXWeeJVeztGTz25sL8AAAA.i2p/announce.php",
"Orion's tracker", "http://gKik1lMlRmuroXVGTZ~7v4Vez3L3ZSpddrGZBrxVriosCQf7iHu6CIk8t15BKsj~P0JJpxrofeuxtm7SCUAJEr0AIYSYw8XOmp35UfcRPQWyb1LsxUkMT4WqxAT3s1ClIICWlBu5An~q-Mm0VFlrYLIPBWlUFnfPR7jZ9uP5ZMSzTKSMYUWao3ejiykr~mtEmyls6g-ZbgKZawa9II4zjOy-hdxHgP-eXMDseFsrym4Gpxvy~3Fv9TuiSqhpgm~UeTo5YBfxn6~TahKtE~~sdCiSydqmKBhxAQ7uT9lda7xt96SS09OYMsIWxLeQUWhns-C~FjJPp1D~IuTrUpAFcVEGVL-BRMmdWbfOJEcWPZ~CBCQSO~VkuN1ebvIOr9JBerFMZSxZtFl8JwcrjCIBxeKPBmfh~xYh16BJm1BBBmN1fp2DKmZ2jBNkAmnUbjQOqWvUcehrykWk5lZbE7bjJMDFH48v3SXwRuDBiHZmSbsTY6zhGY~GkMQHNGxPMMSIAAAA.i2p/bt",
"The freak's tracker", "http://mHKva9x24E5Ygfey2llR1KyQHv5f8hhMpDMwJDg1U-hABpJ2NrQJd6azirdfaR0OKt4jDlmP2o4Qx0H598~AteyD~RJU~xcWYdcOE0dmJ2e9Y8-HY51ie0B1yD9FtIV72ZI-V3TzFDcs6nkdX9b81DwrAwwFzx0EfNvK1GLVWl59Ow85muoRTBA1q8SsZImxdyZ-TApTVlMYIQbdI4iQRwU9OmmtefrCe~ZOf4UBS9-KvNIqUL0XeBSqm0OU1jq-D10Ykg6KfqvuPnBYT1BYHFDQJXW5DdPKwcaQE4MtAdSGmj1epDoaEBUa9btQlFsM2l9Cyn1hzxqNWXELmx8dRlomQLlV4b586dRzW~fLlOPIGC13ntPXogvYvHVyEyptXkv890jC7DZNHyxZd5cyrKC36r9huKvhQAmNABT2Y~pOGwVrb~RpPwT0tBuPZ3lHYhBFYmD8y~AOhhNHKMLzea1rfwTvovBMByDdFps54gMN1mX4MbCGT4w70vIopS9yAAAA.i2p/bytemonsoon/announce.php"
};
private void writeSeedForm(PrintWriter out, HttpServletRequest req) throws IOException {
String uri = req.getRequestURI();
String baseFile = req.getParameter("baseFile");
if (baseFile == null)
baseFile = "";
out.write("<span class=\"snarkNewTorrent\">\n");
// *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
out.write("<form action=\"" + uri + "\" method=\"POST\">\n");
out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n");
//out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br />\n");
out.write("Data to seed: <input type=\"text\" name=\"baseFile\" size=\"50\" value=\"" + baseFile
+ "\" title=\"File within " + _manager.getDataDir().getAbsolutePath() + " to seed\" /><br />\n");
out.write("Tracker: <select name=\"announceURL\"><option value=\"\">Select a tracker</option>\n");
for (int i = 0; i + 1 < DEFAULT_TRACKERS.length; i += 2)
out.write("\t<option value=\"" + DEFAULT_TRACKERS[i+1] + "\">" + DEFAULT_TRACKERS[i] + "</option>\n");
out.write("</select><br />\n");
out.write("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;or: ");
out.write("<input type=\"text\" name=\"announceURLOther\" size=\"50\" value=\"http://\" " +
"title=\"Custom tracker URL\" /><br />\n");
out.write("<input type=\"submit\" value=\"Create torrent\" name=\"action\" />\n");
out.write("</form>\n</span>\n");
}
private void writeConfigForm(PrintWriter out, HttpServletRequest req) throws IOException {
String uri = req.getRequestURI();
String dataDir = _manager.getDataDir().getAbsolutePath();
boolean autoStart = _manager.shouldAutoStart();
int seedPct = 0;
out.write("<form action=\"" + uri + "\" method=\"POST\">\n");
out.write("<span class=\"snarkConfig\"><hr />\n");
out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n");
out.write("<span class=\"snarkConfigTitle\">Configuration:</span><br />\n");
out.write("Data directory: <input type=\"text\" size=\"40\" name=\"dataDir\" value=\"" + dataDir + "\" ");
out.write("title=\"Directory to store torrents and data\" disabled=\"true\" /><br />\n");
out.write("Auto start: <input type=\"checkbox\" name=\"autoStart\" value=\"true\" "
+ (autoStart ? "checked " : "")
+ "title=\"If true, automatically start torrents that are added\" />");
//Auto add: <input type="checkbox" name="autoAdd" value="true" title="If true, automatically add torrents that are found in the data directory" />
//Auto stop: <input type="checkbox" name="autoStop" value="true" title="If true, automatically stop torrents that are removed from the data directory" />
//out.write("<br />\n");
out.write("Seed percentage: <select name=\"seedPct\" disabled=\"true\" >\n\t");
if (seedPct <= 0)
out.write("<option value=\"0\" selected=\"true\">Unlimited</option>\n\t");
else
out.write("<option value=\"0\">Unlimited</option>\n\t");
if (seedPct == 100)
out.write("<option value=\"100\" selected=\"true\">100%</option>\n\t");
else
out.write("<option value=\"100\">100%</option>\n\t");
if (seedPct == 150)
out.write("<option value=\"150\" selected=\"true\">150%</option>\n\t");
else
out.write("<option value=\"150\">150%</option>\n\t");
out.write("</select><br />\n");
//out.write("<hr />\n");
out.write("EepProxy host: <input type=\"text\" name=\"eepHost\" value=\""
+ I2PSnarkUtil.instance().getEepProxyHost() + "\" size=\"15\" /> ");
out.write("port: <input type=\"text\" name=\"eepPort\" value=\""
+ I2PSnarkUtil.instance().getEepProxyPort() + "\" size=\"5\" /><br />\n");
out.write("I2CP host: <input type=\"text\" name=\"i2cpHost\" value=\""
+ I2PSnarkUtil.instance().getI2CPHost() + "\" size=\"15\" /> ");
out.write("port: <input type=\"text\" name=\"i2cpPort\" value=\"" +
+ I2PSnarkUtil.instance().getI2CPPort() + "\" size=\"5\" /> <br />\n");
StringBuffer opts = new StringBuffer(64);
Map options = new TreeMap(I2PSnarkUtil.instance().getI2CPOptions());
for (Iterator iter = options.keySet().iterator(); iter.hasNext(); ) {
String key = (String)iter.next();
String val = (String)options.get(key);
opts.append(key).append('=').append(val).append(' ');
}
out.write("I2CP opts: <input type=\"text\" name=\"i2cpOpts\" size=\"40\" value=\""
+ opts.toString() + "\" /><br />\n");
out.write("<input type=\"submit\" value=\"Save configuration\" name=\"action\" />\n");
out.write("</span>\n");
out.write("</form>\n");
}
private String formatSize(long bytes) {
if (bytes < 5*1024)
return bytes + "B";
else if (bytes < 5*1024*1024)
return (bytes/1024) + "KB";
else if (bytes < 5*1024*1024*1024l)
return (bytes/(1024*1024)) + "MB";
else
return (bytes/(1024*1024*1024)) + "GB";
}
private static final String HEADER_BEGIN = "<html>\n" +
"<head>\n" +
"<title>I2PSnark - anonymous bittorrent</title>\n";
private static final String HEADER = "<style>\n" +
"body {\n" +
" background-color: #C7CFB4;\n" +
"}\n" +
".snarkTitle {\n" +
" text-align: left;\n" +
" float: left;\n" +
" margin: 0px 0px 5px 5px;\n" +
" display: inline;\n" +
" font-size: 16pt;\n" +
" font-weight: bold;\n" +
"}\n" +
".snarkRefresh {\n" +
" font-size: 10pt;\n" +
"}\n" +
".snarkMessages {\n" +
" border: none;\n" +
" background-color: #CECFC6;\n" +
" font-family: monospace;\n" +
" font-size: 10pt;\n" +
" font-weight: 100;\n" +
" width: 100%;\n" +
" text-align: left;\n" +
" margin: 0px 0px 0px 0px;\n" +
" border: 0px;\n" +
" padding: 5px;\n" +
" border-width: 0px;\n" +
" border-spacing: 0px;\n" +
"}\n" +
"table {\n" +
" margin: 0px 0px 0px 0px;\n" +
" border: 0px;\n" +
" padding: 0px;\n" +
" border-width: 0px;\n" +
" border-spacing: 0px;\n" +
"}\n" +
"th {\n" +
" background-color: #C7D5D5;\n" +
" margin: 0px 0px 0px 0px;\n" +
"}\n" +
".snarkTorrentEven {\n" +
" background-color: #E7E7E7;\n" +
"}\n" +
".snarkTorrentOdd {\n" +
" background-color: #DDDDCC;\n" +
"}\n" +
".snarkNewTorrent {\n" +
" font-size: 12pt;\n" +
" font-family: monospace;\n" +
" background-color: #ADAE9;\n" +
"}\n" +
".snarkAddInfo {\n" +
" font-size: 10pt;\n" +
"}\n" +
".snarkConfigTitle {\n" +
" font-size: 12pt;\n" +
" font-weight: bold;\n" +
"}\n" +
".snarkConfig {\n" +
" font-size: 10pt;\n" +
"}\n" +
"</style>\n" +
"</head>\n" +
"<body>\n";
private static final String TABLE_HEADER = "<table border=\"0\" class=\"snarkTorrents\" width=\"100%\">\n" +
"<thead>\n" +
"<tr><th align=\"left\" valign=\"top\">Status</th>\n" +
" <th align=\"left\" valign=\"top\">Torrent</th>\n" +
" <th align=\"left\" valign=\"top\">Downloaded</th>\n" +
" <th align=\"left\" valign=\"top\">Uploaded</th>\n" +
//" <th align=\"left\" valign=\"top\">Rate</th>\n" +
" <th>&nbsp;</th></tr>\n" +
"</thead>\n";
private static final String TABLE_EMPTY = "<tr class=\"snarkTorrentEven\">" +
"<td class=\"snarkTorrentEven\" align=\"left\"" +
" valign=\"top\" colspan=\"5\">No torrents</td></tr>\n";
private static final String TABLE_FOOTER = "</table>\n";
private static final String FOOTER = "</body></html>";
}

View File

@@ -0,0 +1,47 @@
package org.klomp.snark.web;
import java.io.File;
import net.i2p.util.FileUtil;
import org.mortbay.jetty.Server;
public class RunStandalone {
private Server _server;
static {
System.setProperty("org.mortbay.http.Version.paranoid", "true");
System.setProperty("org.mortbay.xml.XmlParser.NotValidating", "true");
}
private RunStandalone(String args[]) {}
public static void main(String args[]) {
RunStandalone runner = new RunStandalone(args);
runner.start();
}
public void start() {
File workDir = new File("work");
boolean workDirRemoved = FileUtil.rmdir(workDir, false);
if (!workDirRemoved)
System.err.println("ERROR: Unable to remove Jetty temporary work directory");
boolean workDirCreated = workDir.mkdirs();
if (!workDirCreated)
System.err.println("ERROR: Unable to create Jetty temporary work directory");
try {
_server = new Server("jetty-i2psnark.xml");
_server.start();
} catch (Exception e) {
e.printStackTrace();
}
}
public void stop() {
try {
_server.stop();
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}

View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure 1.2//EN" "http://jetty.mortbay.org/configure_1_2.dtd">
<!-- =============================================================== -->
<!-- Configure the Jetty Server -->
<!-- =============================================================== -->
<Configure class="org.mortbay.jetty.Server">
<!-- =============================================================== -->
<!-- Configure the Request Listeners -->
<!-- =============================================================== -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Add and configure a HTTP listener to port 8080 -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<Call name="addListener">
<Arg>
<New class="org.mortbay.http.SocketListener">
<Arg>
<New class="org.mortbay.util.InetAddrPort">
<Set name="host">127.0.0.1</Set>
<Set name="port">8002</Set>
</New>
</Arg>
<Set name="MinThreads">3</Set>
<Set name="MaxThreads">10</Set>
<Set name="MaxIdleTimeMs">30000</Set>
<Set name="LowResourcePersistTimeMs">1000</Set>
<Set name="ConfidentialPort">8443</Set>
<Set name="IntegralPort">8443</Set>
<Set name="PoolName">main</Set>
</New>
</Arg>
</Call>
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Add a HTTPS SSL listener on port 8443 -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- UNCOMMENT TO ACTIVATE
<Call name="addListener">
<Arg>
<New class="org.mortbay.http.SunJsseListener">
<Set name="Port">8443</Set>
<Set name="PoolName">main</Set>
<Set name="Keystore"><SystemProperty name="jetty.home" default="."/>/etc/demokeystore</Set>
<Set name="Password">OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4</Set>
<Set name="KeyPassword">OBF:1u2u1wml1z7s1z7a1wnl1u2g</Set>
<Set name="NonPersistentUserAgent">MSIE 5</Set>
</New>
</Arg>
</Call>
-->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Add a AJP13 listener on port 8009 -->
<!-- This protocol can be used with mod_jk in apache, IIS etc. -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!--
<Call name="addListener">
<Arg>
<New class="org.mortbay.http.ajp.AJP13Listener">
<Set name="PoolName">ajp</Set>
<Set name="Port">8009</Set>
<Set name="MinThreads">3</Set>
<Set name="MaxThreads">20</Set>
<Set name="MaxIdleTimeMs">0</Set>
<Set name="confidentialPort">443</Set>
</New>
</Arg>
</Call>
-->
<!-- =============================================================== -->
<!-- Configure the Contexts -->
<!-- =============================================================== -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Add a all web application within the webapps directory. -->
<!-- + No virtual host specified -->
<!-- + Look in the webapps directory relative to jetty.home or . -->
<!-- + Use the default webdefault.xml in jetty's install -->
<!-- + Upack the war file -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<Set name="rootWebApp">i2psnark</Set>
<Call name="addWebApplication">
<Arg>/</Arg>
<Arg>webapps/i2psnark.war</Arg>
</Call>
<!-- =============================================================== -->
<!-- Configure the Request Log -->
<!-- =============================================================== -->
<Set name="RequestLog">
<New class="org.mortbay.http.NCSARequestLog">
<Arg>./logs/yyyy_mm_dd.i2psnark-request.log</Arg>
<Set name="retainDays">90</Set>
<Set name="append">true</Set>
<Set name="extended">false</Set>
<Set name="buffered">false</Set>
<Set name="LogTimeZone">GMT</Set>
</New>
</Set>
<!-- =============================================================== -->
<!-- Configure the Other Server Options -->
<!-- =============================================================== -->
<Set name="requestsPerGC">2000</Set>
<Set name="statsOn">false</Set>
</Configure>

View File

@@ -0,0 +1,6 @@
To run I2PSnark from the command line, run "java -jar lib/i2psnark.jar", but
to run it with the web UI, run "java -jar launch-i2psnark.jar". I2PSnark is
GPL'ed software, based on Snark (http://www.klomp.org/) to run on top of I2P
(http://www.i2p.net/) within a webserver (such as the bundled Jetty from
http://jetty.mortbay.org/). For more information about I2PSnark, get in touch
with the folks at http://forum.i2p.net/

25
apps/i2psnark/web.xml Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2.2.dtd">
<web-app>
<servlet>
<servlet-name>org.klomp.snark.web.I2PSnarkServlet</servlet-name>
<servlet-class>org.klomp.snark.web.I2PSnarkServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- precompiled servlets -->
<servlet-mapping>
<servlet-name>org.klomp.snark.web.I2PSnarkServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
</web-app>

View File

@@ -386,6 +386,18 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
} else {
request = request.substring(pos + 1);
pos = request.indexOf("/");
if (pos < 0) {
l.log("Invalid request url [" + request + "]");
if (out != null) {
out.write(ERR_REQUEST_DENIED);
out.write("<p /><i>Generated on: ".getBytes());
out.write(new Date().toString().getBytes());
out.write("</i></body></html>\n".getBytes());
out.flush();
}
s.close();
return;
}
destination = request.substring(0, pos);
line = method + " " + request.substring(pos);
}
@@ -444,8 +456,13 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
boolean gzip = DEFAULT_GZIP;
if (ok != null)
gzip = Boolean.valueOf(ok).booleanValue();
if (gzip)
newRequest.append("Accept-Encoding: x-i2p-gzip\r\n");
if (gzip) {
// according to rfc2616 s14.3, this *should* force identity, even if
// an explicit q=0 for gzip doesn't. tested against orion.i2p, and it
// seems to work.
newRequest.append("Accept-Encoding: \r\n");
newRequest.append("X-Accept-Encoding: x-i2p-gzip;q=1.0, identity;q=0.5, deflate;q=0, gzip;q=0, *;q=0\r\n");
}
newRequest.append("User-Agent: MYOB/6.66 (AN/ON)\r\n");
newRequest.append("Connection: close\r\n\r\n");
break;
@@ -485,17 +502,20 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
_log.warn("Unable to resolve " + destination + " (proxy? " + usingWWWProxy + ", request: " + targetRequest);
String str;
byte[] header;
boolean showAddrHelper = false;
if (usingWWWProxy)
str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true);
else if(ahelper != 0)
str = FileUtil.readTextFile("docs/dnfb-header.ht", 100, true);
else
else {
str = FileUtil.readTextFile("docs/dnfh-header.ht", 100, true);
showAddrHelper = true;
}
if (str != null)
header = str.getBytes();
else
header = ERR_DESTINATION_UNKNOWN;
writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination);
writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination, showAddrHelper);
s.close();
return;
}
@@ -564,7 +584,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
}
private static void writeErrorMessage(byte[] errMessage, OutputStream out, String targetRequest,
boolean usingWWWProxy, String wwwProxy) throws IOException {
boolean usingWWWProxy, String wwwProxy, boolean showAddrHelper) throws IOException {
if (out != null) {
out.write(errMessage);
if (targetRequest != null) {
@@ -576,6 +596,13 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
out.write(uri.getBytes());
out.write("</a>".getBytes());
if (usingWWWProxy) out.write(("<br>WWW proxy: " + wwwProxy).getBytes());
if (showAddrHelper) {
out.write("<br><br>Click below to try an address helper link:<br><br><a href=\"http://orion.i2p/jump/".getBytes());
out.write(uri.getBytes());
out.write("\">http://orion.i2p/jump/".getBytes());
out.write(uri.getBytes());
out.write("</a>".getBytes());
}
}
out.write("</div><p><i>I2P HTTP Proxy Server<br>Generated on: ".getBytes());
out.write(new Date().toString().getBytes());
@@ -601,7 +628,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable
header = str.getBytes();
else
header = ERR_DESTINATION_UNKNOWN;
writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy);
writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy, false);
} catch (IOException ioe) {
_log.warn(getPrefix(requestId) + "Error writing out the 'destination was unknown' " + "message", ioe);
}

View File

@@ -75,7 +75,11 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
// we keep the enc sent by the browser before clobbering it, since it may have
// been x-i2p-gzip
String enc = headers.getProperty("Accept-encoding");
headers.setProperty("Accept-encoding", "identity;q=1, *;q=0");
String altEnc = headers.getProperty("X-Accept-encoding");
// according to rfc2616 s14.3, this *should* force identity, even if
// "identity;q=1, *;q=0" didn't.
headers.setProperty("Accept-encoding", "");
String modifiedHeader = formatHeaders(headers, command);
//String modifiedHeader = getModifiedHeader(socket);
@@ -97,8 +101,12 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
allowGZIP = false;
}
if (_log.shouldLog(Log.INFO))
_log.info("HTTP server encoding header: " + enc);
if ( allowGZIP && (enc != null) && (enc.indexOf("x-i2p-gzip") >= 0) ) {
_log.info("HTTP server encoding header: " + enc + "/" + altEnc);
boolean useGZIP = ( (enc != null) && (enc.indexOf("x-i2p-gzip") >= 0) );
if ( (!useGZIP) && (altEnc != null) && (altEnc.indexOf("x-i2p-gzip") >= 0) )
useGZIP = true;
if (allowGZIP && useGZIP) {
I2PThread req = new I2PThread(new CompressedRequestor(s, socket, modifiedHeader), "http compressor");
req.start();
} else {
@@ -297,6 +305,8 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer {
String value = buf.substring(split+2); // ": "
if ("Accept-encoding".equalsIgnoreCase(name))
name = "Accept-encoding";
else if ("X-Accept-encoding".equalsIgnoreCase(name))
name = "X-Accept-encoding";
headers.setProperty(name, value);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Read the header [" + name + "] = [" + value + "]");

View File

@@ -139,9 +139,11 @@ public class IndexBean {
return stop();
else if ("start".equals(_action))
return start();
else if ("Save changes".equals(_action))
else if ("Save changes".equals(_action) || // IE workaround:
(_action.toLowerCase().indexOf("s</span>ave") >= 0))
return saveChanges();
else if ("Delete this proxy".equals(_action))
else if ("Delete this proxy".equals(_action) || // IE workaround:
(_action.toLowerCase().indexOf("d</span>elete") >= 0))
return deleteTunnel();
else
return "Action " + _action + " unknown";

View File

@@ -3,30 +3,29 @@
<target name="all" depends="build" />
<target name="fetchJettylib" >
<available property="jetty.available" file="jetty-5.1.2.zip" />
<available property="jetty.available" file="jetty-5.1.6.zip" />
<ant target="doFetchJettylib" />
</target>
<target name="doFetchJettylib" unless="jetty.available" >
<echo message="The libraries contained within the fetched file are from Jetty's 5.1.2" />
<echo message="The libraries contained within the fetched file are from Jetty's 5.1.6" />
<echo message="distribution (http://jetty.mortbay.org/). These are not " />
<echo message="necessary for using I2P, but are used by some applications on top of I2P," />
<echo message="such as the routerconsole." />
<get src="http://mesh.dl.sourceforge.net/sourceforge/jetty/jetty-5.1.2.zip" verbose="true" dest="jetty-5.1.2.zip" />
<get src="http://mesh.dl.sourceforge.net/sourceforge/jetty/jetty-5.1.6.zip" verbose="true" dest="jetty-5.1.6.zip" />
<ant target="doExtract" />
</target>
<target name="doExtract">
<unzip src="jetty-5.1.2.zip" dest="." />
<unzip src="jetty-5.1.6.zip" dest="." />
<mkdir dir="jettylib" />
<copy todir="jettylib">
<fileset dir="jetty-5.1.2/lib">
<fileset dir="jetty-5.1.6/lib">
<include name="*.jar" />
</fileset>
</copy>
<copy todir="jettylib">
<fileset dir="jetty-5.1.2/ext">
<fileset dir="jetty-5.1.6/ext">
<include name="ant.jar" />
<include name="commons-el.jar" />
<include name="commons-logging.jar" />
<include name="jasper-compiler.jar" />
<include name="jasper-runtime.jar" />
<include name="javax.servlet.jar" />
@@ -34,7 +33,9 @@
<include name="xercesImpl.jar" />
</fileset>
</copy>
<delete dir="jetty-5.1.2" />
<!-- note the rename, to keep compat with old rev, since we only used the API anyway -->
<copy file="jetty-5.1.6/ext/commons-logging-api.jar" tofile="jettylib/commons-logging.jar" />
<delete dir="jetty-5.1.6" />
</target>
<target name="build" depends="fetchJettylib" />
<target name="builddep" />

View File

@@ -59,6 +59,8 @@ public class ConfigLoggingHelper {
buf.append(prefix).append('=').append(level).append('\n');
}
buf.append("</textarea><br />\n");
buf.append("<i>Add additional logging statements above. Example: net.i2p.router.tunnel=WARN</i><br>");
buf.append("<i>Or put entries in the logger.config file. Example: logger.record.net.i2p.router.tunnel=WARN</i><br>");
buf.append("<i>Valid levels are DEBUG, INFO, WARN, ERROR, CRIT</i>\n");
return buf.toString();
}

View File

@@ -17,6 +17,10 @@ import java.util.Set;
import net.i2p.time.Timestamper;
import net.i2p.router.transport.udp.UDPTransport;
import net.i2p.router.Router;
import net.i2p.data.RouterInfo;
import net.i2p.router.web.ConfigServiceHandler.UpdateWrapperManagerTask;
import net.i2p.router.web.ConfigServiceHandler.UpdateWrapperManagerAndRekeyTask;
/**
* Handler to deal with form submissions from the main config form and act
@@ -31,6 +35,8 @@ public class ConfigNetHandler extends FormHandler {
private boolean _recheckReachabilityRequested;
private boolean _timeSyncEnabled;
private boolean _requireIntroductions;
private boolean _hiddenMode;
private boolean _dynamicKeys;
private String _tcpPort;
private String _udpPort;
private String _inboundRate;
@@ -62,6 +68,8 @@ public class ConfigNetHandler extends FormHandler {
public void setEnabletimesync(String moo) { _timeSyncEnabled = true; }
public void setRecheckReachability(String moo) { _recheckReachabilityRequested = true; }
public void setRequireIntroductions(String moo) { _requireIntroductions = true; }
public void setHiddenMode(String moo) { _hiddenMode = true; }
public void setDynamicKeys(String moo) { _dynamicKeys = true; }
public void setHostname(String hostname) {
_hostname = (hostname != null ? hostname.trim() : null);
@@ -263,6 +271,28 @@ public class ConfigNetHandler extends FormHandler {
addFormNotice("Updating bandwidth share percentage");
}
}
// If hidden mode value changes, restart is required
if (_hiddenMode && "false".equalsIgnoreCase(_context.getProperty(Router.PROP_HIDDEN, "false"))) {
_context.router().setConfigSetting(Router.PROP_HIDDEN, "true");
_context.router().getRouterInfo().addCapability(RouterInfo.CAPABILITY_HIDDEN);
addFormNotice("Gracefully restarting into Hidden Router Mode. Make sure you have no 0-1 length "
+ "<a href=\"configtunnels.jsp\">tunnels!</a>");
hiddenSwitch();
}
if (!_hiddenMode && "true".equalsIgnoreCase(_context.getProperty(Router.PROP_HIDDEN, "false"))) {
_context.router().removeConfigSetting(Router.PROP_HIDDEN);
_context.router().getRouterInfo().delCapability(RouterInfo.CAPABILITY_HIDDEN);
addFormNotice("Gracefully restarting to exit Hidden Router Mode");
hiddenSwitch();
}
if (_dynamicKeys) {
_context.router().setConfigSetting(Router.PROP_DYNAMIC_KEYS, "true");
} else {
_context.router().removeConfigSetting(Router.PROP_DYNAMIC_KEYS);
}
if (_requireIntroductions) {
_context.router().setConfigSetting(UDPTransport.PROP_FORCE_INTRODUCERS, "true");
@@ -290,6 +320,12 @@ public class ConfigNetHandler extends FormHandler {
addFormNotice("Soft restart complete");
}
}
private void hiddenSwitch() {
// Full restart required to generate new keys
_context.router().addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART));
_context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
}
private void updateRates() {
boolean updated = false;

View File

@@ -6,6 +6,7 @@ import net.i2p.router.CommSystemFacade;
import net.i2p.data.RouterAddress;
import net.i2p.router.transport.udp.UDPAddress;
import net.i2p.router.transport.udp.UDPTransport;
import net.i2p.router.Router;
public class ConfigNetHelper {
private RouterContext _context;
@@ -63,6 +64,22 @@ public class ConfigNetHelper {
return " checked ";
}
public String getHiddenModeChecked() {
String enabled = _context.getProperty(Router.PROP_HIDDEN, "false");
if ( (enabled != null) && ("true".equalsIgnoreCase(enabled)) )
return " checked ";
else
return "";
}
public String getDynamicKeysChecked() {
String enabled = _context.getProperty(Router.PROP_DYNAMIC_KEYS, "false");
if ( (enabled != null) && ("true".equalsIgnoreCase(enabled)) )
return " checked ";
else
return "";
}
public String getRequireIntroductionsChecked() {
short status = _context.commSystem().getReachabilityStatus();
switch (status) {

View File

@@ -33,6 +33,21 @@ public class ConfigServiceHandler extends FormHandler {
}
}
}
public static class UpdateWrapperManagerAndRekeyTask implements Runnable {
private int _exitCode;
public UpdateWrapperManagerAndRekeyTask(int exitCode) {
_exitCode = exitCode;
}
public void run() {
try {
Router.killKeys();
WrapperManager.signalStopped(_exitCode);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
protected void processForm() {
if (_action == null) return;
@@ -56,6 +71,14 @@ public class ConfigServiceHandler extends FormHandler {
_context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART));
_context.router().shutdown(Router.EXIT_HARD_RESTART);
addFormNotice("Hard restart requested");
} else if ("Rekey and Restart".equals(_action)) {
addFormNotice("Rekeying after graceful restart");
_context.router().addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL_RESTART));
_context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
} else if ("Rekey and Shutdown".equals(_action)) {
addFormNotice("Rekeying after graceful shutdown");
_context.router().addShutdownTask(new UpdateWrapperManagerAndRekeyTask(Router.EXIT_GRACEFUL));
_context.router().shutdownGracefully(Router.EXIT_GRACEFUL);
} else if ("Run I2P on startup".equals(_action)) {
installService();
} else if ("Don't run I2P on startup".equals(_action)) {
@@ -195,4 +218,4 @@ public class ConfigServiceHandler extends FormHandler {
addFormError("Error updating the client config");
}
}
}
}

View File

@@ -63,6 +63,8 @@ public class ConfigStatsHandler extends FormHandler {
if (_explicitFilter) {
_stats.clear();
if (_explicitFilterValue == null)
_explicitFilterValue = "";
if (_explicitFilterValue.indexOf(',') != -1) {
StringTokenizer tok = new StringTokenizer(_explicitFilterValue, ",");

View File

@@ -135,7 +135,7 @@ public class ConfigTunnelsHandler extends FormHandler {
if (saveRequired) {
boolean saved = _context.router().saveConfig();
if (saved)
addFormNotice("Configuration saved successfully");
addFormNotice("Exploratory tunnel configuration saved successfully");
else
addFormNotice("Error saving the configuration (applied but not saved) - please see the error logs");
}

View File

@@ -0,0 +1,46 @@
package net.i2p.router.web;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import net.i2p.router.RouterContext;
public class JobQueueHelper {
private RouterContext _context;
private Writer _out;
/**
* Configure this bean to query a particular router context
*
* @param contextId begging few characters of the routerHash, or null to pick
* the first one we come across.
*/
public void setContextId(String contextId) {
try {
_context = ContextHelper.getContext(contextId);
} catch (Throwable t) {
t.printStackTrace();
}
}
public JobQueueHelper() {}
public void setWriter(Writer writer) { _out = writer; }
public String getJobQueueSummary() {
try {
if (_out != null) {
_context.jobQueue().renderStatusHTML(_out);
return "";
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream(32*1024);
_context.jobQueue().renderStatusHTML(new OutputStreamWriter(baos));
return new String(baos.toByteArray());
}
} catch (IOException ioe) {
ioe.printStackTrace();
return "";
}
}
}

View File

@@ -83,14 +83,23 @@ public class SummaryHelper {
long ms = _context.clock().getOffset();
if (ms < 60 * 1000) {
return now + " (" + (ms / 1000) + "s)";
} else if (ms < 60 * 60 * 1000) {
return now + " (" + (ms / (60 * 1000)) + "m)";
} else if (ms < 24 * 60 * 60 * 1000) {
return now + " (" + (ms / (60 * 60 * 1000)) + "h)";
long diff = ms;
if (diff < 0)
diff = 0 - diff;
if (diff == 0) {
return now + " (no skew)";
} else if (diff < 1000) {
return now + " (" + ms + "ms skew)";
} else if (diff < 5 * 1000) {
return now + " (" + (ms / 1000) + "s skew)";
} else if (diff < 60 * 1000) {
return now + " <b>(" + (ms / 1000) + "s skew)</b>";
} else if (diff < 60 * 60 * 1000) {
return now + " <b>(" + (ms / (60 * 1000)) + "m skew)</b>";
} else if (diff < 24 * 60 * 60 * 1000) {
return now + " <b>(" + (ms / (60 * 60 * 1000)) + "h skew)</b>";
} else {
return now + " (" + (ms / (24 * 60 * 60 * 1000)) + "d)";
return now + " <b>(" + (ms / (24 * 60 * 60 * 1000)) + "d skew)</b>";
}
}
@@ -408,6 +417,28 @@ public class SummaryHelper {
return _context.tunnelManager().getOutboundTunnelCount();
}
/**
* How many inbound client tunnels we have.
*
*/
public int getInboundClientTunnels() {
if (_context == null)
return 0;
else
return _context.tunnelManager().getInboundClientTunnelCount();
}
/**
* How many active outbound client tunnels we have.
*
*/
public int getOutboundClientTunnels() {
if (_context == null)
return 0;
else
return _context.tunnelManager().getOutboundClientTunnelCount();
}
/**
* How many tunnels we are participating in.
*
@@ -459,4 +490,4 @@ public class SummaryHelper {
public boolean updateAvailable() {
return NewsFetcher.getInstance(_context).updateAvailable();
}
}
}

View File

@@ -58,6 +58,23 @@
<jsp:getProperty name="nethelper" property="sharePercentageBox" /><br />
Sharing a higher percentage will improve your anonymity and help the network
<hr />
<b>Dynamic Router Keys: </b>
<input type="checkbox" name="dynamicKeys" value="true" <jsp:getProperty name="nethelper" property="dynamicKeysChecked" /> /><br />
<p>
This setting causes your router identity to be regenerated every time your IP address
changes. If you have a dynamic IP this option can speed up your reintegration into
the network (since people will have shitlisted your old router identity), and, for
very weak adversaries, help frustrate trivial
<a href="http://www.i2p.net/how_threatmodel#intersection">intersection
attacks</a> against the NetDB. Your different router identities would only be
'hidden' among other I2P users at your ISP, and further analysis would link
the router identities further.</p>
<p>Note that when I2P detects an IP address change, it will automatically
initiate a restart in order to rekey and to disconnect from peers before they
update their profiles - any long lasting client connections will be disconnected,
though such would likely already be the case anyway, since the IP address changed.
</p>
<hr />
<input type="submit" name="save" value="Save changes" /> <input type="reset" value="Cancel" /><br />
</form>
</div>

View File

@@ -33,6 +33,9 @@
<input type="hidden" name="action" value="blah" />
<jsp:getProperty name="tunnelshelper" property="form" />
<hr />
<i>Note - Exploratory tunnel setting changes are stored in the router.config file.</i></br>
<i>Client tunnel changes are temporary and are not saved.</i><br>
<i>To make permanent client tunnel changes see the </i><a href="i2ptunnel/index.jsp">i2ptunnel page</a>.<br>
<input type="submit" name="shouldsave" value="Save changes" /> <input type="reset" value="Cancel" />
</form>
</div>

View File

@@ -25,6 +25,10 @@ div.logo {
text-align: left;
}
div.toolbar {
font-weight: bold
}
div.routersummary {
/* width: 8em; */
/* height: 5em; */

View File

@@ -0,0 +1,21 @@
<%@page contentType="text/html"%>
<%@page pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html><head>
<title>I2P Router Console - job queue</title>
<link rel="stylesheet" href="default.css" type="text/css" />
</head><body>
<%@include file="nav.jsp" %>
<%@include file="summary.jsp" %>
<div class="main" id="main">
<jsp:useBean class="net.i2p.router.web.JobQueueHelper" id="jobQueueHelper" scope="request" />
<jsp:setProperty name="jobQueueHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
<jsp:setProperty name="jobQueueHelper" property="writer" value="<%=out%>" />
<jsp:getProperty name="jobQueueHelper" property="jobQueueSummary" />
</div>
</body>
</html>

View File

@@ -1,3 +1,4 @@
<%@page import="java.io.File" %>
<% response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control","no-cache");
response.setDateHeader("Expires", 0);
@@ -13,22 +14,32 @@
<a href="index.jsp"><img src="i2plogo.png" alt="Router Console" width="187" height="35" /></a><br />
[<a href="config.jsp">configuration</a> | <a href="help.jsp">help</a>]
</div>
<h4>
<div class="toolbar">
<% if (new File("docs/toolbar.html").exists()) { %>
<jsp:useBean class="net.i2p.router.web.ContentHelper" id="toolbarhelper" scope="request" />
<jsp:setProperty name="toolbarhelper" property="page" value="docs/toolbar.html" />
<jsp:setProperty name="toolbarhelper" property="maxLines" value="300" />
<jsp:getProperty name="toolbarhelper" property="content" />
<% } else { %>
<!-- Could not find docs/toolbar.html! -->
<a href="susimail/susimail">Susimail</a> |
<a href="susidns/index.jsp">SusiDNS</a> |
<a href="syndie/">Syndie</a> |
<a href="i2psnark/">I2PSnark</a> |
<a href="http://localhost:7658/">My Eepsite</a> <br>
<a href="i2ptunnel/index.jsp">I2PTunnel</a> |
<a href="tunnels.jsp">Tunnels</a> |
<a href="profiles.jsp">Profiles</a> |
<a href="netdb.jsp">NetDB</a> |
<a href="logs.jsp">Logs</a> |
<a href="jobs.jsp">Jobs</a> |
<a href="oldstats.jsp">Stats</a> |
<a href="oldconsole.jsp">Internals</a>
<% } %>
<jsp:useBean class="net.i2p.router.web.NavHelper" id="navhelper" scope="request" />
<jsp:setProperty name="navhelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
<jsp:getProperty name="navhelper" property="clientAppLinks" />
</h4>
</div>
<jsp:useBean class="net.i2p.router.web.NoticeHelper" id="noticehelper" scope="request" />
<jsp:setProperty name="noticehelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />

View File

@@ -72,9 +72,9 @@
<jsp:getProperty name="helper" property="destinations" />
<u><b>Tunnels</b></u><br />
<b>Inbound:</b> <jsp:getProperty name="helper" property="inboundTunnels" /><br />
<b>Outbound:</b> <jsp:getProperty name="helper" property="outboundTunnels" /><br />
<u><b>Tunnels in/out</b></u><br />
<b>Exploratory:</b> <jsp:getProperty name="helper" property="inboundTunnels" />/<jsp:getProperty name="helper" property="outboundTunnels" /><br />
<b>Client:</b> <jsp:getProperty name="helper" property="inboundClientTunnels" />/<jsp:getProperty name="helper" property="outboundClientTunnels" /><br />
<b>Participating:</b> <jsp:getProperty name="helper" property="participatingTunnels" /><br />
<hr />

View File

@@ -90,6 +90,7 @@ void string_copy(string_t src,string_t dest) {
void string_copy_raw(string_t src, void* dest,size_t size) {
size = min(src->size,size);
memcpy(dest,src->data,size);
((char*)dest)[size] = '\0';
}
const char* string_data(string_t self) {

View File

@@ -1,321 +0,0 @@
#!/usr/bin/perl
## Copyright 2004 Brian Ristuccia. This program is Free Software;
## You can redistribute it and/or modify it under the same terms as
## Perl itself.
package Net::SAM;
@ISA = ( "IO::Socket::INET" );
use strict;
use POSIX;
use Switch;
use IO::Socket;
use IO::Select;
#use Net::SAM::StreamSession;
#use Net::SAM::DatagramSession;
#use Net::SAM::RawSession;
sub new {
my ($class) = shift;
my $type = ref($class) || $class;
my $self = $type->SUPER::new("127.0.0.1:7656");
${*$self}->{incomingraw} = [];
# Connect us to the local SAM proxy.
# my $samsock = IO::Socket::INET->new('127.0.0.1:7657');
#$self->{samsock}=$samsock;
# Say hello, read response.
$self->SUPER::send("HELLO VERSION MIN=1.0 MAX=1.0\n");
while (! ${*$self}->{greeted}) {
$self->readprocess();
}
print "Created SAM object\n";
return $self;
}
sub lookup {
my $self = shift;
my $name= shift;
$self->SUPER::send("NAMING LOOKUP NAME=$name\n");
undef ${*$self}->{RESULT};
while (! ${*$self}->{RESULT}) {
$self->readprocess();
}
if ( ${*$self}->{RESULT} == "OK" ) {
return ${*$self}->{VALUE};
} else {
return undef;
}
}
#sub createsession {
# my ($self) = shift;
# my ($sesstype) = shift;
# print $self->{samsock} "SESSION CREATE STYLE=$SESSTYPE DESTINATION=$DEST, DIRECTION=
#}
#sub waitfor {
# my ($self) = shift;
# my ($prefix) = shift;
# my ($response) = <$samsock>;#
# if $response =~
#}
sub readprocesswrite {
my $self = shift;
$self->readprocess();
$self->dowrite();
}
sub doread {
my $self = shift;
my $rv;
my $data;
$rv = $self->recv($data, $POSIX::BUFSIZE, 0);
if ( defined($rv) && ( length($data) >= 1 ) ) {
# We received some data. Put it in our buffer.
${*$self}->{inbuffer} += $data;
} else {
# No data. Either we're on a non-blocking socket, or there
# was an error or EOF
if ( $!{EAGAIN} ) {
return 1;
} else {
# I suppose caller can look at $! for details
return undef;
}
}
}
sub dowrite {
my $self = shift;
my $rv;
my $data;
$rv = $self->send(${*$self}->{outbuffer}, 0);
if ( ! defined($rv) ) {
warn "SAM::dowrite - Couldn't write for no apparent reason.\n";
return undef;
}
if ( $rv == length(${*$self}->{outbuffer}) || $!{EWOULDBLOCK} ) {
substr(${*$self}->{outbuffer},0, $rv) = ''; # Remove from buffer
# Nuke buffer if empty
delete ${*$self}->{outbuffer} unless length(${*$self}->{outbuffer});
} else {
# Socket closed on us or something?
return undef;
}
}
sub messages {
my $self = shift;
return @{ ${*$self}->{messages} };
}
sub queuemessage {
my $self = shift;
my $message = shift;
push @{ ${*$self}->{messages} } , $message;
}
sub unqueuemessage {
my $self = shift;
return unshift(@{ ${*$self}->{messages} } );
}
sub readprocess {
my $self = shift;
$self->doread();
$self->process();
}
sub process {
my $self = shift;
my %tvhash;
my $payload;
# Before we can read any new messages, if an existing message has payload
# we must read it in. Otherwise we'll create garbage messages containing
# the payload of previous messages.
if ( ${*$self}->{payloadrequired} >= 1 ) {
if ( length( ${*$self}->{inbuffer} ) >= ${*$self}->{payloadrequired} ) {
# Scarf payload from inbuffer into $payload
$payload = substr(${*$self}->{inbuffer}, 0,
${*$self}->{payloadrequired});
# Nuke payload from inbuffer
substr(${*$self}->{inbuffer}, 0,
${*$self}->{payloadrequired} ) = '';
# Put message with payload into spool
push @{ ${*$self}->{messages} } ,
${*$self}->{messagerequiringpayload}.$payload;
# Delete the saved message requiring payload
delete ${*$self}->{messagerequiringpayload};
} else {
# Insufficient payload in inbuffer. Try again later.
return 1;
}
}
if ( ${*$self}->{inbuffer} =~ s/(.*\n)// ) {
%tvhash = $self->_hashtv($1); # Returns a tag/value hash
if ( $tvhash{SIZE} ) {
# We've got a message with payload on our hands. :(
${*$self}->{payloadrequired} = $tvhash{SIZE};
${*$self}->{messagerequiringpayload} = $1;
return 1; # Could call ourself here, but we'll get called again.
} else {
push @{ ${*$self}->{messages} } , $1;
}
}
return 1;
}
# sub junk {
# print "readprocess: " . $self->connected() . "\n";
# # May block if the SAM bridge gets hosed
# my $response = <$self>;
# print "readprocess: $!" . $self->connected() . "\n";
# chomp $response;
# my ($primative, $more, $extra) = split (' ', $response, 3);
# $primative = uc($primative);
# print "readprocess: " . $self->connected() . " -- $primative -- $more -- $extra\n";
# switch ($primative) {
# case "HELLO" {
# if ($more !~ m/REPLY/ ) { die ("Bogus HELLO response") }
# if ($extra =~ m/NOVERSION/ ) {
# die("SAM Bridge Doesn't support my version") ;
# }
# $self->_hashtv($extra);
# ${*$self}->{greeted} = 1;
# };
# case "SESSION" {
# if ( $more !~ m/STATUS/ ) {
# die("Bogus SESSION response");
# }
# $self->_hashtv($extra);
# }
# case "STREAM" {};
# case "DATAGRAM" {
# if ( $more !~ m/RECEIVE/ ) {
# die("Bogus DATAGRAM response.");
# }
# $self->_hashtv($extra);
# push @{ ${*$self}->{incomingdatagram } },
# [ ${*$self}->{DESTINATION},
# $self->_readblock(${*$self}->{SIZE}) ];
# };
# case "RAW" {
# if ( $more !~ m/RECEIVE/ ) {
# die("Bogus RAW response.");
# }
# $self->_hashtv($extra);
# push @{ $self->{incomingraw} }, $self->_readblock($self->{SIZE});
# };
# case "NAMING" {
# if ( $more !~ m/REPLY/ ) {
# die("Bogus NAMING response");
# }
# $self->_hashtv($extra);
# };
# case "DEST" {};
# }
# return 1;
# }
sub getfh {
# Return the FH of the SAM socket so apps can select() or poll() on it
my $self = shift;
return $self->{samsock};
}
sub _readblock {
my $self = shift;
my $size = shift;
my $chunk;
my $payload;
while ( $size > 1 ) {
# XXX: May block. No error checking.
print "readblock: $size\n";
$size -= $self->SUPER::recv($chunk, $size);
$payload .= $chunk;
}
return $payload;
}
sub _hashtv {
my $self = shift;
my $tvstring = shift;
my $tvhash;
while ( $tvstring =~ m/(\S+)=(\S+)/sg ) {
$tvhash->{$1}=$2;
print "hashtv: $1=$2\n"
}
return $tvhash;
}
sub DESTROY {
# Do nothing yet.
}
#sub StreamSession {
# my $self = shift;
# return Net::SAM::StreamSession->new($self);
#}
#sub DatagramSession {
# return Net::SAM::DatagramSession->new($self);
#}
#sub RawSession {
# return Net::SAM::RawSession->new($self);
#}
1;

View File

@@ -1,48 +0,0 @@
#!/usr/bin/perl
package Net::SAM::DatagramSession;
use Net::SAM;
@ISA = ("Net::SAM");
sub new {
my ($class) = shift;
my ($dest , $direction, $options) = shift;
my $self = $class->SUPER::new(@_);
$self->SUPER::send("SESSION CREATE STYLE=DATAGRAM DESTINATION=$dest DIRECTION=$direction $options\n");
undef ${*$self}->{RESULT};
while ( ! ${*$self}->{RESULT} ) {
$self->readprocess() || return undef;
}
if ( ${*$self}->{RESULT} == "OK" ) {
return $self;
} else {
return undef; # sorry.
}
}
sub send {
my $self = shift;
my $destination = shift;
my $message = shift;
my $size = length($message);
$self->SUPER::send("DATAGRAM SEND DESTINATION=$destination SIZE=$size\n$message");
}
sub receive {
my $self = shift;
# Shift one off the fifo array. Returns undef if none wait.
return shift @{ $self->{incomingdatagram} };
}
1;

View File

@@ -1,45 +0,0 @@
#!/usr/bin/perl
package Net::SAM::RawSession;
use Net::SAM;
@ISA = ("Net::SAM");
sub new {
my ($class) = shift;
my ($dest , $direction, $options) = shift;
my $self = $class->SUPER::new(@_);
$self->send("SESSION CREATE STYLE=RAW DESTINATION=$dest DIRECTION=$direction $options\n");
undef $self->{result};
while ( ! $self->{RESULT} ) {
$self->readprocess();
}
if ( $self->{RESULT} == "OK" ) {
return $self;
} else {
return 0; # sorry.
}
}
sub send {
my $self = shift;
my $destination = shift;
my $message = shift;
my $samsock = $self->{samsock};
my $size = length($message);
print $samsock "RAW SEND DESTINATION=$destination SIZE=$size\n$message";
}
sub receive {
my $self = shift;
# Shift one off the fifo array. Returns undef if none wait.
return shift @{ $self->{incomingraw} };
}
1;

View File

@@ -1,3 +0,0 @@
#!/usr/bin/perl
1;

108
apps/sam/perl/README Normal file
View File

@@ -0,0 +1,108 @@
# BASIC Perl SAM Module
# created 2005 by postman (postman@i2pmail.org)
1. What does it do?
The SAM module is a little Perl add-on that - on one side -
establishes communication with a I2P router's (http://www.i2p.net) SAM bridge
(Simple anonymous messaging ( http://www.i2p.net/sam)). On the
other side it exposes a simple socket like interface to the user.
Over this interface the user can send or receive datastreams from I2P
destinations as if he would communicate with a normal IP socket.
The SAM module can be integrated into perl scripts that
want to communicate with I2P services.
2. Is this code usable?
This perl module should be considered as proof-of-concept
quality. It did surely work for me and my test setups, but
it might not work at all on your system. If you run into problems
you can contact me.
3. Does ist support DATAGRAM and RAW sessions?
No, at the moment the module only supports STREAM sessions.
Support for other session types might be added in the future.
4. How to install it?
Create a Subfolder called I2P in your Perl Installation's Net Module
folder (i.e. /usr/lib/perl5/5.8.4./Net/I2P ) and copy the module there.
You can now use it with use Net::I2P::SAM.
The module only depends on Net::IO::Socket for operations. This
should be already installed.
5. How to debug?
You can switch on debugging with the constructor ( see below ).
6. How to use it?
$sam = new Net::I2P::SAM('127.0.0.1','7656',1);
# you can omit host/port - then the defult is assumed
# the 3rd argument is the debugging switch. If you switched it on
# there'll be a default debug in /tmp/sam-debug
# $sam will now either contain a object reference or 0
# if it's 0 then we coudl not talk to the SAM bridge at all ( connect failed)
# or we could not agree to a version
# next we can tune the tunnel settings we want for this session:
# The syntax is just like the one used on www.i2p.net/sam
$sam->change_settings("inbound.length=1,inbound.lenghVariance=0,outbound.length=1,outbound.lengthVariance=0,inbound.nickname=fun,outbound.nickname=fun");
# next we open a new session.
# only stream is supported
# most of the time we use a transient destination
# otherwise state the hosts.txt name you want to use as in your session
# direction is most of the times both :)
# this cab return 1 for success or 0 for failure
$sam->create_session("STREAM","TRANSIENT","BOTH");
# now connect to our service
$sam->connect($sam->lookup("myservice.i2p"));
# or
$sam->connect("I2PDESTINATIONKEY.....AAAA");
# if this returns 1 - we got a stream session and can now receive and send data
# otherwise consult the debug.
# Send data is just like the the raw perl send
# we send the data as scalar var and optional flags (most of the times 0)
$sam->send($data,0);
# receiving data is similar to the perl recv
# we define the mac number of bytes and optional flags
$indata = $sam->receive(512,0);
# close the session
$sam->close();
# that's the most important things to know.

922
apps/sam/perl/SAM.pm Normal file
View File

@@ -0,0 +1,922 @@
# Perl Basic SAM module
# created 2005 by postman (postman@i2pmail.org)
# This code is under GPL.
# This code is proof of concept
package Net::I2P::SAM;
require 5.001;
use strict;
use POSIX;
# we do not extend the IO::Socket:INET Package
# we just use it, so we keep our stuff sorted
# This is a testsetup - the next release but
# be an extension of IO::Socket::INET;
use IO::Socket::INET;
use vars qw($VERSION @ISA @EXPORT);
sub new {
my ($this,$host,$port,$debug) = @_;
my $classname=ref($this) || $this;
my $self = {};
bless($self,$classname);
#%{$self->{conf}} = The hash where we store tunnel / SAM Settings
#$self->{debug} = Whether we should output debugging lines ( this is very helpful when having problems)
#$self->{debugfile} = Where to log
#%{$self->{samreply}} = This hash stores the key=VALUE pairs of sam reply
#$self->{sock} = The INET Socket over which we talk to the SAM bridge
#$self->{inbuffer} = a simple layouted inbuffer
#$self->{outbuffer} = a simple layouted outbuffer
# ( the buffers are for dealing with differences between user wanted in/outputsizes
# and what we're able to deliver on a machine side)
#$self->{sessiontype} = unused ( will be used when we support different sessiontypes )
#$self->{lookupresult}= contains the result of a SAM host/userhost naming lookup;
#$self->{samerror} = The human readable SAM error message ( if wanted )
#$self->{streamid} = The virtual streamid. It will be created upon connect;
#$self->{bytestoread} = contains the reported size of a packet from sam
#$self->{bytestosend} = contains the wanted size of a packet to sam
#$self->{hasbanner} = is set to 1 when we receive a greeting string upon connect
#
%{$self->{conf}}=();
if($debug==1) {
$self->{debug}=1;
}
$self->{debugfile}="/tmp/sam-debug";
$self->{debughandle}=undef;
%{$self->{samreply}}=undef;
$self->{sock}=undef;
$self->{inbuffer}=undef;
$self->{outbuffer}=undef;
$self->{sessiontype}=undef;
$self->{lookupresult}=undef;
$self->{samerror}=undef;
$self->{streamid}=undef;
$self->{bytestoread}=undef;
$self->{bytestosend}=undef;
$self->{hasbanner}=1;
# state == -1 (no socket exists)
# state == 0 (socket exists, but we're not helloed)
# state == 1 (socket exists, and we're helloed)
# state == 200 ( we bound a session)
# state == 250 ( we have a virtual stream)
$self->{state}=-1;
# if the user has specified a host/port for contacting the SAM
# Bridge, we'll override the defaults. Otherwise we just use
# defaults.
#
if($host) {
${$self->{conf}}{host}=$host;
} else {
${$self->{conf}}{host}="127.0.0.1";
}
if($port) {
${$self->{conf}}{port}=$port;
} else {
${$self->{conf}}{port}=7656;
}
# defaults for the tunnelparameters
# see www.i2p.net/sam
${$self->{conf}}{iblength}=2;
${$self->{conf}}{oblength}=2;
${$self->{conf}}{ibquant}=2;
${$self->{conf}}{obquant}=2;
${$self->{conf}}{ibbackup}=0;
${$self->{conf}}{obbackup}=0;
${$self->{conf}}{ibvariance}=0;
${$self->{conf}}{obvariance}=0;
${$self->{conf}}{iballowzero}="true";
${$self->{conf}}{oballowzero}="true";
${$self->{conf}}{ibduration}=600000;
${$self->{conf}}{obduration}=600000;
${$self->{conf}}{ibnickname}="SAM-Perl-Destination";
${$self->{conf}}{obnickname}="SAM-Perl-Destination";
# ok, let's open a simple debug file
# if the user wants us
if($self->{debug} == 1) {
if ( ! open (LOGFILE,">" .$self->{debugfile})) {
print "constructor: Cannot open debugging file, switching debugging off.";
$self->{debug}=0;
}
# switch off the nerveracking buffer for the debugfile
select((select(LOGFILE), $| = 1)[0]);
}
# ok now, lets move to the manager
# he manages connecting and hello
# if the proc_mgr returns 1 the user will get our
# object reference. if not, he'll just get a 0.
if($self->proc_mgr()) {
return $self;
} else {
return 0;
}
}
sub proc_mgr {
my ($self) = @_;
my $return=undef;
if ($self->{state} == -1) {
$self->log("Debug: SAM::proc_mgr(): Opening Socket Connection to ".${$self->{conf}}{host}.":".${$self->{conf}}{port});
$return=$self->sam_connect();
if($return==0) {
return 0;
}
if($return == 1) {
$self->log("Debug: SAM::proc_mgr(): State Transition -1 => 0");
$self->{state}=0;
}
}
if($self->{state}==0) {
if(!$self->hello()) {
$self->log("Debug: SAM::proc_mgr(): Closing Socket");
$self->{sock}->close();
$self->log("State SAM::proc_mgr(): Transition 0 => -1");
return 0;
}
}
return 1;
}
sub change_settings {
my ($self,$list)=@_;
my (@tochange,$id,$v,$k);
# we cannot change the settings if we have a session already
# so our state must be 1
if($self->{state} >1) {
$self->log("Debug: SAM::change_settings(): Cannot change tunnel settings after establishing a session.");
return 0;
}
@tochange=split(",",$list);
foreach $id (@tochange) {
($k,$v)=split("=",$id);
lc($v);
lc($k);
$self->log("Debug: SAM::change_settings(): Parsed Setting: Key: $k - Value: $v");
if($k eq "inbound.length" || $k eq "outbound.length") {
# make v an int
$v*1;
if ($v >3 || $v < 0) {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (out of range)");
return 0;
}
if(${$self->{conf}}{iballowzero} eq "false" && $k eq "inbound.length" && $v==0) {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (length forbidden by allowzero=false)");
return 0;
}
if(${$self->{conf}}{oballowzero} eq "false" && $k eq "outbound.length" && $v==0) {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (length forbidden by allowzero=false)");
return 0;
}
if($k eq "inbound.length") {
${$self->{conf}}{iblength}=$v;
}
if($k eq "outbound.length") {
${$self->{conf}}{oblength}=$v;
}
}
if($k eq "inbound.quantity" || $k eq "outbound.quantity") {
$v*1;
if($v < 0 || $v >3 ) {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (out of range)");
return 0;
}
if ($k eq "inbound.quantity") {
${$self->{conf}}{ibquant}=$v;
}
if($k eq "outbound.quantity") {
${$self->{conf}}{obquant}=$v;
}
}
if($k eq "inbound.backupquantity" || $k eq "outbound.backupquantity") {
$v*1;
if($v < 0 || $v >2 ) {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (out of range)");
return 0;
}
if ($k eq "inbound.backupquantity") {
${$self->{conf}}{ibbackup}=$v;
}
if($k eq "outbound.backupquantity") {
${$self->{conf}}{obbackup}=$v;
}
}
if($k eq "inbound.lengthvariance" || $k eq "outbound.lengthvariance") {
$v*1;
if($v < -2 || $v >2 ) {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (out of range)");
return 0;
}
if ($k eq "inbound.lengthvariance") {
${$self->{conf}}{ibvariance}=$v;
}
if($k eq "outbound.lengthvariance") {
${$self->{conf}}{ibvariance}=$v;
}
}
if($k eq "inbound.duration" || $k eq "outbound.duration") {
$v*1;
if($v < 300000 || $v >1200000 ) {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (out of range)");
return 0;
}
if ($k eq "inbound.duration") {
${$self->{conf}}{ibduration}=$v;
}
if($k eq "outbound.duration") {
${$self->{conf}}{obduration}=$v;
}
}
if($k eq "inbound.nickname" || $k eq "outbound.nickname") {
$v=substr($v,0,20);
if ($k eq "inbound.nickname") {
${$self->{conf}}{ibnickname}=$v;
}
if($k eq "outbound.nickname") {
${$self->{conf}}{obnickname}=$v;
}
}
if($k eq "inbound.allowzerohop" || $k eq "outbound.allowzerohop") {
if($v ne "true" && $v ne "false") {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (must be boolean)");
return 0;
}
if(${$self->{conf}}{iblength} ==0 && $k eq "inbound.allowzerohop" && $v eq "false") {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (length forbidden by allowzero=false)");
return 0;
}
if(${$self->{conf}}{oblength} == 0 && $k eq "outbound.allowzerohop" && $v eq "false") {
$self->log("Error: SAM::change_settings(): Wrong value $v is not valid for $k (length forbidden by allowzero=false)");
return 0;
}
if ($k eq "inbound.allowzerohop") {
${$self->{conf}}{iballowzero}=$v;
}
if($k eq "outbound.allowzerohop") {
${$self->{conf}}{oballowzero}=$v;
}
}
$self->log("Debug: SAM::change_settings(): Setting $k to $v");
}
return 1;
}
sub hello {
my ($self) = @_;
my $greeting="HELLO VERSION MIN=1.0 MAX=1.0\n";
my $return=undef;
my $return2=undef;
$self->{outbuffer} .= $greeting;
$return=$self->raw_send();
if($return == 1) {
if($self->raw_read()) {
if($self->parse_sam("HELLO")) {
$self->{state}=1;
$self->log("Debug: SAM::hello(): State Transition 0 => 1");
delete $self->{inbuffer};
delete $self->{outbuffer};
return 1;
}
} else {
$self->log("Error: SAM::hello(): HELLO Failed. Cannot read HELLO response");
return 0;
}
}
if($return == 0) {
$self->log("Error: SAM::hello(): HELLO Failed. Cannot send HELLO String");
return 0;
}
}
sub create_session {
my ($self,$type,$destination,$direction) = @_;
my $line="SESSION CREATE ";
my $return;
uc($type);
# WE ARE ONLY DOING STREAMS ATM
if ($type ne "STREAM") {
$self->log("Error: SAM::create_session(): SESSION failed. Only session of STREAM type are allowed");
return 0;
}
if(length($destination)==0) {
$self->log("Warn: SAM::create_session(): SESSION: fallback setting on destination to TRANSIENT.");
$destination="TRANSIENT";
}
$line.="STYLE=$type DESTINATION=$destination";
uc($direction);
if($direction ne "BOTH" && $direction ne "CREATE" && $direction ne "RECEIVE") {
$self->log("Warn: SAM::create_session(): SESSION: fallback setting on direction to BOTH.");
$direction="BOTH";
}
$line .= " DIRECTION=$direction";
$line .= " inbound.length=".${$self->{conf}}{iblength}." outbound.length=".${$self->{conf}}{oblength};
$line .= " inbound.quantity=".${$self->{conf}}{ibquant}." outbound.quantity=".${$self->{conf}}{obquant};
$line .= " inbound.backupQuantity=".${$self->{conf}}{ibbackup}." outbound.backupQuantity=".${$self->{conf}}{obbackup};
$line .= " inbound.lengthVariance=".${$self->{conf}}{ibvariance}." outbound.lengthVariance=".${$self->{conf}}{obvariance};
$line .= " inbound.duration=".${$self->{conf}}{ibduration}." outbound.duration=".${$self->{conf}}{obduration};
$line .= " inbound.nickname=".${$self->{conf}}{ibnickname}." outbound.nickname=".${$self->{conf}}{obnickname};
$line .= " inbound.allowZeroHop=".${$self->{conf}}{iballowzero}." outbound.allowZeroHop=".${$self->{conf}}{oballowzero};
$line .="\n";
$self->{outbuffer}.=$line;
$return=$self->raw_send();
if($return == 1) {
if($self->raw_read()) {
if($self->parse_sam("SESSION ")) {
$self->{state}=200;
$self->log("Debug: SAM::create_session(): State Transition 1 => 200");
# flush the whole inbuffer;
delete $self->{inbuffer};
delete $self->{outbuffer};
return 1;
}
} else {
$self->log("Error: SAM::create_session(): SESSION Failed. Cannot read SAM Response");
return 0;
}
}
if($return == 0) {
$self->log("Error: SAM::create_session(): SESSION Failed. Cannot send SESSION String");
return 0;
}
}
sub parse_sam {
# this is the main function that parses all SAM replies
# depending on wanted action and state we'll set different
# properties like $self->{bytestoread} etc.
# parse_sam does not CUT OUT SAM messages from the payloads
# (look at $self->recv for that)
#
my ($self,$state) = @_;
my (@data,$id,$k,$v);
%{$self->{samreply}}=();
uc($state);
if( $self->{inbuffer} =~ /^(.[^ ]*) (.[^ ]*) (.*)$/m ) {
${$self->{samreply}}{COMMAND}=$1;
${$self->{samreply}}{REPLY}=$2;
@data=split(" ",$3);
foreach $id (@data) {
($k,$v)=split("=",$id);
#print "k: $k - v: $v\n";
${$self->{samreply}}{$k}=$v;
}
} else {
$self->log("Error: SAM::parse_sam(): Could not parse the SAM Reply. Has the specs changed?");
return 0;
}
if($state eq "HELLO") {
if (${$self->{samreply}}{COMMAND} ne "HELLO") {
$self->log("Error: SAM::parse_sam(): We're in state HELLO but got no proper response from SAM");
return 0;
}
if(${$self->{samreply}}{REPLY} eq "REPLY") {
if(${$self->{samreply}}{RESULT} eq "OK") {
$self->log("Debug: SAM::parse_sam(): Got a OK result for HELLO");
return 1;
} else {
$self->log("Error :SAM::parse_sam(): Got no OK Result for HELLO");
return 0;
}
} else {
$self->log("Error: SAM::parse_sam(): Unknown Reply type for HELLO dialogue");
return 0;
}
}
if($state eq "SESSION") {
if (${$self->{samreply}}{COMMAND} ne "SESSION") {
$self->log("Error: SAM::parse_sam(): We're in state SESSION but got no proper response from SAM");
return 0;
}
if(${$self->{samreply}}{REPLY} eq "STATUS") {
if(${$self->{samreply}}{RESULT} eq "OK") {
$self->log("Debug: SAM::parse_sam(): Got a OK result for SESSION");
return 1;
} else {
$self->log("Error: SAM::parse_sam(): Got no OK Result for SESSION: ".${$self->{samreply}}{RESULT});
return 0;
}
} else {
$self->log("Error: SAM::parse_sam(): Unknown Reply type for SESSION dialogue");
return 0;
}
}
if($state eq "NAMING") {
if (${$self->{samreply}}{COMMAND} ne "NAMING") {
$self->log("Error: SAM::parse_sam(): We're in state NAMING but got no proper response from SAM");
return 0;
}
if(${$self->{samreply}}{REPLY} eq "REPLY") {
if(${$self->{samreply}}{RESULT} eq "OK") {
$self->log("Debug: SAM::parse_sam(): Got a OK result for NAMING");
$self->{lookupresult}=${$self->{samreply}}{VALUE};
return 1;
} else {
$self->log("Error: SAM::parse_sam(): Got no OK Result for NAMING: ".${$self->{samreply}}{RESULT});
return 0;
}
} else {
$self->log("Error: SAM::parse_sam(): Unknown Reply type for NAMING dialogue");
return 0;
}
}
if($state eq "STREAM") {
if (${$self->{samreply}}{COMMAND} ne "STREAM") {
$self->log("Error: SAM::parse_sam(): We're in state STREAM but got no proper response from SAM");
return 0;
}
# CREATING STREAMS
if(${$self->{samreply}}{REPLY} eq "STATUS") {
if(${$self->{samreply}}{RESULT} eq "OK") {
$self->log("Debug: SAM::parse_sam(): STREAM STATUS OK - Next action is awaited");
return 1;
} else {
$self->log("Error: SAM::parse_sam(): STREAM STATUS NOT OK.: ".${$self->{samreply}}{RESULT});
if(length(${$self->{samreply}}{MESSAGE}) == 0) {
$self->{samerror}=${$self->{samreply}}{RESULT};
} else {
$self->{samerror}=${$self->{samreply}}{MESSAGE};
}
return 0;
}
}
# SEND/RECEIVING STREAMS
# this can happen directly after a connect
if(${$self->{samreply}}{REPLY} eq "RECEIVED") {
if(${$self->{samreply}}{SIZE} > 0) {
$self->log("Debug: SAM::parse_sam(): SAM notify: RECEIVED Data. SIZE=".${$self->{samreply}}{SIZE});
$self->{bytestoread}=${$self->{samreply}}{SIZE};
return 1;
} else {
$self->log("Error: SAM::parse_sam(): Received empty payload");
return 0;
}
}
# STREAMS are closed - bad thing
# this can happen directly after a connect
if(${$self->{samreply}}{REPLY} eq "CLOSED") {
$self->log("Error: SAM::parse_sam(): Stream is closed: We need to interupt the session");
return 0;
}
}
}
sub raw_send {
# this function sends a crafted SAM request + payload to the
# SAM socket
my ($self) = @_;
my $return;
$self->log("Debug: SAM::raw_send(): >>> ".$self->{outbuffer});
$return = $self->{sock}->send($self->{outbuffer},0);
if(! defined($return)) {
$self->log("Error: SAM::raw_send(): Cannot send to Socket");
$self->close();
return 0;
}
if ( $return == length($self->{outbuffer}) || $!{EWOULDBLOCK} ) {
substr($self->{outbuffer},0, $return) = '';
delete $self->{outbuffer} unless length($self->{outbuffer});
return 1;
} else {
$self->log("Error :SAM::raw_send(): Could send, but length does not match. Closing");
$self->close();
return 0;
}
}
sub raw_read {
my ($self,$size) = @_;
my $input;
my $data;
# this reads SAM replies from the SAM socket and fills the
# inbuffer
if(!$size || $size > POSIX::BUFSIZ) {
$size=POSIX::BUFSIZ;
}
$input = $self->{sock}->recv($data, $size, 0);
if(defined($input) && length($data) >= 1) {
$self->log("Debug: SAM::raw_read(): <<< $data");
if(length($self->{inbuffer}) == 0 ) {
$self->{inbuffer} = $data;
} else {
$self->{inbuffer} .= $data;
}
return 1;
} else {
if ( $!{EAGAIN} ) {
$self->{bytestoread}=0;
return 1;
} else {
return 0;
}
}
}
sub sam_connect {
# bsic connect to the sam bridge socket itself
my ($self)=@_;
my $return=undef;
$return=$self->{sock}=IO::Socket::INET->new(${$self->{conf}}{host}.":".${$self->{conf}}{port});
if($return==0) {
$self->log("Debug: SAM::sam_connect(): Connection failed to ".${$self->{conf}}{host}.":".${$self->{conf}}{port});
return 0;
} else {
$self->log("Debug: SAM::sam_connect(): Connection established to ".${$self->{conf}}{host}.":".${$self->{conf}}{port});
return 1;
}
}
sub send {
# the public fumction to send data to sam
# the user gives his payload and we create
# valid SAM requests + payload from it ( as much as needed)
my ($self,$content,$flags)=@_;
my $return;
my $maxsize=(POSIX::BUFSIZ-100);
if($self->{state}!=250) {
$self->log("Error: SAM::send(): wrong state for send command: Needed:250 Recent:".$self->{state});
return 0;
}
# ok, what can happen?
# it could be that $content is far bigger than
# POSIX::BUFSIZ; so we need to do a little loop
# apart from that sending is in our hand
if(length($content) > $maxsize) {
$self->{outbuffer}.="STREAM SEND ID=".$self->{streamid}." SIZE=$maxsize\n".substr($content,0,$maxsize);
$content=substr($content,$maxsize,length($content));
} else {
$self->{outbuffer}.="STREAM SEND ID=".$self->{streamid}." SIZE=".length($content)."\n".$content;
}
if( $self->raw_send()) {
return 1;
} else {
$self->log("Error: SAM::send(): Could not send. Closing Link");
}
}
sub recv {
my($self,$varname,$size,$flags)=@_;
my $return;
my $tunebuffer;
my $counter;
my $chunk;
# main recv wrapper. We need to invest a few thoughts
# for this. To the user it will be like a $socket->recv
# (hopefully)
# at first some sanity checks
if(!$flags) {
$flags=0;
}
$self->{bytestoread}=0;
# size must not exceed The posix limit;
if(!$size || $size > POSIX::BUFSIZ) {
$self->log("Warn: SAM::recv(): Setting buffersize to POSIX::BUFSIZ");
$size=POSIX::BUFSIZ;
}
# nobody should call is prior to state 250
if($self->{state}!=250) {
$self->log("Error: SAM::recv(): wrong state for rcv command: Needed:250 Recent:".$self->{state});
return 0;
}
# we have a greeting banner left from connect
# flush it to the user. This can happen on several services
# like smtp/pop3/nntp but not on HTTP and other stuff
if($self->{hasbanner}) {
#print "D: ".$self->{inbuffer};
if(length($self->{inbuffer}) >0 ) {
$chunk=substr($self->{inbuffer},0, $size);
$self->{hasbanner}=0;
substr($self->{inbuffer},0, $size)='';
return $chunk;
} else {
$self->log("Error: SAM::recv(): Should have a banner but i have empty inbuffer?");
return 0;
}
# should never reach here
return 1;
}
# when there's something in the inbuffer left
# flush it to the user. If the amount of data is bigger than
# the userspecified limit, only transfer $size Bytes to the
# client
if(length($self->{inbuffer}) > 0) {
$chunk=substr($self->{inbuffer},0, $size);
substr($self->{inbuffer},0, $size)='';
return $chunk;
}
# OK, we got noting in the inbuffer
# we'll fetch a new chunk of data and then add the data to the inbuffer
# if bytestoread is bigger than POSIX::BUFSIZ we'll internally use
# a loop of reads and fill the buffer til bytestoread is 0
if(length($self->{inbuffer}) == 0) {
# read the first packet
if($self->raw_read()) {
if($self->parse_sam("STREAM") && ${$self->{samreply}}{REPLY} eq "RECEIVED") {
# it's possible that the packet does not contain any payload at all!!
# if this is the case we need
if($self->{inbuffer}=~/^.*$/) {
$self->log("Warn: SAM::recv(): Got only only one line from SAM - but i expected some payload too. Calling raw_read again ");
$self->raw_read();
}
# ok, cut the SAM HEADER from the payload
$self->{inbuffer}=substr($self->{inbuffer},(length($self->{inbuffer})-$self->{bytestoread}), length($self->{inbuffer}));
$self->log("Debug: SAM::recv(): Recived a Stream Packet and cut the SAM header out");
# ok, check if bytestoread is still bigger than our buffersize
# this means we can load more. Dangerous loop but well...
while(length($self->{inbuffer}) < $self->{bytestoread}) {
# this should definately end some day
$counter++;
if($counter > 10000) {
$self->log("Error: SAM::recv(): WTF, could not fill inbuffer as predicted by SAM header");
last;
}
# read as long til we have read all of the payload provided by SAM
$self->log("Debug: SAM::recv(): Load another chunk. Buffersize:".length($self->{inbuffer})." / Bytestoread:".$self->{bytestoread} );
$self->raw_read();
}
} else {
$self->log("Error: SAM::recv(): parse_sam() did not succeed. Interupting");
delete $self->{inbuffer};
return 0;
}
} else {
$self->log("Error: SAM::recv(): Could not read from the socket");
delete $self->{inbuffer};
return 0;
}
$chunk=substr($self->{inbuffer},0, $size);
substr($self->{inbuffer},0, $size)='';
return $chunk;
}
return 1;
}
sub lookup {
my($self,$name)=@_;
my $return;
$self->{outbuffer}="NAMING LOOKUP NAME=$name\n";
$return=$self->raw_send();
if($return == 1) {
if($self->raw_read()) {
if($self->parse_sam("NAMING")) {
$self->log("Debug: SAM::lookup(): Naming Lookup successful");
delete $self->{inbuffer};
delete $self->{outbuffer};
return $self->{lookupresult};
}
} else {
$self->log("Error :SAM::lookup(): NAMING Failed. Cannot read SAM Response");
return 0;
}
}
if($return == 0) {
$self->log("Error :SAM::lookup(): NAMING Failed. Cannot send NAMING String");
return 0;
}
}
sub sam_close {
my ($self) =@_;
$self->log("Debug: SAM::sam_close(): Closing Socket to SAM");
$self->{sock}->close();
return 1;
}
sub close {
my ($self)=@_;
my $return;
$self->{outbuffer}.="STREAM CLOSE ID=".$self->{streamid}."\n";
$self->log("Debug: SAM::close(): CLosing Stream with id: ".$self->{streamid});
$return=$self->raw_send();
# well, we do not care wether this worked or not
$self->sam_close();
return 1;
}
sub connect {
my ($self,$destination)=@_;
my $return;
$self->{streamid}= int(rand(200000000));
if(length($destination) == 0) {
$self->log("Error: SAM::connect(): need I2P destination to connect to with the SAM Bridge");
return 0;
}
$self->{outbuffer}.="STREAM CONNECT ID=".$self->{streamid}." DESTINATION=$destination\n";
$return=$self->raw_send();
if($return == 1) {
if($self->raw_read()) {
if($self->parse_sam("STREAM")) {
if(${$self->{samreply}}{REPLY} eq "STATUS") {
$self->{state}=250;
$self->log("Debug: SAM::connect(): State Transition 200 => 250");
# flush the whole inbuffer;
delete $self->{inbuffer};
delete $self->{outbuffer};
return 1;
}
if(${$self->{samreply}}{REPLY} eq "RECEIVED") {
$self->{state}=250;
$self->log("Debug: SAM::connect(): State Transition 200 => 250. Got a banner");
delete $self->{inbuffer};
#print "D: toread:".$self->{bytestoread};
#print "D: buffer:".$self->{inbuffer}."\n";
#print "D: buffersize:".length($self->{inbuffer})."\n";
$self->raw_read();
#print "D: toread:".$self->{bytestoread};
#print "D: buffer:".$self->{inbuffer}."\n";
#print "D: buffersize:".length($self->{inbuffer})."\n";
$self->{inbuffer}=substr($self->{inbuffer}, 0, $self->{bytestoread});
$self->{hasbanner}=1;
#print "D: toread:".$self->{bytestoread};
#print "D: buffer:".$self->{inbuffer}."\n";
#print "D: buffersize:".length($self->{inbuffer})."\n";
return 1;
}
}
} else {
$self->log("Error: SAM::connect(): STREAM Failed. Cannot read SAM Response");
return 0;
}
}
if($return == 0) {
$self->log("Error: SAM::connect(): STREAM Failed. Cannot send SESSION String");
return 0;
}
}
sub log {
my($self,$line)=@_;
if($line=~/.*\n$/) {
chomp $line;
}
if ( $self->{debug} ) {
print LOGFILE "$line\n";
}
return 1;
}

View File

@@ -1,18 +0,0 @@
#!/usr/bin/perl
use Net::SAM::RawSession;
use Net::SAM::DatagramSession;
$sam=Net::SAM::DatagramSession->new($ARGV[0], "BOTH", "tunnels.depthInbound=0");
print "Connected? " . $sam->connected() . "\n";
$me = $sam->lookup("ME");
print "Sending to $me.\n";
$sam->send($me,"fooquux");
$sam->readprocess();
($source, $message) = @{ $sam->receive() };
print "$source -- $message";

View File

@@ -72,7 +72,7 @@ public class Connection {
private long _lifetimeDupMessageSent;
private long _lifetimeDupMessageReceived;
public static final long MAX_RESEND_DELAY = 15*1000;
public static final long MAX_RESEND_DELAY = 10*1000;
public static final long MIN_RESEND_DELAY = 2*1000;
/** wait up to 5 minutes after disconnection so we can ack/close packets */
@@ -298,7 +298,7 @@ public class Connection {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Resend in " + timeout + " for " + packet, new Exception("Sent by"));
SimpleTimer.getInstance().addEvent(new ResendPacketEvent(packet, timeout + _context.clock().now()), timeout);
RetransmissionTimer.getInstance().addEvent(new ResendPacketEvent(packet, timeout + _context.clock().now()), timeout);
}
_context.statManager().getStatLog().addData(Packet.toId(_sendStreamId), "stream.rtt", _options.getRTT(), _options.getWindowSize());
@@ -758,8 +758,8 @@ public class Connection {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Resetting the inactivity timer to " + howLong, new Exception("Reset by"));
// this will get rescheduled, and rescheduled, and rescheduled...
SimpleTimer.getInstance().removeEvent(_activityTimer);
SimpleTimer.getInstance().addEvent(_activityTimer, howLong);
RetransmissionTimer.getInstance().removeEvent(_activityTimer);
RetransmissionTimer.getInstance().addEvent(_activityTimer, howLong);
}
private class ActivityTimer implements SimpleTimer.TimedEvent {
@@ -773,7 +773,7 @@ public class Connection {
long left = getTimeLeft();
if (left > 0) {
if (_log.shouldLog(Log.DEBUG)) _log.debug("Inactivity timeout reached, but there is time left (" + left + ")");
SimpleTimer.getInstance().addEvent(ActivityTimer.this, left);
RetransmissionTimer.getInstance().addEvent(ActivityTimer.this, left);
return;
}
// these are either going to time out or cause further rescheduling
@@ -963,7 +963,7 @@ public class Connection {
if (_log.shouldLog(Log.WARN))
_log.warn("Delaying resend of " + _packet + " as there are "
+ _activeResends + " active resends already in play");
SimpleTimer.getInstance().addEvent(ResendPacketEvent.this, 1000);
RetransmissionTimer.getInstance().addEvent(ResendPacketEvent.this, 1000);
_nextSendTime = 1000 + _context.clock().now();
return false;
}
@@ -992,7 +992,7 @@ public class Connection {
newWindowSize = 1;
// setRTT has its own ceiling
getOptions().setRTT(getOptions().getRTT() + 10*1000);
//getOptions().setRTT(getOptions().getRTT() + 10*1000);
getOptions().setWindowSize(newWindowSize);
if (_log.shouldLog(Log.WARN))
@@ -1055,7 +1055,7 @@ public class Connection {
timeout = MAX_RESEND_DELAY;
if (_log.shouldLog(Log.DEBUG))
_log.debug("Scheduling resend in " + timeout + "ms for " + _packet);
SimpleTimer.getInstance().addEvent(ResendPacketEvent.this, timeout);
RetransmissionTimer.getInstance().addEvent(ResendPacketEvent.this, timeout);
_nextSendTime = timeout + _context.clock().now();
}
return true;

View File

@@ -50,7 +50,7 @@ class ConnectionHandler {
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Receive new SYN: " + packet + ": timeout in " + _acceptTimeout);
SimpleTimer.getInstance().addEvent(new TimeoutSyn(packet), _acceptTimeout);
RetransmissionTimer.getInstance().addEvent(new TimeoutSyn(packet), _acceptTimeout);
synchronized (_synQueue) {
_synQueue.add(packet);
_synQueue.notifyAll();

View File

@@ -89,6 +89,8 @@ public class ConnectionOptions extends I2PSocketOptionsImpl {
setInboundBufferSize(opts.getInboundBufferSize());
setCongestionAvoidanceGrowthRateFactor(opts.getCongestionAvoidanceGrowthRateFactor());
setSlowStartGrowthRateFactor(opts.getSlowStartGrowthRateFactor());
setWriteTimeout(opts.getWriteTimeout());
setReadTimeout(opts.getReadTimeout());
}
}

View File

@@ -80,7 +80,7 @@ public class ConnectionPacketHandler {
if (packet.getOptionalDelay() > 60000) {
// requested choke
choke = true;
con.getOptions().setRTT(con.getOptions().getRTT() + 10*1000);
//con.getOptions().setRTT(con.getOptions().getRTT() + 10*1000);
}
}
@@ -155,7 +155,7 @@ public class ConnectionPacketHandler {
// take note of congestion
if (_log.shouldLog(Log.WARN))
_log.warn("congestion.. dup " + packet);
SimpleTimer.getInstance().addEvent(new AckDup(con), con.getOptions().getSendAckDelay());
RetransmissionTimer.getInstance().addEvent(new AckDup(con), con.getOptions().getSendAckDelay());
//con.setNextSendTime(_context.clock().now() + con.getOptions().getSendAckDelay());
//fastAck = true;
} else {
@@ -272,7 +272,7 @@ public class ConnectionPacketHandler {
oldSize = 1;
// setRTT has its own ceiling
con.getOptions().setRTT(con.getOptions().getRTT() + 10*1000);
//con.getOptions().setRTT(con.getOptions().getRTT() + 10*1000);
con.getOptions().setWindowSize(oldSize);
if (_log.shouldLog(Log.DEBUG))

View File

@@ -109,4 +109,11 @@ public class I2PSocketFull implements I2PSocket {
_connection = null;
_listener = null;
}
public String toString() {
Connection c = _connection;
if (c == null)
return super.toString();
else
return c.toString();
}
}

View File

@@ -194,7 +194,7 @@ public class MessageOutputStream extends OutputStream {
// no need to be overly worried about duplicates - it would just
// push it further out
if (!_enqueued) {
SimpleTimer.getInstance().addEvent(_flusher, _passiveFlushDelay);
RetransmissionTimer.getInstance().addEvent(_flusher, _passiveFlushDelay);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Enqueueing the flusher for " + _passiveFlushDelay + "ms out");
} else {
@@ -256,8 +256,12 @@ public class MessageOutputStream extends OutputStream {
long begin = _context.clock().now();
WriteStatus ws = null;
synchronized (_dataLock) {
if (_buf == null) throw new IOException("closed (buffer went away)");
if (_buf == null) {
_dataLock.notifyAll();
throw new IOException("closed (buffer went away)");
}
if (_dataReceiver == null) {
_dataLock.notifyAll();
throwAnyError();
return;
}
@@ -293,7 +297,10 @@ public class MessageOutputStream extends OutputStream {
}
public void close() throws IOException {
if (_closed) return;
if (_closed) {
synchronized (_dataLock) { _dataLock.notifyAll(); }
return;
}
_closed = true;
flush();
_log.debug("Output stream closed after writing " + _written);
@@ -305,6 +312,7 @@ public class MessageOutputStream extends OutputStream {
_valid = 0;
locked_updateBufferSize();
}
_dataLock.notifyAll();
}
if (ba != null) {
_dataCache.release(ba);

View File

@@ -236,14 +236,14 @@ public class PacketHandler {
}
packet.releasePayload();
} else {
if (_log.shouldLog(Log.DEBUG) && !packet.isFlagSet(Packet.FLAG_SYNCHRONIZE))
_log.debug("Packet received on an unknown stream (and not an ECHO or SYN): " + packet);
//if (_log.shouldLog(Log.DEBUG) && !packet.isFlagSet(Packet.FLAG_SYNCHRONIZE))
// _log.debug("Packet received on an unknown stream (and not an ECHO or SYN): " + packet);
if (sendId <= 0) {
Connection con = _manager.getConnectionByOutboundId(packet.getReceiveStreamId());
if (con != null) {
if ( (con.getHighestAckedThrough() <= 5) && (packet.getSequenceNum() <= 5) ) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Received additional packets before the syn on " + con + ": " + packet);
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Received additional packets before the syn on " + con + ": " + packet);
receiveKnownCon(con, packet);
return;
} else {

View File

@@ -0,0 +1,12 @@
package net.i2p.client.streaming;
import net.i2p.util.SimpleTimer;
/**
*
*/
class RetransmissionTimer extends SimpleTimer {
private static final RetransmissionTimer _instance = new RetransmissionTimer();
public static final SimpleTimer getInstance() { return _instance; }
protected RetransmissionTimer() { super("StreamingTimer"); }
}

View File

@@ -0,0 +1,3 @@
To run Syndie, fire it up with "java -jar launch-syndie.jar" (or, on windows,
to run without the dos box, "javaw -jar launch-syndie.jar"). For further
information, swing to http://localhost:8001/

View File

@@ -11,7 +11,7 @@
<mkdir dir="./build" />
<mkdir dir="./build/obj" />
<javac
srcdir="./src"
srcdir="./src"
debug="true" deprecation="on" source="1.3" target="1.3"
destdir="./build/obj"
classpath="../../../core/java/build/i2p.jar:../../jetty/jettylib/org.mortbay.jetty.jar:../../jetty/jettylib/javax.servlet.jar:../../jdom/jdom.jar:../../rome/rome-0.7.jar" />
@@ -43,6 +43,55 @@
</war>
<delete dir="./tmpwar" />
</target>
<target name="standalone" depends="standalone_prep">
<zip destfile="syndie-standalone.zip">
<zipfileset dir="./dist/" prefix="syndie/" />
</zip>
</target>
<target name="standalone_prep" depends="war">
<javac debug="true" deprecation="on" source="1.3" target="1.3"
destdir="./build" srcdir="src/" includes="net/i2p/syndie/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-syndie.jar" basedir="./build/" includes="net/i2p/syndie/web/RunStandalone.class">
<manifest>
<attribute name="Main-Class" value="net.i2p.syndie.web.RunStandalone" />
<attribute name="Class-Path" value="lib/i2p.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" />
<mkdir dir="./dist" />
<copy file="./build/launch-syndie.jar" tofile="./dist/launch-syndie.jar" />
<copy file="../syndie.war" tofile="./dist/syndie.war" />
<mkdir dir="./dist/lib" />
<copy file="../../../core/java/build/i2p.jar" tofile="./dist/lib/i2p.jar" />
<copy file="../../jetty/jettylib/commons-el.jar" tofile="./dist/lib/commons-el.jar" />
<copy file="../../jetty/jettylib/commons-logging.jar" tofile="./dist/lib/commons-logging.jar" />
<copy file="../../jetty/jettylib/javax.servlet.jar" tofile="./dist/lib/javax.servlet.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" />
<copy file="../../jetty/jettylib/jasper-runtime.jar" tofile="./dist/lib/jasper-runtime.jar" />
<copy file="../jetty-syndie.xml" tofile="./dist/jetty-syndie.xml" />
<copy file="../doc/readme-standalone.txt" tofile="./dist/readme.txt" />
<mkdir dir="./dist/work" />
<mkdir dir="./dist/logs" />
<mkdir dir="./dist/archive" />
<copy file="../../../installer/resources/blogMeta.snm" tofile="dist/archive/ovpBy2mpO1CQ7deYhQ1cDGAwI6pQzLbWOm1Sdd0W06c=/meta.snm" />
<copy file="../../../installer/resources/blogPost.snd" tofile="dist/archive/ovpBy2mpO1CQ7deYhQ1cDGAwI6pQzLbWOm1Sdd0W06c=/1132012800001.snd" />
<zip destfile="syndie-standalone.zip">
<zipfileset dir="./dist/" prefix="syndie/" />
</zip>
</target>
<target name="precompilejsp">
<delete dir="../jsp/WEB-INF/" />
<delete file="../jsp/web-fragment.xml" />
@@ -107,6 +156,8 @@
</target>
<target name="clean">
<delete dir="./build" />
<delete dir="./dist" />
<delete file="./syndie-standalone.zip" />
</target>
<target name="cleandep" depends="clean">
<ant dir="../../../core/java/" target="distclean" />

View File

@@ -67,8 +67,10 @@ public class Archive {
File meta = new File(f[i], METADATA_FILE);
if (meta.exists()) {
BlogInfo bi = new BlogInfo();
FileInputStream fi = null;
try {
bi.load(new FileInputStream(meta));
fi = new FileInputStream(meta);
bi.load(fi);
if (bi.verify(_context)) {
info.add(bi);
} else {
@@ -77,6 +79,8 @@ public class Archive {
}
} catch (IOException ioe) {
_log.error("Error loading the blog", ioe);
} finally {
if (fi != null) try { fi.close(); } catch (IOException ioe) {}
}
}
}
@@ -276,7 +280,13 @@ public class Archive {
} else {
// we have an explicit key - no caching
entry = new EntryContainer();
entry.load(new FileInputStream(entries[i]));
FileInputStream fi = null;
try {
fi = new FileInputStream(entries[i]);
entry.load(fi);
} finally {
if (fi != null) try { fi.close(); } catch (IOException ioe) {}
}
boolean ok = entry.verifySignature(_context, info);
if (!ok) {
_log.error("Keyed entry " + entries[i].getPath() + " is not valid");

View File

@@ -26,8 +26,9 @@ class ArchiveIndexer {
File headerFile = new File(rootDir, Archive.HEADER_FILE);
if (headerFile.exists()) {
BufferedReader in = null;
try {
BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(headerFile), "UTF-8"));
in = new BufferedReader(new InputStreamReader(new FileInputStream(headerFile), "UTF-8"));
String line = null;
while ( (line = in.readLine()) != null) {
StringTokenizer tok = new StringTokenizer(line, ":");
@@ -36,6 +37,8 @@ class ArchiveIndexer {
}
} catch (IOException ioe) {
log.error("Error reading header file", ioe);
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
@@ -81,6 +84,7 @@ class ArchiveIndexer {
totalSize += entry.getCompleteSize();
String entryTags[] = entry.getTags();
threads.addEntry(entry.getURI(), entryTags);
log.debug("Adding entry " + entry.getURI() + " to the threads, with tag count " + (entryTags != null ? entryTags.length : 0));
for (int t = 0; t < entryTags.length; t++) {
if (!tags.containsKey(entryTags[t])) {
tags.put(entryTags[t], new TreeMap());
@@ -103,6 +107,7 @@ class ArchiveIndexer {
String forceNewThread = rec.getHeader(HTMLRenderer.HEADER_FORCE_NEW_THREAD);
if ( (forceNewThread != null) && (Boolean.valueOf(forceNewThread).booleanValue()) ) {
// ignore the parent
log.warn("Ignore the parent of " + entry.getURI() + ": " + reply);
} else {
BlogURI parent = new BlogURI(reply.trim());
if ( (parent.getKeyHash() != null) && (parent.getEntryId() >= 0) ) {
@@ -178,7 +183,7 @@ class ArchiveIndexer {
public void receiveAddress(String name, String schema, String protocol, String location, String anchorText) {}
public void receiveArchive(String name, String description, String locationSchema, String location, String postingKey, String anchorText) {}
public void receiveAttachment(int id, String anchorText) {}
public void receiveAttachment(int id, int thumbnail, String anchorText) {}
public void receiveBegin() {}
public void receiveBlog(String name, String blogKeyHash, String blogPath, long blogEntryId, List blogArchiveLocations, String anchorText) {}
public void receiveBold(String text) {}

View File

@@ -193,9 +193,11 @@ public class BlogManager {
List rv = new ArrayList();
for (int i = 0; i < files.length; i++) {
if (files[i].isFile() && !files[i].isHidden()) {
FileInputStream in = null;
try {
SigningPublicKey pub = new SigningPublicKey();
pub.readBytes(new FileInputStream(files[i]));
in = new FileInputStream(files[i]);
pub.readBytes(in);
BlogInfo info = _archive.getBlogInfo(pub.calculateHash());
if (info != null)
rv.add(info);
@@ -203,6 +205,8 @@ public class BlogManager {
_log.error("Error listing the blog", ioe);
} catch (DataFormatException dfe) {
_log.error("Error listing the blog", dfe);
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
}
@@ -212,8 +216,9 @@ public class BlogManager {
public SigningPrivateKey getMyPrivateKey(BlogInfo blog) {
if (blog == null) return null;
File keyFile = new File(_privKeyDir, Base64.encode(blog.getKey().calculateHash().getData()) + ".priv");
FileInputStream in = null;
try {
FileInputStream in = new FileInputStream(keyFile);
in = new FileInputStream(keyFile);
SigningPublicKey pub = new SigningPublicKey();
pub.readBytes(in);
SigningPrivateKey priv = new SigningPrivateKey();
@@ -225,6 +230,8 @@ public class BlogManager {
} catch (DataFormatException dfe) {
_log.error("Error reading the blog key", dfe);
return null;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
@@ -264,10 +271,11 @@ public class BlogManager {
}
private Properties loadUserProps(File userFile) {
BufferedReader in = null;
try {
Properties props = new Properties();
FileInputStream fin = new FileInputStream(userFile);
BufferedReader in = new BufferedReader(new InputStreamReader(fin, "UTF-8"));
in = new BufferedReader(new InputStreamReader(fin, "UTF-8"));
String line = null;
while ( (line = in.readLine()) != null) {
int split = line.indexOf('=');
@@ -281,6 +289,8 @@ public class BlogManager {
return props;
} catch (IOException ioe) {
return null;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
@@ -514,9 +524,22 @@ public class BlogManager {
String ok = register(user, getDefaultLogin(), getDefaultPass(), "", "default", "Default Syndie blog", "");
if (User.LOGIN_OK.equals(ok)) {
_log.info("Default user created: " + user);
for (int i = 0; i < DEFAULT_SINGLE_USER_ARCHIVES.length; i++)
user.getPetNameDB().add(new PetName("DefaultArchive" + i, "syndie", "syndiearchive", DEFAULT_SINGLE_USER_ARCHIVES[i]));
scheduleSyndication(DEFAULT_SINGLE_USER_ARCHIVES);
String altArchives = _context.getProperty("syndie.defaultSingleUserArchives");
String archives[] = DEFAULT_SINGLE_USER_ARCHIVES;
if ( (altArchives != null) && (altArchives.trim().length() > 0) ) {
ArrayList list = new ArrayList();
StringTokenizer tok = new StringTokenizer(altArchives, ",\t ");
while (tok.hasMoreTokens())
list.add(tok.nextToken());
if (list.size() > 0) {
archives = new String[list.size()];
for (int i = 0; i < list.size(); i++)
archives[i] = (String)list.get(i);
}
}
for (int i = 0; i < archives.length; i++)
user.getPetNameDB().add(new PetName("DefaultArchive" + i, "syndie", "syndiearchive", archives[i]));
scheduleSyndication(archives);
saveUser(user);
return;
} else {
@@ -540,11 +563,17 @@ public class BlogManager {
}
public boolean authorizeRemote(String pass) {
if (isSingleUser()) return true;
String rem = getRemotePasswordHash();
if ( (rem == null) || (rem.trim().length() <= 0) )
return false;
String hash = Base64.encode(_context.sha().calculateHash(DataHelper.getUTF8(pass.trim())).getData());
return (hash.equals(rem));
String rem = getRemotePasswordHash();
boolean ok = false;
if ( (rem != null) && (rem.trim().length() > 0) )
ok = hash.equals(rem);
if (!ok) {
rem = getAdminPasswordHash();
if ( (rem != null) && (rem.trim().length() > 0) )
ok = hash.equals(rem);
}
return ok;
}
public boolean authorizeRemote(User user) {
if (isSingleUser()) return true;
@@ -737,6 +766,8 @@ public class BlogManager {
long entryId = getNextBlogEntry(user);
_log.debug("Next blog entry ID = " + entryId + " for user " + user.getUsername());
StringTokenizer tok = new StringTokenizer(tags, " ,\n\t");
String tagList[] = new String[tok.countTokens()];
for (int i = 0; i < tagList.length; i++)
@@ -803,11 +834,14 @@ public class BlogManager {
boolean ok = getArchive().storeEntry(c);
if (ok) {
getArchive().regenerateIndex();
long prevEntryId = user.getMostRecentEntry();
user.setMostRecentEntry(entryId);
if(shouldAuthenticate)
if(shouldAuthenticate) {
saveUser(user);
else
} else {
storeUser(user);
}
_log.debug("New entry posted, entryId=" + entryId + " prev=" + prevEntryId);
return uri;
} else {
return null;
@@ -887,7 +921,13 @@ public class BlogManager {
private Properties getKnownHosts(File filename) throws IOException {
Properties rv = new Properties();
if (filename.exists()) {
rv.load(new FileInputStream(filename));
FileInputStream in = null;
try {
in = new FileInputStream(filename);
rv.load(in);
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
return rv;
}
@@ -928,7 +968,8 @@ public class BlogManager {
}
private final SimpleDateFormat _dateFormat = new SimpleDateFormat("yyyy/MM/dd", Locale.UK);
private final long getDayBegin(long now) {
public final long getDayBegin() { return getDayBegin(_context.clock().now()); }
public final long getDayBegin(long now) {
synchronized (_dateFormat) {
try {
String str = _dateFormat.format(new Date(now));
@@ -952,6 +993,7 @@ public class BlogManager {
if ( (location != null) && (location.trim().length() > 0) )
buf.append(location.trim());
System.setProperty("syndie.updateArchives", buf.toString());
writeConfig();
Updater.wakeup();
}
public void scheduleSyndication(String locations[]) {
@@ -966,6 +1008,7 @@ public class BlogManager {
for (Iterator iter = locs.iterator(); iter.hasNext(); )
buf.append(iter.next().toString().trim()).append(',');
System.setProperty("syndie.updateArchives", buf.toString());
writeConfig();
Updater.wakeup();
}
public void unscheduleSyndication(String location) {
@@ -977,6 +1020,7 @@ public class BlogManager {
buf.append(archives[i]).append(",");
System.setProperty("syndie.updateArchives", buf.toString());
}
writeConfig();
}
public boolean syndicationScheduled(String location) {
String archives[] = getUpdateArchives();

View File

@@ -37,7 +37,13 @@ public class EntryExtractor {
public boolean extract(File entryFile, File entryDir, SessionKey entryKey, BlogInfo info) throws IOException {
EntryContainer entry = new EntryContainer();
entry.load(new FileInputStream(entryFile));
FileInputStream in = null;
try {
in = new FileInputStream(entryFile);
entry.load(in);
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
boolean ok = entry.verifySignature(_context, info);
if (!ok) {
return false;

View File

@@ -14,7 +14,7 @@ public class HeaderReceiver implements SMLParser.EventReceiver {
public void receiveAddress(String name, String schema, String protocol, String location, String anchorText) {}
public void receiveArchive(String name, String description, String locationSchema, String location, String postingKey, String anchorText) {}
public void receiveAttachment(int id, String anchorText) {}
public void receiveAttachment(int id, int thumbnail, String anchorText) {}
public void receiveBegin() {}
public void receiveBlog(String name, String blogKeyHash, String blogPath, long blogEntryId, List blogArchiveLocations, String anchorText) {}
public void receiveBold(String text) {}

View File

@@ -0,0 +1,20 @@
package net.i2p.syndie;
import java.util.*;
import net.i2p.data.*;
import net.i2p.syndie.data.*;
/** sort BlogURI instances with the highest entryId first */
public class NewestEntryFirstComparator implements Comparator {
public int compare(Object lhs, Object rhs) {
BlogURI left = (BlogURI)lhs;
BlogURI right = (BlogURI)rhs;
if (left.getEntryId() > right.getEntryId()) {
return -1;
} else if (left.getEntryId() == right.getEntryId()) {
return DataHelper.compareTo(left.getKeyHash().getData(), right.getKeyHash().getData());
} else {
return 1;
}
}
}

View File

@@ -0,0 +1,30 @@
package net.i2p.syndie;
import java.util.*;
import net.i2p.data.*;
/** sort ThreadNodeImpl instances with the highest entryId first */
public class NewestNodeFirstComparator implements Comparator {
public int compare(Object lhs, Object rhs) {
ThreadNodeImpl left = (ThreadNodeImpl)lhs;
ThreadNodeImpl right = (ThreadNodeImpl)rhs;
long l = left.getMostRecentPostDate();
long r = right.getMostRecentPostDate();
if (l > r) {
return -1;
} else if (l == r) {
// ok, the newest responses match, so lets fall back and compare the roots themselves
l = left.getEntry().getEntryId();
r = right.getEntry().getEntryId();
if (l > r) {
return -1;
} else if (l == r) {
return DataHelper.compareTo(left.getEntry().getKeyHash().getData(), right.getEntry().getKeyHash().getData());
} else {
return 1;
}
} else {
return 1;
}
}
}

View File

@@ -158,12 +158,15 @@ public class Sucker {
if (!lastIdFile.exists())
lastIdFile.createNewFile();
FileInputStream fis = new FileInputStream(lastIdFile);
String number = readLine(fis);
FileInputStream fis = null;
try {
fis = new FileInputStream(lastIdFile);
String number = readLine(fis);
messageNumber = Integer.parseInt(number);
} catch (NumberFormatException e) {
messageNumber = 0;
} finally {
if (fis != null) try { fis.close(); } catch (IOException ioe) {}
}
// Create outputDir if missing
@@ -226,28 +229,39 @@ public class Sucker {
_log.debug("entries: " + entries.size());
FileOutputStream hos = new FileOutputStream(historyFile, true);
FileOutputStream hos = null;
// Process list backwards to get syndie to display the
// entries in the right order. (most recent at top)
for (int i = entries.size()-1; i >= 0; i--) {
SyndEntry e = (SyndEntry) entries.get(i);
try {
hos = new FileOutputStream(historyFile, true);
attachmentCounter=0;
if (_log.shouldLog(Log.DEBUG))
_log.debug("Syndicate entry: " + e.getLink());
String messageId = convertToSml(e);
if (messageId!=null) {
hos.write(messageId.getBytes());
hos.write("\n".getBytes());
// Process list backwards to get syndie to display the
// entries in the right order. (most recent at top)
for (int i = entries.size()-1; i >= 0; i--) {
SyndEntry e = (SyndEntry) entries.get(i);
attachmentCounter=0;
if (_log.shouldLog(Log.DEBUG))
_log.debug("Syndicate entry: " + e.getLink());
String messageId = convertToSml(e);
if (messageId!=null) {
hos.write(messageId.getBytes());
hos.write("\n".getBytes());
}
}
} finally {
if (hos != null) try { hos.close(); } catch (IOException ioe) {}
}
if(!pushToSyndie) {
FileOutputStream fos = new FileOutputStream(lastIdFile);
fos.write(("" + messageNumber).getBytes());
FileOutputStream fos = null;
try {
fos = new FileOutputStream(lastIdFile);
fos.write(("" + messageNumber).getBytes());
} finally {
if (fos != null) try { fos.close(); } catch (IOException ioe) {}
}
}
_log.debug("done fetching");
@@ -734,8 +748,9 @@ public class Sucker {
String lineToCompare = messageId.substring(0, idx-1);
idx = lineToCompare.lastIndexOf(":");
lineToCompare = lineToCompare.substring(0, idx-1);
FileInputStream his = null;
try {
FileInputStream his = new FileInputStream(historyFile);
his = new FileInputStream(historyFile);
String line;
while ((line = readLine(his)) != null) {
idx = line.lastIndexOf(":");
@@ -751,6 +766,8 @@ public class Sucker {
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (his != null) try { his.close(); } catch (IOException ioe) {}
}
return false;
}

View File

@@ -53,15 +53,19 @@ class ThreadNodeImpl implements ThreadNode {
void summarizeThread() {
_recursiveAuthors.add(_entry.getKeyHash());
_recursiveEntries.add(_entry);
_mostRecentPostDate = _entry.getEntryId();
_mostRecentPostAuthor = _entry.getKeyHash();
// children are always 'newer' than parents, even if their dates are older
// (e.g. post #1 for a child on tuesday is 'newer' than post #5 for the parent on tuesday)
_mostRecentPostDate = -1;
_mostRecentPostAuthor = null;
// we need to go through all children (recursively), in case the
// tree is out of order (which it shouldn't be, if its built carefully...)
for (int i = 0; i < _children.size(); i++) {
ThreadNodeImpl node = (ThreadNodeImpl)_children.get(i);
node.summarizeThread();
if (node.getMostRecentPostDate() > _mostRecentPostDate) {
// >= so we can give reasonable order when a child is a reply to a parent
// (since the child must have been posted after the parent)
if (node.getMostRecentPostDate() >= _mostRecentPostDate) {
_mostRecentPostDate = node.getMostRecentPostDate();
_mostRecentPostAuthor = node.getMostRecentPostAuthor();
}
@@ -69,6 +73,22 @@ class ThreadNodeImpl implements ThreadNode {
_recursiveAuthors.addAll(node.getRecursiveAuthors());
_recursiveEntries.addAll(node.getRecursiveEntries());
}
if (_mostRecentPostDate < 0) {
_mostRecentPostDate = _entry.getEntryId();
_mostRecentPostAuthor = _entry.getKeyHash();
}
// now reorder the children
TreeSet ordered = new TreeSet(new NewestNodeFirstComparator());
for (int i = 0; i < _children.size(); i++) {
ThreadNodeImpl kid = (ThreadNodeImpl)_children.get(i);
ordered.add(kid);
}
List kids = new ArrayList(ordered.size());
for (Iterator iter = ordered.iterator(); iter.hasNext(); )
kids.add(iter.next());
_children = kids;
}
public String toString() {

View File

@@ -5,7 +5,9 @@ import java.io.IOException;
import java.util.*;
import net.i2p.I2PAppContext;
import net.i2p.client.naming.PetNameDB;
import net.i2p.client.naming.PetName;
import net.i2p.data.*;
import net.i2p.syndie.web.AddressesServlet;
/**
* User session state and preferences.
@@ -44,6 +46,15 @@ public class User {
private boolean _dataImported;
static final String PROP_USERHASH = "__userHash";
private static final String DEFAULT_FAVORITE_TAGS[] = {
"syndie", "syndie.tech", "syndie.intro", "syndie.bugs", "syndie.featurerequest", "syndie.announce",
"i2p", "i2p.tech", "i2p.bugs", "i2p.i2phex", "i2p.susimail", "i2p.irc",
"bt.i2psnark", "bt.i2prufus", "bt.i2p-bt", "bt.azureus", "bt.misc",
"security.misc",
"chat",
"test"
};
/**
* Ugly hack to fetch the default User instance - this is the default
@@ -99,6 +110,24 @@ public class User {
public long getMostRecentEntry() { return _mostRecentEntry; }
public Map getBlogGroups() { return _blogGroups; }
public List getShitlistedBlogs() { return _shitlistedBlogs; }
public List getFavoriteTags() {
List rv = new ArrayList();
for (Iterator iter = _petnames.getNames().iterator(); iter.hasNext(); ) {
String name = (String)iter.next();
PetName pn = _petnames.getByName(name);
if (AddressesServlet.PROTO_TAG.equals(pn.getProtocol()))
rv.add(pn.getLocation());
}
if (rv.size() <= 0) {
for (int i = 0; i < DEFAULT_FAVORITE_TAGS.length; i++) {
if (!_petnames.containsName(DEFAULT_FAVORITE_TAGS[i])) {
_petnames.add(new PetName(DEFAULT_FAVORITE_TAGS[i], AddressesServlet.NET_SYNDIE,
AddressesServlet.PROTO_TAG, DEFAULT_FAVORITE_TAGS[i]));
}
}
}
return rv;
}
public String getAddressbookLocation() { return _addressbookLocation; }
public boolean getShowImages() { return _showImagesByDefault; }
public boolean getShowExpanded() { return _showExpandedByDefault; }
@@ -282,7 +311,7 @@ public class User {
// shitlist=hash,hash,hash
List shitlistedBlogs = getShitlistedBlogs();
if (shitlistedBlogs.size() > 0) {
buf.setLength(0);
//buf.setLength(0);
buf.append("shitlistedblogs=");
for (int i = 0; i < shitlistedBlogs.size(); i++) {
Hash blog = (Hash)shitlistedBlogs.get(i);
@@ -292,6 +321,13 @@ public class User {
}
buf.append('\n');
}
List favoriteTags = getFavoriteTags();
if (favoriteTags.size() > 0) {
buf.append("favoritetags=");
for (int i = 0; i < favoriteTags.size(); i++)
buf.append(((String)favoriteTags.get(i)).trim()).append(" ");
buf.append('\n');
}
return buf.toString();
}

View File

@@ -29,7 +29,12 @@ class WritableThreadIndex extends ThreadIndex {
void addParent(BlogURI parent, BlogURI child) { _parents.put(child, parent); }
void addEntry(BlogURI entry, String tags[]) {
if (tags == null) tags = NO_TAGS;
String oldTags[] = (String[])_tags.put(entry, tags);
Object old = _tags.get(entry);
if (old != null) {
System.err.println("Old value: " + old + " new tags: " + tags + " entry: " + entry);
} else {
_tags.put(entry, tags);
}
}
/**
@@ -108,7 +113,9 @@ class WritableThreadIndex extends ThreadIndex {
while (node.getParent() != null)
node = node.getParent();
roots.add(node);
if (!roots.contains(node)) {
roots.add(node);
}
}
// store them, sorted by most recently updated thread first
@@ -129,35 +136,4 @@ class WritableThreadIndex extends ThreadIndex {
buf.append("</threadIndex>\n");
return buf.toString();
}
/** sort BlogURI instances with the highest entryId first */
private class NewestEntryFirstComparator implements Comparator {
public int compare(Object lhs, Object rhs) {
BlogURI left = (BlogURI)lhs;
BlogURI right = (BlogURI)rhs;
if (left.getEntryId() > right.getEntryId()) {
return -1;
} else if (left.getEntryId() == right.getEntryId()) {
return DataHelper.compareTo(left.getKeyHash().getData(), right.getKeyHash().getData());
} else {
return 1;
}
}
}
/** sort ThreadNodeImpl instances with the highest entryId first */
private class NewestNodeFirstComparator implements Comparator {
public int compare(Object lhs, Object rhs) {
ThreadNodeImpl left = (ThreadNodeImpl)lhs;
ThreadNodeImpl right = (ThreadNodeImpl)rhs;
long l = left.getMostRecentPostDate();
long r = right.getMostRecentPostDate();
if (l > r) {
return -1;
} else if (l == r) {
return DataHelper.compareTo(left.getEntry().getKeyHash().getData(), right.getEntry().getKeyHash().getData());
} else {
return 1;
}
}
}
}

View File

@@ -281,6 +281,9 @@ public class ArchiveIndex {
*
*/
public void selectMatchesOrderByEntryId(List out, Hash blog, String tag) {
selectMatchesOrderByEntryId(out, blog, tag, 0);
}
public void selectMatchesOrderByEntryId(List out, Hash blog, String tag, long lowestEntryId) {
TreeMap ordered = new TreeMap();
for (int i = 0; i < _blogs.size(); i++) {
BlogSummary summary = (BlogSummary)_blogs.get(i);
@@ -288,7 +291,8 @@ public class ArchiveIndex {
if (!blog.equals(summary.blog))
continue;
}
if (tag != null) {
if ( (tag != null) && (tag.trim().length() > 0) ) {
if (!tag.equals(summary.tag)) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Tag [" + summary.tag + "] does not match the requested [" + tag + "] in " + summary.blog.toBase64());
@@ -312,13 +316,21 @@ public class ArchiveIndex {
for (int j = 0; j < summary.entries.size(); j++) {
EntrySummary entry = (EntrySummary)summary.entries.get(j);
String k = (Long.MAX_VALUE-entry.entry.getEntryId()) + "-" + entry.entry.getKeyHash().toBase64();
ordered.put(k, entry.entry);
//System.err.println("Including match: " + k);
if (entry.entry.getEntryId() < lowestEntryId) {
long daysAgo1 = entry.entry.getEntryId() / (24*60*60*1000l);
long daysAgo2 = lowestEntryId / (24*60*60*1000l);
continue;
} else {
String k = (Long.MAX_VALUE-entry.entry.getEntryId()) + "-" + entry.entry.getKeyHash().toBase64();
ordered.put(k, entry.entry);
}
}
}
for (Iterator iter = ordered.values().iterator(); iter.hasNext(); ) {
BlogURI entry = (BlogURI)iter.next();
if (entry.getEntryId() < lowestEntryId) {
continue;
}
if (!out.contains(entry))
out.add(entry);
}

View File

@@ -10,6 +10,8 @@ public class BlogURI {
private Hash _blogHash;
private long _entryId;
public static final Comparator COMPARATOR = new NewestFirstComparator();
public BlogURI() {
this(null, -1);
}
@@ -95,4 +97,20 @@ public class BlogURI {
if (!u.toString().equals(uri))
System.err.println("Not a match: [" + uri + "] != [" + u.toString() + "]");
}
/**
* Order the BlogURIs by entryId, with the highest entryId first
*/
private static class NewestFirstComparator implements Comparator {
public int compare(Object lhs, Object rhs) {
BlogURI l = (BlogURI)lhs;
BlogURI r = (BlogURI)rhs;
if (l.getEntryId() > r.getEntryId())
return -1;
else if (l.getEntryId() < r.getEntryId())
return 1;
else // same date, compare by blog hash (aka randomly)
return DataHelper.compareTo(l.getKeyHash().getData(), r.getKeyHash().getData());
}
}
}

View File

@@ -16,11 +16,12 @@ public class FilteredThreadIndex extends ThreadIndex {
private List _roots;
private List _ignoredAuthors;
private Collection _filteredAuthors;
private boolean _filterAuthorsByRoot;
public static final String GROUP_FAVORITE = "Favorite";
public static final String GROUP_IGNORE = "Ignore";
public FilteredThreadIndex(User user, Archive archive, Collection tags, Collection authors) {
public FilteredThreadIndex(User user, Archive archive, Collection tags, Collection authors, boolean filterAuthorsByRoot) {
super();
_user = user;
_archive = archive;
@@ -31,6 +32,7 @@ public class FilteredThreadIndex extends ThreadIndex {
_filteredAuthors = authors;
if (_filteredAuthors == null)
_filteredAuthors = Collections.EMPTY_SET;
_filterAuthorsByRoot = filterAuthorsByRoot;
_ignoredAuthors = new ArrayList();
for (Iterator iter = user.getPetNameDB().iterator(); iter.hasNext(); ) {
@@ -53,12 +55,12 @@ public class FilteredThreadIndex extends ThreadIndex {
_roots = new ArrayList(_baseIndex.getRootCount());
for (int i = 0; i < _baseIndex.getRootCount(); i++) {
ThreadNode node = _baseIndex.getRoot(i);
if (!isIgnored(node, _ignoredAuthors, _filteredTags, _filteredAuthors))
if (!isIgnored(node, _ignoredAuthors, _filteredTags, _filteredAuthors, _filterAuthorsByRoot))
_roots.add(node);
}
}
private boolean isIgnored(ThreadNode node, List ignoredAuthors, Collection requestedTags, Collection filteredAuthors) {
private boolean isIgnored(ThreadNode node, List ignoredAuthors, Collection requestedTags, Collection filteredAuthors, boolean filterAuthorsByRoot) {
if (filteredAuthors.size() <= 0) {
boolean allAuthorsIgnored = true;
for (Iterator iter = node.getRecursiveAuthorIterator(); iter.hasNext(); ) {
@@ -75,9 +77,16 @@ public class FilteredThreadIndex extends ThreadIndex {
boolean filteredAuthorMatches = false;
for (Iterator iter = filteredAuthors.iterator(); iter.hasNext(); ) {
Hash author = (Hash)iter.next();
if (node.containsAuthor(author)) {
filteredAuthorMatches = true;
break;
if (filterAuthorsByRoot) {
if (node.getEntry().getKeyHash().equals(author)) {
filteredAuthorMatches = true;
break;
}
} else {
if (node.containsAuthor(author)) {
filteredAuthorMatches = true;
break;
}
}
}
if (!filteredAuthorMatches)
@@ -107,4 +116,5 @@ public class FilteredThreadIndex extends ThreadIndex {
public ThreadNode getNode(BlogURI uri) { return _baseIndex.getNode(uri); }
public Collection getFilteredTags() { return _filteredTags; }
public Collection getFilteredAuthors() { return _filteredAuthors; }
public boolean getFilterAuthorsByRoot() { return _filterAuthorsByRoot; }
}

View File

@@ -111,5 +111,5 @@ public class EventReceiverImpl implements SMLParser.EventReceiver {
public void receiveH5(String text) {}
public void receivePre(String text) {}
public void receiveHR() {}
public void receiveAttachment(int id, String anchorText) {}
public void receiveAttachment(int id, int thumbnail, String anchorText) {}
}

View File

@@ -31,16 +31,27 @@ public class HTMLPreviewRenderer extends HTMLRenderer {
ArchiveViewerBean.PARAM_ATTACHMENT + "=" + id;
}
public void receiveAttachment(int id, String anchorText) {
public void receiveAttachment(int id, int thumb, String anchorText) {
anchorText = sanitizeString(anchorText);
if (!continueBody()) { return; }
if ( (id < 0) || (_files == null) || (id >= _files.size()) ) {
_bodyBuffer.append(sanitizeString(anchorText));
_bodyBuffer.append(anchorText);
} else {
File f = (File)_files.get(id);
String name = (String)_filenames.get(id);
String type = (String)_fileTypes.get(id);
_bodyBuffer.append("<a ").append(getClass("attachmentView")).append(" href=\"").append(getAttachmentURL(id)).append("\">");
_bodyBuffer.append(sanitizeString(anchorText)).append("</a>");
if(thumb >= 0) {
_bodyBuffer.append("<img src=\"").
append(getAttachmentURL(thumb)).
append("\" alt=\"").append(anchorText).
append("\" title=\"").append(anchorText).
append("\" />");
} else {
_bodyBuffer.append(anchorText);
}
_bodyBuffer.append("</a>");
_bodyBuffer.append(getSpan("attachmentSummary")).append(" (");
_bodyBuffer.append(getSpan("attachmentSummarySize")).append(f.length()/1024).append("KB</span>, ");
_bodyBuffer.append(getSpan("attachmentSummaryName")).append(" \"").append(sanitizeString(name)).append("\"</span>, ");
@@ -87,11 +98,13 @@ public class HTMLPreviewRenderer extends HTMLRenderer {
_postBodyBuffer.append(getSpan("summDetailExternal")).append("External links:</span> ");
for (int i = 0; i < _links.size(); i++) {
Link l = (Link)_links.get(i);
_postBodyBuffer.append("<a ").append(getClass("summDetailExternalLink")).append(" href=\"externallink.jsp?");
String schema = l.schema;
_postBodyBuffer.append("<a ");
_postBodyBuffer.append(getClass("summDetailExternalLink")).append(" href=\"externallink.jsp?");
if (l.schema != null)
_postBodyBuffer.append("schema=").append(sanitizeURL(l.schema)).append('&');
_postBodyBuffer.append("schema=").append(sanitizeURL(l.schema)).append('&');
if (l.location != null)
_postBodyBuffer.append("location=").append(sanitizeURL(l.location)).append('&');
_postBodyBuffer.append("location=").append(sanitizeURL(l.location)).append('&');
_postBodyBuffer.append("\">").append(sanitizeString(l.location));
_postBodyBuffer.append(getSpan("summDetailExternalNet")).append(" (").append(sanitizeString(l.schema)).append(")</span></a> ");
}

View File

@@ -341,14 +341,16 @@ public class HTMLRenderer extends EventReceiverImpl {
}
String url = getPageURL(blog, null, -1, -1, -1, (_user != null ? _user.getShowExpanded() : false), (_user != null ? _user.getShowImages() : false));
//String url = getPageURL(blog, null, -1, -1, -1, (_user != null ? _user.getShowExpanded() : false), (_user != null ? _user.getShowImages() : false));
String url = getMetadataURL(blog);
_bodyBuffer.append(getSpan("blogEntrySummary")).append(" [<a ").append(getClass("blogLink")).append(" href=\"").append(url);
_bodyBuffer.append("\">");
if ( (name != null) && (name.trim().length() > 0) )
_bodyBuffer.append(sanitizeString(name));
else
_bodyBuffer.append("view");
_bodyBuffer.append("</a> (<a ").append(getClass("blogMeta")).append(" href=\"").append(getMetadataURL(blog)).append("\">meta</a>)");
_bodyBuffer.append("</a> ");
//_bodyBuffer.append("</a> (<a ").append(getClass("blogMeta")).append(" href=\"").append(getMetadataURL(blog)).append("\">meta</a>)");
if ( (tag != null) && (tag.trim().length() > 0) ) {
url = getPageURL(blog, tag, -1, -1, -1, false, false);
_bodyBuffer.append(" <a ").append(getClass("blogTagLink")).append(" href=\"").append(url);
@@ -434,10 +436,14 @@ public class HTMLRenderer extends EventReceiverImpl {
_links.add(l);
if (!continueBody()) { return; }
if ( (schema == null) || (location == null) ) return;
_bodyBuffer.append("<a ").append(getClass("externalLink")).append(" href=\"externallink.jsp?schema=");
_bodyBuffer.append("<a ");
_bodyBuffer.append(getClass("externalLink")).append(" href=\"externallink.jsp?schema=");
_bodyBuffer.append(sanitizeURL(schema)).append("&location=");
_bodyBuffer.append(sanitizeURL(location)).append("&description=");
_bodyBuffer.append(sanitizeURL(text)).append("\">").append(sanitizeString(text)).append("</a>");
_bodyBuffer.append(sanitizeURL(text));
_bodyBuffer.append("\">").
append(sanitizeString(text)).
append("</a>");
}
protected static class Address {
@@ -507,14 +513,23 @@ public class HTMLRenderer extends EventReceiverImpl {
}
}
public void receiveAttachment(int id, String anchorText) {
public void receiveAttachment(int id, int thumb, String anchorText) {
if (!continueBody()) { return; }
Attachment attachments[] = _entry.getAttachments();
if ( (id < 0) || (id >= attachments.length)) {
_bodyBuffer.append(getSpan("attachmentUnknown")).append(sanitizeString(anchorText)).append("</span>");
} else {
_bodyBuffer.append("<a ").append(getClass("attachmentView")).append(" href=\"").append(getAttachmentURL(id)).append("\">");
_bodyBuffer.append(sanitizeString(anchorText)).append("</a>");
_bodyBuffer.append("<a ").append(getClass("attachmentView")).append(" href=\"").append(getAttachmentURL(id)).append("\">");
if(thumb >= 0) {
_bodyBuffer.append("<img src=\"").
append(getAttachmentURL(thumb)).
append("\" alt=\"").append(anchorText).
append("\" title=\"").append(anchorText).
append("\" />");
} else {
_bodyBuffer.append(anchorText);
}
_bodyBuffer.append("</a>");
_bodyBuffer.append(getSpan("attachmentSummary")).append(" (");
_bodyBuffer.append(getSpan("attachmentSummarySize")).append(attachments[id].getDataLength()/1024).append("KB</span>, ");
_bodyBuffer.append(getSpan("attachmentSummaryName")).append(" \"").append(sanitizeString(attachments[id].getName())).append("\"</span>, ");
@@ -637,12 +652,14 @@ public class HTMLRenderer extends EventReceiverImpl {
_postBodyBuffer.append(getSpan("summDetailExternal")).append("External links:</span> ");
for (int i = 0; i < _links.size(); i++) {
Link l = (Link)_links.get(i);
_postBodyBuffer.append("<a ").append(getClass("summDetailExternalLink")).append(" href=\"externallink.jsp?");
String schema = l.schema;
_postBodyBuffer.append("<a ");
_postBodyBuffer.append(getClass("summDetailExternalLink")).append(" href=\"externallink.jsp?");
if (l.schema != null)
_postBodyBuffer.append("schema=").append(sanitizeURL(l.schema)).append('&');
_postBodyBuffer.append("schema=").append(sanitizeURL(l.schema)).append('&');
if (l.location != null)
_postBodyBuffer.append("location=").append(sanitizeURL(l.location)).append('&');
_postBodyBuffer.append("\">").append(sanitizeString(l.location));
_postBodyBuffer.append("location=").append(sanitizeURL(l.location)).append('&');
_postBodyBuffer.append("\">").append(sanitizeString(l.location, 30));
_postBodyBuffer.append(getSpan("summDetailExternalNet")).append(" (").append(sanitizeString(l.schema)).append(")</span></a> ");
}
_postBodyBuffer.append("<br />\n");
@@ -973,6 +990,13 @@ public class HTMLRenderer extends EventReceiverImpl {
else
return orig.toString();
}
public static final String sanitizeStrippedXML(String orig) {
if (orig == null) return "";
orig = orig.replaceAll("&", "&amp;");
orig = orig.replaceAll("<", "&lt;");
orig = orig.replaceAll(">", "&gt;");
return orig;
}
private static final String STYLE_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
public static String sanitizeStyle(String style) {
@@ -1062,8 +1086,9 @@ public class HTMLRenderer extends EventReceiverImpl {
buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(blog.toBase64()).append('&');
if (tag != null)
buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(sanitizeTagParam(tag)).append('&');
String entry = null;
if (entryId >= 0) {
String entry = blog.toBase64() + '/' + entryId;
entry = blog.toBase64() + '/' + entryId;
buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(entry).append('&');
buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=').append(entry).append('&');
}

View File

@@ -208,7 +208,7 @@ public class RSSRenderer extends HTMLRenderer {
_bodyBuffer.append(sanitizeString(anchorText));
}
}
public void receiveAttachment(int id, String anchorText) {
public void receiveAttachment(int id, int thumb, String anchorText) {
if (!continueBody()) { return; }
_bodyBuffer.append(sanitizeString(anchorText));
}
@@ -314,4 +314,4 @@ public class RSSRenderer extends HTMLRenderer {
String sanitized = sanitizeXML(t);
System.out.println("[" + str + "] --> [" + sanitized + "]");
}
}
}

View File

@@ -211,6 +211,7 @@ public class SMLParser {
private static final String T_ATTACHMENT = "attachment";
private static final String T_ARCHIVE = "archive";
private static final String P_THUMBNAIL = "thumbnail";
private static final String P_ATTACHMENT = "attachment";
private static final String P_WHO_QUOTED = "author";
private static final String P_QUOTE_LOCATION = "location";
@@ -284,7 +285,10 @@ public class SMLParser {
} else if (T_PRE.equals(tagName)) {
receiver.receivePre(body);
} else if (T_ATTACHMENT.equals(tagName)) {
receiver.receiveAttachment((int)getLong(P_ATTACHMENT_ID, attr), body);
receiver.receiveAttachment(
(int)getLong(P_ATTACHMENT_ID, attr),
(int)getLong(P_THUMBNAIL, attr),
body);
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("need to learn how to parse the tag [" + tagName + "]");
@@ -402,7 +406,7 @@ public class SMLParser {
String postingKey, String anchorText);
public void receiveImage(String alternateText, int attachmentId);
public void receiveAddress(String name, String schema, String protocol, String location, String anchorText);
public void receiveAttachment(int id, String anchorText);
public void receiveAttachment(int id, int thumb, String anchorText);
public void receiveBold(String text);
public void receiveItalic(String text);
public void receiveUnderline(String text);

View File

@@ -36,10 +36,17 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
public static final String PARAM_REMOVE_FROM_GROUP_NAME = "removeName";
/** group to remove from the bookmarked entry, or if blank, remove the entry itself */
public static final String PARAM_REMOVE_FROM_GROUP = "removeGroup";
/** add the specified tag to the favorites list */
public static final String PARAM_ADD_TAG = "addTag";
/** index into the nav tree to start displaying */
public static final String PARAM_OFFSET = "offset";
public static final String PARAM_TAGS = "tags";
/** only show threads that the given author participates in */
public static final String PARAM_AUTHOR = "author";
/** only show threads started by the given author */
public static final String PARAM_THREAD_AUTHOR = "threadAuthorOnly";
/** search back through the blog for entries this many days */
public static final String PARAM_DAYS_BACK = "daysBack";
// parameters for editing one's profile
public static final String PARAM_PROFILE_NAME = "profileName";
public static final String PARAM_PROFILE_DESC = "profileDesc";
@@ -64,7 +71,33 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
return buf.toString();
}
public static String getNavLink(String uri, String viewPost, String viewThread, String tags, String author, int offset) {
public static String getAddTagToFavoritesLink(String uri, String tag, String author, String visible, String viewPost,
String viewThread, String offset) {
//protected String getAddToGroupLink(User user, Hash author, String group, String uri, String visible,
// String viewPost, String viewThread, String offset, String tags, String filteredAuthor) {
StringBuffer buf = new StringBuffer(64);
buf.append(uri);
buf.append('?');
if (!empty(visible))
buf.append(PARAM_VISIBLE).append('=').append(visible).append('&');
buf.append(PARAM_ADD_TAG).append('=').append(sanitizeTagParam(tag)).append('&');
if (!empty(viewPost))
buf.append(PARAM_VIEW_POST).append('=').append(viewPost).append('&');
else if (!empty(viewThread))
buf.append(PARAM_VIEW_THREAD).append('=').append(viewThread).append('&');
if (!empty(offset))
buf.append(PARAM_OFFSET).append('=').append(offset).append('&');
if (!empty(author))
buf.append(PARAM_AUTHOR).append('=').append(author).append('&');
BaseServlet.addAuthActionParams(buf);
return buf.toString();
}
public static String getNavLink(String uri, String viewPost, String viewThread, String tags, String author, boolean authorOnly, int offset) {
StringBuffer buf = new StringBuffer(64);
buf.append(uri);
buf.append('?');
@@ -76,8 +109,11 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
if (!empty(tags))
buf.append(PARAM_TAGS).append('=').append(tags).append('&');
if (!empty(author))
if (!empty(author)) {
buf.append(PARAM_AUTHOR).append('=').append(author).append('&');
if (authorOnly)
buf.append(PARAM_THREAD_AUTHOR).append("=true&");
}
buf.append(PARAM_OFFSET).append('=').append(offset).append('&');
@@ -85,7 +121,7 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
}
public static String getViewPostLink(String uri, ThreadNode node, User user, boolean isPermalink,
String offset, String tags, String author) {
String offset, String tags, String author, boolean authorOnly) {
StringBuffer buf = new StringBuffer(64);
buf.append(uri);
if (node.getChildCount() > 0) {
@@ -107,13 +143,42 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
buf.append(PARAM_OFFSET).append('=').append(offset).append('&');
if (!empty(tags))
buf.append(PARAM_TAGS).append('=').append(tags).append('&');
if (!empty(author))
buf.append(PARAM_AUTHOR).append('=').append(author).append('&');
}
if (authorOnly && !empty(author)) {
buf.append(PARAM_AUTHOR).append('=').append(author).append('&');
buf.append(PARAM_THREAD_AUTHOR).append("=true&");
} else if (!isPermalink && !empty(author))
buf.append(PARAM_AUTHOR).append('=').append(author).append('&');
return buf.toString();
}
public static String getViewPostLink(String uri, BlogURI post, User user, boolean isPermalink,
String offset, String tags, String author, boolean authorOnly) {
StringBuffer buf = new StringBuffer(64);
buf.append(uri);
buf.append('?').append(PARAM_VISIBLE).append('=');
buf.append(post.getKeyHash().toBase64()).append('/');
buf.append(post.getEntryId()).append('&');
buf.append(PARAM_VIEW_POST).append('=');
buf.append(post.getKeyHash().toBase64()).append('/');
buf.append(post.getEntryId()).append('&');
if (!isPermalink) {
if (!empty(offset))
buf.append(PARAM_OFFSET).append('=').append(offset).append('&');
if (!empty(tags))
buf.append(PARAM_TAGS).append('=').append(tags).append('&');
if (!empty(author)) {
buf.append(PARAM_AUTHOR).append('=').append(author).append('&');
if (authorOnly)
buf.append(PARAM_THREAD_AUTHOR).append("=true&");
}
}
return buf.toString();
}
private static final boolean empty(String val) { return (val == null) || (val.trim().length() <= 0); }
@@ -122,9 +187,14 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
*/
public void render(User user, Writer out, Archive archive, BlogURI post,
boolean inlineReply, ThreadIndex index, String baseURI, String replyHiddenFields,
String offset, String requestTags, String filteredAuthor) throws IOException {
String offset, String requestTags, String filteredAuthor, boolean authorOnly) throws IOException {
EntryContainer entry = archive.getEntry(post);
if (entry == null) return;
ThreadNode node = index.getNode(post);
if (node == null) {
_log.error("Post is not in the index: " + post.toString());
return;
}
_entry = entry;
_baseURI = baseURI;
@@ -171,8 +241,6 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
if ( (author == null) || (author.trim().length() <= 0) )
author = post.getKeyHash().toBase64().substring(0,6);
ThreadNode node = index.getNode(post);
out.write(author);
out.write("</a> @ ");
out.write(getEntryDate(post.getEntryId()));
@@ -189,15 +257,25 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
out.write("'\">");
out.write(" " + tag);
out.write("</a>\n");
if (user.getAuthenticated() && (!user.getFavoriteTags().contains(tag)) && (!"[none]".equals(tag)) ) {
out.write("<a href=\"");
String cur = node.getEntry().getKeyHash().toBase64() + '/' + node.getEntry().getEntryId();
out.write(getAddTagToFavoritesLink(baseURI, tag, filteredAuthor, cur, null, cur, offset));
out.write("\" title=\"Add the tag '");
out.write(tag);
out.write("' to your favorites list\">");
out.write("<img src=\"images/addToFavorites.png\" alt=\":)\" border=\"0\" />");
out.write("</a>\n");
}
}
}
out.write("\n<a href=\"");
out.write(getViewPostLink(baseURI, node, user, true, offset, requestTags, filteredAuthor));
out.write(getViewPostLink(baseURI, node, user, true, offset, requestTags, filteredAuthor, authorOnly));
out.write("\" title=\"Select a shareable link directly to this post\">permalink</a>\n");
if (!inlineReply) {
if (true || (!inlineReply) ) {
String refuseReply = (String)_headers.get(HEADER_REFUSE_REPLIES);
boolean allowReply = false;
if ( (refuseReply != null) && (Boolean.valueOf(refuseReply).booleanValue()) ) {
@@ -273,9 +351,11 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
out.write("<tr class=\"postReplyOptions\">\n");
out.write(" <td colspan=\"3\">\n");
out.write(" <input type=\"submit\" value=\"Preview...\" name=\"Post\" />\n");
out.write(" Tags: <input type=\"text\" size=\"10\" name=\"" + PostServlet.PARAM_TAGS + "\" />\n");
out.write(" in a new thread? <input type=\"checkbox\" name=\"" + PostServlet.PARAM_IN_NEW_THREAD + "\" value=\"true\" />\n");
out.write(" refuse replies? <input type=\"checkbox\" name=\"" + PostServlet.PARAM_REFUSE_REPLIES + "\" value=\"true\" />\n");
out.write(" Tags: ");
BaseServlet.writeTagField(_user, "", out, "Optional tags to categorize your response", "No tags", false);
// <input type=\"text\" size=\"10\" name=\"" + PostServlet.PARAM_TAGS + "\" title=\"Optional tags to categorize your response\" />\n");
out.write(" in a new thread? <input type=\"checkbox\" name=\"" + PostServlet.PARAM_IN_NEW_THREAD + "\" value=\"true\" title=\"If true, this will fork a new top level thread\" />\n");
out.write(" refuse replies? <input type=\"checkbox\" name=\"" + PostServlet.PARAM_REFUSE_REPLIES + "\" value=\"true\" title=\"If true, only you will be able to reply to the post\" />\n");
out.write(" attachment: <input type=\"file\" name=\"entryfile0\" />\n");
out.write(" </td>\n</tr>\n</form>\n");
out.write("<!-- body reply end -->\n");
@@ -312,10 +392,10 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
for (int i = 0; i < _entry.getAttachments().length; i++) {
_postBodyBuffer.append("<option value=\"").append(i).append("\">");
Attachment a = _entry.getAttachments()[i];
_postBodyBuffer.append(sanitizeString(a.getName()));
_postBodyBuffer.append(sanitizeString(a.getName(), 30));
if ( (a.getDescription() != null) && (a.getDescription().trim().length() > 0) ) {
_postBodyBuffer.append(": ");
_postBodyBuffer.append(sanitizeString(a.getDescription()));
_postBodyBuffer.append(sanitizeString(a.getDescription(), 30));
}
_postBodyBuffer.append(" (").append(a.getDataLength()/1024).append("KB");
_postBodyBuffer.append(", type ").append(sanitizeString(a.getMimeType())).append(")</option>\n");
@@ -332,7 +412,7 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
boolean expanded = (_user != null ? _user.getShowExpanded() : false);
boolean images = (_user != null ? _user.getShowImages() : false);
_postBodyBuffer.append(getPageURL(new Hash(Base64.decode(b.hash)), b.tag, b.entryId, -1, -1, expanded, images));
_postBodyBuffer.append("\">").append(sanitizeString(b.name)).append("</a> ");
_postBodyBuffer.append("\">").append(sanitizeString(b.name, 30)).append("</a> ");
}
_postBodyBuffer.append("<br />\n");
}
@@ -341,12 +421,14 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
_postBodyBuffer.append(getSpan("summDetailExternal")).append("External links:</span> ");
for (int i = 0; i < _links.size(); i++) {
Link l = (Link)_links.get(i);
_postBodyBuffer.append("<a ").append(getClass("summDetailExternalLink")).append(" href=\"externallink.jsp?");
String schema = l.schema;
_postBodyBuffer.append("<a ");
_postBodyBuffer.append(getClass("summDetailExternalLink")).append(" href=\"externallink.jsp?");
if (l.schema != null)
_postBodyBuffer.append("schema=").append(sanitizeURL(l.schema)).append('&');
_postBodyBuffer.append("schema=").append(sanitizeURL(l.schema)).append('&');
if (l.location != null)
_postBodyBuffer.append("location=").append(sanitizeURL(l.location)).append('&');
_postBodyBuffer.append("\">").append(sanitizeString(l.location, 60));
_postBodyBuffer.append("location=").append(sanitizeURL(l.location)).append('&');
_postBodyBuffer.append("\">").append(sanitizeString(l.location, 30));
_postBodyBuffer.append(getSpan("summDetailExternalNet")).append(" (").append(sanitizeString(l.schema)).append(")</span></a> ");
}
_postBodyBuffer.append("<br />\n");
@@ -373,7 +455,7 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
_postBodyBuffer.append(AddressesServlet.PARAM_NAME).append("=").append(sanitizeTagParam(a.name)).append('&');
if (a.protocol != null)
_postBodyBuffer.append(AddressesServlet.PARAM_PROTO).append("=").append(sanitizeTagParam(a.protocol)).append('&');
_postBodyBuffer.append("\">").append(sanitizeString(a.name)).append("</a>");
_postBodyBuffer.append("\">").append(sanitizeString(a.name, 30)).append("</a>");
}
}
_postBodyBuffer.append("<br />\n");
@@ -424,8 +506,12 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
String inReplyTo = (String)_headers.get(HEADER_IN_REPLY_TO);
if ( (inReplyTo != null) && (inReplyTo.trim().length() > 0) ) {
BlogURI replyURI = new BlogURI(inReplyTo);
if (replyURI.getEntryId() > 0)
_postBodyBuffer.append(" <a ").append(getClass("summDetailParent")).append(" href=\"").append(getPageURL(replyURI.getKeyHash(), null, replyURI.getEntryId(), 0, 0, true, true)).append("\">(view parent)</a><br />\n");
if (replyURI.getEntryId() > 0) {
_postBodyBuffer.append(" <a ").append(getClass("summDetailParent"));
_postBodyBuffer.append(" href=\"");
_postBodyBuffer.append(getPageURL(replyURI.getKeyHash(), null, replyURI.getEntryId(), 0, 0, true, true));
_postBodyBuffer.append("\">(view parent)</a><br />\n");
}
}
_postBodyBuffer.append(" </td>\n");
@@ -465,12 +551,18 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
public String getPageURL(Hash blog, String tag, long entryId, String group, int numPerPage, int pageNum, boolean expandEntries, boolean showImages) {
StringBuffer buf = new StringBuffer(128);
buf.append(_baseURI).append('?');
String entry = null;
if ( (blog != null) && (entryId > 0) ) {
buf.append(PARAM_VIEW_POST).append('=').append(Base64.encode(blog.getData())).append('/').append(entryId).append('&');
entry = blog.toBase64() + '/' + entryId;
buf.append(PARAM_VIEW_THREAD).append('=').append(entry).append('&');
buf.append(PARAM_VISIBLE).append('=').append(Base64.encode(blog.getData())).append('/').append(entryId).append('&');
} else if (blog != null) {
buf.append(PARAM_AUTHOR).append('=').append(blog.toBase64()).append('&');
}
if (tag != null)
buf.append(PARAM_TAGS).append('=').append(sanitizeTagParam(tag)).append('&');
if ( (blog != null) && (entryId > 0) )
buf.append("#blog://").append(entry);
return buf.toString();
}
}

View File

@@ -28,12 +28,14 @@ public class AddressesServlet extends BaseServlet {
public static final String PARAM_NET = "addrNet";
public static final String PARAM_PROTO = "addrProto";
public static final String PARAM_SYNDICATE = "addrSyndicate";
public static final String PARAM_TAG = "addrTag";
public static final String PARAM_ACTION = "action";
public static final String PROTO_BLOG = "syndieblog";
public static final String PROTO_ARCHIVE = "syndiearchive";
public static final String PROTO_I2PHEX = "i2phex";
public static final String PROTO_EEPSITE = "eep";
public static final String PROTO_TAG = "syndietag";
public static final String NET_SYNDIE = "syndie";
public static final String NET_I2P = "i2p";
@@ -57,6 +59,10 @@ public class AddressesServlet extends BaseServlet {
public static final String ACTION_UPDATE_EEPSITE = "Update eepsite";
public static final String ACTION_ADD_EEPSITE = "Add eepsite";
public static final String ACTION_DELETE_TAG = "Delete tag";
public static final String ACTION_UPDATE_TAG = "Update tag";
public static final String ACTION_ADD_TAG = "Add tag";
public static final String ACTION_DELETE_OTHER = "Delete address";
public static final String ACTION_UPDATE_OTHER = "Update address";
public static final String ACTION_ADD_OTHER = "Add other address";
@@ -75,6 +81,9 @@ public class AddressesServlet extends BaseServlet {
pn = buildNewName(req, PROTO_ARCHIVE);
_log.debug("pn for protoArchive [" + req.getParameter(PARAM_PROTO) + "]: " + pn);
renderArchives(user, db, uri, pn, out);
pn = buildNewName(req, PROTO_TAG);
_log.debug("pn for protoTag [" + req.getParameter(PARAM_TAG) + "]: " + pn);
renderTags(user, db, uri, pn, out);
pn = buildNewName(req, PROTO_I2PHEX);
_log.debug("pn for protoPhex [" + req.getParameter(PARAM_PROTO) + "]: " + pn);
renderI2Phex(user, db, uri, pn, out);
@@ -103,18 +112,25 @@ public class AddressesServlet extends BaseServlet {
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_SYNDIE + "\" />");
writeAuthActionFields(out);
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" value=\"" + pn.getName() + "\" />" + pn.getName() + " ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + pn.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" value=\"" + pn.getName()
+ "\" title=\"Short, locally unique 'pet name' for the author\" />" + pn.getName() + " ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + pn.getLocation()
+ "\" title=\"Blog hash for the author\" /> ");
if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE))
out.write("Favorite? <input type=\"checkbox\" name=\"" + PARAM_FAVORITE + "\" checked=\"true\" value=\"true\" /> ");
out.write("Favorite? <input type=\"checkbox\" name=\"" + PARAM_FAVORITE
+ "\" checked=\"true\" value=\"true\" title=\"If true, their posts are highlighted\" /> ");
else
out.write("Favorite? <input type=\"checkbox\" name=\"" + PARAM_FAVORITE + "\" value=\"true\" /> ");
out.write("Favorite? <input type=\"checkbox\" name=\"" + PARAM_FAVORITE
+ "\" value=\"true\" title=\"If true, their posts are highlighted\" /> ");
if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) {
out.write("Ignored? <input type=\"checkbox\" name=\"" + PARAM_IGNORE + "\" checked=\"true\" value=\"true\" /> ");
out.write("Ignored? <input type=\"checkbox\" name=\"" + PARAM_IGNORE
+ "\" checked=\"true\" value=\"true\" title=\"If true, their threads are hidden\" /> ");
} else {
out.write("Ignored? <input type=\"checkbox\" name=\"" + PARAM_IGNORE + "\" value=\"true\" /> ");
out.write("Ignored? <input type=\"checkbox\" name=\"" + PARAM_IGNORE
+ "\" value=\"true\" title=\"If true, their threads are hidden\" /> ");
out.write("<a href=\"" + getControlTarget() + "?" + ThreadedHTMLRenderer.PARAM_AUTHOR + '='
+ pn.getLocation() + "\" title=\"View threads by the given author\">View posts</a> ");
}
@@ -130,18 +146,25 @@ public class AddressesServlet extends BaseServlet {
out.write("<input type=\"hidden\" name=\"" + PARAM_PROTO + "\" value=\"" + PROTO_BLOG + "\" />");
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_SYNDIE + "\" />");
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName() + "\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + newName.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName()
+ "\" title=\"Short, locally unique 'pet name' for the author\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + newName.getLocation()
+ "\" title=\"Blog hash for the author\" /> ");
if (newName.isMember(FilteredThreadIndex.GROUP_FAVORITE))
out.write("Favorite? <input type=\"checkbox\" name=\"" + PARAM_FAVORITE + "\" checked=\"true\" value=\"true\" /> ");
out.write("Favorite? <input type=\"checkbox\" name=\"" + PARAM_FAVORITE
+ "\" checked=\"true\" value=\"true\" title=\"If true, their posts are highlighted\" /> ");
else
out.write("Favorite? <input type=\"checkbox\" name=\"" + PARAM_FAVORITE + "\" value=\"true\" /> ");
out.write("Favorite? <input type=\"checkbox\" name=\"" + PARAM_FAVORITE
+ "\" value=\"true\" title=\"If true, their posts are highlighted\" /> ");
if (newName.isMember(FilteredThreadIndex.GROUP_IGNORE)) {
out.write("Ignored? <input type=\"checkbox\" name=\"" + PARAM_IGNORE + "\" checked=\"true\" value=\"true\" /> ");
out.write("Ignored? <input type=\"checkbox\" name=\"" + PARAM_IGNORE
+ "\" checked=\"true\" value=\"true\" title=\"If true, their threads are hidden\" /> ");
} else {
out.write("Ignored? <input type=\"checkbox\" name=\"" + PARAM_IGNORE + "\" value=\"true\" /> ");
out.write("Ignored? <input type=\"checkbox\" name=\"" + PARAM_IGNORE
+ "\" value=\"true\" title=\"If true, their threads are hidden\" /> ");
}
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_ADD_BLOG + "\" /> ");
@@ -167,18 +190,24 @@ public class AddressesServlet extends BaseServlet {
out.write("<input type=\"hidden\" name=\"" + PARAM_PROTO + "\" value=\"" + PROTO_ARCHIVE + "\" />");
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_SYNDIE + "\" />");
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + pn.getName() + "\" />" + pn.getName() + " ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"20\" value=\"" + pn.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + pn.getName()
+ "\" title=\"Short, locally unique 'pet name' for the remote archive\" />" + pn.getName() + " ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"20\" value=\"" + pn.getLocation()
+ "\" title=\"URL to the remote archive's archive/archive.txt\" /> ");
if (BlogManager.instance().authorizeRemote(user)) {
if (BlogManager.instance().syndicationScheduled(pn.getLocation()))
out.write("Syndicate? <input type=\"checkbox\" name=\"" + PARAM_SYNDICATE + "\" checked=\"true\" value=\"true\" />");
out.write("Syndicate? <input type=\"checkbox\" name=\"" + PARAM_SYNDICATE
+ "\" checked=\"true\" value=\"true\" title=\"If true, periodically pull down posts they have\" />");
else
out.write("Syndicate? <input type=\"checkbox\" name=\"" + PARAM_SYNDICATE + "\" value=\"true\" />");
out.write("Syndicate? <input type=\"checkbox\" name=\"" + PARAM_SYNDICATE
+ "\" value=\"true\" title=\"If true, periodically pull down posts they have\" />");
out.write("<a href=\"" + getSyndicateLink(user, pn.getLocation())
+ "\" title=\"Synchronize manually with the peer\">Sync manually</a> ");
} else {
out.write("You are not <a href=\"admin.jsp\">authorized</a> to syndicate with the archive ");
}
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_DELETE_ARCHIVE + "\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_UPDATE_ARCHIVE + "\" /> ");
@@ -191,14 +220,19 @@ public class AddressesServlet extends BaseServlet {
out.write("<input type=\"hidden\" name=\"" + PARAM_PROTO + "\" value=\"" + PROTO_ARCHIVE + "\" />");
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_SYNDIE + "\" />");
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName() + "\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"20\" value=\"" + newName.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName()
+ "\" title=\"Short, locally unique 'pet name' for the remote archive\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"20\" value=\"" + newName.getLocation()
+ "\" title=\"URL to the remote archive's archive/archive.txt\" /> ");
if (BlogManager.instance().authorizeRemote(user)) {
if (BlogManager.instance().syndicationScheduled(newName.getLocation()))
out.write("Syndicate? <input type=\"checkbox\" name=\"" + PARAM_SYNDICATE + "\" checked=\"true\" value=\"true\" />");
out.write("Syndicate? <input type=\"checkbox\" name=\"" + PARAM_SYNDICATE
+ "\" checked=\"true\" value=\"true\" title=\"If true, periodically pull down posts they have\" />");
else
out.write("Syndicate? <input type=\"checkbox\" name=\"" + PARAM_SYNDICATE + "\" value=\"true\" />");
out.write("Syndicate? <input type=\"checkbox\" name=\"" + PARAM_SYNDICATE
+ "\" value=\"true\" title=\"If true, periodically pull down posts they have\" />");
}
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_ADD_ARCHIVE + "\" /> ");
@@ -225,9 +259,12 @@ public class AddressesServlet extends BaseServlet {
out.write("<input type=\"hidden\" name=\"" + PARAM_PROTO + "\" value=\"" + PROTO_I2PHEX + "\" />");
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_I2P + "\" />");
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" value=\"" + pn.getName() + "\" />" + pn.getName() + " ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + pn.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" value=\"" + pn.getName()
+ "\" title=\"Short, locally unique 'pet name' for the I2Phex peer\" />" + pn.getName() + " ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + pn.getLocation()
+ "\" title=\"I2P destination of the I2Phex peer\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_DELETE_PEER + "\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_UPDATE_PEER + "\" /> ");
@@ -240,9 +277,12 @@ public class AddressesServlet extends BaseServlet {
out.write("<input type=\"hidden\" name=\"" + PARAM_PROTO + "\" value=\"" + PROTO_I2PHEX + "\" />");
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_I2P + "\" />");
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName() + "\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + newName.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName()
+ "\" title=\"Short, locally unique 'pet name' for the I2Phex peer\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + newName.getLocation()
+ "\" title=\"I2P destination of the I2Phex peer\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_ADD_PEER + "\" /> ");
out.write("</td></tr>\n");
@@ -267,9 +307,12 @@ public class AddressesServlet extends BaseServlet {
out.write("<input type=\"hidden\" name=\"" + PARAM_PROTO + "\" value=\"" + PROTO_EEPSITE + "\" />");
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_I2P + "\" />");
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" value=\"" + pn.getName() + "\" />" + pn.getName() + " ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + pn.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" value=\"" + pn.getName()
+ "\" title=\"Short, locally unique 'pet name' for the eepsite\" />" + pn.getName() + " ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + pn.getLocation()
+ "\" title=\"I2P destination of the eepsite\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_DELETE_EEPSITE + "\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_UPDATE_EEPSITE + "\" /> ");
@@ -282,9 +325,12 @@ public class AddressesServlet extends BaseServlet {
out.write("<input type=\"hidden\" name=\"" + PARAM_PROTO + "\" value=\"" + PROTO_EEPSITE + "\" />");
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_I2P + "\" />");
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName() + "\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + newName.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName()
+ "\" title=\"Short, locally unique 'pet name' for the eepsite\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + newName.getLocation()
+ "\" title=\"I2P destination of the eepsite\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_ADD_EEPSITE + "\" /> ");
out.write("</td></tr>\n");
@@ -292,6 +338,53 @@ public class AddressesServlet extends BaseServlet {
out.write("<tr><td colspan=\"3\"><hr /></td></tr>\n");
}
private void renderTags(User user, PetNameDB db, String baseURI, PetName newName, PrintWriter out) throws IOException {
TreeSet names = new TreeSet();
for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) {
String name = (String)iter.next();
PetName pn = db.getByName(name);
if (PROTO_TAG.equals(pn.getProtocol()))
names.add(name);
}
out.write("<tr><td colspan=\"3\"><b>Favorite tags</b></td></tr>\n");
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
PetName pn = db.getByName((String)iter.next());
out.write("<form action=\"" + baseURI + "\" method=\"POST\">");
writeAuthActionFields(out);
out.write("<input type=\"hidden\" name=\"" + PARAM_PROTO + "\" value=\"" + PROTO_TAG + "\" />");
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_SYNDIE + "\" />");
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" value=\"" + pn.getName()
+ "\" />" + pn.getName() + " ");
out.write("<input type=\"hidden\" name=\"" + PARAM_LOC + "\" value=\"" + pn.getLocation()
+ "\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_DELETE_TAG + "\" /> ");
out.write("</td></tr>\n");
out.write("</form>\n");
}
out.write("<form action=\"" + baseURI + "\" method=\"POST\">");
writeAuthActionFields(out);
out.write("<input type=\"hidden\" name=\"" + PARAM_PROTO + "\" value=\"" + PROTO_TAG + "\" />");
out.write("<input type=\"hidden\" name=\"" + PARAM_NET + "\" value=\"" + NET_SYNDIE + "\" />");
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName()
+ "\" title=\"Tag (or group of tags)\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_ADD_TAG + "\" /> ");
out.write("</td></tr>\n");
out.write("</form>\n");
out.write("<tr><td colspan=\"3\"><hr /></td></tr>\n");
}
private void renderOther(User user, PetNameDB db, String baseURI, PetName newName, PrintWriter out) throws IOException {
TreeSet names = new TreeSet();
for (Iterator iter = db.getNames().iterator(); iter.hasNext(); ) {
@@ -307,11 +400,16 @@ public class AddressesServlet extends BaseServlet {
out.write("<form action=\"" + baseURI + "\" method=\"POST\">");
writeAuthActionFields(out);
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Network: <input type=\"text\" name=\"" + PARAM_NET + "\" value=\"" + pn.getNetwork() + "\" /> ");
out.write("Protocol: <input type=\"text\" name=\"" + PARAM_PROTO + "\" value=\"" + pn.getProtocol() + "\" /> ");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" value=\"" + pn.getName() + "\" />" + pn.getName() +" ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + pn.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (pn.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Network: <input type=\"text\" name=\"" + PARAM_NET + "\" value=\"" + pn.getNetwork()
+ "\" title=\"What network is this on - i2p, tor, internet, freenet, etc\" /> ");
out.write("Protocol: <input type=\"text\" name=\"" + PARAM_PROTO + "\" value=\"" + pn.getProtocol()
+ "\" title=\"How do we access/interact with this resource\" /> ");
out.write("Name: <input type=\"hidden\" name=\"" + PARAM_NAME + "\" value=\"" + pn.getName()
+ "\" title=\"Short, locally unique 'pet name' for the location\" />" + pn.getName() +" ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + pn.getLocation()
+ "\" title=\"URL\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_DELETE_OTHER + "\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_UPDATE_OTHER + "\" /> ");
@@ -323,11 +421,16 @@ public class AddressesServlet extends BaseServlet {
writeAuthActionFields(out);
out.write("<tr><td colspan=\"3\">");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "") + " />\n");
out.write("Network: <input type=\"text\" name=\"" + PARAM_NET + "\" value=\"" + newName.getNetwork() + "\" /> ");
out.write("Protocol: <input type=\"text\" name=\"" + PARAM_PROTO + "\" value=\"" + newName.getProtocol() + "\" /> ");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName() + "\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + newName.getLocation() + "\" /> ");
out.write("<input type=\"checkbox\" name=\"" + PARAM_IS_PUBLIC + "\" value=\"true\" " + (newName.getIsPublic() ? " checked=\"true\" " : "")
+ " title=\"If checked, this name can be shared with one click when posting\" />\n");
out.write("Network: <input type=\"text\" name=\"" + PARAM_NET + "\" value=\"" + newName.getNetwork()
+ "\" title=\"What network is this on - i2p, tor, internet, freenet, etc\" /> ");
out.write("Protocol: <input type=\"text\" name=\"" + PARAM_PROTO + "\" value=\"" + newName.getProtocol()
+ "\" title=\"How do we access/interact with this resource\" /> ");
out.write("Name: <input type=\"text\" name=\"" + PARAM_NAME + "\" size=\"10\" value=\"" + newName.getName()
+ "\" title=\"Short, locally unique 'pet name' for the location\" /> ");
out.write("Location: <input type=\"text\" name=\"" + PARAM_LOC + "\" size=\"3\" value=\"" + newName.getLocation()
+ "\" title=\"URL\" /> ");
out.write("<input type=\"submit\" name=\"" + PARAM_ACTION + "\" value=\"" + ACTION_ADD_OTHER + "\" /> ");
out.write("</td></tr>\n");
@@ -382,6 +485,7 @@ public class AddressesServlet extends BaseServlet {
if (PROTO_ARCHIVE.equals(reqProto) ||
PROTO_BLOG.equals(reqProto) ||
PROTO_EEPSITE.equals(reqProto) ||
PROTO_TAG.equals(reqProto) ||
PROTO_I2PHEX.equals(reqProto))
return false;
else // its something other than the four default types

View File

@@ -196,13 +196,20 @@ public class ArchiveServlet extends HttpServlet {
}
private void dump(File source, HttpServletResponse resp) throws ServletException, IOException {
FileInputStream in = new FileInputStream(source);
OutputStream out = resp.getOutputStream();
byte buf[] = new byte[1024];
int read = 0;
while ( (read = in.read(buf)) != -1)
out.write(buf, 0, read);
out.close();
in.close();
FileInputStream in = null;
OutputStream out = null;
try {
in = new FileInputStream(source);
out = resp.getOutputStream();
byte buf[] = new byte[1024];
int read = 0;
while ( (read = in.read(buf)) != -1)
out.write(buf, 0, read);
out.close();
in.close();
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
if (out != null) try { out.close(); } catch (IOException ioe) {}
}
}
}

View File

@@ -14,6 +14,7 @@ import net.i2p.data.*;
import net.i2p.syndie.*;
import net.i2p.syndie.data.*;
import net.i2p.syndie.sml.*;
import net.i2p.util.FileUtil;
import net.i2p.util.Log;
/**
@@ -63,19 +64,21 @@ public abstract class BaseServlet extends HttpServlet {
* key=value& of params that need to be tacked onto an http request that updates data, to
* prevent spoofing
*/
protected String getAuthActionParams() { return PARAM_AUTH_ACTION + '=' + _authNonce + '&'; }
protected static String getAuthActionParams() { return PARAM_AUTH_ACTION + '=' + _authNonce + '&'; }
/**
* key=value& of params that need to be tacked onto an http request that updates data, to
* prevent spoofing
*/
protected void addAuthActionParams(StringBuffer buf) {
public static void addAuthActionParams(StringBuffer buf) {
buf.append(PARAM_AUTH_ACTION).append('=').append(_authNonce).append('&');
}
public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
resp.setContentType("text/html;charset=UTF-8");
resp.setHeader("cache-control", "no-cache");
resp.setHeader("pragma", "no-cache");
User user = (User)req.getSession().getAttribute("user");
String login = req.getParameter("login");
@@ -136,6 +139,7 @@ public abstract class BaseServlet extends HttpServlet {
forceNewIndex = handleAddressbook(user, req) || forceNewIndex;
forceNewIndex = handleBookmarking(user, req) || forceNewIndex;
forceNewIndex = handleManageTags(user, req) || forceNewIndex;
handleUpdateProfile(user, req);
}
@@ -148,17 +152,28 @@ public abstract class BaseServlet extends HttpServlet {
FilteredThreadIndex index = (FilteredThreadIndex)req.getSession().getAttribute("threadIndex");
boolean authorOnly = Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue();
if ( (index != null) && (authorOnly != index.getFilterAuthorsByRoot()) )
forceNewIndex = true;
Collection tags = getFilteredTags(req);
Collection filteredAuthors = getFilteredAuthors(req);
boolean tagsChanged = ( (index != null) && (!index.getFilteredTags().equals(tags)) );
boolean authorsChanged = ( (index != null) && (!index.getFilteredAuthors().equals(filteredAuthors)) );
if (_log.shouldLog(Log.DEBUG))
_log.debug("authorOnly=" + authorOnly + " forceNewIndex? " + forceNewIndex + " authors=" + filteredAuthors);
if (forceNewIndex || (index == null) || (tagsChanged) || (authorsChanged) ) {
index = new FilteredThreadIndex(user, BlogManager.instance().getArchive(), getFilteredTags(req), filteredAuthors);
index = new FilteredThreadIndex(user, BlogManager.instance().getArchive(), getFilteredTags(req), filteredAuthors, authorOnly);
req.getSession().setAttribute("threadIndex", index);
if (_log.shouldLog(Log.INFO))
_log.info("New filtered index created (forced? " + forceNewIndex + ", tagsChanged? " + tagsChanged + ", authorsChanged? " + authorsChanged + ")");
}
render(user, req, resp, index);
}
protected void render(User user, HttpServletRequest req, HttpServletResponse resp, ThreadIndex index) throws IOException, ServletException {
render(user, req, resp.getWriter(), index);
}
@@ -237,6 +252,35 @@ public abstract class BaseServlet extends HttpServlet {
return rv;
}
private boolean handleManageTags(User user, HttpServletRequest req) {
if (!user.getAuthenticated())
return false;
boolean rv = false;
String tag = req.getParameter(ThreadedHTMLRenderer.PARAM_ADD_TAG);
if ( (tag != null) && (tag.trim().length() > 0) ) {
tag = HTMLRenderer.sanitizeString(tag, false);
String name = tag;
PetNameDB db = user.getPetNameDB();
PetName pn = db.getByLocation(tag);
if (pn == null) {
if (db.containsName(name)) {
int i = 0;
while (db.containsName(name + i))
i++;
name = tag + i;
}
pn = new PetName(name, AddressesServlet.NET_SYNDIE, AddressesServlet.PROTO_TAG, tag);
db.add(pn);
BlogManager.instance().saveUser(user);
}
}
return false;
}
private boolean handleAddressbook(User user, HttpServletRequest req) {
if ( (!user.getAuthenticated()) || (empty(AddressesServlet.PARAM_ACTION)) ) {
return false;
@@ -244,7 +288,15 @@ public abstract class BaseServlet extends HttpServlet {
String action = req.getParameter(AddressesServlet.PARAM_ACTION);
if ( (AddressesServlet.ACTION_ADD_ARCHIVE.equals(action)) ||
if (AddressesServlet.ACTION_ADD_TAG.equals(action)) {
String name = req.getParameter(AddressesServlet.PARAM_NAME);
if (!user.getPetNameDB().containsName(name)) {
PetName pn = new PetName(name, AddressesServlet.NET_SYNDIE, AddressesServlet.PROTO_TAG, name);
user.getPetNameDB().add(pn);
BlogManager.instance().saveUser(user);
}
return false;
} else if ( (AddressesServlet.ACTION_ADD_ARCHIVE.equals(action)) ||
(AddressesServlet.ACTION_ADD_BLOG.equals(action)) ||
(AddressesServlet.ACTION_ADD_EEPSITE.equals(action)) ||
(AddressesServlet.ACTION_ADD_OTHER.equals(action)) ||
@@ -276,8 +328,10 @@ public abstract class BaseServlet extends HttpServlet {
(AddressesServlet.ACTION_DELETE_BLOG.equals(action)) ||
(AddressesServlet.ACTION_DELETE_EEPSITE.equals(action)) ||
(AddressesServlet.ACTION_DELETE_OTHER.equals(action)) ||
(AddressesServlet.ACTION_DELETE_TAG.equals(action)) ||
(AddressesServlet.ACTION_DELETE_PEER.equals(action)) ) {
PetName pn = user.getPetNameDB().getByName(req.getParameter(AddressesServlet.PARAM_NAME));
String name = req.getParameter(AddressesServlet.PARAM_NAME);
PetName pn = user.getPetNameDB().getByName(name);
if (pn != null) {
user.getPetNameDB().remove(pn);
BlogManager.instance().saveUser(user);
@@ -414,7 +468,18 @@ public abstract class BaseServlet extends HttpServlet {
if ( (pass0 != null) && (pass1 != null) && (pass0.equals(pass1)) ) {
BlogManager.instance().changePasswrd(user, oldPass, pass0, pass1);
}
if (user.getAuthenticated() && !BlogManager.instance().authorizeRemote(user)) {
String adminPass = req.getParameter("adminPass");
if (adminPass != null) {
boolean authorized = BlogManager.instance().authorizeRemote(adminPass);
if (authorized) {
user.setAllowAccessRemote(authorized);
BlogManager.instance().saveUser(user);
}
}
}
boolean updated = BlogManager.instance().updateMetadata(user, user.getBlog(), opts);
}
@@ -488,24 +553,47 @@ public abstract class BaseServlet extends HttpServlet {
}
protected void renderBegin(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
out.write("<html>\n<head><title>" + getTitle() + "</title>\n" + BEGIN_HTML);
out.write("<html>\n<head><title>" + getTitle() + "</title>\n");
out.write("<meta http-equiv=\"cache-control\" content=\"no-cache\" />");
out.write("<meta http-equiv=\"pragma\" content=\"no-cache\" />");
out.write("<style>");
out.write(STYLE_HTML);
Reader css = null;
try {
InputStream in = req.getSession().getServletContext().getResourceAsStream("/syndie.css");
if (in != null) {
css = new InputStreamReader(in, "UTF-8");
char buf[] = new char[1024];
int read = 0;
while ( (read = css.read(buf)) != -1)
out.write(buf, 0, read);
}
} finally {
if (css != null)
css.close();
}
String content = FileUtil.readTextFile("./docs/syndie_standard.css", -1, true);
if (content != null) out.write(content);
out.write("</style>");
out.write(BEGIN_HTML);
}
protected void renderNavBar(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
//out.write("<tr class=\"topNav\"><td class=\"topNav_user\" colspan=\"2\" nowrap=\"true\">\n");
out.write("<tr class=\"topNav\"><td colspan=\"3\" nowrap=\"true\"><span class=\"topNav_user\">\n");
out.write("<!-- nav bar begin -->\n");
out.write("<a href=\"threads.jsp\" title=\"Syndie home\">Threads</a> <a href=\"blogs.jsp\" title=\"Blog summary\">Blogs</a> ");
if (user.getAuthenticated() && (user.getBlog() != null) ) {
out.write("Logged in as <a href=\"" + getProfileLink(req, user.getBlog()) + "\" title=\"Edit your profile\">");
out.write(user.getUsername());
out.write("</a>\n");
out.write("(<a href=\"switchuser.jsp\" title=\"Log in as another user\">switch</a>)\n");
out.write("<a href=\"" + getPostURI() + "\" title=\"Post a new thread\">Post a new thread</a>\n");
out.write("<a href=\"" + getPostURI() + "\" title=\"Post a new thread\">Post</a>\n");
out.write("<a href=\"addresses.jsp\" title=\"View your addressbook\">Addressbook</a>\n");
} else {
out.write("<form action=\"" + req.getRequestURI() + "\" method=\"POST\">\n");
writeAuthActionFields(out);
out.write("Login: <input type=\"text\" name=\"login\" />\n");
out.write("Password: <input type=\"password\" name=\"password\" />\n");
out.write("Login: <input type=\"text\" name=\"login\" title=\"Login name for your Syndie account\" />\n");
out.write("Password: <input type=\"password\" name=\"password\" title=\"Password to get into your Syndie account\" />\n");
out.write("<input type=\"submit\" name=\"action\" value=\"Login\" /></form>\n");
}
//out.write("</td><td class=\"topNav_admin\">\n");
@@ -531,10 +619,11 @@ public abstract class BaseServlet extends HttpServlet {
SKIP_TAGS.add("filter");
// post and visible are skipped since we aren't good at filtering by tag when the offset will
// skip around randomly. at least, not yet.
SKIP_TAGS.add("visible");
SKIP_TAGS.add(ThreadedHTMLRenderer.PARAM_VISIBLE);
//SKIP_TAGS.add("post");
//SKIP_TAGS.add("thread");
SKIP_TAGS.add("offset"); // if we are adjusting the filter, ignore the previous offset
SKIP_TAGS.add(ThreadedHTMLRenderer.PARAM_OFFSET); // if we are adjusting the filter, ignore the previous offset
SKIP_TAGS.add(ThreadedHTMLRenderer.PARAM_DAYS_BACK);
SKIP_TAGS.add("addLocation");
SKIP_TAGS.add("addGroup");
SKIP_TAGS.add("login");
@@ -575,6 +664,12 @@ public abstract class BaseServlet extends HttpServlet {
PetNameDB db = user.getPetNameDB();
TreeSet names = new TreeSet(db.getNames());
out.write("<option value=\"\">Any authors</option>\n");
if (user.getAuthenticated()) {
if ("favorites".equals(author))
out.write("<option selected=\"true\" value=\"favorites\">All recent posts by favorite authors</option>\n");
else
out.write("<option value=\"favorites\">All recent posts by favorite authors</option>\n");
}
if (user.getBlog() != null) {
if ( (author != null) && (author.equals(user.getBlog().toBase64())) )
out.write("<option value=\"" + user.getBlog().toBase64() + "\" selected=\"true\">Threads you posted in</option>\n");
@@ -585,7 +680,7 @@ public abstract class BaseServlet extends HttpServlet {
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
PetName pn = db.getByName(name);
if ("syndieblog".equals(pn.getProtocol())) {
if ("syndieblog".equals(pn.getProtocol()) && pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) {
if ( (author != null) && (author.equals(pn.getLocation())) )
out.write("<option value=\"" + pn.getLocation() + "\" selected=\"true\">Threads " + name + " posted in</option>\n");
else
@@ -594,14 +689,74 @@ public abstract class BaseServlet extends HttpServlet {
}
out.write("</select>\n");
out.write("Tags: <input type=\"text\" name=\"" + ThreadedHTMLRenderer.PARAM_TAGS + "\" size=\"10\" value=\"" + tags + "\" />\n");
out.write("Tags: ");
writeTagField(user, tags, out);
String days = req.getParameter(ThreadedHTMLRenderer.PARAM_DAYS_BACK);
if (days == null)
days = "";
out.write("Age: <input type=\"text\" name=\"" + ThreadedHTMLRenderer.PARAM_DAYS_BACK + "\" size=\"2\" value=\"" + days
+ "\" title=\"Posts are filtered to include only ones which were made within this many days ago\" /> days\n");
out.write("<input type=\"submit\" name=\"action\" value=\"Go\" />\n");
out.write("</td><td class=\"controlBarRight\" width=\"1%\"><a href=\"#threads\" title=\"Jump to the thread navigation\">Threads</a></td>\n");
out.write("</td><td class=\"controlBarRight\" width=\"1%\">");
if ( (req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST) != null) ||
(req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD) != null) )
out.write("<a href=\"#threads\" title=\"Jump to the thread navigation\">Threads</a>");
out.write("</td>\n");
out.write("<!-- control bar end -->\n");
out.write("</tr>\n");
out.write("</form>\n");
}
protected void writeTagField(User user, String selectedTags, PrintWriter out) throws IOException {
writeTagField(user, selectedTags, out, "Threads are filtered to include only ones with posts containing these tags", "Any tags - no filtering", true);
}
public static void writeTagField(User user, String selectedTags, Writer out, String title, String blankTitle, boolean includeFavoritesTag) throws IOException {
Set favoriteTags = new TreeSet(user.getFavoriteTags());
if (favoriteTags.size() <= 0) {
out.write("<input type=\"text\" name=\"" + ThreadedHTMLRenderer.PARAM_TAGS + "\" size=\"10\" value=\"" + selectedTags
+ "\" title=\"" + title + "\" />\n");
} else {
out.write("<select name=\"" + ThreadedHTMLRenderer.PARAM_TAGS
+ "\" title=\"" + title + "\">");
out.write("<option value=\"\">" + blankTitle + "</option>\n");
boolean matchFound = false;
if (includeFavoritesTag) {
out.write("<option value=\"");
StringBuffer combinedBuf = new StringBuffer();
for (Iterator iter = favoriteTags.iterator(); iter.hasNext(); ) {
String curFavTag = (String)iter.next();
combinedBuf.append(HTMLRenderer.sanitizeTagParam(curFavTag)).append(" ");
}
String combined = combinedBuf.toString();
if (selectedTags.equals(combined)) {
out.write(combined + "\" selected=\"true\" >All favorite tags</option>\n");
matchFound = true;
} else {
out.write(combined + "\" >All favorite tags</option>\n");
}
}
for (Iterator iter = favoriteTags.iterator(); iter.hasNext(); ) {
String curFavTag = (String)iter.next();
if (selectedTags.equals(curFavTag)) {
out.write("<option value=\"" + HTMLRenderer.sanitizeTagParam(curFavTag) + "\" selected=\"true\" >"
+ HTMLRenderer.sanitizeString(curFavTag) + "</option>\n");
matchFound = true;
} else {
out.write("<option value=\"" + HTMLRenderer.sanitizeTagParam(curFavTag) + "\">"
+ HTMLRenderer.sanitizeString(curFavTag) + "</option>\n");
}
}
if ( (!matchFound) && (selectedTags != null) && (selectedTags.trim().length() > 0) )
out.write("<option value=\"" + HTMLRenderer.sanitizeTagParam(selectedTags)
+ "\" selected=\"true\">" + HTMLRenderer.sanitizeString(selectedTags) + "</option>\n");
out.write("</select>\n");
}
}
protected abstract void renderServletDetails(User user, HttpServletRequest req, PrintWriter out,
ThreadIndex index, int threadOffset, BlogURI visibleEntry,
@@ -798,28 +953,41 @@ public abstract class BaseServlet extends HttpServlet {
return ThreadedHTMLRenderer.getViewPostLink(req.getRequestURI(), node, user, isPermalink,
req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET),
req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR),
Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue());
}
protected String getViewPostLink(HttpServletRequest req, BlogURI post, User user) {
return ThreadedHTMLRenderer.getViewPostLink(req.getRequestURI(), post, user, false,
req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET),
req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR),
Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue());
}
protected String getViewThreadLink(HttpServletRequest req, ThreadNode node, User user) {
return getViewThreadLink(req.getRequestURI(), node, user,
req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET),
req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR),
Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue());
}
protected static String getViewThreadLink(String uri, ThreadNode node, User user, String offset,
String tags, String author) {
String tags, String author, boolean authorOnly) {
StringBuffer buf = new StringBuffer(64);
buf.append(uri);
BlogURI expandTo = node.getEntry();
if (node.getChildCount() > 0) {
buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
ThreadNode child = node.getChild(0);
buf.append(child.getEntry().getKeyHash().toBase64()).append('/');
buf.append(child.getEntry().getEntryId()).append('&');
} else {
buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
buf.append(node.getEntry().getKeyHash().toBase64()).append('/');
buf.append(node.getEntry().getEntryId()).append('&');
}
if (true) {
// lets expand to the leaf
expandTo = new BlogURI(node.getMostRecentPostAuthor(), node.getMostRecentPostDate());
} else {
// only expand one level
expandTo = node.getChild(0).getEntry();
}
}
buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
buf.append(expandTo.getKeyHash().toBase64()).append('/');
buf.append(expandTo.getEntryId()).append('&');
buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=');
buf.append(node.getEntry().getKeyHash().toBase64()).append('/');
buf.append(node.getEntry().getEntryId()).append('&');
@@ -830,8 +998,11 @@ public abstract class BaseServlet extends HttpServlet {
if (!empty(tags))
buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
if (!empty(author))
if (!empty(author)) {
buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append('&');
if (authorOnly)
buf.append(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR).append("=true&");
}
buf.append("#").append(node.getEntry().toString());
return buf.toString();
@@ -845,6 +1016,7 @@ public abstract class BaseServlet extends HttpServlet {
req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD),
req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR),
Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue(),
offset);
}
@@ -869,7 +1041,13 @@ public abstract class BaseServlet extends HttpServlet {
}
protected Collection getFilteredAuthors(HttpServletRequest req) {
String authors = req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR);
List rv = new ArrayList();
rv.addAll(getAuthors(req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR)));
//rv.addAll(getAuthors(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)));
return rv;
}
private Collection getAuthors(String authors) {
if (authors != null) {
StringTokenizer tok = new StringTokenizer(authors, "\n\t ");
ArrayList rv = new ArrayList();
@@ -886,8 +1064,13 @@ public abstract class BaseServlet extends HttpServlet {
}
}
private static final String BEGIN_HTML = "<style>\n" +
".overallTable {\n" +
private static final String BEGIN_HTML = "<link href=\"rss.jsp\" rel=\"alternate\" type=\"application/rss+xml\" >\n" +
"</head>\n" +
"<body>\n" +
"<span style=\"display: none\"><a href=\"#bodySubject\">Jump to the beginning of the first post rendered, if any</a>\n" +
"<a href=\"#threads\">Jump to the thread navigation</a>\n</span>\n" +
"<table border=\"0\" width=\"100%\" class=\"overallTable\">\n";
private static final String STYLE_HTML = ".overallTable {\n" +
" border-spacing: 0px;\n" +
" border-width: 0px;\n" +
" border: 0px;\n" +
@@ -926,15 +1109,29 @@ public abstract class BaseServlet extends HttpServlet {
" text-align: left;\n" +
" align: left;\n" +
"}\n" +
".threadRight {\n" +
" text-align: right;\n" +
"}\n" +
".threadNav {\n" +
" background-color: #BBBBBB;\n" +
"}\n" +
".threadNavRight {\n" +
" text-align: right;\n" +
" float: right;\n" +
" background-color: #BBBBBB;\n" +
"}\n" +
".rightOffset {\n" +
" float: right;\n" +
" margin: 0 5px 0 0;\n" +
" display: inline;\n" +
"}\n" +
".threadInfoLeft {\n" +
" float: left;\n" +
" margin: 5px 0px 0 0;\n" +
" display: inline;\n" +
"}\n" +
".threadInfoRight {\n" +
" float: right;\n" +
" margin: 0 5px 0 0;\n" +
" display: inline;\n" +
"}\n" +
".postMeta {\n" +
" background-color: #BBBBFF;\n" +
"}\n" +
@@ -956,14 +1153,17 @@ public abstract class BaseServlet extends HttpServlet {
".postReplyOptions {\n" +
" background-color: #BBBBFF;\n" +
"}\n" +
"</style>\n" +
"<link href=\"style.jsp\" rel=\"stylesheet\" type=\"text/css\" >\n" +
"<link href=\"rss.jsp\" rel=\"alternate\" type=\"application/rss+xml\" >\n" +
"</head>\n" +
"<body>\n" +
"<span style=\"display: none\"><a href=\"#bodySubject\">Jump to the beginning of the first post rendered, if any</a>\n" +
"<a href=\"#threads\">Jump to the thread navigation</a>\n</span>\n" +
"<table border=\"0\" width=\"100%\" class=\"overallTable\">\n";
".syndieBlogFavorites {\n" +
" float: left;\n" +
" margin: 5px 0px 0 0;\n" +
" display: inline;\n" +
"}\n" +
".syndieBlogList {\n" +
" float: right;\n" +
" margin: 5px 0px 0 0;\n" +
" display: inline;\n" +
"}\n";
private static final String END_HTML = "</table>\n" +
"</body>\n";

View File

@@ -136,10 +136,15 @@ public class ExportServlet extends HttpServlet {
ze = new ZipEntry("meta" + i);
ze.setTime(0);
zo.putNextEntry(ze);
FileInputStream in = new FileInputStream((File)metaFiles.get(i));
while ( (read = in.read(buf)) != -1)
zo.write(buf, 0, read);
zo.closeEntry();
FileInputStream in = null;
try {
in = new FileInputStream((File)metaFiles.get(i));
while ( (read = in.read(buf)) != -1)
zo.write(buf, 0, read);
zo.closeEntry();
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
List entryFiles = getEntryFiles(entries);
@@ -147,10 +152,15 @@ public class ExportServlet extends HttpServlet {
ze = new ZipEntry("entry" + i);
ze.setTime(0);
zo.putNextEntry(ze);
FileInputStream in = new FileInputStream((File)entryFiles.get(i));
while ( (read = in.read(buf)) != -1)
zo.write(buf, 0, read);
zo.closeEntry();
FileInputStream in = null;
try {
in = new FileInputStream((File)entryFiles.get(i));
while ( (read = in.read(buf)) != -1)
zo.write(buf, 0, read);
zo.closeEntry();
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
if (zo != null) {

View File

@@ -0,0 +1,424 @@
// see below for license info
package net.i2p.syndie.web;
import org.mortbay.servlet.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import javax.servlet.http.HttpServletRequest;
import org.mortbay.http.HttpFields;
import org.mortbay.util.LineInput;
import org.mortbay.util.MultiMap;
import org.mortbay.util.StringUtil;
/* ------------------------------------------------------------ */
/**
* Hacked version of Jetty's MultiPartRequest handler, applying a tiny patch for
* charset handling [1]. These changes are public domain, and will hopefully
* be integrated into Jetty so we can drop this file altogether. Of course,
* until then, this file is APL2 licensed.
*
* Original code is up at [2]
*
* [1] http://article.gmane.org/gmane.comp.java.jetty.general/6031
* [2] http://cvs.sourceforge.net/viewcvs.py/jetty/Jetty/src/org/mortbay/servlet/
* (rev 1.15)
*
*/
public class MultiPartRequest
{
/* ------------------------------------------------------------ */
HttpServletRequest _request;
LineInput _in;
String _boundary;
String _encoding;
byte[] _byteBoundary;
MultiMap _partMap = new MultiMap(10);
int _char=-2;
boolean _lastPart=false;
/* ------------------------------------------------------------ */
/** Constructor.
* @param request The request containing a multipart/form-data
* request
* @exception IOException IOException
*/
public MultiPartRequest(HttpServletRequest request)
throws IOException
{
_request=request;
String content_type = request.getHeader(HttpFields.__ContentType);
if (!content_type.startsWith("multipart/form-data"))
throw new IOException("Not multipart/form-data request");
//if(log.isDebugEnabled())log.debug("Multipart content type = "+content_type);
_encoding = request.getCharacterEncoding();
if (_encoding != null)
_in = new LineInput(request.getInputStream(), 2048, _encoding);
else
_in = new LineInput(request.getInputStream());
// Extract boundary string
_boundary="--"+
value(content_type.substring(content_type.indexOf("boundary=")));
//if(log.isDebugEnabled())log.debug("Boundary="+_boundary);
_byteBoundary= (_boundary+"--").getBytes(StringUtil.__ISO_8859_1);
loadAllParts();
}
/* ------------------------------------------------------------ */
/** Get the part names.
* @return an array of part names
*/
public String[] getPartNames()
{
Set s = _partMap.keySet();
return (String[]) s.toArray(new String[s.size()]);
}
/* ------------------------------------------------------------ */
/** Check if a named part is present
* @param name The part
* @return true if it was included
*/
public boolean contains(String name)
{
Part part = (Part)_partMap.get(name);
return (part!=null);
}
/* ------------------------------------------------------------ */
/** Get the data of a part as a string.
* @param name The part name
* @return The part data
*/
public String getString(String name)
{
List part = (List)_partMap.getValues(name);
if (part==null)
return null;
if (_encoding != null)
{
try
{
return new String(((Part)part.get(0))._data, _encoding);
}
catch (UnsupportedEncodingException uee)
{
//if (log.isDebugEnabled()) log.debug("Invalid character set: " + uee);
return null;
}
}
else
{
return new String(((Part)part.get(0))._data);
}
}
/* ------------------------------------------------------------ */
/**
* @param name The part name
* @return The parts data
*/
public String[] getStrings(String name)
{
List parts = (List)_partMap.getValues(name);
if (parts==null)
return null;
String[] strings = new String[parts.size()];
if (_encoding == null) {
for (int i=0; i<strings.length; i++) {
strings[i] = new String(((Part)parts.get(i))._data);
}
} else {
try
{
for (int i=0; i<strings.length; i++)
strings[i] = new String(((Part)parts.get(i))._data, _encoding);
}
catch (UnsupportedEncodingException uee)
{
//if (log.isDebugEnabled()) log.debug("Invalid character set: " + uee);
return null;
}
}
return strings;
}
/* ------------------------------------------------------------ */
/** Get the data of a part as a stream.
* @param name The part name
* @return Stream providing the part data
*/
public InputStream getInputStream(String name)
{
List part = (List)_partMap.getValues(name);
if (part==null)
return null;
return new ByteArrayInputStream(((Part)part.get(0))._data);
}
/* ------------------------------------------------------------ */
public InputStream[] getInputStreams(String name)
{
List parts = (List)_partMap.getValues(name);
if (parts==null)
return null;
InputStream[] streams = new InputStream[parts.size()];
for (int i=0; i<streams.length; i++) {
streams[i] = new ByteArrayInputStream(((Part)parts.get(i))._data);
}
return streams;
}
/* ------------------------------------------------------------ */
/** Get the MIME parameters associated with a part.
* @param name The part name
* @return Hashtable of parameters
*/
public Hashtable getParams(String name)
{
List part = (List)_partMap.getValues(name);
if (part==null)
return null;
return ((Part)part.get(0))._headers;
}
/* ------------------------------------------------------------ */
public Hashtable[] getMultipleParams(String name)
{
List parts = (List)_partMap.getValues(name);
if (parts==null)
return null;
Hashtable[] params = new Hashtable[parts.size()];
for (int i=0; i<params.length; i++) {
params[i] = ((Part)parts.get(i))._headers;
}
return params;
}
/* ------------------------------------------------------------ */
/** Get any file name associated with a part.
* @param name The part name
* @return The filename
*/
public String getFilename(String name)
{
List part = (List)_partMap.getValues(name);
if (part==null)
return null;
return ((Part)part.get(0))._filename;
}
/* ------------------------------------------------------------ */
public String[] getFilenames(String name)
{
List parts = (List)_partMap.getValues(name);
if (parts==null)
return null;
String[] filenames = new String[parts.size()];
for (int i=0; i<filenames.length; i++) {
filenames[i] = ((Part)parts.get(i))._filename;
}
return filenames;
}
/* ------------------------------------------------------------ */
private void loadAllParts()
throws IOException
{
// Get first boundary
String line = _in.readLine();
if (!line.equals(_boundary))
{
//log.warn(line);
throw new IOException("Missing initial multi part boundary");
}
// Read each part
while (!_lastPart)
{
// Read Part headers
Part part = new Part();
String content_disposition=null;
while ((line=_in.readLine())!=null)
{
// If blank line, end of part headers
if (line.length()==0)
break;
//if(log.isDebugEnabled())log.debug("LINE="+line);
// place part header key and value in map
int c = line.indexOf(':',0);
if (c>0)
{
String key = line.substring(0,c).trim().toLowerCase();
String value = line.substring(c+1,line.length()).trim();
String ev = (String) part._headers.get(key);
part._headers.put(key,(ev!=null)?(ev+';'+value):value);
//if(log.isDebugEnabled())log.debug(key+": "+value);
if (key.equals("content-disposition"))
content_disposition=value;
}
}
// Extract content-disposition
boolean form_data=false;
if (content_disposition==null)
{
throw new IOException("Missing content-disposition");
}
StringTokenizer tok =
new StringTokenizer(content_disposition,";");
while (tok.hasMoreTokens())
{
String t = tok.nextToken().trim();
String tl = t.toLowerCase();
if (t.startsWith("form-data"))
form_data=true;
else if (tl.startsWith("name="))
part._name=value(t);
else if (tl.startsWith("filename="))
part._filename=value(t);
}
// Check disposition
if (!form_data)
{
//log.warn("Non form-data part in multipart/form-data");
continue;
}
if (part._name==null || part._name.length()==0)
{
//log.warn("Part with no name in multipart/form-data");
continue;
}
//if(log.isDebugEnabled())log.debug("name="+part._name);
//if(log.isDebugEnabled())log.debug("filename="+part._filename);
_partMap.add(part._name,part);
part._data=readBytes();
}
}
/* ------------------------------------------------------------ */
private byte[] readBytes()
throws IOException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int c;
boolean cr=false;
boolean lf=false;
// loop for all lines`
while (true)
{
int b=0;
while ((c=(_char!=-2)?_char:_in.read())!=-1)
{
_char=-2;
// look for CR and/or LF
if (c==13 || c==10)
{
if (c==13) _char=_in.read();
break;
}
// look for boundary
if (b>=0 && b<_byteBoundary.length && c==_byteBoundary[b])
b++;
else
{
// this is not a boundary
if (cr) baos.write(13);
if (lf) baos.write(10);
cr=lf=false;
if (b>0)
baos.write(_byteBoundary,0,b);
b=-1;
baos.write(c);
}
}
// check partial boundary
if ((b>0 && b<_byteBoundary.length-2) ||
(b==_byteBoundary.length-1))
{
if (cr) baos.write(13);
if (lf) baos.write(10);
cr=lf=false;
baos.write(_byteBoundary,0,b);
b=-1;
}
// boundary match
if (b>0 || c==-1)
{
if (b==_byteBoundary.length)
_lastPart=true;
if (_char==10) _char=-2;
break;
}
// handle CR LF
if (cr) baos.write(13);
if (lf) baos.write(10);
cr=(c==13);
lf=(c==10 || _char==10);
if (_char==10) _char=-2;
}
//if(log.isTraceEnabled())log.trace(baos.toString());
return baos.toByteArray();
}
/* ------------------------------------------------------------ */
private String value(String nameEqualsValue)
{
String value =
nameEqualsValue.substring(nameEqualsValue.indexOf('=')+1).trim();
int i=value.indexOf(';');
if (i>0)
value=value.substring(0,i);
if (value.startsWith("\""))
{
value=value.substring(1,value.indexOf('"',1));
}
else
{
i=value.indexOf(' ');
if (i>0)
value=value.substring(0,i);
}
return value;
}
/* ------------------------------------------------------------ */
private class Part
{
String _name=null;
String _filename=null;
Hashtable _headers= new Hashtable(10);
byte[] _data=null;
}
};

View File

@@ -74,12 +74,17 @@ public class PostBean {
}
public void writeAttachmentData(int id, OutputStream out) throws IOException {
FileInputStream in = new FileInputStream((File)_localFiles.get(id));
byte buf[] = new byte[1024];
int read = 0;
while ( (read = in.read(buf)) != -1)
out.write(buf, 0, read);
out.close();
FileInputStream in = null;
try {
in = new FileInputStream((File)_localFiles.get(id));
byte buf[] = new byte[1024];
int read = 0;
while ( (read = in.read(buf)) != -1)
out.write(buf, 0, read);
out.close();
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
public void addAttachment(String filename, InputStream fileStream, String mimeType) {
@@ -167,6 +172,7 @@ public class PostBean {
private static final int MAX_SIZE = 256*1024;
private void cacheAttachments() throws IOException {
if (_user == null) throw new IOException("User not specified");
File postCacheDir = new File(BlogManager.instance().getTempDir(), _user.getBlog().toBase64());
if (!postCacheDir.exists())
postCacheDir.mkdirs();

View File

@@ -8,7 +8,8 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import org.mortbay.servlet.MultiPartRequest;
// temporarily, we use our overwride, until jetty applies our patches
//import org.mortbay.servlet.MultiPartRequest;
import net.i2p.I2PAppContext;
import net.i2p.client.naming.*;
@@ -26,7 +27,7 @@ public class PostServlet extends BaseServlet {
public static final String ACTION_CONFIRM = "confirm";
public static final String PARAM_SUBJECT = "entrysubject";
public static final String PARAM_TAGS = "entrytags";
public static final String PARAM_TAGS = ThreadedHTMLRenderer.PARAM_TAGS;
public static final String PARAM_INCLUDENAMES = "includenames";
public static final String PARAM_TEXT = "entrytext";
public static final String PARAM_HEADERS = "entryheaders";
@@ -48,6 +49,8 @@ public class PostServlet extends BaseServlet {
String action = req.getParameter(PARAM_ACTION);
if (!empty(action) && ACTION_CONFIRM.equals(action)) {
postEntry(user, req, archive, post, out);
post.reinitialize();
post.setUser(user);
} else {
String contentType = req.getContentType();
if (!empty(contentType) && (contentType.indexOf("boundary=") != -1)) {
@@ -72,8 +75,8 @@ public class PostServlet extends BaseServlet {
out.write("<tr><td colspan=\"3\">");
post.reinitialize();
post.setUser(user);
//post.reinitialize();
//post.setUser(user);
boolean inNewThread = getInNewThread(req.getString(PARAM_IN_NEW_THREAD));
boolean refuseReplies = getRefuseReplies(req.getString(PARAM_REFUSE_REPLIES));
@@ -101,6 +104,8 @@ public class PostServlet extends BaseServlet {
}
}
if (entryTags == null) entryTags = "";
if ( (entryHeaders == null) || (entryHeaders.trim().length() <= 0) )
entryHeaders = ThreadedHTMLRenderer.HEADER_FORCE_NEW_THREAD + ": " + inNewThread + '\n' +
ThreadedHTMLRenderer.HEADER_REFUSE_REPLIES + ": " + refuseReplies;
@@ -148,9 +153,7 @@ public class PostServlet extends BaseServlet {
writeAuthActionFields(out);
out.write("Please confirm that the above is ok");
if (BlogManager.instance().authorizeRemote(user)) {
out.write(", and select what additional archives you want the post transmitted to. ");
out.write("To make changes, hit your browser's back arrow and try again.\n");
out.write("Remote archive to push this post to: ");
out.write(", and select what additional archive you want the post transmitted to: ");
out.write("<select class=\"b_postConfirm\" name=\"" + PARAM_REMOTE_ARCHIVE + "\">\n");
PetNameDB db = user.getPetNameDB();
TreeSet names = new TreeSet();
@@ -174,6 +177,10 @@ public class PostServlet extends BaseServlet {
out.write("</span><input class=\"b_postConfirm\" type=\"submit\" name=\"" + PARAM_ACTION
+ "\" value=\"" + ACTION_CONFIRM + "\" />\n");
out.write("</form>\n");
displayEditForm(user, req, post, out);
out.write("</td></tr>\n");
}
@@ -200,37 +207,42 @@ public class PostServlet extends BaseServlet {
post.reinitialize();
post.setUser(user);
String parentURI = req.getParameter(PARAM_PARENT);
String subject = getParam(req, PARAM_SUBJECT);
out.write("<form action=\"" + getPostURI() + "\" method=\"POST\" enctype=\"multipart/form-data\">\n");
writeAuthActionFields(out);
out.write("<tr><td colspan=\"3\">\n");
out.write("<span class=\"b_postField\">Post subject:</span> ");
out.write("<input type=\"text\" class=\"b_postSubject\" size=\"80\" name=\"" + PARAM_SUBJECT
+ "\" value=\"" + getParam(req,PARAM_SUBJECT) + "\" /><br />\n");
out.write("<span class=\"b_postField\">Post tags:</span> ");
out.write("<input type=\"text\" class=\"b_postTags\" size=\"20\" name=\"" + PARAM_TAGS
+ "\" value=\"" + getParam(req, PARAM_TAGS) + "\" /><br />\n");
out.write("<span class=\"b_postField\">Include public names?</span> ");
out.write("<input class=\"b_postNames\" type=\"checkbox\" name=\"" + PARAM_INCLUDENAMES
+ "\" value=\"true\" /><br />\n");
+ "\" value=\"" + HTMLRenderer.sanitizeTagParam(subject) + "\" title=\"One line summary\" /><br />\n");
out.write("<span class=\"b_postField\">Post content (in raw <a href=\"smlref.jsp\" target=\"_blank\">SML</a>, no headers):</span><br />\n");
out.write("<textarea class=\"b_postText\" rows=\"6\" cols=\"80\" name=\"" + PARAM_TEXT + "\">" + getParam(req, PARAM_TEXT) + "</textarea><br />\n");
out.write("<span class=\"b_postField\">SML post headers:</span><br />\n");
out.write("<textarea class=\"b_postHeaders\" rows=\"3\" cols=\"80\" name=\"" + PARAM_HEADERS + "\">" + getParam(req, PARAM_HEADERS) + "</textarea><br />\n");
out.write("<textarea class=\"b_postHeaders\" rows=\"2\" cols=\"80\" name=\"" + PARAM_HEADERS + "\" title=\"Most people can leave this empty\" >" + getParam(req, PARAM_HEADERS) + "</textarea><br />\n");
String parentURI = req.getParameter(PARAM_PARENT);
if ( (parentURI != null) && (parentURI.trim().length() > 0) )
out.write("<input type=\"hidden\" name=\"" + PARAM_PARENT + "\" value=\"" + parentURI + "\" />\n");
out.write(" Tags: <input type=\"text\" size=\"10\" name=\"" + PARAM_TAGS + "\" value=\"" + getParam(req, PARAM_TAGS) + "\" />\n");
out.write(" Tags: ");
BaseServlet.writeTagField(user, getParam(req, PARAM_TAGS), out, "Optional tags to categorize your post", "No tags", false);
//<input type=\"text\" size=\"10\" name=\"" + PARAM_TAGS + "\" value=\"" + getParam(req, PARAM_TAGS) + "\" title=\"Optional tags to categorize your response\" /><br />\n");
out.write("<br />\n");
boolean inNewThread = getInNewThread(req);
boolean refuseReplies = getRefuseReplies(req);
out.write(" in a new thread? <input type=\"checkbox\" value=\"true\" name=\"" + PARAM_IN_NEW_THREAD +
(inNewThread ? "\" checked=\"true\" " : "\" " ) + " />\n");
out.write(" refuse replies? <input type=\"checkbox\" value=\"true\" name=\"" + PARAM_REFUSE_REPLIES +
(refuseReplies ? "\" checked=\"true\" " : "\" " ) + " />\n");
out.write("In a new thread? <input type=\"checkbox\" value=\"true\" name=\"" + PARAM_IN_NEW_THREAD +
(inNewThread ? "\" checked=\"true\" " : "\" " )
+ " title=\"If true, this will fork a new top level thread\" /><br />\n");
out.write("Refuse replies? <input type=\"checkbox\" value=\"true\" name=\"" + PARAM_REFUSE_REPLIES +
(refuseReplies ? "\" checked=\"true\" " : "\" " )
+ " title=\"If true, only you will be able to reply to the post\" /><br />\n");
out.write("<span class=\"b_postField\">Include public names?</span> ");
out.write("<input class=\"b_postNames\" type=\"checkbox\" name=\"" + PARAM_INCLUDENAMES
+ "\" value=\"true\" title=\"If true, everything marked 'public' in your addressbook is shared\" /><br />\n");
out.write(ATTACHMENT_FIELDS);
@@ -249,9 +261,72 @@ public class PostServlet extends BaseServlet {
out.write("</form>\n");
}
private void displayEditForm(User user, MultiPartRequest req, PostBean post, PrintWriter out) throws IOException {
String parentURI = req.getString(PARAM_PARENT);
String subject = getParam(req, PARAM_SUBJECT);
out.write("<hr />\n");
out.write("<form action=\"" + getPostURI() + "\" method=\"POST\" enctype=\"multipart/form-data\">\n");
writeAuthActionFields(out);
out.write("<tr><td colspan=\"3\">\n");
out.write("<span class=\"b_postField\">Post subject:</span> ");
out.write("<input type=\"text\" class=\"b_postSubject\" size=\"80\" name=\"" + PARAM_SUBJECT
+ "\" value=\"" + HTMLRenderer.sanitizeTagParam(subject) + "\" /><br />\n");
out.write("<span class=\"b_postField\">Post content (in raw <a href=\"smlref.jsp\" target=\"_blank\">SML</a>, no headers):</span><br />\n");
out.write("<textarea class=\"b_postText\" rows=\"6\" cols=\"80\" name=\"" + PARAM_TEXT + "\">" + getParam(req, PARAM_TEXT) + "</textarea><br />\n");
out.write("<span class=\"b_postField\">SML post headers:</span><br />\n");
out.write("<textarea class=\"b_postHeaders\" rows=\"3\" cols=\"80\" name=\"" + PARAM_HEADERS + "\">" + getParam(req, PARAM_HEADERS) + "</textarea><br />\n");
if ( (parentURI != null) && (parentURI.trim().length() > 0) )
out.write("<input type=\"hidden\" name=\"" + PARAM_PARENT + "\" value=\"" + parentURI + "\" />\n");
out.write(" Tags: ");
//<input type=\"text\" size=\"10\" name=\"" + PARAM_TAGS + "\" value=\"" + getParam(req, PARAM_TAGS) + "\" /><br />\n");
out.write(" Tags: ");
BaseServlet.writeTagField(user, getParam(req, PARAM_TAGS), out, "Optional tags to categorize your post", "No tags", false);
out.write("<br />\n");
boolean inNewThread = getInNewThread(req);
boolean refuseReplies = getRefuseReplies(req);
out.write("In a new thread? <input type=\"checkbox\" value=\"true\" name=\"" + PARAM_IN_NEW_THREAD +
(inNewThread ? "\" checked=\"true\" " : "\" " ) + " /><br />\n");
out.write("Refuse replies? <input type=\"checkbox\" value=\"true\" name=\"" + PARAM_REFUSE_REPLIES +
(refuseReplies ? "\" checked=\"true\" " : "\" " ) + " /><br />\n");
out.write("<span class=\"b_postField\">Include public names?</span> ");
out.write("<input class=\"b_postNames\" type=\"checkbox\" name=\"" + PARAM_INCLUDENAMES
+ "\" value=\"true\" /><br />\n");
int newCount = 0;
for (int i = 0; i < 32 && newCount < 3; i++) {
String filename = req.getFilename("entryfile" + i);
if ( (filename != null) && (filename.trim().length() > 0) ) {
out.write("<span class=\"b_postField\">Attachment " + i + ":</span> ");
out.write(HTMLRenderer.sanitizeString(filename));
out.write("<br />");
} else {
out.write("<span class=\"b_postField\">Attachment " + i + ":</span> ");
out.write("<input class=\"b_postField\" type=\"file\" name=\"entryfile" + i + "\" ");
out.write("/><br />");
newCount++;
}
}
out.write("<hr />\n");
out.write("<input class=\"b_postPreview\" type=\"submit\" name=\"Post\" value=\"Preview...\" /> ");
out.write("<input class=\"b_postReset\" type=\"reset\" value=\"Cancel\" />\n");
out.write("</form>\n");
}
private boolean getInNewThread(HttpServletRequest req) {
return getInNewThread(req.getParameter(PARAM_IN_NEW_THREAD));
}
private boolean getInNewThread(MultiPartRequest req) {
return getInNewThread(getParam(req, PARAM_IN_NEW_THREAD));
}
private boolean getInNewThread(String val) {
boolean rv = false;
String inNewThread = val;
@@ -262,6 +337,9 @@ public class PostServlet extends BaseServlet {
private boolean getRefuseReplies(HttpServletRequest req) {
return getRefuseReplies(req.getParameter(PARAM_REFUSE_REPLIES));
}
private boolean getRefuseReplies(MultiPartRequest req) {
return getRefuseReplies(getParam(req, PARAM_REFUSE_REPLIES));
}
private boolean getRefuseReplies(String val) {
boolean rv = false;
String refuseReplies = val;
@@ -276,6 +354,7 @@ public class PostServlet extends BaseServlet {
bean = new PostBean();
req.getSession().setAttribute(ATTR_POST_BEAN, bean);
}
bean.setUser(user);
return bean;
}
@@ -284,6 +363,11 @@ public class PostServlet extends BaseServlet {
if (val == null) val = "";
return val;
}
private String getParam(MultiPartRequest req, String param) {
String val = req.getString(param);
if (val == null) return "";
return val;
}
private static final String ATTACHMENT_FIELDS = ""
+ "<span class=\"b_postField\">Attachment 0:</span> <input class=\"b_postField\" type=\"file\" name=\"entryfile0\" /><br />"

View File

@@ -96,6 +96,10 @@ public class ProfileServlet extends BaseServlet {
out.write("<tr><td colspan=\"3\">Password: <input type=\"password\" name=\"password\" /></td></tr>\n");
out.write("<tr><td colspan=\"3\">Password again: <input type=\"password\" name=\"passwordConfirm\" /></td></tr>\n");
}
if (!BlogManager.instance().authorizeRemote(user)) {
out.write("<tr><td colspan=\"3\">To access the remote functionality, please specify the administrative password: <br />\n" +
"<input type=\"password\" name=\"adminPass\" /></td></tr>\n");
}
}
out.write("<tr><td colspan=\"3\"><input type=\"submit\" name=\"action\" value=\"Update profile\" /></td></tr>\n");
@@ -103,14 +107,13 @@ public class ProfileServlet extends BaseServlet {
}
private void renderProfile(User user, String baseURI, PrintWriter out, Hash author, Archive archive) throws IOException {
out.write("<tr><td colspan=\"3\">Profile for <a href=\"" + getControlTarget() + "?"
+ ThreadedHTMLRenderer.PARAM_AUTHOR + '=' + author.toBase64()
+ "\" title=\"View threads by the profiled author\">");
out.write("<tr><td colspan=\"3\">Profile for ");
PetName pn = user.getPetNameDB().getByLocation(author.toBase64());
String name = null;
BlogInfo info = archive.getBlogInfo(author);
if (pn != null) {
out.write(pn.getName());
String name = null;
name = null;
if (info != null)
name = info.getProperty(BlogInfo.NAME);
@@ -119,7 +122,6 @@ public class ProfileServlet extends BaseServlet {
out.write(" (" + name + ")");
} else {
String name = null;
if (info != null)
name = info.getProperty(BlogInfo.NAME);
@@ -130,6 +132,12 @@ public class ProfileServlet extends BaseServlet {
out.write("</a>");
if (info != null)
out.write(" [edition " + info.getEdition() + "]");
out.write("<br />\n");
out.write("<a href=\"" + getControlTarget() + "?" + ThreadedHTMLRenderer.PARAM_AUTHOR
+ '=' + author.toBase64() + "&" + ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR + "=true&\""
+ " title=\"View '" + HTMLRenderer.sanitizeTagParam(name) + "'s blog\">View their blog</a> or ");
out.write("<a href=\"" + getControlTarget() + "?" + ThreadedHTMLRenderer.PARAM_AUTHOR
+ '=' + author.toBase64() + "&\">threads they have participated in</a>\n");
out.write("</td></tr>\n");
out.write("<tr><td colspan=\"3\"><hr /></td></tr>\n");

View File

@@ -60,7 +60,7 @@ public class RSSServlet extends HttpServlet {
if (count > 100) count = 100;
Archive archive = BlogManager.instance().getArchive();
FilteredThreadIndex index = new FilteredThreadIndex(user, archive, tagSet, null);
FilteredThreadIndex index = new FilteredThreadIndex(user, archive, tagSet, null, false);
List entries = new ArrayList();
// depth first search of the most recent threads
for (int i = 0; i < count && i < index.getRootCount(); i++) {

View File

@@ -362,6 +362,7 @@ public class RemoteArchiveBean {
ArchiveIndex i = new ArchiveIndex(I2PAppContext.getGlobalContext(), false);
if (notModified) {
_statusMessages.add("Archive unchanged since last fetch.");
_statusMessages.add("If you want to force a refetch, make a trivial modification to the URL, such as adding a \"?\"");
} else {
try {
i.load(_archiveFile);

View File

@@ -0,0 +1,51 @@
package net.i2p.syndie.web;
import java.io.File;
import net.i2p.util.FileUtil;
import org.mortbay.jetty.Server;
public class RunStandalone {
private Server _server;
static {
System.setProperty("org.mortbay.http.Version.paranoid", "true");
System.setProperty("org.mortbay.xml.XmlParser.NotValidating", "true");
System.setProperty("syndie.rootDir", ".");
System.setProperty("syndie.defaultSingleUserArchives", "http://syndiemedia.i2p.net:8000/archive/archive.txt");
System.setProperty("syndie.defaultProxyHost", "");
System.setProperty("syndie.defaultProxyPort", "");
}
private RunStandalone(String args[]) {}
public static void main(String args[]) {
RunStandalone runner = new RunStandalone(args);
runner.start();
}
public void start() {
File workDir = new File("work");
boolean workDirRemoved = FileUtil.rmdir(workDir, false);
if (!workDirRemoved)
System.err.println("ERROR: Unable to remove Jetty temporary work directory");
boolean workDirCreated = workDir.mkdirs();
if (!workDirCreated)
System.err.println("ERROR: Unable to create Jetty temporary work directory");
try {
_server = new Server("jetty-syndie.xml");
_server.start();
} catch (Exception e) {
e.printStackTrace();
}
}
public void stop() {
try {
_server.stop();
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}

View File

@@ -34,8 +34,9 @@ public class SwitchServlet extends BaseServlet {
"<input type=\"submit\" name=\"action\" value=\"Logout\" /></td></tr>\n" +
"</form>\n" +
"<tr><td colspan=\"3\"><hr /></td></tr>\n" +
"<form action=\"" + ThreadedHTMLRenderer.buildProfileURL(null) + "\" method=\"POST\">\n" +
"<tr><td colspan=\"3\"><b>Register a new account</b></td></tr>\n" +
"<form action=\"" + ThreadedHTMLRenderer.buildProfileURL(null) + "\" method=\"POST\">\n");
writeAuthActionFields(out);
out.write("<tr><td colspan=\"3\"><b>Register a new account</b></td></tr>\n" +
"<tr><td colspan=\"3\">Login: <input type=\"text\" name=\"login\" /> (only known locally)</td></tr>\n" +
"<tr><td colspan=\"3\">Password: <input type=\"password\" name=\"password\" /></td></tr>\n" +
"<tr><td colspan=\"3\">Public name: <input type=\"text\" name=\"accountName\" /></td></tr>\n" +

View File

@@ -111,12 +111,12 @@ public class SyndicateServlet extends BaseServlet {
out.write("</select>\n");
out.write("<span class=\"b_remoteChooserField\">Proxy</span>\n");
out.write("<input class=\"b_remoteChooserHost\" type=\"text\" size=\"10\" name=\"proxyhost\" value=\"");
out.write("<input class=\"b_remoteChooserHost\" type=\"text\" size=\"12\" name=\"proxyhost\" value=\"");
out.write(BlogManager.instance().getDefaultProxyHost());
out.write("\" />\n");
out.write("\" title=\"hostname that your HTTP proxy is on, or blank for no proxy\" />\n");
out.write("<input class=\"b_remoteChooserPort\" type=\"text\" size=\"4\" name=\"proxyport\" value=\"");
out.write(BlogManager.instance().getDefaultProxyPort());
out.write("\" /><br />\n");
out.write("\" title=\"port number that your HTTP proxy is on, or blank for no proxy\" /><br />\n");
out.write("<span class=\"b_remoteChooserField\">Bookmarked archives:</span>\n");
out.write("<select class=\"b_remoteChooserPN\" name=\"" + PARAM_PETNAME + "\">");
out.write("<option value=\"\">Custom location</option>");
@@ -136,7 +136,7 @@ public class SyndicateServlet extends BaseServlet {
String reqLoc = req.getParameter("location");
if (reqLoc != null)
out.write(reqLoc);
out.write("\" />\n");
out.write("\" title=\"full URL to the remote location, to be sent to your HTTP proxy\" />\n");
out.write("<input class=\"b_remoteChooserContinue\" type=\"submit\" name=\"action\" value=\"Continue...\" /><br />\n");
out.write("</span>\n");
}

View File

@@ -0,0 +1,150 @@
package net.i2p.syndie.web;
import java.io.*;
import java.util.*;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.i2p.I2PAppContext;
import net.i2p.client.naming.*;
import net.i2p.data.*;
import net.i2p.syndie.*;
import net.i2p.syndie.data.*;
import net.i2p.syndie.sml.*;
/**
* Export the thread nav as either RDF or XML
*
*/
public class ThreadNavServlet extends BaseServlet {
public static final String PARAM_COUNT = "count";
public static final String PARAM_OFFSET = "offset";
public static final String PARAM_FORMAT = "format";
public static final String FORMAT_RDF = "rdf";
public static final String FORMAT_XML = "xml";
protected void render(User user, HttpServletRequest req, HttpServletResponse resp, ThreadIndex index) throws ServletException, IOException {
int threadCount = empty(req, PARAM_COUNT) ? index.getRootCount() : getInt(req, PARAM_COUNT);
int offset = getInt(req, PARAM_OFFSET);
String uri = req.getRequestURI();
if (uri.endsWith(FORMAT_XML)) {
resp.setContentType("text/xml; charset=UTF-8");
render(user, index, resp.getWriter(), threadCount, offset, FORMAT_XML);
} else {
resp.setContentType("application/rdf+xml; charset=UTF-8");
render(user, index, resp.getWriter(), threadCount, offset, FORMAT_RDF);
}
}
private int getInt(HttpServletRequest req, String param) {
String val = req.getParameter(param);
if (val != null) {
try {
return Integer.parseInt(val);
} catch (NumberFormatException nfe) {
// ignore
}
}
return -1;
}
private static final int DEFAULT_THREADCOUNT = 10;
private static final int DEFAULT_THREADOFFSET = 0;
private void render(User user, ThreadIndex index, PrintWriter out, int threadCount, int offset, String format) throws IOException {
int startRoot = DEFAULT_THREADOFFSET;
if (offset >= 0)
startRoot = offset;
renderStart(out, format);
int endRoot = startRoot + (threadCount > 0 ? threadCount : DEFAULT_THREADCOUNT);
if (endRoot >= index.getRootCount())
endRoot = index.getRootCount() - 1;
for (int i = startRoot; i <= endRoot; i++) {
ThreadNode node = index.getRoot(i);
if (FORMAT_XML.equals(format))
out.write(node.toString());
else
render(user, node, out);
}
renderEnd(out, format);
}
private void renderStart(PrintWriter out, String format) throws IOException {
out.write("<?xml version=\"1.0\" ?>\n");
if (FORMAT_XML.equals(format)) {
out.write("<threadTree>");
} else {
out.write("<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" " +
" xmlns:syndie=\"http://syndie.i2p.net/syndie.ns#\">\n");
out.write("<rdf:Seq rdf:about=\"http://syndie.i2p.net/threads\">\n");
}
}
private void renderEnd(PrintWriter out, String format) throws IOException {
if (FORMAT_XML.equals(format)) {
out.write("</threadTree>");
} else {
out.write("</rdf:Seq>\n");
out.write("</rdf:RDF>\n");
}
}
private void render(User user, ThreadNode node, PrintWriter out) throws IOException {
Archive archive = BlogManager.instance().getArchive();
String blog = node.getEntry().getKeyHash().toBase64();
out.write("<rdf:li rdf:resource=\"entry://" + blog + "/" + node.getEntry().getEntryId() + "\">\n");
out.write("<rdf:Description rdf:about=\"entry://" + blog + "/" + node.getEntry().getEntryId() + "\">");
PetName pn = user.getPetNameDB().getByLocation(blog);
String name = null;
if (pn != null) {
if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE))
out.write("<syndie:favoriteauthor />\n");
if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE))
out.write("<syndie:ignoredauthor />\n");
name = pn.getName();
} else {
BlogInfo info = archive.getBlogInfo(node.getEntry().getKeyHash());
if (info != null)
name = info.getProperty(BlogInfo.NAME);
if ( (name == null) || (name.trim().length() <= 0) )
name = node.getEntry().getKeyHash().toBase64().substring(0,6);
}
out.write("<syndie:author syndie:blog=\"" + blog + "\">" + HTMLRenderer.sanitizeStrippedXML(name) + "</syndie:author>\n");
if ( (user.getBlog() != null) && (node.containsAuthor(user.getBlog())) )
out.write("<syndie:threadself />\n");
EntryContainer entry = archive.getEntry(node.getEntry());
if (entry == null) throw new RuntimeException("Unable to fetch the entry " + node.getEntry());
SMLParser parser = new SMLParser(I2PAppContext.getGlobalContext());
HeaderReceiver rec = new HeaderReceiver();
parser.parse(entry.getEntry().getText(), rec);
String subject = rec.getHeader(HTMLRenderer.HEADER_SUBJECT);
if ( (subject == null) || (subject.trim().length() <= 0) )
subject = "(no subject)";
out.write("<syndie:subject>" + HTMLRenderer.sanitizeStrippedXML(subject) + "</syndie:subject>\n");
long dayBegin = BlogManager.instance().getDayBegin();
long postId = node.getEntry().getEntryId();
int daysAgo = (int)((dayBegin - postId + 24*60*60*1000l-1l)/(24*60*60*1000l));
out.write("<syndie:age>" + daysAgo + "</syndie:age>\n");
out.write("<syndie:children>");
out.write("<rdf:Seq rdf:about=\"entry://" + blog + "/" + node.getEntry().getEntryId() + "\">");
for (int i = 0; i < node.getChildCount(); i++)
render(user, node.getChild(i), out);
out.write("</rdf:Seq>\n");
out.write("</syndie:children>\n");
out.write("</rdf:Description>\n");
out.write("</rdf:li>\n");
}
protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index,
int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException {
throw new UnsupportedOperationException("Not relevant...");
}
}

View File

@@ -0,0 +1,158 @@
package net.i2p.syndie.web;
import java.io.*;
import java.util.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.i2p.I2PAppContext;
import net.i2p.client.naming.*;
import net.i2p.data.*;
import net.i2p.syndie.*;
import net.i2p.syndie.data.*;
import net.i2p.syndie.sml.*;
/**
* List the blogs known in the archive
*
*/
public class ViewBlogsServlet extends BaseServlet {
private static final int MAX_AUTHORS_AT_ONCE = 20;
private static final int MAX_TAGS = 50;
/** renders the posts from the last 3 days */
private String getViewBlogLink(Hash blog, long lastPost) {
long dayBegin = BlogManager.instance().getDayBegin();
int daysAgo = 2;
if ( (lastPost > 0) && (dayBegin - 3*24*60*60*1000l >= lastPost) ) // last post was old 3 days ago
daysAgo = (int)((dayBegin - lastPost + 24*60*60*1000l-1)/(24*60*60*1000l));
daysAgo++;
return getControlTarget() + "?" + ThreadedHTMLRenderer.PARAM_AUTHOR + '=' + blog.toBase64()
+ '&' + ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR + "=true&daysBack=" + daysAgo;
}
private String getPostDate(long when) {
String age = null;
long dayBegin = BlogManager.instance().getDayBegin();
long postId = when;
if (postId >= dayBegin) {
age = "today";
} else if (postId >= dayBegin - 24*60*60*1000) {
age = "yesterday";
} else {
int daysAgo = (int)((dayBegin - postId + 24*60*60*1000-1)/(24*60*60*1000));
age = daysAgo + " days ago";
}
return age;
}
protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index,
int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException {
TreeSet orderedRoots = new TreeSet(new NewestEntryFirstComparator());
// The thread index is ordered by last updated date, as opposed to root posting date,
// so lets reorder things
int count = index.getRootCount();
for (int i = 0; i < count; i++) {
ThreadNode node = index.getRoot(i);
orderedRoots.add(node.getEntry());
}
TreeSet tags = new TreeSet();
List writtenAuthors = new ArrayList();
out.write("<tr><td colspan=\"3\" valign=\"top\" align=\"left\"><span class=\"syndieBlogFavorites\">");
if ( (user != null) && (user.getAuthenticated()) ) {
out.write("<b>Favorite blogs:</b> <a href=\"" + getControlTarget() + "?author=favorites&daysBack=3\" title=\"View all posts by your favorite authors in the last 3 days\">view all</a><br />\n");
out.write("<a href=\"" + getViewBlogLink(user.getBlog(), user.getLastMetaEntry())
+ "\" title=\"View your blog\">Your blog</a><br />\n");
PetNameDB db = user.getPetNameDB();
for (Iterator iter = orderedRoots.iterator(); iter.hasNext() && writtenAuthors.size() < MAX_AUTHORS_AT_ONCE; ) {
BlogURI uri= (BlogURI)iter.next();
if (writtenAuthors.contains(uri.getKeyHash())) {
// skip
} else {
PetName pn = db.getByLocation(uri.getKeyHash().toBase64());
if (pn != null) {
if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) {
out.write("<a href=\"" + getViewBlogLink(uri.getKeyHash(), uri.getEntryId())
+ "\" title=\"View " + HTMLRenderer.sanitizeTagParam(pn.getName()) +"'s blog\">");
out.write(HTMLRenderer.sanitizeString(pn.getName(), 32));
out.write("</a> (" + getPostDate(uri.getEntryId()) + ")<br />\n");
writtenAuthors.add(uri.getKeyHash());
} else if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) {
// ignore 'em
writtenAuthors.add(uri.getKeyHash());
} else {
// bookmarked, but not a favorite... leave them for later
}
} else {
// not bookmarked, leave them for later
}
}
}
}
out.write("</span>\n");
// now for the non-bookmarked people
out.write("<span class=\"syndieBlogList\">");
out.write("<b>Most recently updated blogs:</b><br />\n");
for (Iterator iter = orderedRoots.iterator(); iter.hasNext() && writtenAuthors.size() < MAX_AUTHORS_AT_ONCE; ) {
BlogURI uri= (BlogURI)iter.next();
String curTags[] = archive.getEntry(uri).getTags();
if (curTags != null)
for (int i = 0; i < curTags.length && tags.size() < MAX_TAGS; i++)
tags.add(curTags[i]);
if (writtenAuthors.contains(uri.getKeyHash())) {
// skip
} else {
BlogInfo info = archive.getBlogInfo(uri);
if (info == null)
continue;
String name = info.getProperty(BlogInfo.NAME);
if ( (name == null) || (name.trim().length() <= 0) )
name = uri.getKeyHash().toBase64().substring(0,8);
String desc = info.getProperty(BlogInfo.DESCRIPTION);
if ( (desc == null) || (desc.trim().length() <= 0) )
desc = name + "'s blog";
String age = null;
long dayBegin = BlogManager.instance().getDayBegin();
long postId = uri.getEntryId();
if (postId >= dayBegin) {
age = "today";
} else if (postId >= dayBegin - 24*60*60*1000) {
age = "yesterday";
} else {
int daysAgo = (int)((dayBegin - postId + 24*60*60*1000-1)/(24*60*60*1000));
age = daysAgo + " days ago";
}
out.write("<a href=\"" + getViewBlogLink(uri.getKeyHash(), uri.getEntryId())
+ "\" title=\"View " + trim(HTMLRenderer.sanitizeTagParam(name), 32)
+ "'s blog\">");
out.write(HTMLRenderer.sanitizeString(desc, 32));
out.write("</a> (" + getPostDate(uri.getEntryId()) + ")<br />\n");
writtenAuthors.add(uri.getKeyHash());
}
}
out.write("</span>\n");
/*
out.write("<tr><td colspan=\"3\"><b>Topics:</b></td></tr>\n");
out.write("<tr><td colspan=\"3\">");
for (Iterator iter = tags.iterator(); iter.hasNext(); ) {
String tag = (String)iter.next();
out.write("<a href=\"" + ThreadedHTMLRenderer.getFilterByTagLink(getControlTarget(), null, user, tag, null)
+ "\" title=\"View threads flagged with the tag '" + HTMLRenderer.sanitizeTagParam(tag) + "'\">");
out.write(HTMLRenderer.sanitizeString(tag, 32));
out.write("</a> ");
}
*/
out.write("</td></tr>\n");
}
protected String getTitle() { return "Syndie :: View blogs"; }
}

View File

@@ -21,50 +21,138 @@ import net.i2p.syndie.sml.*;
public class ViewThreadedServlet extends BaseServlet {
protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index,
int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException {
renderBody(user, req, out, index);
List posts = getPosts(user, archive, req, index);
renderBody(user, req, out, index, archive, posts);
renderThreadNav(user, req, out, threadOffset, index);
renderThreadTree(user, req, out, threadOffset, visibleEntry, archive, index);
renderThreadTree(user, req, out, threadOffset, visibleEntry, archive, index, posts);
renderThreadNav(user, req, out, threadOffset, index);
}
}
private void renderBody(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
private void renderBody(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, Archive archive, List posts) throws IOException {
ThreadedHTMLRenderer renderer = new ThreadedHTMLRenderer(I2PAppContext.getGlobalContext());
Archive archive = BlogManager.instance().getArchive();
List posts = getPosts(archive, req, index);
String uri = req.getRequestURI();
String off = req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET);
String tags = req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS);
String author = req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR);
boolean authorOnly = Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR)).booleanValue();
for (int i = 0; i < posts.size(); i++) {
BlogURI post = (BlogURI)posts.get(i);
renderer.render(user, out, archive, post, posts.size() == 1, index, uri, getAuthActionFields(), off, tags, author);
boolean inlineReply = (posts.size() == 1);
//if (true)
// inlineReply = true;
renderer.render(user, out, archive, post, inlineReply, index, uri, getAuthActionFields(), off, tags, author, authorOnly);
}
}
private List getPosts(Archive archive, HttpServletRequest req, ThreadIndex index) {
private List getPosts(User user, Archive archive, HttpServletRequest req, ThreadIndex index) {
List rv = new ArrayList(1);
String author = req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR);
String tags = req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS);
String post = req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST);
String thread = req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD);
boolean threadAuthorOnly = Boolean.valueOf(req.getParameter(ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR) + "").booleanValue();
long dayBegin = BlogManager.instance().getDayBegin();
String daysStr = req.getParameter(ThreadedHTMLRenderer.PARAM_DAYS_BACK);
int days = 1;
try {
if (daysStr != null)
days = Integer.parseInt(daysStr);
} catch (NumberFormatException nfe) {
days = 1;
}
dayBegin -= (days-1) * 24*60*60*1000l;
if ( (author != null) && empty(post) && empty(thread) ) {
ArchiveIndex aindex = archive.getIndex();
PetNameDB db = user.getPetNameDB();
if ("favorites".equals(author)) {
for (Iterator nameIter = db.getNames().iterator(); nameIter.hasNext(); ) {
PetName pn = db.getByName((String)nameIter.next());
if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE) && AddressesServlet.PROTO_BLOG.equals(pn.getProtocol()) ) {
Hash loc = new Hash();
byte key[] = Base64.decode(pn.getLocation());
if ( (key != null) && (key.length == Hash.HASH_LENGTH) ) {
loc.setData(key);
aindex.selectMatchesOrderByEntryId(rv, loc, tags, dayBegin);
}
}
}
// always include ourselves...
aindex.selectMatchesOrderByEntryId(rv, user.getBlog(), tags, dayBegin);
Collections.sort(rv, BlogURI.COMPARATOR);
} else {
Hash loc = new Hash();
byte key[] = Base64.decode(author);
if ( (key != null) && (key.length == Hash.HASH_LENGTH) ) {
loc.setData(key);
aindex.selectMatchesOrderByEntryId(rv, loc, tags, dayBegin);
} else {
}
}
// how inefficient can we get?
if (threadAuthorOnly && (rv.size() > 0)) {
// lets filter out any posts that are not roots
for (int i = 0; i < rv.size(); i++) {
BlogURI curURI = (BlogURI)rv.get(i);
ThreadNode node = index.getNode(curURI);
if ( (node != null) && (node.getParent() == null) ) {
// ok, its a root
} else {
rv.remove(i);
i--;
}
}
}
}
BlogURI uri = getAsBlogURI(post);
if ( (uri != null) && (uri.getEntryId() > 0) ) {
rv.add(uri);
} else {
String thread = req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD);
uri = getAsBlogURI(thread);
if ( (uri != null) && (uri.getEntryId() > 0) ) {
ThreadNode node = index.getNode(uri);
if (node != null) {
while (node.getParent() != null)
node = node.getParent(); // hope the structure is loopless...
// depth first traversal
walkTree(rv, node);
if (false) {
// entire thread, as a depth first search
while (node.getParent() != null)
node = node.getParent(); // hope the structure is loopless...
// depth first traversal
walkTree(rv, node);
} else {
// only the "current" unforked thread, as suggested by cervantes.
// e.g.
// a--b--c--d
// \-e--f--g
// \-h
// would show "a--e--f--g" if node == {e, f, or g},
// or "a--b--c--d" if node == {a, b, c, or d},
// or "a--e--f--h" if node == h
rv.add(node.getEntry());
ThreadNode cur = node;
while (cur.getParent() != null) {
cur = cur.getParent();
rv.add(0, cur.getEntry()); // parents go before children...
}
cur = node;
while ( (cur != null) && (cur.getChildCount() > 0) ) {
cur = cur.getChild(0);
rv.add(cur.getEntry()); // and children after parents
}
}
} else {
rv.add(uri);
}
}
}
return rv;
}
@@ -99,6 +187,7 @@ public class ViewThreadedServlet extends BaseServlet {
}
out.write("</td><td class=\"threadNavRight\" nowrap=\"true\">\n");
out.write("<span class=\"rightOffset\">");
int max = index.getRootCount();
if (threadOffset + 10 > max) {
out.write("Next Page&gt; Last Page&gt;&gt;\n");
@@ -109,17 +198,18 @@ public class ViewThreadedServlet extends BaseServlet {
out.write(getNavLink(req, -1));
out.write("\">Last Page&gt;&gt;</a>\n");
}
out.write("<!-- thread nav end -->\n");
out.write("</span>");
//out.write("<!-- thread nav end -->\n");
out.write("</td></tr>\n");
}
private void renderThreadTree(User user, HttpServletRequest req, PrintWriter out, int threadOffset, BlogURI visibleEntry, Archive archive, ThreadIndex index) throws IOException {
private void renderThreadTree(User user, HttpServletRequest req, PrintWriter out, int threadOffset, BlogURI visibleEntry, Archive archive, ThreadIndex index, List visibleURIs) throws IOException {
int numThreads = 10;
renderThreadTree(user, out, index, archive, req, threadOffset, numThreads, visibleEntry);
renderThreadTree(user, out, index, archive, req, threadOffset, numThreads, visibleEntry, visibleURIs);
}
private void renderThreadTree(User user, PrintWriter out, ThreadIndex index, Archive archive, HttpServletRequest req,
int threadOffset, int numThreads, BlogURI visibleEntry) {
int threadOffset, int numThreads, BlogURI visibleEntry, List visibleURIs) {
if ( (visibleEntry != null) && (empty(req, ThreadedHTMLRenderer.PARAM_OFFSET)) ) {
// we want to jump to a specific thread in the nav
@@ -137,7 +227,7 @@ public class ViewThreadedServlet extends BaseServlet {
for (int curRoot = threadOffset; curRoot < numThreads + threadOffset; curRoot++) {
ThreadNode node = index.getRoot(curRoot);
out.write("<!-- thread begin curRoot=" + curRoot + " threadOffset=" + threadOffset + " -->\n");
renderThread(user, out, index, archive, req, node, 0, visibleEntry, state);
renderThread(user, out, index, archive, req, node, 0, visibleEntry, state, visibleURIs);
out.write("<!-- thread end -->\n");
written++;
}
@@ -149,9 +239,13 @@ public class ViewThreadedServlet extends BaseServlet {
}
private boolean renderThread(User user, PrintWriter out, ThreadIndex index, Archive archive, HttpServletRequest req,
ThreadNode node, int depth, BlogURI visibleEntry, TreeRenderState state) {
ThreadNode node, int depth, BlogURI visibleEntry, TreeRenderState state, List visibleURIs) {
boolean isFavorite = false;
boolean ignored = false;
boolean displayed = false;
if ( (visibleURIs != null) && (visibleURIs.contains(node.getEntry())) )
displayed = true;
HTMLRenderer rend = new HTMLRenderer(I2PAppContext.getGlobalContext());
SMLParser parser = new SMLParser(I2PAppContext.getGlobalContext());
@@ -171,9 +265,11 @@ public class ViewThreadedServlet extends BaseServlet {
else
out.write("<tr class=\"threadOdd\">\n");
out.write("<td class=\"threadFlag\">");
out.write("<td class=\"thread\" colspan=\"3\">");
out.write("<span class=\"threadInfoLeft\">");
//out.write("<td class=\"threadFlag\">");
out.write(getFlagHTML(user, node));
out.write("</td>\n<td class=\"threadLeft\">\n");
//out.write("</td>\n<td class=\"threadLeft\" colspan=\"2\">\n");
for (int i = 0; i < depth; i++)
out.write("<img src=\"images/threadIndent.png\" alt=\"\" border=\"0\" />");
@@ -212,6 +308,8 @@ public class ViewThreadedServlet extends BaseServlet {
out.write(getProfileLink(req, node.getEntry().getKeyHash()));
out.write("\" title=\"View the user's profile\">");
if (displayed) out.write("<b>");
if (pn == null) {
BlogInfo info = archive.getBlogInfo(node.getEntry().getKeyHash());
String name = null;
@@ -223,6 +321,9 @@ public class ViewThreadedServlet extends BaseServlet {
} else {
out.write(trim(pn.getName(), 30));
}
if (displayed) out.write("</b>");
out.write("</a>\n");
if ( (user.getBlog() != null) && (node.getEntry().getKeyHash().equals(user.getBlog())) ) {
@@ -243,24 +344,80 @@ public class ViewThreadedServlet extends BaseServlet {
}
}
out.write(" @ ");
out.write("<a href=\"");
out.write(getViewPostLink(req, node, user, false));
out.write("\" title=\"View post\">");
out.write(rend.getEntryDate(node.getEntry().getEntryId()));
out.write(": ");
out.write("<a href=\"");
if (false) {
out.write(getViewPostLink(req, node, user, false));
} else {
out.write(getViewThreadLink(req, node, user));
}
out.write("\" title=\"View post\">");
EntryContainer entry = archive.getEntry(node.getEntry());
if (entry == null) throw new RuntimeException("Unable to fetch the entry " + node.getEntry());
HeaderReceiver rec = new HeaderReceiver();
parser.parse(entry.getEntry().getText(), rec);
String subject = rec.getHeader(HTMLRenderer.HEADER_SUBJECT);
if (subject == null)
subject = "";
out.write(trim(subject, 40));
out.write("</a>\n</td><td class=\"threadRight\">\n");
out.write("<a href=\"");
if ( (subject == null) || (subject.trim().length() <= 0) )
subject = "(no subject)";
if (displayed) {
// currently being rendered
out.write("<b>");
out.write(trim(subject, 40));
out.write("</b>");
} else {
out.write(trim(subject, 40));
}
//out.write("</a>\n</td><td class=\"threadRight\">\n");
out.write("</a>");
if (false) {
out.write(" (<a href=\"");
out.write(getViewThreadLink(req, node, user));
out.write("\" title=\"View all posts in the thread\">full thread</a>)\n");
}
out.write("</span><span class=\"threadInfoRight\">");
out.write(" <a href=\"");
BlogURI newestURI = new BlogURI(node.getMostRecentPostAuthor(), node.getMostRecentPostDate());
if (false) {
out.write(getViewPostLink(req, newestURI, user));
} else {
List paths = new ArrayList();
paths.add(node);
ThreadNode cur = null;
while (paths.size() > 0) {
cur = (ThreadNode)paths.remove(0);
if (cur.getEntry().equals(newestURI))
break;
for (int i = cur.getChildCount() - 1; i >= 0; i--)
paths.add(cur.getChild(i));
if (paths.size() <= 0)
cur = null;
}
if (cur != null)
out.write(getViewThreadLink(req, cur, user));
}
out.write("\" title=\"View the most recent post\">latest - ");
long dayBegin = BlogManager.instance().getDayBegin();
long postId = node.getMostRecentPostDate();
if (postId >= dayBegin) {
out.write("<b>today</b>");
} else if (postId >= dayBegin - 24*60*60*1000) {
out.write("<b>yesterday</b>");
} else {
int daysAgo = (int)((dayBegin - postId + 24*60*60*1000-1)/(24*60*60*1000));
out.write(daysAgo + " days ago");
}
out.write("</a>\n");
/*
out.write(" <a href=\"");
out.write(getViewThreadLink(req, node, user));
out.write("\" title=\"View all posts in the thread\">view thread</a>\n");
out.write("\" title=\"View all posts in the thread\">full thread</a>\n");
*/
out.write("</span>");
out.write("</td></tr>\n");
boolean rendered = true;
@@ -268,7 +425,7 @@ public class ViewThreadedServlet extends BaseServlet {
if (showChildren) {
for (int i = 0; i < node.getChildCount(); i++) {
ThreadNode child = node.getChild(i);
boolean childRendered = renderThread(user, out, index, archive, req, child, depth+1, visibleEntry, state);
boolean childRendered = renderThread(user, out, index, archive, req, child, depth+1, visibleEntry, state, visibleURIs);
rendered = rendered || childRendered;
}
}

View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure 1.2//EN" "http://jetty.mortbay.org/configure_1_2.dtd">
<!-- =============================================================== -->
<!-- Configure the Jetty Server -->
<!-- =============================================================== -->
<Configure class="org.mortbay.jetty.Server">
<!-- =============================================================== -->
<!-- Configure the Request Listeners -->
<!-- =============================================================== -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Add and configure a HTTP listener to port 8080 -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<Call name="addListener">
<Arg>
<New class="org.mortbay.http.SocketListener">
<Arg>
<New class="org.mortbay.util.InetAddrPort">
<Set name="host">0.0.0.0</Set>
<Set name="port">8001</Set>
</New>
</Arg>
<Set name="MinThreads">3</Set>
<Set name="MaxThreads">10</Set>
<Set name="MaxIdleTimeMs">30000</Set>
<Set name="LowResourcePersistTimeMs">1000</Set>
<Set name="ConfidentialPort">8443</Set>
<Set name="IntegralPort">8443</Set>
<Set name="PoolName">main</Set>
</New>
</Arg>
</Call>
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Add a HTTPS SSL listener on port 8443 -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- UNCOMMENT TO ACTIVATE
<Call name="addListener">
<Arg>
<New class="org.mortbay.http.SunJsseListener">
<Set name="Port">8443</Set>
<Set name="PoolName">main</Set>
<Set name="Keystore"><SystemProperty name="jetty.home" default="."/>/etc/demokeystore</Set>
<Set name="Password">OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4</Set>
<Set name="KeyPassword">OBF:1u2u1wml1z7s1z7a1wnl1u2g</Set>
<Set name="NonPersistentUserAgent">MSIE 5</Set>
</New>
</Arg>
</Call>
-->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Add a AJP13 listener on port 8009 -->
<!-- This protocol can be used with mod_jk in apache, IIS etc. -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!--
<Call name="addListener">
<Arg>
<New class="org.mortbay.http.ajp.AJP13Listener">
<Set name="PoolName">ajp</Set>
<Set name="Port">8009</Set>
<Set name="MinThreads">3</Set>
<Set name="MaxThreads">20</Set>
<Set name="MaxIdleTimeMs">0</Set>
<Set name="confidentialPort">443</Set>
</New>
</Arg>
</Call>
-->
<!-- =============================================================== -->
<!-- Configure the Contexts -->
<!-- =============================================================== -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Add a all web application within the webapps directory. -->
<!-- + No virtual host specified -->
<!-- + Look in the webapps directory relative to jetty.home or . -->
<!-- + Use the default webdefault.xml in jetty's install -->
<!-- + Upack the war file -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<Set name="rootWebApp">syndie</Set>
<Call name="addWebApplication">
<Arg>/</Arg>
<Arg>syndie.war</Arg>
</Call>
<!-- =============================================================== -->
<!-- Configure the Request Log -->
<!-- =============================================================== -->
<Set name="RequestLog">
<New class="org.mortbay.http.NCSARequestLog">
<Arg>./logs/yyyy_mm_dd.syndie-request.log</Arg>
<Set name="retainDays">90</Set>
<Set name="append">true</Set>
<Set name="extended">false</Set>
<Set name="buffered">false</Set>
<Set name="LogTimeZone">GMT</Set>
</New>
</Set>
<!-- =============================================================== -->
<!-- Configure the Other Server Options -->
<!-- =============================================================== -->
<Set name="requestsPerGC">2000</Set>
<Set name="statsOn">false</Set>
</Configure>

Some files were not shown because too many files have changed in this diff Show More