diff --git a/LICENSE.txt b/LICENSE.txt
index a4bd67dcf1d27197eb682e436bb61be263b3a836..e3b631a3bae576f9d09254ddcc45e485b719babf 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -68,6 +68,10 @@ Public domain except as listed below:
    Copyright (C) 2001, 2007 Free Software Foundation, Inc.
    See licenses/LICENSE-LGPLv2.1.txt
 
+   SSLEepGet:
+   Contains some code Copyright 2006 Sun Microsystems, Inc.
+   See licenses/LICENSE-InstallCert.txt
+
 
 Router:
 Public domain except as listed below:
diff --git a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
index ac471df80c321164d930f6f934af56e6defedb6c..7efc27ea5cae8eff4353dc94c3469054a70c663d 100644
--- a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
+++ b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java
@@ -49,6 +49,8 @@ import net.i2p.util.SecureFileOutputStream;
  */
 class ConfigParser {
 
+    private static final boolean isWindows = System.getProperty("os.name").startsWith("Win");
+
     /**
      * Strip the comments from a String. Lines that begin with '#' and ';' are
      * considered comments, as well as any part of a line after a '#'.
@@ -276,7 +278,8 @@ class ConfigParser {
      * Write contents of Map map to the File file. Output is written
      * with one key, value pair on each line, in the format: key=value.
      * Write to a temp file in the same directory and then rename, to not corrupt
-     * simultaneous accesses by the router.
+     * simultaneous accesses by the router. Except on Windows where renameTo()
+     * will fail if the target exists.
      *
      * @param map
      *            A Map to write to file.
@@ -286,14 +289,19 @@ class ConfigParser {
      *             if file cannot be written to.
      */
     public static void write(Map map, File file) throws IOException {
-        File tmp = SecureFile.createTempFile("hoststxt-", ".tmp", file.getAbsoluteFile().getParentFile());
-        ConfigParser
+        boolean success = false;
+        if (!isWindows) {
+            File tmp = SecureFile.createTempFile("temp-", ".tmp", file.getAbsoluteFile().getParentFile());
+            ConfigParser
                 .write(map, new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(tmp), "UTF-8")));
-        boolean success = tmp.renameTo(file);
+            success = tmp.renameTo(file);
+            if (!success) {
+                tmp.delete();
+                //System.out.println("Warning: addressbook rename fail from " + tmp + " to " + file);
+            }
+        }
         if (!success) {
             // hmm, that didn't work, try it the old way
-            System.out.println("Warning: addressbook rename fail from " + tmp + " to " + file);
-            tmp.delete();
             ConfigParser
                 .write(map, new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8")));
         }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/DataLoader.java b/apps/i2psnark/java/src/org/klomp/snark/DataLoader.java
new file mode 100644
index 0000000000000000000000000000000000000000..0bd974f53817d70ba46e9d9c249adaa5030ea94b
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/DataLoader.java
@@ -0,0 +1,14 @@
+package org.klomp.snark;
+
+/**
+ * Callback used to fetch data
+ * @since 0.8.2
+ */
+interface DataLoader
+{
+  /**
+   *  This is the callback that PeerConnectionOut calls to get the data from disk
+   *  @return bytes or null for errors
+   */
+    public byte[] loadData(int piece, int begin, int length);
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Message.java b/apps/i2psnark/java/src/org/klomp/snark/Message.java
index a91e5ca234a86dd6f912e280a948cb5a7f4f7694..cdde79a18191f7c2c65dc064d2ec300c311e0c6e 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Message.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Message.java
@@ -39,23 +39,28 @@ class Message
   final static byte REQUEST      = 6;
   final static byte PIECE        = 7;
   final static byte CANCEL       = 8;
+  final static byte EXTENSION    = 20;
   
   // Not all fields are used for every message.
   // KEEP_ALIVE doesn't have a real wire representation
   byte type;
 
   // Used for HAVE, REQUEST, PIECE and CANCEL messages.
+  // low byte used for EXTENSION message
   int piece;
 
   // Used for REQUEST, PIECE and CANCEL messages.
   int begin;
   int length;
 
-  // Used for PIECE and BITFIELD messages
+  // Used for PIECE and BITFIELD and EXTENSION messages
   byte[] data;
   int off;
   int len;
 
+  // Used to do deferred fetch of data
+  DataLoader dataLoader;
+
   SimpleTimer.TimedEvent expireEvent;
   
   /** Utility method for sending a message through a DataStream. */
@@ -68,6 +73,13 @@ class Message
         return;
       }
 
+    // Get deferred data
+    if (data == null && dataLoader != null) {
+        data = dataLoader.loadData(piece, begin, length);
+        if (data == null)
+            return;  // hmm will get retried, but shouldn't happen
+    }
+
     // Calculate the total length in bytes
 
     // Type is one byte.
@@ -85,8 +97,12 @@ class Message
     if (type == REQUEST || type == CANCEL)
       datalen += 4;
 
+    // length is 1 byte
+    if (type == EXTENSION)
+      datalen += 1;
+
     // add length of data for piece or bitfield array.
-    if (type == BITFIELD || type == PIECE)
+    if (type == BITFIELD || type == PIECE || type == EXTENSION)
       datalen += len;
 
     // Send length
@@ -105,8 +121,11 @@ class Message
     if (type == REQUEST || type == CANCEL)
         dos.writeInt(length);
 
+    if (type == EXTENSION)
+        dos.writeByte((byte) piece & 0xff);
+
     // Send actual data
-    if (type == BITFIELD || type == PIECE)
+    if (type == BITFIELD || type == PIECE || type == EXTENSION)
       dos.write(data, off, len);
   }
 
@@ -135,6 +154,8 @@ class Message
         return "PIECE(" + piece + "," + begin + "," + length + ")";
       case CANCEL:
         return "CANCEL(" + piece + "," + begin + "," + length + ")";
+      case EXTENSION:
+        return "EXTENSION(" + piece + ',' + data.length + ')';
       default:
         return "<UNKNOWN>";
       }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
index 55b6dcb2ca7a7c7682c56fd0f9da98b650d9e38e..e747332d8391a553935e519ab0cc4622202c73f4 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java
@@ -59,6 +59,11 @@ public class Peer implements Comparable
   private long uploaded_old[] = {-1,-1,-1};
   private long downloaded_old[] = {-1,-1,-1};
 
+  //  bytes per bt spec:                 0011223344556677
+  static final long OPTION_EXTENSION = 0x0000000000100000l;
+  static final long OPTION_FAST      = 0x0000000000000004l;
+  private long options;
+
   /**
    * Creates a disconnected peer given a PeerID, your own id and the
    * relevant MetaInfo.
@@ -285,9 +290,8 @@ public class Peer implements Comparable
     // Handshake write - header
     dout.write(19);
     dout.write("BitTorrent protocol".getBytes("UTF-8"));
-    // Handshake write - zeros
-    byte[] zeros = new byte[8];
-    dout.write(zeros);
+    // Handshake write - options
+    dout.writeLong(OPTION_EXTENSION);
     // Handshake write - metainfo hash
     byte[] shared_hash = metainfo.getInfoHash();
     dout.write(shared_hash);
@@ -312,8 +316,8 @@ public class Peer implements Comparable
                             + "'Bittorrent protocol', got '"
                             + bittorrentProtocol + "'");
     
-    // Handshake read - zeros
-    din.readFully(zeros);
+    // Handshake read - options
+    options = din.readLong();
     
     // Handshake read - metainfo hash
     bs = new byte[20];
@@ -325,6 +329,15 @@ public class Peer implements Comparable
     din.readFully(bs);
     if (_log.shouldLog(Log.DEBUG))
         _log.debug("Read the remote side's hash and peerID fully from " + toString());
+
+//  if ((options & OPTION_EXTENSION) != 0) {
+    if (options != 0) {
+        // send them something
+        if (_log.shouldLog(Log.DEBUG))
+            //_log.debug("Peer supports extension message, what should we say? " + toString());
+            _log.debug("Peer supports options 0x" + Long.toString(options, 16) + ", what should we say? " + toString());
+    }
+
     return bs;
   }
 
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
index 9acfb60e3c9edfd7d28e204e403a839ca4979135..d0abe06efdac40bfff9b047f5cdc03426d249339 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java
@@ -171,6 +171,13 @@ class PeerConnectionIn implements Runnable
                 if (_log.shouldLog(Log.DEBUG)) 
                     _log.debug("Received cancel(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName());
                 break;
+              case 20:  // Extension message
+                int id = din.readUnsignedByte();
+                byte[] payload = new byte[i-2];
+                din.readFully(payload);
+                ps.extensionMessage(id, payload);
+                if (_log.shouldLog(Log.DEBUG)) 
+                    _log.debug("Received extension message from " + peer + " on " + peer.metainfo.getName());
               default:
                 byte[] bs = new byte[i-1];
                 din.readFully(bs);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
index 81525a1ea82d2d7405f7ca60b824889a0fa0e6d2..8ab0d555944965bddc9b544108ada0446c54d63f 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java
@@ -430,6 +430,33 @@ class PeerConnectionOut implements Runnable
     return total;
   }
 
+  /** @since 0.8.2 */
+  void sendPiece(int piece, int begin, int length, DataLoader loader)
+  {
+      boolean sendNow = false;
+      // are there any cases where we should?
+
+      if (sendNow) {
+        // queue the real thing
+        byte[] bytes = loader.loadData(piece, begin, length);
+        if (bytes != null)
+            sendPiece(piece, begin, length, bytes);
+        return;
+      }
+
+      // queue a fake message... set everything up,
+      // except save the PeerState instead of the bytes.
+      Message m = new Message();
+      m.type = Message.PIECE;
+      m.piece = piece;
+      m.begin = begin;
+      m.length = length;
+      m.dataLoader = loader;
+      m.off = 0;
+      m.len = length;
+      addMessage(m);
+  }
+
   void sendPiece(int piece, int begin, int length, byte[] bytes)
   {
     Message m = new Message();
@@ -488,4 +515,16 @@ class PeerConnectionOut implements Runnable
           }
       }
   }
+
+  /** @since 0.8.2 */
+  void sendExtension(int id, byte[] bytes) {
+    Message m = new Message();
+    m.type = Message.EXTENSION;
+    m.piece = id;
+    m.data = bytes;
+    m.begin = 0;
+    m.length = bytes.length;
+    addMessage(m);
+
+  }
 }
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
index 4d34736a89463f1dca89b5bf27a779a989275c06..8ba5c89696f2581022274f98dd8076225a9f9213 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java
@@ -20,14 +20,20 @@
 
 package org.klomp.snark;
 
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 
 import net.i2p.I2PAppContext;
 import net.i2p.util.Log;
 
-class PeerState
+import org.klomp.snark.bencode.BDecoder;
+import org.klomp.snark.bencode.BEValue;
+
+class PeerState implements DataLoader
 {
   private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerState.class);
   final Peer peer;
@@ -201,13 +207,28 @@ class PeerState
         return;
       }
 
+    if (_log.shouldLog(Log.INFO))
+      _log.info("Queueing (" + piece + ", " + begin + ", "
+                + length + ")" + " to " + peer);
+
+    // don't load the data into mem now, let PeerConnectionOut do it
+    out.sendPiece(piece, begin, length, this);
+  }
+
+  /**
+   *  This is the callback that PeerConnectionOut calls
+   *
+   *  @return bytes or null for errors
+   *  @since 0.8.2
+   */
+  public byte[] loadData(int piece, int begin, int length) {
     byte[] pieceBytes = listener.gotRequest(peer, piece, begin, length);
     if (pieceBytes == null)
       {
         // XXX - Protocol error-> diconnect?
         if (_log.shouldLog(Log.WARN))
           _log.warn("Got request for unknown piece: " + piece);
-        return;
+        return null;
       }
 
     // More sanity checks
@@ -219,13 +240,13 @@ class PeerState
                       + ", " + begin
                       + ", " + length
                       + "' message from " + peer);
-        return;
+        return null;
       }
 
     if (_log.shouldLog(Log.INFO))
       _log.info("Sending (" + piece + ", " + begin + ", "
                 + length + ")" + " to " + peer);
-    out.sendPiece(piece, begin, length, pieceBytes);
+    return pieceBytes;
   }
 
   /**
@@ -413,6 +434,24 @@ class PeerState
     out.cancelRequest(piece, begin, length);
   }
 
+  /** @since 0.8.2 */
+  void extensionMessage(int id, byte[] bs)
+  {
+      if (id == 0) {
+          InputStream is = new ByteArrayInputStream(bs);
+          try {
+              BDecoder dec = new BDecoder(is);
+              BEValue bev = dec.bdecodeMap();
+              Map map = bev.getMap();
+              if (_log.shouldLog(Log.DEBUG))
+                  _log.debug("Got extension handshake message " + bev.toString());
+          } catch (Exception e) {}
+      } else {
+          if (_log.shouldLog(Log.DEBUG))
+              _log.debug("Got extended message type: " + id + " length: " + bs.length);
+      }
+  }
+
   void unknownMessage(int type, byte[] bs)
   {
     if (_log.shouldLog(Log.WARN))
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index 4ebdbac26d82730cdcd948c79b39ceb5addc87f3..b9f06c8778dbcd7158b2169d155cd4d54138f172 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -242,6 +242,13 @@ public class I2PSnarkServlet extends Default {
 
         List snarks = getSortedSnarks(req);
         String uri = req.getRequestURI();
+        boolean isForm = _manager.util().connected() || !snarks.isEmpty();
+        if (isForm) {
+            out.write("<form action=\"");
+            out.write(uri);
+            out.write("\" method=\"POST\">\n");
+            out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n");
+        }
         out.write(TABLE_HEADER);
         out.write("<img border=\"0\" src=\"/themes/snark/ubergine/images/status.png\"");
         out.write(" title=\"");
@@ -301,25 +308,17 @@ public class I2PSnarkServlet extends Default {
         out.write("</th>\n");
         out.write("<th align=\"center\">");
         if (_manager.util().connected()) {
-            out.write("<a href=\"" + uri + "?action=StopAll&nonce=" + _nonce +
-                      "\" title=\"");
-            out.write(_("Stop all torrents and the I2P tunnel"));
-            out.write("\">");
-            out.write("<img src=\"/themes/snark/ubergine/images/stop_all.png\" title=\"");
+            out.write("<input type=\"image\" name=\"action\" value=\"StopAll\" title=\"");
             out.write(_("Stop all torrents and the I2P tunnel"));
-            out.write("\" alt=\"");
+            out.write("\" src=\"/themes/snark/ubergine/images/stop_all.png\" alt=\"");
             out.write(_("Stop All"));
             out.write("\">");
-            out.write("</a>");
         } else if (!snarks.isEmpty()) {
-            out.write("<a href=\"" + uri + "?action=StartAll&nonce=" + _nonce +
-                      "\" title=\"");
+            out.write("<input type=\"image\" name=\"action\" value=\"StartAll\" title=\"");
             out.write(_("Start all torrents and the I2P tunnel"));
+            out.write("\" src=\"/themes/snark/ubergine/images/start_all.png\" alt=\"");
+            out.write(_("Start All"));
             out.write("\">");
-            out.write("<img src=\"/themes/snark/ubergine/images/start_all.png\" title=\"");
-            out.write(_("Start all torrents and the I2P tunnel"));
-            out.write("\" alt=\"Start All\">");
-            out.write("</a>");
         } else {
             out.write("&nbsp;");
         }
@@ -357,6 +356,8 @@ public class I2PSnarkServlet extends Default {
         }
         
         out.write("</table>");
+        if (isForm)
+            out.write("</form>\n");
     }
     
     /**
@@ -366,7 +367,11 @@ public class I2PSnarkServlet extends Default {
         String action = req.getParameter("action");
         if (action == null) {
             // noop
-        } else if ("Add".equals(action)) {
+            return;
+        }
+        if (!"POST".equals(req.getMethod()))
+            return;
+        if ("Add".equals(action)) {
             String newFile = req.getParameter("newFile");
             String newURL = req.getParameter("newURL");
             // NOTE - newFile currently disabled in HTML form - see below
@@ -410,8 +415,8 @@ public class I2PSnarkServlet extends Default {
             } else {
                 // no file or URL specified
             }
-        } else if ("Stop".equals(action)) {
-            String torrent = req.getParameter("torrent");
+        } else if (action.startsWith("Stop_")) {
+            String torrent = action.substring(5);
             if (torrent != null) {
                 byte infoHash[] = Base64.decode(torrent);
                 if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
@@ -425,8 +430,8 @@ public class I2PSnarkServlet extends Default {
                     }
                 }
             }
-        } else if ("Start".equals(action)) {
-            String torrent = req.getParameter("torrent");
+        } else if (action.startsWith("Start_")) {
+            String torrent = action.substring(6);
             if (torrent != null) {
                 byte infoHash[] = Base64.decode(torrent);
                 if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
@@ -441,8 +446,8 @@ public class I2PSnarkServlet extends Default {
                     }
                 }
             }
-        } else if ("Remove".equals(action)) {
-            String torrent = req.getParameter("torrent");
+        } else if (action.startsWith("Remove_")) {
+            String torrent = action.substring(7);
             if (torrent != null) {
                 byte infoHash[] = Base64.decode(torrent);
                 if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
@@ -461,8 +466,8 @@ public class I2PSnarkServlet extends Default {
                     }
                 }
             }
-        } else if ("Delete".equals(action)) {
-            String torrent = req.getParameter("torrent");
+        } else if (action.startsWith("Delete_")) {
+            String torrent = action.substring(7);
             if (torrent != null) {
                 byte infoHash[] = Base64.decode(torrent);
                 if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
@@ -618,10 +623,10 @@ public class I2PSnarkServlet extends Default {
             if (r.startsWith(skip))
                 r = r.substring(skip.length());
             String llc = l.toLowerCase();
-            if (llc.startsWith("the ") || llc.startsWith("the."))
+            if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_"))
                 l = l.substring(4);
             String rlc = r.toLowerCase();
-            if (rlc.startsWith("the ") || rlc.startsWith("the."))
+            if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_"))
                 r = r.substring(4);
             return collator.compare(l, r);
         }
@@ -828,62 +833,55 @@ public class I2PSnarkServlet extends Default {
         out.write("</td>\n\t");
         out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">");
         String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.meta.getInfoHash());
+        String b64 = Base64.encode(snark.meta.getInfoHash());
         if (showPeers)
             parameters = parameters + "&p=1";
         if (isRunning) {
-            out.write("<a href=\"" + uri + "?action=Stop" + parameters
-                      + "\" title=\"");
-            out.write(_("Stop the torrent"));
-            out.write("\">");
-            out.write("<img src=\"/themes/snark/ubergine/images/stop.png\" title=\"");
+            out.write("<input type=\"image\" name=\"action\" value=\"Stop_");
+            out.write(b64);
+            out.write("\" title=\"");
             out.write(_("Stop the torrent"));
-            out.write("\" alt=\"");
+            out.write("\" src=\"/themes/snark/ubergine/images/stop.png\" alt=\"");
             out.write(_("Stop"));
             out.write("\">");
-            out.write("</a>");
         } else {
             if (isValid) {
-                out.write("<a href=\"" + uri + "?action=Start" + parameters
-                          + "\" title=\"");
+                out.write("<input type=\"image\" name=\"action\" value=\"Start_");
+                out.write(b64);
+                out.write("\" title=\"");
                 out.write(_("Start the torrent"));
-                out.write("\">");
-                out.write("<img src=\"/themes/snark/ubergine/images/start.png\" title=\"");
-                out.write(_("Start the torrent"));
-                out.write("\" alt=\"");
+                out.write("\" src=\"/themes/snark/ubergine/images/start.png\" alt=\"");
                 out.write(_("Start"));
                 out.write("\">");
-                out.write("</a>");
             }
-            out.write("<a href=\"" + uri + "?action=Remove" + parameters
-                      + "\" title=\"");
+
+            out.write("<input type=\"image\" name=\"action\" value=\"Remove_");
+            out.write(b64);
+            out.write("\" title=\"");
             out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
             out.write("\" onclick=\"if (!confirm('");
             // Can't figure out how to escape double quotes inside the onclick string.
             // Single quotes in translate strings with parameters must be doubled.
             // Then the remaining single quite must be escaped
             out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
-            out.write("')) { return false; }\">");
-            out.write("<img src=\"/themes/snark/ubergine/images/remove.png\" title=\"");
-            out.write(_("Remove the torrent from the active list, deleting the .torrent file"));
-            out.write("\" alt=\"");
+            out.write("')) { return false; }\"");
+            out.write(" src=\"/themes/snark/ubergine/images/remove.png\" alt=\"");
             out.write(_("Remove"));
             out.write("\">");
-            out.write("</a>");
-            out.write("<a href=\"" + uri + "?action=Delete" + parameters
-                      + "\" title=\"");
+
+            out.write("<input type=\"image\" name=\"action\" value=\"Delete_");
+            out.write(b64);
+            out.write("\" title=\"");
             out.write(_("Delete the .torrent file and the associated data file(s)"));
             out.write("\" onclick=\"if (!confirm('");
             // Can't figure out how to escape double quotes inside the onclick string.
             // Single quotes in translate strings with parameters must be doubled.
             // Then the remaining single quite must be escaped
             out.write(_("Are you sure you want to delete the torrent \\''{0}\\'' and all downloaded data?", fullFilename));
-            out.write("')) { return false; }\">");
-            out.write("<img src=\"/themes/snark/ubergine/images/delete.png\" title=\"");
-            out.write(_("Delete the .torrent file and the associated data file(s)"));
-            out.write("\" alt=\"");
+            out.write("')) { return false; }\"");
+            out.write(" src=\"/themes/snark/ubergine/images/delete.png\" alt=\"");
             out.write(_("Delete"));
             out.write("\">");
-            out.write("</a>");
         }
         out.write("</td>\n</tr>\n");
 
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHandler.java
index a0a3df8c9d4b15f40020cf86b7d612b3651656ce..5af095d2c49489eec5101cf5e977eac81b5c9aa5 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigPeerHandler.java
@@ -36,7 +36,7 @@ public class ConfigPeerHandler extends FormHandler {
                 return;
             }
             addFormError(_("Invalid peer"));
-        } else if (_action.equals(_("Adjust Profile Bonuses"))) {
+        } else if (_action.equals(_("Adjust peer bonuses"))) {
             Hash h = getHash();
             if (h != null) {
                 PeerProfile prof = _context.profileOrganizer().getProfile(h);
@@ -59,6 +59,8 @@ public class ConfigPeerHandler extends FormHandler {
             addFormError(_("Invalid peer"));
         } else if (_action.startsWith("Check")) {
             addFormError(_("Unsupported"));
+        } else {
+            addFormError("Unknown action \"" + _action + '"');
         }
     }
     
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/FormHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/FormHandler.java
index f064c5abcd59f32188f4b18fc65bd778a518d4fd..88abf360db641b6baf3c2e4cff634895d3667594 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/FormHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/FormHandler.java
@@ -20,26 +20,23 @@ public class FormHandler {
     protected Log _log;
     private String _nonce;
     protected String _action;
+    protected String _method;
     protected String _passphrase;
-    private List<String> _errors;
-    private List<String> _notices;
+    private final List<String> _errors;
+    private final List<String> _notices;
     private boolean _processed;
     private boolean _valid;
     
     public FormHandler() {
         _errors = new ArrayList();
         _notices = new ArrayList();
-        _action = null;
-        _processed = false;
         _valid = true;
-        _nonce = null;
-        _passphrase = null;
     }
     
     /**
      * Configure this bean to query a particular router context
      *
-     * @param contextId begging few characters of the routerHash, or null to pick
+     * @param contextId beginning few characters of the routerHash, or null to pick
      *                  the first one we come across.
      */
     public void setContextId(String contextId) {
@@ -54,6 +51,14 @@ public class FormHandler {
     public void setNonce(String val) { _nonce = val; }
     public void setAction(String val) { _action = val; }
     public void setPassphrase(String val) { _passphrase = val; }
+
+    /**
+     * Call this to prevent changes using GET
+     *
+     * @param the request method
+     * @since 0.8.2
+     */
+    public void storeMethod(String val) { _method = val; }
     
     /**
      * Override this to perform the final processing (in turn, adding formNotice
@@ -145,6 +150,12 @@ public class FormHandler {
             _valid = false;
             return;
         }
+        // To prevent actions with GET, jsps must call storeMethod()
+        if (_method != null && !"POST".equals(_method)) {
+            addFormError("Invalid form submission, requires POST not " + _method);
+            _valid = false;
+            return;
+        }
         
         String sharedNonce = System.getProperty("router.consoleNonce");
         if ( (sharedNonce != null) && (sharedNonce.equals(_nonce) ) ) {
@@ -211,4 +222,8 @@ public class FormHandler {
         return Messages.getString(s, o, _context);
     }
 
+    /** two params @since 0.8.2 */
+    public String _(String s, Object o, Object o2) {
+        return Messages.getString(s, o, o2, _context);
+    }
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java
index 31b67dc81332ba73f054a2846e1ed00921c9c277..b3ce2fa83237dce542606c94fc40746a6bbb15ac 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/GraphHelper.java
@@ -1,6 +1,7 @@
 package net.i2p.router.web;
 
 import java.io.IOException;
+import java.io.Writer;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
@@ -9,7 +10,8 @@ import java.util.TreeSet;
 import net.i2p.data.DataHelper;
 import net.i2p.stat.Rate;
 
-public class GraphHelper extends HelperBase {
+public class GraphHelper extends FormHandler {
+    protected Writer _out;
     private int _periodCount;
     private boolean _showEvents;
     private int _width;
@@ -29,9 +31,6 @@ public class GraphHelper extends HelperBase {
     static final int MAX_Y = 1024;
     private static final int MIN_REFRESH = 15;
     
-    public GraphHelper() {
-    }
-
     /** set the defaults after we have a context */
     @Override
     public void setContextId(String contextId) {
@@ -43,6 +42,12 @@ public class GraphHelper extends HelperBase {
         _showEvents = Boolean.valueOf(_context.getProperty(PROP_EVENTS)).booleanValue();
     }
     
+    /**
+     *  This was a HelperBase but now it's a FormHandler
+     *  @since 0.8.2
+     */
+    public void storeWriter(Writer out) { _out = out; }
+
     public void setPeriodCount(String str) { 
         try { _periodCount = Integer.parseInt(str); } catch (NumberFormatException nfe) {}
     }
@@ -125,10 +130,15 @@ public class GraphHelper extends HelperBase {
     }
 
     public String getForm() { 
-        saveSettings();
+        String prev = System.getProperty("net.i2p.router.web.GraphHelper.nonce");
+        if (prev != null) System.setProperty("net.i2p.router.web.GraphHelper.noncePrev", prev);
+        String nonce = "" + _context.random().nextLong();
+        System.setProperty("net.i2p.router.web.GraphHelper.nonce", nonce);
         try {
             _out.write("<br><h3>" + _("Configure Graph Display") + " [<a href=\"configstats\">" + _("Select Stats") + "</a>]</h3>");
-            _out.write("<form action=\"graphs\" method=\"POST\">");
+            _out.write("<form action=\"graphs\" method=\"POST\">\n" +
+                       "<input type=\"hidden\" name=\"action\" value=\"foo\">\n" +
+                       "<input type=\"hidden\" name=\"nonce\" value=\"" + nonce + "\" >\n");
             _out.write(_("Periods") + ": <input size=\"3\" type=\"text\" name=\"periodCount\" value=\"" + _periodCount + "\"><br>\n");
             _out.write(_("Plot averages") + ": <input type=\"radio\" class=\"optbox\" name=\"showEvents\" value=\"false\" " + (_showEvents ? "" : "checked=\"true\" ") + "> ");
             _out.write(_("or")+ " " +_("plot events") + ": <input type=\"radio\" class=\"optbox\" name=\"showEvents\" value=\"true\" "+ (_showEvents ? "checked=\"true\" " : "") + "><br>\n");
@@ -143,6 +153,15 @@ public class GraphHelper extends HelperBase {
         return ""; 
     }
 
+    /**
+     *  This was a HelperBase but now it's a FormHandler
+     *  @since 0.8.2
+     */
+    @Override
+    protected void processForm() {
+        saveSettings();
+    }
+
     /**
      *  Silently save settings if changed, no indication of success or failure
      *  @since 0.7.10
@@ -159,6 +178,7 @@ public class GraphHelper extends HelperBase {
             _context.router().setConfigSetting(PROP_REFRESH, "" + _refreshDelaySeconds);
             _context.router().setConfigSetting(PROP_EVENTS, "" + _showEvents);
             _context.router().saveConfig();
+            addFormNotice(_("Graph settings saved"));
         }
     }
 
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java b/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java
index 802e059adf280f3daf6e9a7aa5f07ccc5dbd114e..8ceec1098b1a2acf44debf7b8cdfb94f66c51583 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/HelperBase.java
@@ -28,7 +28,13 @@ public abstract class HelperBase {
     /** might be useful in the jsp's */
     //public RouterContext getContext() { return _context; }
 
-    public void setWriter(Writer out) { _out = out; }
+
+    /**
+     *  Renamed from setWriter, we realy don't want setFoo(non-String)
+     *  Prevent jsp.error.beans.property.conversion 500 error for ?writer=foo
+     *  @since 0.8.2
+     */
+    public void storeWriter(Writer out) { _out = out; }
 
     /** translate a string */
     public String _(String s) {
diff --git a/apps/routerconsole/jsp/config.jsp b/apps/routerconsole/jsp/config.jsp
index 2cb8a3aed8cde690f8b6795249ddf7a33462c736..74f1bf4656eacd21986d6067023659e79b6f8578 100644
--- a/apps/routerconsole/jsp/config.jsp
+++ b/apps/routerconsole/jsp/config.jsp
@@ -16,6 +16,7 @@
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigNetHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="*" />
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:getProperty name="formhandler" property="allMessages" />
diff --git a/apps/routerconsole/jsp/configadvanced.jsp b/apps/routerconsole/jsp/configadvanced.jsp
index 566d0c1bad29ffef47acd2b3c723b110fb3fadb7..0381aef9ab62c0f8ff22a55da27a99999e47411b 100644
--- a/apps/routerconsole/jsp/configadvanced.jsp
+++ b/apps/routerconsole/jsp/configadvanced.jsp
@@ -18,6 +18,7 @@
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigAdvancedHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="*" />
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:getProperty name="formhandler" property="allMessages" />
diff --git a/apps/routerconsole/jsp/configclients.jsp b/apps/routerconsole/jsp/configclients.jsp
index 49430e2065b1f8697dc1b83591aa1b617cdfc7bd..4f633c8949a3438e8c6e2f21ba90709d7afcdba9 100644
--- a/apps/routerconsole/jsp/configclients.jsp
+++ b/apps/routerconsole/jsp/configclients.jsp
@@ -21,6 +21,7 @@ button span.hide{
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigClientsHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:setProperty name="formhandler" property="action" value="<%=request.getParameter("action")%>" />
  <jsp:setProperty name="formhandler" property="nonce" value="<%=request.getParameter("nonce")%>" />
diff --git a/apps/routerconsole/jsp/configkeyring.jsp b/apps/routerconsole/jsp/configkeyring.jsp
index 84cd543845ba7e2d8458740ec2dd463c6e5c52a3..b260f843a4e60c8f7ee83379989fec1ec43c7d52 100644
--- a/apps/routerconsole/jsp/configkeyring.jsp
+++ b/apps/routerconsole/jsp/configkeyring.jsp
@@ -13,6 +13,7 @@
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigKeyringHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="*" />
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:getProperty name="formhandler" property="allMessages" />
diff --git a/apps/routerconsole/jsp/configlogging.jsp b/apps/routerconsole/jsp/configlogging.jsp
index a90c362d73112b3a8b5e28eabb86b292c8d1e163..c87fd367db1845f070a5f759ef1ba36b5471746e 100644
--- a/apps/routerconsole/jsp/configlogging.jsp
+++ b/apps/routerconsole/jsp/configlogging.jsp
@@ -15,6 +15,7 @@
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigLoggingHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="*" />
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:getProperty name="formhandler" property="allMessages" />
diff --git a/apps/routerconsole/jsp/confignav.jsi b/apps/routerconsole/jsp/confignav.jsi
index adbf46fc78482a1ef6ca2c0c0c00a998a79c3cd1..87d0ec5b8c3c87da77cbee480c21908a9c63d18a 100644
--- a/apps/routerconsole/jsp/confignav.jsi
+++ b/apps/routerconsole/jsp/confignav.jsi
@@ -5,7 +5,7 @@
 %>
 <jsp:useBean class="net.i2p.router.web.ConfigNavHelper" id="navHelper" scope="request" />
 <jsp:setProperty name="navHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
-<jsp:setProperty name="navHelper" property="writer" value="<%=out%>" />
+<% navHelper.storeWriter(out); %>
 <div class="confignav" id="confignav">
 <center>
 <%
diff --git a/apps/routerconsole/jsp/configpeer.jsp b/apps/routerconsole/jsp/configpeer.jsp
index 0dfb550851b9a52c74fe057561a2534df622bb8a..188ca56d4d9b68eb4f131a54e59cc6c3b263092f 100644
--- a/apps/routerconsole/jsp/configpeer.jsp
+++ b/apps/routerconsole/jsp/configpeer.jsp
@@ -13,6 +13,7 @@
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigPeerHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="*" />
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:getProperty name="formhandler" property="allMessages" />
@@ -27,7 +28,7 @@
         peer = net.i2p.data.DataHelper.stripHTML(request.getParameter("peer"));  // XSS
  %>
  <div class="configure">
- <form action="" method="POST">
+ <form action="configpeer" method="POST">
  <% String prev = System.getProperty("net.i2p.router.web.ConfigPeerHandler.nonce");
     if (prev != null) System.setProperty("net.i2p.router.web.ConfigPeerHandler.noncePrev", prev);
     System.setProperty("net.i2p.router.web.ConfigPeerHandler.nonce", new java.util.Random().nextLong()+""); %>
@@ -64,7 +65,7 @@
  <a name="shitlist"> </a><h2><%=intl._("Banned Peers")%></h2>
  <jsp:useBean class="net.i2p.router.web.ProfilesHelper" id="profilesHelper" scope="request" />
  <jsp:setProperty name="profilesHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
- <jsp:setProperty name="profilesHelper" property="writer" value="<%=out%>" />
+ <% profilesHelper.storeWriter(out); %>
  <jsp:getProperty name="profilesHelper" property="shitlistSummary" />
  <div class="wideload"><h2><%=intl._("Banned IPs")%></h2>
  <jsp:getProperty name="peerhelper" property="blocklistSummary" />
diff --git a/apps/routerconsole/jsp/configservice.jsp b/apps/routerconsole/jsp/configservice.jsp
index d71c8ee39979541ea45ae7a85c693809f0f22655..f87479a92feca0339b59a678b3c5d8f18f178ab4 100644
--- a/apps/routerconsole/jsp/configservice.jsp
+++ b/apps/routerconsole/jsp/configservice.jsp
@@ -13,6 +13,7 @@
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigServiceHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="*" />
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:getProperty name="formhandler" property="allMessages" />
diff --git a/apps/routerconsole/jsp/configstats.jsp b/apps/routerconsole/jsp/configstats.jsp
index 1f3dd6354d8e98012db0df5b4c7962df319a6a95..fde10837db0c98212924fa8b488aa4e315367e37 100644
--- a/apps/routerconsole/jsp/configstats.jsp
+++ b/apps/routerconsole/jsp/configstats.jsp
@@ -58,8 +58,9 @@ function toggleAll(category)
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigStatsHandler" id="formhandler" scope="request" />
- <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="*" />
+ <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:getProperty name="formhandler" property="allMessages" />
 
  <jsp:useBean class="net.i2p.router.web.ConfigStatsHelper" id="statshelper" scope="request" />
diff --git a/apps/routerconsole/jsp/configtunnels.jsp b/apps/routerconsole/jsp/configtunnels.jsp
index 9a6a11a3781fc370cf1ac16d9d99099102bfdba1..1c1393280a549b76bc9f4686934230256cf358f9 100644
--- a/apps/routerconsole/jsp/configtunnels.jsp
+++ b/apps/routerconsole/jsp/configtunnels.jsp
@@ -15,6 +15,7 @@
 <div class="main" id="main">
  <%@include file="confignav.jsi" %>
  <jsp:useBean class="net.i2p.router.web.ConfigTunnelsHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:setProperty name="formhandler" property="shouldsave" value="<%=request.getParameter("shouldsave")%>" />
  <jsp:setProperty name="formhandler" property="action" value="<%=request.getParameter("action")%>" />
diff --git a/apps/routerconsole/jsp/configui.jsp b/apps/routerconsole/jsp/configui.jsp
index f2a0f5b0fc77452f054acee0e4b5b02a2b874b79..9f3d32e4fada8cc7f0cb104b7895df1fdb02c14d 100644
--- a/apps/routerconsole/jsp/configui.jsp
+++ b/apps/routerconsole/jsp/configui.jsp
@@ -18,6 +18,7 @@
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigUIHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="*" />
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:getProperty name="formhandler" property="allMessages" />
diff --git a/apps/routerconsole/jsp/configupdate.jsp b/apps/routerconsole/jsp/configupdate.jsp
index 21ae4fedff26d5dc04af2d0a56d3132bcb5551fe..1930614218374158f8d7a011da36a6c55c363a31 100644
--- a/apps/routerconsole/jsp/configupdate.jsp
+++ b/apps/routerconsole/jsp/configupdate.jsp
@@ -13,6 +13,7 @@
  <%@include file="confignav.jsi" %>
 
  <jsp:useBean class="net.i2p.router.web.ConfigUpdateHandler" id="formhandler" scope="request" />
+ <% formhandler.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="formhandler" property="*" />
  <jsp:setProperty name="formhandler" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
  <jsp:getProperty name="formhandler" property="allMessages" />
diff --git a/apps/routerconsole/jsp/dumpprofile.jsp b/apps/routerconsole/jsp/dumpprofile.jsp
index f13a4bcd77e976a212fd273daa4b84d8202950ea..dfdcc0b6e52ffb3687165dce6ff2af104797b8b1 100644
--- a/apps/routerconsole/jsp/dumpprofile.jsp
+++ b/apps/routerconsole/jsp/dumpprofile.jsp
@@ -1,5 +1,5 @@
 <%@page contentType="text/plain"
 %><jsp:useBean id="helper" class="net.i2p.router.web.StatHelper"
 /><jsp:setProperty name="helper" property="peer" value="<%=request.getParameter("peer")%>"
-/><jsp:setProperty name="helper" property="writer" value="<%=out%>"
-/><jsp:getProperty name="helper" property="profile" />
\ No newline at end of file
+/><% helper.storeWriter(out);
+%><jsp:getProperty name="helper" property="profile" />
diff --git a/apps/routerconsole/jsp/graphs.jsp b/apps/routerconsole/jsp/graphs.jsp
index e3d5c01154f33b7c8c98cd14cb2b5778d911c36f..bbda259441a9564c05abfcd7bc3a193e23c389fc 100644
--- a/apps/routerconsole/jsp/graphs.jsp
+++ b/apps/routerconsole/jsp/graphs.jsp
@@ -13,9 +13,12 @@
  <div class="graphspanel">
  <div class="widepanel">
  <jsp:useBean class="net.i2p.router.web.GraphHelper" id="graphHelper" scope="request" />
+ <% graphHelper.storeMethod(request.getMethod()); %>
  <jsp:setProperty name="graphHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
+<% /* GraphHelper sets the defaults in setContextId, so setting the properties must be after the context */ %>
  <jsp:setProperty name="graphHelper" property="*" />
- <jsp:setProperty name="graphHelper" property="writer" value="<%=out%>" />
+ <% graphHelper.storeWriter(out); %>
+ <jsp:getProperty name="graphHelper" property="allMessages" />
  <jsp:getProperty name="graphHelper" property="images" />
  <jsp:getProperty name="graphHelper" property="form" />
 </div></div></div></body></html>
diff --git a/apps/routerconsole/jsp/i2psnark/index.html b/apps/routerconsole/jsp/i2psnark/index.html
index a1500fad320bcf8b33cec983ad1eeb4feb6892d9..681b946562776f185d74ae4946525aacd148ec96 100644
--- a/apps/routerconsole/jsp/i2psnark/index.html
+++ b/apps/routerconsole/jsp/i2psnark/index.html
@@ -3,6 +3,6 @@
 <meta http-equiv="pragma" content="no-cache" />
 </head>
 <body>
-The I2PSnark Anonymous BitTorrent Client is not running. Please visit the <a href="/configclients.jsp">config clients page</a>
+The I2PSnark Anonymous BitTorrent Client is not running. Please visit the <a href="/configclients#webapp">config clients page</a>
 to start it.
 </body></html>
diff --git a/apps/routerconsole/jsp/i2ptunnel/index.jsp b/apps/routerconsole/jsp/i2ptunnel/index.jsp
index f3cceda0d37cc6baf009e708846baad36df27e9f..db3648aa07e410fe3ed9fbfb80c45e739ee819b3 100644
--- a/apps/routerconsole/jsp/i2ptunnel/index.jsp
+++ b/apps/routerconsole/jsp/i2ptunnel/index.jsp
@@ -3,5 +3,5 @@
 <meta http-equiv="pragma" content="no-cache" />
 </head>
 <body>
-The I2P Tunnel Manager is not currently running. Please visit the <a href="/configclients.jsp">Client Configuration</a> page to start it.
+The I2P Tunnel Manager is not currently running. Please visit the <a href="/configclients#webapp">Client Configuration</a> page to start it.
 </body></html>
diff --git a/apps/routerconsole/jsp/jobs.jsp b/apps/routerconsole/jsp/jobs.jsp
index ef84c8f8c56daa82b66201402f88da2112302ee2..53916dcd44ed0d2ff65f9eb389b6edb66487974e 100644
--- a/apps/routerconsole/jsp/jobs.jsp
+++ b/apps/routerconsole/jsp/jobs.jsp
@@ -10,6 +10,6 @@
 <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%>" />
+ <% jobQueueHelper.storeWriter(out); %>
  <jsp:getProperty name="jobQueueHelper" property="jobQueueSummary" />
 <hr></div></body></html>
diff --git a/apps/routerconsole/jsp/logs.jsp b/apps/routerconsole/jsp/logs.jsp
index caefed97a9899d84e8b506df8ecfe68fc9a60026..75f052e9b22c3136aa20e926e2605c09f3242fa5 100644
--- a/apps/routerconsole/jsp/logs.jsp
+++ b/apps/routerconsole/jsp/logs.jsp
@@ -10,7 +10,8 @@
 <h1><%=intl._("I2P Router Logs")%></h1>
 <div class="main" id="main">
  <div class="joblog"><h3><%=intl._("I2P Version & Running Environment")%></h3><a name="version"> </a>
-<p><%=intl._("Please report bugs on <a href=\"http://trac.i2p2.i2p/newticket\">trac.i2p2.i2p</a>.")%>
+<p><%=intl._("Please report bugs on <a href=\"http://trac.i2p2.i2p/newticket\">trac.i2p2.i2p</a> or <a href=\"http://trac.i2p2.de/newticket\">trac.i2p2.de</a>.")%>
+<%=intl._("You may use the username \"guest\" and password \"guest\" if you do not wish to register.")%>
 <p><i><%=intl._("Please include this information in bug reports")%>:</i>
  <p>
 <b>I2P version:</b> <jsp:getProperty name="helper" property="version" /><br>
diff --git a/apps/routerconsole/jsp/netdb.jsp b/apps/routerconsole/jsp/netdb.jsp
index 9cbac25e74cc0f379819a3b205ab27701aac293c..2862e9e0e727df2c9502352bf92d2db603edb2ea 100644
--- a/apps/routerconsole/jsp/netdb.jsp
+++ b/apps/routerconsole/jsp/netdb.jsp
@@ -12,7 +12,7 @@
  <div class="wideload">
  <jsp:useBean class="net.i2p.router.web.NetDbHelper" id="netdbHelper" scope="request" />
  <jsp:setProperty name="netdbHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
- <jsp:setProperty name="netdbHelper" property="writer" value="<%=out%>" />
+ <% netdbHelper.storeWriter(out); %>
  <jsp:setProperty name="netdbHelper" property="full" value="<%=request.getParameter("f")%>" />
  <jsp:setProperty name="netdbHelper" property="router" value="<%=request.getParameter("r")%>" />
  <jsp:setProperty name="netdbHelper" property="lease" value="<%=request.getParameter("l")%>" />
diff --git a/apps/routerconsole/jsp/oldconsole.jsp b/apps/routerconsole/jsp/oldconsole.jsp
index 701d70f7d3f73d7344ef631dd7da5c4df73ae870..3612169e41f1ff9be503ca06e273206d7db3980b 100644
--- a/apps/routerconsole/jsp/oldconsole.jsp
+++ b/apps/routerconsole/jsp/oldconsole.jsp
@@ -12,7 +12,7 @@
 <%@include file="summary.jsi" %>
 <jsp:useBean class="net.i2p.router.web.OldConsoleHelper" id="conhelper" scope="request" />
 <jsp:setProperty name="conhelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
-<jsp:setProperty name="conhelper" property="writer" value="<%=out%>" />
+<% conhelper.storeWriter(out); %>
  <h1>I2P Router &raquo; Old Console</h1>
 <div class="main" id="main">
  <jsp:getProperty name="conhelper" property="console" />
diff --git a/apps/routerconsole/jsp/peers.jsp b/apps/routerconsole/jsp/peers.jsp
index c4b72b9315b9c09845c99c93a61bdf6379ad81c8..4b48c5957d56b214c2ba61e574e5903f22241736 100644
--- a/apps/routerconsole/jsp/peers.jsp
+++ b/apps/routerconsole/jsp/peers.jsp
@@ -11,7 +11,7 @@
 <div class="main" id="main"><div class="wideload">
  <jsp:useBean class="net.i2p.router.web.PeerHelper" id="peerHelper" scope="request" />
  <jsp:setProperty name="peerHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
- <jsp:setProperty name="peerHelper" property="writer" value="<%=out%>" />
+ <% peerHelper.storeWriter(out); %>
  <jsp:setProperty name="peerHelper" property="urlBase" value="peers.jsp" />
  <jsp:setProperty name="peerHelper" property="sort" value="<%=request.getParameter("sort") != null ? request.getParameter("sort") : ""%>" />
  <jsp:getProperty name="peerHelper" property="peerSummary" />
diff --git a/apps/routerconsole/jsp/profiles.jsp b/apps/routerconsole/jsp/profiles.jsp
index 69100e349b45336038bab79d5fcfe956101afc44..98ec4882818f2942d11676d4867e2244f1964b94 100644
--- a/apps/routerconsole/jsp/profiles.jsp
+++ b/apps/routerconsole/jsp/profiles.jsp
@@ -10,7 +10,7 @@
 <div class="main" id="main"><div class="wideload">
  <jsp:useBean class="net.i2p.router.web.ProfilesHelper" id="profilesHelper" scope="request" />
  <jsp:setProperty name="profilesHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
- <jsp:setProperty name="profilesHelper" property="writer" value="<%=out%>" />
+ <% profilesHelper.storeWriter(out); %>
  <jsp:setProperty name="profilesHelper" property="full" value="<%=request.getParameter("f")%>" />
  <jsp:getProperty name="profilesHelper" property="profileSummary" />
  <a name="shitlist"> </a><h2><%=intl._("Banned Peers")%></h2>
diff --git a/apps/routerconsole/jsp/stats.jsp b/apps/routerconsole/jsp/stats.jsp
index 61466d98617fb8b4a1b05991938148824b691c77..d2787dea968f2091ff1fdc4f5f57be5c0c20a959 100644
--- a/apps/routerconsole/jsp/stats.jsp
+++ b/apps/routerconsole/jsp/stats.jsp
@@ -9,7 +9,7 @@
 <%@include file="summary.jsi" %>
 <jsp:useBean class="net.i2p.router.web.OldConsoleHelper" id="oldhelper" scope="request" />
 <jsp:setProperty name="oldhelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
-<jsp:setProperty name="oldhelper" property="writer" value="<%=out%>" />
+<% oldhelper.storeWriter(out); %>
 <jsp:setProperty name="oldhelper" property="full" value="<%=request.getParameter("f")%>" />
  <h1><%=intl._("I2P Router Statistics")%></h1>
 <div class="main" id="main">
diff --git a/apps/routerconsole/jsp/summarynoframe.jsi b/apps/routerconsole/jsp/summarynoframe.jsi
index 33a029e7657d5bd04a8520b7d027bc8dec89b5e7..355f4bc0b24d08ed9971810dbcd42819fd8a7575 100644
--- a/apps/routerconsole/jsp/summarynoframe.jsi
+++ b/apps/routerconsole/jsp/summarynoframe.jsi
@@ -11,7 +11,7 @@
 <jsp:setProperty name="helper" property="updateNonce" value="<%=request.getParameter("updateNonce")%>" />
 <jsp:setProperty name="helper" property="consoleNonce" value="<%=request.getParameter("consoleNonce")%>" />
 <jsp:setProperty name="helper" property="requestURI" value="<%=request.getRequestURI()%>" />
-<jsp:setProperty name="helper" property="writer" value="<%=out%>" />
+<% helper.storeWriter(out); %>
 <%
 /*
  * The following is required for the reseed button to work, although we probably
diff --git a/apps/routerconsole/jsp/susidns/index.jsp b/apps/routerconsole/jsp/susidns/index.jsp
index 5fb267a83cae77f8d4d0677cdda69b35a827df6e..2964615e3153b78ca2dd880c574c22356a81f2b9 100644
--- a/apps/routerconsole/jsp/susidns/index.jsp
+++ b/apps/routerconsole/jsp/susidns/index.jsp
@@ -3,6 +3,6 @@
 <meta http-equiv="pragma" content="no-cache" />
 </head>
 <body>
-SusiDNS is not running. Go to <a href="/configclients.jsp">the config clients page</a>
+SusiDNS is not running. Go to <a href="/configclients#webapp">the config clients page</a>
 to start it.
 </body></html>
diff --git a/apps/routerconsole/jsp/susimail/susimail b/apps/routerconsole/jsp/susimail/susimail
index 9852921214f68858df775d54a37f8727a5d2eb79..32e9d3787b118f6051fa0ff0adb672c6fb47be0a 100644
--- a/apps/routerconsole/jsp/susimail/susimail
+++ b/apps/routerconsole/jsp/susimail/susimail
@@ -3,6 +3,6 @@
 <meta http-equiv="pragma" content="no-cache" />
 </head>
 <body>
-SusiMail is not running. Go to <a href="/configclients.jsp">the config clients page</a>
+SusiMail is not running. Go to <a href="/configclients#webapp">the config clients page</a>
 to start it.
 </body></html>
diff --git a/apps/routerconsole/jsp/tunnels.jsp b/apps/routerconsole/jsp/tunnels.jsp
index 63045519c574db9b8bf447c46184d98be5cb64de..e01b331f6bb2a2775b43f4d1b65579791c4665ca 100644
--- a/apps/routerconsole/jsp/tunnels.jsp
+++ b/apps/routerconsole/jsp/tunnels.jsp
@@ -10,6 +10,6 @@
 <div class="main" id="main">
  <jsp:useBean class="net.i2p.router.web.TunnelHelper" id="tunnelHelper" scope="request" />
  <jsp:setProperty name="tunnelHelper" property="contextId" value="<%=(String)session.getAttribute("i2p.contextId")%>" />
- <jsp:setProperty name="tunnelHelper" property="writer" value="<%=out%>" />
+ <% tunnelHelper.storeWriter(out); %>
  <jsp:getProperty name="tunnelHelper" property="tunnelSummary" />
 </div></body></html>
diff --git a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
index e662ecc60a95174749196a406bf4636b2cfe53aa..20b1a0f0169f9aec06187a48f43799018ed749ce 100644
--- a/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
+++ b/apps/streaming/java/src/net/i2p/client/streaming/Connection.java
@@ -98,6 +98,7 @@ public class Connection {
         _log = _context.logManager().getLog(Connection.class);
         _receiver = new ConnectionDataReceiver(_context, this);
         _inputStream = new MessageInputStream(_context);
+        // FIXME pass through a passive flush delay setting as the 4th arg
         _outputStream = new MessageOutputStream(_context, _receiver, (opts == null ? Packet.MAX_PAYLOAD_SIZE : opts.getMaxMessageSize()));
         _outboundPackets = new TreeMap();
         _options = (opts != null ? opts : new ConnectionOptions());
diff --git a/build.xml b/build.xml
index 0e749ce499ae1aff63cbb998d450c22041eb88bb..44011705d0a202d5adb04d7e3dca3287af708e34 100644
--- a/build.xml
+++ b/build.xml
@@ -245,7 +245,7 @@
         <delete file="debian/substvars"/>
     </target>
     <target name="distclean" depends="clean">
-        <delete includeemptydirs="true" removeNotFollowedSymlinks="true" failonerror="false" >
+        <delete includeemptydirs="true" failonerror="false" >
             <fileset dir="debian/packages" followSymlinks="false" />
         </delete>
         <delete dir="debian/repo" />
@@ -368,7 +368,7 @@
         </copy>
     </target>
 
-    <target name="preppkg-base" depends="build, preplicenses, prepconsoleDocs">
+    <target name="preppkg-base" depends="build, preplicenses, prepConsoleDocs, prepthemeupdates, prepCertificates">
         <!-- if updater200 was run previously, it left *.pack files in pkg-temp -->
         <delete>
             <fileset dir="pkg-temp" includes="**/*.jar.pack **/*.war.pack" />
@@ -419,10 +419,6 @@
         <copy file="installer/resources/start.ico" todir="pkg-temp/docs/" />
         <copy file="installer/resources/console.ico" todir="pkg-temp/docs/" />
         <copy file="installer/resources/uninstall.ico" todir="pkg-temp/docs/" />
-        <mkdir dir="pkg-temp/docs/themes/" />
-        <copy todir="pkg-temp/docs/themes/" >
-          <fileset dir="installer/resources/themes/" />
-        </copy>
         <!-- Eepsite stuff here -->
         <mkdir dir="pkg-temp/eepsite" />
         <mkdir dir="pkg-temp/eepsite/webapps" />
@@ -455,42 +451,17 @@
         <copy file="installer/lib/launch4j/lib/JGoodies.Looks.LICENSE.txt" tofile="pkg-temp/licenses/LICENSE-JGoodies-Looks.txt" />
         <copy file="installer/lib/launch4j/lib/XStream.LICENSE.txt" tofile="pkg-temp/licenses/LICENSE-XStream.txt" />
     </target>
+
     <target name="prepthemeupdates">
-        <!-- Migrated all Snark content to its own dir. Need to ensure snark dir excluded from console theme choices!! -->
-        <!-- Snark's visible Assets -->
-        <copy todir="pkg-temp/docs/themes/snark/ubergine/" >
-            <fileset dir="installer/resources/themes/snark/ubergine/" />
-        </copy>
-        <!-- No need to copy these individually, we're copying the whole dir below.. 
-        <copy file="installer/resources/themes/console/images/favicon.ico" todir="pkg-temp/docs/themes/console/images/" />
-        <copy file="installer/resources/themes/console/images/i2plogo.png" todir="pkg-temp/docs/themes/console/images/" />
-        -->
-        <!-- Since the logo moved, we have to update the error pages -->
-        <copy todir="pkg-temp/docs/" >
-          <fileset dir="installer/resources/proxy" />
-        </copy>
-        <!-- make a "classic" theme -->
-        <copy todir="pkg-temp/docs/themes/console/classic/" >
-            <fileset  dir="installer/resources/themes/console/classic/" />
-        </copy>
-        <!-- Add dark theme -->
-        <copy todir="pkg-temp/docs/themes/console/dark/" >
-            <fileset  dir="installer/resources/themes/console/dark/" />
-        </copy>
-        <!-- Add light theme -->
-        <copy todir="pkg-temp/docs/themes/console/light/" >
-            <fileset  dir="installer/resources/themes/console/light/" />
+        <copy todir="pkg-temp/docs/themes/" >
+            <fileset dir="installer/resources/themes/" />
         </copy>
-         <!-- Add midnight theme -->
-        <copy todir="pkg-temp/docs/themes/console/midnight/" >
-            <fileset  dir="installer/resources/themes/console/midnight/" />
-        </copy>        
-        <!-- Add shared images.. these are subject to flux and change! -->
-        <copy todir="pkg-temp/docs/themes/console/images/" >
-            <fileset  dir="installer/resources/themes/console/images/" />
-        </copy>          
-        <copy todir="pkg-temp/docs/" >
-          <fileset dir="installer/resources/readme/" includes="readme*.html" />
+    </target>
+
+    <!-- SSL Certs -->
+    <target name="prepCertificates">
+        <copy todir="pkg-temp/certificates/" >
+          <fileset dir="installer/resources/certificates/" />
         </copy>
     </target>
 
@@ -500,16 +471,23 @@
             <tarfileset dir="pkg-temp" includes="**/*" prefix="i2p" />
         </tar>
     </target>
+
     <target name="deletepkg-temp">
         <delete dir="pkg-temp" />
     </target>
-    <target name="prepconsoleDocs" depends="prepgeoupdate">
+
+    <!-- readme and proxy error page files, GeoIP files, and flag icons -->
+    <target name="prepConsoleDocs" depends="prepConsoleDocUpdates, prepgeoupdate" />
+
+    <!-- readme and proxy error page files -->
+    <target name="prepConsoleDocUpdates">
         <copy todir="pkg-temp/docs/" >
           <fileset dir="installer/resources/readme/" includes="readme*.html" />
-          <fileset dir="installer/resources/proxy" />
+          <fileset dir="installer/resources/proxy/" includes="*.ht" />
         </copy>
     </target>
-    <target name="consoleDocs" depends="deletepkg-temp, prepconsoleDocs">
+
+    <target name="consoleDocs" depends="deletepkg-temp, prepConsoleDocs">
         <zip destfile="docs.zip" basedir="pkg-temp" whenempty="fail" />
     </target>
 
@@ -560,7 +538,8 @@
         <copy file="core/java/build/i2ptest.jar" todir="pkg-temp/lib" />
         <zip destfile="i2pupdate.zip" basedir="pkg-temp" />
     </target>
-    <target name="prepupdate" depends="build2, prepupdateSmall">
+
+    <target name="prepupdate" depends="build2, prepupdateSmall, prepConsoleDocUpdates, prepCertificates">
         <copy file="build/BOB.jar" todir="pkg-temp/lib/" />
         <copy file="build/sam.jar" todir="pkg-temp/lib/" />
         <copy file="build/i2psnark.jar" todir="pkg-temp/lib" />
@@ -587,6 +566,7 @@
         <copy file="installer/resources/news.xml" todir="pkg-temp/docs/" />
        -->
     </target>
+
     <target name="prepupdateSmall" depends="buildSmall, prepupdateRouter, prepthemeupdates">
         <copy file="build/i2ptunnel.jar" todir="pkg-temp/lib/" />
         <copy file="build/mstreaming.jar" todir="pkg-temp/lib/" />
@@ -601,10 +581,13 @@
         <!-- decapitalized the file in 0.7.8 -->
        <copy file="installer/resources/countries.txt" todir="pkg-temp/geoip/" />
     </target>
+
     <target name="prepupdateRouter" depends="buildrouter, deletepkg-temp">
         <copy file="build/i2p.jar" todir="pkg-temp/lib/" />
         <copy file="build/router.jar" todir="pkg-temp/lib/" />
     </target>
+
+    <!-- GeoIP files and flag icons -->
     <target name="prepgeoupdate">
         <copy file="installer/resources/geoip.txt" todir="pkg-temp/geoip/" />
         <copy file="installer/resources/countries.txt" todir="pkg-temp/geoip/" />
@@ -612,6 +595,7 @@
           <fileset dir="installer/resources/icons/flags" />
         </copy>
     </target>
+
     <target name="prepjupdate" depends="prepupdate, buildWEB">
         <copy file="build/jasper-compiler.jar" todir="pkg-temp/lib/" />
         <copy file="build/jasper-runtime.jar" todir="pkg-temp/lib/" />
diff --git a/core/java/src/net/i2p/util/EepGet.java b/core/java/src/net/i2p/util/EepGet.java
index 5b3f25c1925103246a43e741f133965feb55d8bc..f055ce98cbbeb4594cc1a53d5de10bd4bf97a0a5 100644
--- a/core/java/src/net/i2p/util/EepGet.java
+++ b/core/java/src/net/i2p/util/EepGet.java
@@ -456,7 +456,7 @@ public class EepGet {
                 for (int i = 0; i < _listeners.size(); i++) 
                     _listeners.get(i).attemptFailed(_url, _bytesTransferred, _bytesRemaining, _currentAttempt, _numRetries, ioe);
                 if (_log.shouldLog(Log.WARN))
-                    _log.warn("ERR: doFetch failed " +  ioe);
+                    _log.warn("ERR: doFetch failed ", ioe);
                 if (ioe instanceof MalformedURLException)
                     _keepFetching = false;
             } finally {
diff --git a/core/java/src/net/i2p/util/SSLEepGet.java b/core/java/src/net/i2p/util/SSLEepGet.java
index 8a5bf364af9a3aa5cec0ed02c07fe9e34612b326..e3ba43ae7a3ad12138f924749fbecfef74b188c9 100644
--- a/core/java/src/net/i2p/util/SSLEepGet.java
+++ b/core/java/src/net/i2p/util/SSLEepGet.java
@@ -1,55 +1,131 @@
 package net.i2p.util;
 
+/*
+ * Contains code from:
+ * http://blogs.sun.com/andreas/resource/InstallCert.java
+ * http://blogs.sun.com/andreas/entry/no_more_unable_to_find
+ *
+ * ===============
+ *
+ * Copyright 2006 Sun Microsystems, Inc.  All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ *   - Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *
+ *   - Redistributions in binary form must reproduce the above copyright
+ *     notice, this list of conditions and the following disclaimer in the
+ *     documentation and/or other materials provided with the distribution.
+ *
+ *   - Neither the name of Sun Microsystems nor the names of its
+ *     contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.PrintWriter;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.security.KeyStore;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Enumeration;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.SSLSocketFactory;
-
-// all part of the CA experiment below
-//import java.io.FileInputStream;
-//import java.io.InputStream;
-//import java.util.Enumeration;
-//import java.security.KeyStore;
-//import java.security.GeneralSecurityException;
-//import java.security.cert.CertificateExpiredException;
-//import java.security.cert.CertificateNotYetValidException;
-//import java.security.cert.CertificateFactory;
-//import java.security.cert.X509Certificate;
-//import javax.net.ssl.KeyManagerFactory;
-//import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
 
 import net.i2p.I2PAppContext;
+import net.i2p.data.Base64;
 import net.i2p.data.DataHelper;
 
 /**
  * HTTPS only, non-proxied only, no retries, no min and max size options, no timeout option
  * Fails on 301 or 302 (doesn't follow redirect)
- * Fails on self-signed certs (must have a valid cert chain)
+ * Fails on bad certs (must have a valid cert chain)
+ * Self-signed certs or CAs not in the JVM key store must be loaded to be trusted.
+ *
+ * Since 0.8.2, loads additional trusted CA certs from $I2P/certificates/ and ~/.i2p/certificates/
  *
  * @author zzz
  * @since 0.7.10
  */
 public class SSLEepGet extends EepGet {
-    //private static SSLContext _sslContext;
+    /** if true, save cert chain on cert error */
+    private boolean _saveCerts;
+    /** true if called from main(), used for logging */
+    private boolean _commandLine;
+    /** may be null if init failed */
+    private final SSLContext _sslContext;
+    /** may be null if init failed */
+    private SavingTrustManager _stm;
 
+    /**
+     *  A new SSLEepGet with a new SSLState
+     */
     public SSLEepGet(I2PAppContext ctx, OutputStream outputStream, String url) {
+        this(ctx, outputStream, url, null);
+    }
+
+    /**
+     *  @param state an SSLState retrieved from a previous SSLEepGet with getSSLState(), or null.
+     *               This makes repeated fetches from the same host MUCH faster,
+     *               and prevents repeated key store loads even for different hosts.
+     *  @since 0.8.2
+     */
+    public SSLEepGet(I2PAppContext ctx, OutputStream outputStream, String url, SSLState state) {
         // we're using this constructor:
         // public EepGet(I2PAppContext ctx, boolean shouldProxy, String proxyHost, int proxyPort, int numRetries, long minSize, long maxSize, String outputFile, OutputStream outputStream, String url, boolean allowCaching, String etag, String postData) {
         super(ctx, false, null, -1, 0, -1, -1, null, outputStream, url, true, null, null);
+        if (state != null && state.context != null)
+            _sslContext = state.context;
+        else
+            _sslContext = initSSLContext();
+        if (_sslContext == null)
+            _log.error("Failed to initialize custom SSL context, using default context");
     }
    
     /**
-     * SSLEepGet url
-     * no command line options supported
+     * SSLEepGet https://foo/bar
+     *   or to save cert chain:
+     * SSLEepGet -s https://foo/bar
      */ 
     public static void main(String args[]) {
         String url = null;
+        boolean saveCerts = false;
         try {
             for (int i = 0; i < args.length; i++) {
-                if (args[i].startsWith("-")) {
+                if (args[i].equals("-s")) {
+                    saveCerts = true;
+                } else if (args[i].startsWith("-")) {
                     usage();
                     return;
                 } else {
@@ -77,94 +153,323 @@ public class SSLEepGet extends EepGet {
             return;
         }
 
-        /******
-         *  This is all an experiment to add a CA cert loaded from a file so we can use
-         *  selfsigned certs on our servers.
-         *  But it's failing.
-         *  Run as java -Djava.security.debug=certpath -Djavax.net.debug=trustmanager -cp $I2P/lib/i2p.jar net.i2p.util.SSLEepGet "$@"
-         *  to see the problems. It isn't including the added cert in the Trust Anchor list.
-         ******/
-
-        /******
-        String foo = System.getProperty("javax.net.ssl.keyStore");
-        if (foo == null) {
-            File cacerts = new File(System.getProperty("java.home"), "lib/security/cacerts");
-            foo = cacerts.getAbsolutePath();
-        }
-        System.err.println("Location is: " + foo);
+        SSLEepGet get = new SSLEepGet(I2PAppContext.getGlobalContext(), out, url);
+        if (saveCerts)
+            get._saveCerts = true;
+        get._commandLine = true;
+        get.addStatusListener(get.new CLIStatusListener(1024, 40));
+        get.fetch(45*1000, -1, 60*1000);
+    }
+    
+    private static void usage() {
+        System.err.println("Usage: SSLEepGet https://url");
+        System.err.println("To save unknown certs, use: SSLEepGet -s https://url");
+    }
+
+    /**
+     *  Loads certs from location of javax.net.ssl.keyStore property,
+     *  else from $JAVA_HOME/lib/security/jssacacerts,
+     *  else from $JAVA_HOME/lib/security/cacerts.
+     *
+     *  Then adds certs found in the $I2P/certificates/ directory
+     *  and in the ~/.i2p/certificates/ directory.
+     *
+     *  @return null on failure
+     *  @since 0.8.2
+     */
+    private SSLContext initSSLContext() {
+        KeyStore ks;
         try {
-            KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+            ks = KeyStore.getInstance(KeyStore.getDefaultType());
+        } catch (GeneralSecurityException gse) {
+            _log.error("Key Store init error", gse);
+            return null;
+        }
+        boolean success = false;
+        String override = System.getProperty("javax.net.ssl.keyStore");
+        if (override != null)
+            success = loadCerts(new File(override), ks);
+        if (!success)
+            success = loadCerts(new File(System.getProperty("java.home"), "lib/security/jssecacerts"), ks);
+        if (!success)
+            success = loadCerts(new File(System.getProperty("java.home"), "lib/security/cacerts"), ks);
+
+        if (!success) {
+            _log.error("All key store loads failed, will only load local certificates");
+        } else if (_log.shouldLog(Log.INFO)) {
+            int count = 0;
             try {
-                InputStream fis = new FileInputStream(foo);
-                ks.load(fis, "changeit".toCharArray());
-                fis.close();
-            } catch (GeneralSecurityException gse) {
-                System.err.println("KS error, no default keys: " + gse);
-                ks.load(null, "changeit".toCharArray());
-            } catch (IOException ioe) {
-                System.err.println("IO error, no default keys: " + ioe);
-                ks.load(null, "changeit".toCharArray());
-            }
+                for(Enumeration<String> e = ks.aliases(); e.hasMoreElements();) {
+                    String alias = e.nextElement();
+                    if (ks.isCertificateEntry(alias))
+                        count++;
+                }
+            } catch (Exception foo) {}
+            _log.info("Loaded " + count + " default trusted certificates");
+        }
 
-            addCert(ks, "cacert");
+        File dir = new File(_context.getBaseDir(), "certificates");
+        int adds = addCerts(dir, ks);
+        int totalAdds = adds;
+        if (adds > 0 && _log.shouldLog(Log.INFO))
+            _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath());
+        if (!_context.getBaseDir().getAbsolutePath().equals(_context.getConfigDir().getAbsolutePath())) {
+            dir = new File(_context.getConfigDir(), "certificates");
+            adds = addCerts(dir, ks);
+            totalAdds += adds;
+            if (adds > 0 && _log.shouldLog(Log.INFO))
+                _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath());
+        }
+        dir = new File(System.getProperty("user.dir"));
+        if (!_context.getBaseDir().getAbsolutePath().equals(dir.getAbsolutePath())) {
+            dir = new File(_context.getConfigDir(), "certificates");
+            adds = addCerts(dir, ks);
+            totalAdds += adds;
+            if (adds > 0 && _log.shouldLog(Log.INFO))
+                _log.info("Loaded " + adds + " trusted certificates from " + dir.getAbsolutePath());
+        }
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Loaded total of " + totalAdds + " new trusted certificates");
 
-            for(Enumeration<String> e = ks.aliases(); e.hasMoreElements();) {
-                String alias = e.nextElement();
-                System.err.println("Aliases: " + alias + " isCert? " + ks.isCertificateEntry(alias));
-            }
+        try {
+            SSLContext sslc = SSLContext.getInstance("TLS");
+            TrustManagerFactory tmf =   TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            tmf.init(ks);
+            X509TrustManager defaultTrustManager = (X509TrustManager)tmf.getTrustManagers()[0];
+	    _stm = new SavingTrustManager(defaultTrustManager);
+            sslc.init(null, new TrustManager[] {_stm}, null);
+            return sslc;
+        } catch (GeneralSecurityException gse) {
+            _log.error("Key Store update error", gse);
+        }
+        return null;
+    }
 
-            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
-            kmf.init(ks, "".toCharArray());
-            SSLContext sslc = SSLContext.getInstance("SSL");
-            sslc.init(kmf.getKeyManagers(), null, null);
-            _sslContext = sslc;
+    /**
+     *  Load all X509 Certs from a key store File into a KeyStore
+     *  Note that each call reinitializes the KeyStore
+     *
+     *  @return success
+     *  @since 0.8.2
+     */
+    private boolean loadCerts(File file, KeyStore ks) {
+        if (!file.exists())
+            return false;
+        InputStream fis = null;
+        try {
+            fis = new FileInputStream(file);
+            // "changeit" is the default password
+            ks.load(fis, "changeit".toCharArray());
         } catch (GeneralSecurityException gse) {
-            System.err.println("KS error: " + gse);
-            return;
+            _log.error("KeyStore load error, no default keys: " + file.getAbsolutePath(), gse);
+            try {
+                // not clear if null is allowed for password
+                ks.load(null, "changeit".toCharArray());
+            } catch (Exception foo) {}
+            return false;
         } catch (IOException ioe) {
-            System.err.println("IO error: " + ioe);
-            return;
+            _log.error("KeyStore load error, no default keys: " + file.getAbsolutePath(), ioe);
+            try {
+                ks.load(null, "changeit".toCharArray());
+            } catch (Exception foo) {}
+            return false;
+        } finally {
+            try { if (fis != null) fis.close(); } catch (IOException foo) {}
         }
-        *******/
-
-        EepGet get = new SSLEepGet(I2PAppContext.getGlobalContext(), out, url);
-        get.addStatusListener(get.new CLIStatusListener(1024, 40));
-        get.fetch(45*1000, -1, 60*1000);
-    }
-    
-    private static void usage() {
-        System.err.println("SSLEepGet url");
+        return true;
     }
 
-/******
-    private static boolean addCert(KeyStore ks, String file) {
+    /**
+     *  Load all X509 Certs from a directory and add them to the
+     *  trusted set of certificates in the key store
+     *
+     *  @return number successfully added
+     *  @since 0.8.2
+     */
+    private int addCerts(File dir, KeyStore ks) {
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Looking for X509 Certificates in " + dir.getAbsolutePath());
+        int added = 0;
+        if (dir.exists() && dir.isDirectory()) {
+            File[] files = dir.listFiles();
+            if (files != null) {
+                for (int i = 0; i < files.length; i++) {
+                    File f = files[i];
+                    if (!f.isFile())
+                        continue;
+                    // use file name as alias
+                    // https://www.sslshopper.com/ssl-converter.html
+                    // No idea if all these formats can actually be read by CertificateFactory
+                    String alias = f.getName().toLowerCase();
+                    if (alias.endsWith(".crt") || alias.endsWith(".pem") || alias.endsWith(".key") ||
+                        alias.endsWith(".der") || alias.endsWith(".key") || alias.endsWith(".p7b") ||
+                        alias.endsWith(".p7c") || alias.endsWith(".pfx") || alias.endsWith(".p12"))
+                        alias = alias.substring(0, alias.length() - 4);
+                    boolean success = addCert(f, alias, ks);
+                    if (success)
+                        added++;
+                }
+            }
+        }
+        return added;
+    }
 
+    /**
+     *  Load an X509 Cert from a file and add it to the
+     *  trusted set of certificates in the key store
+     *
+     *  @return success
+     *  @since 0.8.2
+     */
+    private boolean addCert(File file, String alias, KeyStore ks) {
+        InputStream fis = null;
+        try {
+            fis = new FileInputStream(file);
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            X509Certificate cert = (X509Certificate)cf.generateCertificate(fis);
+            if (_log.shouldLog(Log.INFO)) {
+                _log.info("Read X509 Certificate from " + file.getAbsolutePath() +
+                          " Issuer: " + cert.getIssuerX500Principal() +
+                          "; Valid From: " + cert.getNotBefore() +
+                          " To: " + cert.getNotAfter());
+            }
             try {
-                InputStream fis = new FileInputStream(file);
-                CertificateFactory cf = CertificateFactory.getInstance("X.509");
-                X509Certificate cert = (X509Certificate)cf.generateCertificate(fis);
-                fis.close();
-                System.err.println("Adding cert, Issuer: " + cert.getIssuerX500Principal());
-                try {
-                    cert.checkValidity();
-                } catch (CertificateExpiredException cee) {
-                    System.err.println("Warning - expired cert: " + cee);
-                } catch (CertificateNotYetValidException cnyve) {
-                    System.err.println("Warning - not yet valid cert: " + cnyve);
-                }
-                // use file name as alias
-                ks.setCertificateEntry(file, cert);
-            } catch (GeneralSecurityException gse) {
-                System.err.println("Read cert error: " + gse);
+                cert.checkValidity();
+            } catch (CertificateExpiredException cee) {
+                _log.error("Rejecting expired X509 Certificate: " + file.getAbsolutePath(), cee);
                 return false;
-            } catch (IOException ioe) {
-                System.err.println("Read cert error: " + ioe);
+            } catch (CertificateNotYetValidException cnyve) {
+                _log.error("Rejecting X509 Certificate not yet valid: " + file.getAbsolutePath(), cnyve);
                 return false;
             }
+            ks.setCertificateEntry(alias, cert);
+            if (_log.shouldLog(Log.INFO))
+                _log.info("Now trusting X509 Certificate, Issuer: " + cert.getIssuerX500Principal());
+        } catch (GeneralSecurityException gse) {
+            _log.error("Error reading X509 Certificate: " + file.getAbsolutePath(), gse);
+            return false;
+        } catch (IOException ioe) {
+            _log.error("Error reading X509 Certificate: " + file.getAbsolutePath(), ioe);
+            return false;
+        } finally {
+            try { if (fis != null) fis.close(); } catch (IOException foo) {}
+        }
         return true;
     }
-*******/
     
+    /**
+     *  From http://blogs.sun.com/andreas/resource/InstallCert.java
+     *  This just saves the certificate chain for later inspection.
+     *  @since 0.8.2
+     */
+    private static class SavingTrustManager implements X509TrustManager {
+	private final X509TrustManager tm;
+	private X509Certificate[] chain;
+
+	SavingTrustManager(X509TrustManager tm) {
+	    this.tm = tm;
+	}
+
+	public X509Certificate[] getAcceptedIssuers() {
+	    throw new UnsupportedOperationException();
+	}
+
+	public void checkClientTrusted(X509Certificate[] chain, String authType)
+		throws CertificateException {
+	    throw new UnsupportedOperationException();
+	}
+
+	public void checkServerTrusted(X509Certificate[] chain, String authType)
+		throws CertificateException {
+	    this.chain = chain;
+	    tm.checkServerTrusted(chain, authType);
+	}
+    }
+
+    /**
+     *  Modified from http://blogs.sun.com/andreas/resource/InstallCert.java
+     *  @since 0.8.2
+     */
+    private static void saveCerts(String host, SavingTrustManager stm) {
+        X509Certificate[] chain = stm.chain;
+        if (chain == null) {
+            System.out.println("Could not obtain server certificate chain");
+            return;
+	}
+        for (int k = 0; k < chain.length; k++) {
+            X509Certificate cert = chain[k];
+            String name = host + '-' + (k + 1) + ".crt";
+            System.out.println("NOTE: Saving untrusted X509 certificate as " + name);
+            System.out.println("      Issuer:     " + cert.getIssuerX500Principal());
+            System.out.println("      Valid From: " + cert.getNotBefore());
+            System.out.println("      Valid To:   " + cert.getNotAfter());
+            try {
+                cert.checkValidity();
+            } catch (Exception e) {
+                System.out.println("      WARNING: Certificate is not currently valid, it cannot be used");
+            }
+            saveCert(cert, new File(name));
+        }
+        System.out.println("NOTE: To trust them, copy the certificate file(s) to the certificates directory and rerun without the -s option");
+        System.out.println("NOTE: EepGet failed, certificate error follows:");
+    }
+
+    private static final int LINE_LENGTH = 64;
+
+    /**
+     *  Modified from:
+     *  http://www.exampledepot.com/egs/java.security.cert/ExportCert.html
+     *
+     *  This method writes a certificate to a file in base64 format.
+     *  @since 0.8.2
+     */
+    private static void saveCert(Certificate cert, File file) {
+        OutputStream os = null;
+        try {
+           // Get the encoded form which is suitable for exporting
+           byte[] buf = cert.getEncoded();
+           os = new FileOutputStream(file);
+           PrintWriter wr = new PrintWriter(os);
+           wr.println("-----BEGIN CERTIFICATE-----");
+           String b64 = Base64.encode(buf, true);     // true = use standard alphabet
+           for (int i = 0; i < b64.length(); i += LINE_LENGTH) {
+               wr.println(b64.substring(i, Math.min(i + LINE_LENGTH, b64.length())));
+           }
+           wr.println("-----END CERTIFICATE-----");
+           wr.flush();
+        } catch (CertificateEncodingException cee) {
+            System.out.println("Error writing X509 Certificate " + file.getAbsolutePath() + ' ' + cee);
+        } catch (IOException ioe) {
+            System.out.println("Error writing X509 Certificate " + file.getAbsolutePath() + ' ' + ioe);
+        } finally {
+            try { if (os != null) os.close(); } catch (IOException foo) {}
+        }
+    }
+
+    /**
+     *  An opaque class for the caller to pass to repeated instantiations of SSLEepGet.
+     *  @since 0.8.2
+     */
+    public static class SSLState {
+        private SSLContext context;
+
+        private SSLState(SSLContext ctx) {
+             context = ctx;
+        }
+    }
+
+    /**
+     *  Pass this back to the next SSLEepGet constructor for faster fetches.
+     *  This may be called either after the constructor or after the fetch.
+     *  @since 0.8.2
+     */
+    public SSLState getSSLState() {
+        return new SSLState(_sslContext);
+    }
+
+    ///// end of all the SSL stuff
+    ///// start of overrides
+
     @Override
     protected void doFetch(SocketTimeout timeout) throws IOException {
         _headersRead = false;
@@ -288,15 +593,16 @@ public class SSLEepGet extends EepGet {
 
         //try {
             URL url = new URL(_actualURL);
+            String host = null;
+            int port = 0;
             if ("https".equals(url.getProtocol())) {
-                String host = url.getHost();
-                int port = url.getPort();
+                host = url.getHost();
+                port = url.getPort();
                 if (port == -1)
                     port = 443;
-                // part of the experiment above
-                //if (_sslContext != null)
-                //    _proxy = _sslContext.getSocketFactory().createSocket(host, port);
-                //else
+                if (_sslContext != null)
+                    _proxy = _sslContext.getSocketFactory().createSocket(host, port);
+                else
                     _proxy = SSLSocketFactory.getDefault().createSocket(host, port);
             } else {
                 throw new IOException("Only https supported: " + _actualURL);
@@ -309,8 +615,23 @@ public class SSLEepGet extends EepGet {
         _proxyIn = _proxy.getInputStream();
         _proxyOut = _proxy.getOutputStream();
         
-        _proxyOut.write(DataHelper.getUTF8(req));
-        _proxyOut.flush();
+        // This is where the cert errors happen
+        try {
+            _proxyOut.write(DataHelper.getUTF8(req));
+            _proxyOut.flush();
+        } catch (SSLHandshakeException sslhe) {
+            // this maybe would be better done in the catch in super.fetch(), but
+            // then we'd have to copy it all over here.
+            _log.error("SSL negotiation error with " + host + ':' + port +
+                       " - self-signed certificate or untrusted certificate authority?", sslhe);
+            if (_saveCerts && _stm != null)
+                saveCerts(host, _stm);
+            else if (_commandLine) {
+                System.out.println("FAILED (probably due to untrusted certificates) - Run with -s option to save certificates");
+            }
+            // this is an IOE
+            throw sslhe;
+        }
         
         if (_log.shouldLog(Log.DEBUG))
             _log.debug("Request flushed");
diff --git a/history.txt b/history.txt
index ad7880fb1df9932c0ddccba6f64bc844e0a566ce..398b6ca0c8fa803fc677d92605ae817e97399536 100644
--- a/history.txt
+++ b/history.txt
@@ -1,3 +1,26 @@
+2010-11-22 zzz
+    * Addressbook: Fix rename error on Windows (tkt 323 - thanks RN!)
+    * build.xml: Cleanup, fix distclean error in older ants.
+    * Console:
+      - Convert GraphHelper to a FormHandler
+      - Require POST for all forms
+      - Change the way we store the Writer to prevent problems
+      - Fix bonus setting on configpeer.jsp
+      - More ".jsp" removal
+    * i2psnark:
+      - Defer piece loading until required
+      - Stub out Extension message support
+      - Convert GET to POST, require POST
+    * NTCP: Log tweak
+    * SSLEepGet, Reseeder:
+      - Implement additional CA loading
+      - Provide facility to reuse SSL state for speed
+      - Provide facility to store previously untrusted certificates
+      - Add www.cacert.org cert to the installer and updater so
+        SSL on a.netdb.i2p2.de and c.netdb.i2p2.de will work
+      - Add SSL reseed hosts, prefer them by default
+      - Reseed message cleanup
+
 2010-11-19 zzz
     * Addressbook
       - Store last-fetched time so we don't always fetch subscriptions after restart
diff --git a/installer/resources/certificates/www.cacert.org.crt b/installer/resources/certificates/www.cacert.org.crt
new file mode 100644
index 0000000000000000000000000000000000000000..e7dfc82947e30af0637575869fba5eef4c683b7d
--- /dev/null
+++ b/installer/resources/certificates/www.cacert.org.crt
@@ -0,0 +1,41 @@
+-----BEGIN CERTIFICATE-----
+MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290
+IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB
+IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA
+Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO
+BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi
+MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
+ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
+CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ
+8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6
+zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y
+fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7
+w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc
+G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k
+epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q
+laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ
+QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU
+fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826
+YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w
+ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY
+gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe
+MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0
+IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy
+dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw
+czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0
+dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl
+aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC
+AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg
+b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB
+ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc
+nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg
+18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c
+gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl
+Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY
+sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T
+SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF
+CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum
+GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk
+zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW
+omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD
+-----END CERTIFICATE-----
diff --git a/installer/resources/themes/snark/ubergine/snark.css b/installer/resources/themes/snark/ubergine/snark.css
index 535c329549e71d8a1cac703342be2628a3ca7015..52e9f53459644177c8cf7e1617d4b06b3aa12519 100644
--- a/installer/resources/themes/snark/ubergine/snark.css
+++ b/installer/resources/themes/snark/ubergine/snark.css
@@ -474,6 +474,20 @@ input[type=submit]:active {
      text-shadow: 0 !important;
 }
 
+input[type=image] {
+     padding: 0 !important;
+     background: #000;
+     -moz-border-radius: 0px;
+     -khtml-border-radius: 0px;
+     border-radius: 0px;
+     border: medium none;
+     margin: 0 2px;
+}
+
+input[type=image]:hover {
+     border: 1px outset #bbb;
+}
+
 input[type=text]:active, input[type=text]:hover, input.r:hover {
      background: #f60;
      color: #fff;
diff --git a/licenses/LICENSE-InstallCert.txt b/licenses/LICENSE-InstallCert.txt
new file mode 100644
index 0000000000000000000000000000000000000000..17620f66de99b44599efef3e989f69153813d6f6
--- /dev/null
+++ b/licenses/LICENSE-InstallCert.txt
@@ -0,0 +1,29 @@
+Copyright 2006 Sun Microsystems, Inc.  All Rights Reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+  - Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+
+  - Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+
+  - Neither the name of Sun Microsystems nor the names of its
+    contributors may be used to endorse or promote products derived
+    from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index 0725033fa600cfb5180ac82283f5194ee35b34b0..f6e6df3a5c0d10fbf1efc2f7069c342c2fe16f4e 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -18,7 +18,7 @@ public class RouterVersion {
     /** deprecated */
     public final static String ID = "Monotone";
     public final static String VERSION = CoreVersion.VERSION;
-    public final static long BUILD = 3;
+    public final static long BUILD = 4;
 
     /** for example "-test" */
     public final static String EXTRA = "";
diff --git a/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java b/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java
index 776d7eee46b4fa0445be32cb80f84b54cdc6748e..9577b2b200e9e00bc8adf72c055a310b6531b671 100644
--- a/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java
+++ b/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java
@@ -29,6 +29,8 @@ import net.i2p.util.Translate;
  * specified below unless the I2P configuration property "i2p.reseedURL" is
  * set.  It always writes to ./netDb/, so don't mess with that.
  *
+ * This is somewhat complicated by trying to log to three places - the console,
+ * the router log, and the wrapper log.
  */
 public class Reseeder {
     private static ReseedRunner _reseedRunner;
@@ -40,12 +42,25 @@ public class Reseeder {
 
     private static final String DEFAULT_SEED_URL =
               "http://a.netdb.i2p2.de/,http://b.netdb.i2p2.de/,http://c.netdb.i2p2.de/," +
-              "http://reseed.i2p-projekt.de/,http://i2pbote.net/netDb/,http://r31453.ovh.net/static_media/netDb/";
+              "http://reseed.i2p-projekt.de/,http://www.i2pbote.net/netDb/,http://r31453.ovh.net/static_media/netDb/";
+
+    /** @since 0.8.2 */
+    private static final String DEFAULT_SSL_SEED_URL =
+              "https://a.netdb.i2p2.de/,https://c.netdb.i2p2.de/," +
+              "https://www.i2pbote.net/netDb/";
+
     private static final String PROP_INPROGRESS = "net.i2p.router.web.ReseedHandler.reseedInProgress";
+    /** the console shows this message while reseedInProgress == false */
     private static final String PROP_ERROR = "net.i2p.router.web.ReseedHandler.errorMessage";
+    /** the console shows this message while reseedInProgress == true */
     private static final String PROP_STATUS = "net.i2p.router.web.ReseedHandler.statusMessage";
     public static final String PROP_PROXY_HOST = "router.reseedProxyHost";
     public static final String PROP_PROXY_PORT = "router.reseedProxyPort";
+    /** @since 0.8.2 */
+    public static final String PROP_PROXY_ENABLE = "router.reseedProxyEnable";
+    /** @since 0.8.2 */
+    public static final String PROP_SSL_DISABLE = "router.reseedSSLDisable";
+
     private static final String RESEED_TIPS =
             _x("Ensure that nothing blocks outbound HTTP, check <a target=\"_top\" href=\"logs.jsp\">logs</a> " +
                "and if nothing helps, read the <a target=\"_top\" href=\"http://www.i2p2.de/faq.html\">FAQ</a> about reseeding manually.");
@@ -63,7 +78,6 @@ public class Reseeder {
             if (_reseedRunner.isRunning()) {
                 return;
             } else {
-                System.setProperty(PROP_INPROGRESS, "true");
                 // set to daemon so it doesn't hang a shutdown
                 Thread reseed = new I2PAppThread(_reseedRunner, "Reseed", true);
                 reseed.start();
@@ -76,20 +90,46 @@ public class Reseeder {
         private boolean _isRunning;
         private String _proxyHost;
         private int _proxyPort;
+        private SSLEepGet.SSLState _sslState;
 
         public ReseedRunner() {
             _isRunning = false; 
+            System.clearProperty(PROP_ERROR);
             System.setProperty(PROP_STATUS, _("Reseeding"));
+            System.setProperty(PROP_INPROGRESS, "true");
         }
         public boolean isRunning() { return _isRunning; }
+
+        /*
+         * Do it.
+         * We update PROP_ERROR here.
+         */
         public void run() {
             _isRunning = true;
-            _proxyHost = _context.getProperty(PROP_PROXY_HOST);
-            _proxyPort = _context.getProperty(PROP_PROXY_PORT, -1);
+            _sslState = null;  // start fresh
+            if (_context.getBooleanProperty(PROP_PROXY_ENABLE)) {
+                _proxyHost = _context.getProperty(PROP_PROXY_HOST);
+                _proxyPort = _context.getProperty(PROP_PROXY_PORT, -1);
+            }
             System.out.println("Reseed start");
-            reseed(false);
-            System.out.println("Reseed complete");
+            int total = reseed(false);
+            if (total >= 50) {
+                System.out.println("Reseed complete, " + total + " received");
+                System.clearProperty(PROP_ERROR);
+            } else if (total > 0) {
+                System.out.println("Reseed complete, only " + total + " received");
+                System.setProperty(PROP_ERROR, ngettext("Reseed fetched only 1 router.",
+                                                        "Reseed fetched only {0} routers.", total));
+            } else {
+                System.out.println("Reseed failed, check network connection");
+                System.out.println(
+                     "Ensure that nothing blocks outbound HTTP, check the logs, " +
+                     "and if nothing helps, read the FAQ about reseeding manually.");
+                System.setProperty(PROP_ERROR, _("Reseed failed.") + ' '  + _(RESEED_TIPS));
+            }	
             System.setProperty(PROP_INPROGRESS, "false");
+            System.clearProperty(PROP_STATUS);
+            _sslState = null;  // don't hold ref
             _isRunning = false;
         }
 
@@ -112,16 +152,56 @@ public class Reseeder {
         * the routerInfo-*.dat files from the specified URL (or the default) and
         * save them into this router's netDb dir.
         *
+        * - If list specified in the properties, use it randomly, without regard to http/https
+        * - If SSL not disabled, use the https randomly then
+        *   the http randomly
+        * - Otherwise just the http randomly.
+        *
+        * @param echoStatus apparently always false
+        * @return count of routerinfos successfully fetched
         */
-        private void reseed(boolean echoStatus) {
-            List URLList = new ArrayList();
-            String URLs = _context.getProperty("i2p.reseedURL", DEFAULT_SEED_URL);
+        private int reseed(boolean echoStatus) {
+            List<String> URLList = new ArrayList();
+            String URLs = _context.getProperty("i2p.reseedURL");
+            boolean defaulted = URLs == null;
+            boolean SSLDisable = _context.getBooleanProperty(PROP_SSL_DISABLE);
+            if (defaulted) {
+                if (SSLDisable)
+                    URLs = DEFAULT_SEED_URL;
+                else
+                    URLs = DEFAULT_SSL_SEED_URL;
+            }
             StringTokenizer tok = new StringTokenizer(URLs, " ,");
             while (tok.hasMoreTokens())
                 URLList.add(tok.nextToken().trim());
             Collections.shuffle(URLList);
-            for (int i = 0; i < URLList.size() && _isRunning; i++)
-                reseedOne((String) URLList.get(i), echoStatus);
+            if (defaulted && !SSLDisable) {
+                // put the non-SSL at the end of the SSL
+                List<String> URLList2 = new ArrayList();
+                tok = new StringTokenizer(DEFAULT_SSL_SEED_URL, " ,");
+                while (tok.hasMoreTokens())
+                    URLList2.add(tok.nextToken().trim());
+                Collections.shuffle(URLList2);
+                URLList.addAll(URLList2);
+            }
+            int total = 0;
+            for (int i = 0; i < URLList.size() && _isRunning; i++) {
+                String url = URLList.get(i);
+                int dl = reseedOne(url, echoStatus);
+                if (dl > 0) {
+                    total += dl;
+                    // remove alternate version if we haven't tried it yet
+                    String alt;
+                    if (url.startsWith("http://"))
+                        alt = url.replace("http://", "https://");
+                    else
+                        alt = url.replace("https://", "http://");
+                    int idx = URLList.indexOf(alt);
+                    if (idx > i)
+                        URLList.remove(i);
+                }
+            }
+            return total;
         }
 
         /**
@@ -138,22 +218,23 @@ public class Reseeder {
          *
          * Jetty directory listings are not compatible, as they look like
          * HREF="/full/path/to/routerInfo-...
+         *
+         * We update PROP_STATUS here.
+         *
+         * @param echoStatus apparently always false
+         * @return count of routerinfos successfully fetched
          **/
-        private void reseedOne(String seedURL, boolean echoStatus) {
-
+        private int reseedOne(String seedURL, boolean echoStatus) {
             try {
-                System.setProperty(PROP_ERROR, "");
                 System.setProperty(PROP_STATUS, _("Reseeding: fetching seed URL."));
-                System.err.println("Reseed from " + seedURL);
+                System.err.println("Reseeding from " + seedURL);
                 URL dir = new URL(seedURL);
                 byte contentRaw[] = readURL(dir);
                 if (contentRaw == null) {
-                    System.setProperty(PROP_ERROR,
-                        _("Last reseed failed fully (failed reading seed URL).") + ' '  +
-                        _(RESEED_TIPS));
                     // Logging deprecated here since attemptFailed() provides better info
-                    _log.debug("Failed reading seed URL: " + seedURL);
-                    return;
+                    _log.warn("Failed reading seed URL: " + seedURL);
+                    System.err.println("Reseed got no router infos from " + seedURL);
+                    return 0;
                 }
                 String content = new String(contentRaw);
                 Set<String> urls = new HashSet(1024);
@@ -173,11 +254,9 @@ public class Reseeder {
                     cur = end + 1;
                 }
                 if (total <= 0) {
-                    _log.error("Read " + contentRaw.length + " bytes from seed " + seedURL + ", but found no routerInfo URLs.");
-                    System.setProperty(PROP_ERROR,
-                        _("Last reseed failed fully (no routerInfo URLs at seed URL).") + ' ' +
-                        _(RESEED_TIPS));
-                    return;
+                    _log.warn("Read " + contentRaw.length + " bytes from seed " + seedURL + ", but found no routerInfo URLs.");
+                    System.err.println("Reseed got no router infos from " + seedURL);
+                    return 0;
                 }
 
                 List<String> urlList = new ArrayList(urls);
@@ -201,32 +280,18 @@ public class Reseeder {
                         errors++;
                     }
                 }
-                System.err.println("Reseed got " + fetched + " router infos from " + seedURL);
-                
-                int failPercent = 100 * errors / total;
-                
-                // Less than 10% of failures is considered success,
-                // because some routerInfos will always fail.
-                if ((failPercent >= 10) && (failPercent < 90)) {
-                    System.setProperty(PROP_ERROR,
-                        _("Last reseed failed partly ({0}% of {1}).", failPercent, total) + ' ' +
-                        _(RESEED_TIPS));
-                }
-                if (failPercent >= 90) {
-                    System.setProperty(PROP_ERROR,
-                        _("Last reseed failed ({0}% of {1}).", failPercent, total) + ' ' +
-                        _(RESEED_TIPS));
-                }
+                System.err.println("Reseed got " + fetched + " router infos from " + seedURL + " with " + errors + " errors");
+
                 if (fetched > 0)
                     _context.netDb().rescan();
                 // Don't go on to the next URL if we have enough
                 if (fetched >= 100)
                     _isRunning = false;
+                return fetched;
             } catch (Throwable t) {
-                System.setProperty(PROP_ERROR,
-                    _("Last reseed failed fully (exception caught).") + ' ' +
-                    _(RESEED_TIPS));
-                _log.error("Error reseeding", t);
+                _log.warn("Error reseeding", t);
+                System.err.println("Reseed got no router infos from " + seedURL);
+                return 0;
             }
         }
     
@@ -248,8 +313,17 @@ public class Reseeder {
             ByteArrayOutputStream baos = new ByteArrayOutputStream(4*1024);
 
             EepGet get;
-            if (url.toString().startsWith("https")) {
-                get = new SSLEepGet(I2PAppContext.getGlobalContext(), baos, url.toString());
+            boolean ssl = url.toString().startsWith("https");
+            if (ssl) {
+                SSLEepGet sslget;
+                if (_sslState == null) {
+                    sslget = new SSLEepGet(I2PAppContext.getGlobalContext(), baos, url.toString());
+                    // save state for next time
+                    _sslState = sslget.getSSLState();
+                } else {
+                    sslget = new SSLEepGet(I2PAppContext.getGlobalContext(), baos, url.toString(), _sslState);
+                }
+                get = sslget;
             } else {
                 // Do a (probably) non-proxied eepget into our ByteArrayOutputStream with 0 retries
                 boolean shouldProxy = _proxyHost != null && _proxyHost.length() > 0 && _proxyPort > 0;
@@ -257,7 +331,9 @@ public class Reseeder {
                                  null, baos, url.toString(), false, null, null);
             }
             get.addStatusListener(ReseedRunner.this);
-            if (get.fetch()) return baos.toByteArray(); else return null;
+            if (get.fetch())
+                return baos.toByteArray();
+            return null;
         }
     
         private void writeSeed(String name, byte data[]) throws Exception {
@@ -295,6 +371,11 @@ public class Reseeder {
         return Translate.getString(s, o, o2, _context, BUNDLE_NAME);
     }
 
+    /** translate */
+    private String ngettext(String s, String p, int n) {
+        return Translate.getString(n, s, p, _context, BUNDLE_NAME);
+    }
+
 /******
     public static void main(String args[]) {
         if ( (args != null) && (args.length == 1) && (!Boolean.valueOf(args[0]).booleanValue()) ) {
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
index 58ebe3634739a6b92df4f5ea25bc09b2cf13cb77..4041f3dadbe3830ed096c913e02ca6a9d56b9a0d 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
@@ -1274,10 +1274,10 @@ public class NTCPConnection implements FIFOBandwidthLimiter.CompleteListener {
                     return;
                 }
             } else {
-                if (_log.shouldLog(Log.ERROR))
-                    _log.error("CRC incorrect for message " + _messagesRead + " (calc=" + val + " expected=" + _expectedCrc + ") size=" + _size + " blocks " + _blocks);
+                if (_log.shouldLog(Log.WARN))
+                    _log.warn("CRC incorrect for message " + _messagesRead + " (calc=" + val + " expected=" + _expectedCrc + ") size=" + _size + " blocks " + _blocks);
                     _context.statManager().addRateData("ntcp.corruptI2NPCRC", 1, getUptime());
-                // should we try to read in the message and keep going?
+                // FIXME should we try to read in the message and keep going?
                 close();
                 // databuf is lost
                 return;