diff --git a/LICENSE.txt b/LICENSE.txt
index a4bd67dcf..e3b631a3b 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/build.xml b/build.xml
index 0e749ce49..44011705d 100644
--- a/build.xml
+++ b/build.xml
@@ -245,7 +245,7 @@
-
+
@@ -368,7 +368,7 @@
-
+
@@ -419,10 +419,6 @@
-
-
-
-
@@ -455,42 +451,17 @@
+
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -500,16 +471,23 @@
+
-
+
+
+
+
+
+
-
+
-
+
+
@@ -560,7 +538,8 @@
-
+
+
@@ -587,6 +566,7 @@
-->
+
@@ -601,10 +581,13 @@
+
+
+
@@ -612,6 +595,7 @@
+
diff --git a/core/java/src/net/i2p/util/EepGet.java b/core/java/src/net/i2p/util/EepGet.java
index 5b3f25c19..f055ce98c 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 8a5bf364a..e3ba43ae7 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);
- try {
- KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
- 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());
- }
-
- addCert(ks, "cacert");
-
- for(Enumeration e = ks.aliases(); e.hasMoreElements();) {
- String alias = e.nextElement();
- System.err.println("Aliases: " + alias + " isCert? " + ks.isCertificateEntry(alias));
- }
-
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmf.init(ks, "".toCharArray());
- SSLContext sslc = SSLContext.getInstance("SSL");
- sslc.init(kmf.getKeyManagers(), null, null);
- _sslContext = sslc;
- } catch (GeneralSecurityException gse) {
- System.err.println("KS error: " + gse);
- return;
- } catch (IOException ioe) {
- System.err.println("IO error: " + ioe);
- return;
- }
- *******/
-
- EepGet get = new SSLEepGet(I2PAppContext.getGlobalContext(), out, url);
+ 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("SSLEepGet url");
+ System.err.println("Usage: SSLEepGet https://url");
+ System.err.println("To save unknown certs, use: SSLEepGet -s https://url");
}
-/******
- private static boolean addCert(KeyStore ks, String file) {
+ /**
+ * 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 {
+ 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(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);
+ for(Enumeration e = ks.aliases(); e.hasMoreElements();) {
+ String alias = e.nextElement();
+ if (ks.isCertificateEntry(alias))
+ count++;
}
- // use file name as alias
- ks.setCertificateEntry(file, cert);
- } catch (GeneralSecurityException gse) {
- System.err.println("Read cert error: " + gse);
- return false;
- } catch (IOException ioe) {
- System.err.println("Read cert error: " + ioe);
- return false;
- }
+ } catch (Exception foo) {}
+ _log.info("Loaded " + count + " default trusted certificates");
+ }
+
+ 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");
+
+ 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;
+ }
+
+ /**
+ * 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) {
+ _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) {
+ _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) {}
+ }
+ return true;
+ }
+
+ /**
+ * 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 {
+ cert.checkValidity();
+ } catch (CertificateExpiredException cee) {
+ _log.error("Rejecting expired X509 Certificate: " + file.getAbsolutePath(), cee);
+ return false;
+ } 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/installer/resources/certificates/www.cacert.org.crt b/installer/resources/certificates/www.cacert.org.crt
new file mode 100644
index 000000000..e7dfc8294
--- /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/licenses/LICENSE-InstallCert.txt b/licenses/LICENSE-InstallCert.txt
new file mode 100644
index 000000000..17620f66d
--- /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/networkdb/reseed/Reseeder.java b/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java
index 776d7eee4..9577b2b20 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 logs " +
"and if nothing helps, read the FAQ 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 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 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 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 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()) ) {