diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
index 04ca75e75a..884795ce7f 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
@@ -13,6 +13,7 @@ import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;
+import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketOptions;
@@ -20,7 +21,6 @@ import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.util.EventDispatcher;
-import net.i2p.util.FileUtil;
import net.i2p.util.Log;
import net.i2p.util.PortMapper;
@@ -58,6 +58,8 @@ import net.i2p.util.PortMapper;
*/
public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements Runnable {
+ private static final String AUTH_REALM = "I2P SSL Proxy";
+
private final static byte[] ERR_DESTINATION_UNKNOWN =
("HTTP/1.1 503 Service Unavailable\r\n"+
"Content-Type: text/html; charset=iso-8859-1\r\n"+
@@ -94,7 +96,7 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
"Content-Type: text/html; charset=UTF-8\r\n"+
"Cache-control: no-cache\r\n"+
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.5\r\n" + // try to get a UTF-8-encoded response back for the password
- "Proxy-Authenticate: Basic realm=\"I2P SSL Proxy\"\r\n" +
+ "Proxy-Authenticate: Basic realm=\"" + AUTH_REALM + "\"\r\n" +
"\r\n"+
"
I2P ERROR: PROXY AUTHENTICATION REQUIRED
"+
"This proxy is configured to require authentication.
")
@@ -165,6 +167,11 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
return super.close(forced);
}
+ /** @since 0.9.4 */
+ protected String getRealm() {
+ return AUTH_REALM;
+ }
+
protected void clientConnectionRun(Socket s) {
InputStream in = null;
OutputStream out = null;
@@ -237,10 +244,10 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
_log.debug(getPrefix(requestId) + "REST :" + restofline + ":");
_log.debug(getPrefix(requestId) + "DEST :" + destination + ":");
}
- } else if (line.toLowerCase(Locale.US).startsWith("proxy-authorization: basic ")) {
+ } else if (line.toLowerCase(Locale.US).startsWith("proxy-authorization: ")) {
// strip Proxy-Authenticate from the response in HTTPResponseOutputStream
// save for auth check below
- authorization = line.substring(27); // "proxy-authorization: basic ".length()
+ authorization = line.substring(21); // "proxy-authorization: ".length()
line = null;
} else if (line.length() > 0) {
// Additional lines - shouldn't be too many. Firefox sends:
@@ -295,16 +302,11 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
Destination clientDest = _context.namingService().lookup(destination);
if (clientDest == null) {
- String str;
byte[] header;
if (usingWWWProxy)
- str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
+ header = getErrorPage("dnfp-header.ht", ERR_DESTINATION_UNKNOWN);
else
- str = FileUtil.readTextFile((new File(_errorDir, "dnfh-header.ht")).getAbsolutePath(), 100, true);
- if (str != null)
- header = str.getBytes();
- else
- header = ERR_DESTINATION_UNKNOWN;
+ header = getErrorPage("dnfh-header.ht", ERR_DESTINATION_UNKNOWN);
writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination);
s.close();
return;
@@ -341,12 +343,13 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
}
private static class OnTimeout implements Runnable {
- private Socket _socket;
- private OutputStream _out;
- private String _target;
- private boolean _usingProxy;
- private String _wwwProxy;
- private long _requestId;
+ private final Socket _socket;
+ private final OutputStream _out;
+ private final String _target;
+ private final boolean _usingProxy;
+ private final String _wwwProxy;
+ private final long _requestId;
+
public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) {
_socket = s;
_out = out;
@@ -355,6 +358,7 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
_wwwProxy = wwwProxy;
_requestId = id;
}
+
public void run() {
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Timeout occured requesting " + _target);
@@ -391,17 +395,12 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
boolean usingWWWProxy, String wwwProxy, long requestId) {
if (out == null)
return;
+ byte[] header;
+ if (usingWWWProxy)
+ header = getErrorPage(I2PAppContext.getGlobalContext(), "dnfp-header.ht", ERR_DESTINATION_UNKNOWN);
+ else
+ header = getErrorPage(I2PAppContext.getGlobalContext(), "dnf-header.ht", ERR_DESTINATION_UNKNOWN);
try {
- String str;
- byte[] header;
- if (usingWWWProxy)
- str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
- else
- str = FileUtil.readTextFile((new File(_errorDir, "dnf-header.ht")).getAbsolutePath(), 100, true);
- if (str != null)
- header = str.getBytes();
- else
- header = ERR_DESTINATION_UNKNOWN;
writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy);
} catch (IOException ioe) {}
}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
index bc7a474774..e7d7ecea7a 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
@@ -3,9 +3,7 @@
*/
package net.i2p.i2ptunnel;
-import java.io.ByteArrayOutputStream;
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -73,10 +71,14 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
* via address helper links
*/
private final ConcurrentHashMap addressHelpers = new ConcurrentHashMap(8);
+
/**
* Used to protect actions via http://proxy.i2p/
*/
private final String _proxyNonce;
+
+ private static final String AUTH_REALM = "I2P HTTP Proxy";
+
/**
* These are backups if the xxx.ht error page is missing.
*/
@@ -167,12 +169,13 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
"\r\n" +
"I2P ERROR: REQUEST DENIED
" +
"Your browser is misconfigured. Do not use the proxy to access the router console or other localhost destinations.
").getBytes();
+
private final static byte[] ERR_AUTH =
("HTTP/1.1 407 Proxy Authentication Required\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"Cache-control: no-cache\r\n" +
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.5\r\n" + // try to get a UTF-8-encoded response back for the password
- "Proxy-Authenticate: Basic realm=\"I2P HTTP Proxy\"\r\n" +
+ "Proxy-Authenticate: Basic realm=\"" + AUTH_REALM + "\"\r\n" +
"\r\n" +
"I2P ERROR: PROXY AUTHENTICATION REQUIRED
" +
"This proxy is configured to require authentication.
").getBytes();
@@ -300,6 +303,12 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
}
return rv;
}
+
+ /** @since 0.9.4 */
+ protected String getRealm() {
+ return AUTH_REALM;
+ }
+
private static final String HELPER_PARAM = "i2paddresshelper";
public static final String LOCAL_SERVER = "proxy.i2p";
private static final boolean DEFAULT_GZIP = true;
@@ -769,10 +778,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
// hop-by-hop header, and we definitely want to block Windows NTLM after a far-end 407.
// Response to far-end shouldn't happen, as we
// strip Proxy-Authenticate from the response in HTTPResponseOutputStream
- if(lowercaseLine.startsWith("proxy-authorization: basic ")) // save for auth check below
- {
- authorization = line.substring(27); // "proxy-authorization: basic ".length()
- }
+ authorization = line.substring(21); // "proxy-authorization: ".length()
line = null;
continue;
} else if(lowercaseLine.startsWith("icy")) {
@@ -858,7 +864,11 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
_log.warn(getPrefix(requestId) + "Auth required, sending 407");
}
}
- out.write(getErrorPage("auth", ERR_AUTH));
+ if (isDigestAuthRequired()) {
+ // weep
+ } else {
+ out.write(getErrorPage("auth", ERR_AUTH));
+ }
writeFooter(out);
s.close();
return;
@@ -1095,61 +1105,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
return Base32.encode(_dest.calculateHash().getData()) + ".b32.i2p";
}
- /**
- * foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
- * or the backup byte array on fail.
- *
- * .ht files must be UTF-8 encoded and use \r\n terminators so the
- * HTTP headers are conformant.
- * We can't use FileUtil.readFile() because it strips \r
- *
- * @return non-null
- */
- private byte[] getErrorPage(String base, byte[] backup) {
- return getErrorPage(_context, base, backup);
- }
-
- private static byte[] getErrorPage(I2PAppContext ctx, String base, byte[] backup) {
- File errorDir = new File(ctx.getBaseDir(), "docs");
- String lang = ctx.getProperty("routerconsole.lang", Locale.getDefault().getLanguage());
- if(lang != null && lang.length() > 0 && !lang.equals("en")) {
- File file = new File(errorDir, base + "-header_" + lang + ".ht");
- try {
- return readFile(file);
- } catch(IOException ioe) {
- // try the english version now
- }
- }
- File file = new File(errorDir, base + "-header.ht");
- try {
- return readFile(file);
- } catch(IOException ioe) {
- return backup;
- }
- }
-
- private static byte[] readFile(File file) throws IOException {
- FileInputStream fis = null;
- byte[] buf = new byte[512];
- ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
- try {
- int len = 0;
- fis = new FileInputStream(file);
- while((len = fis.read(buf)) > 0) {
- baos.write(buf, 0, len);
- }
- return baos.toByteArray();
- } finally {
- try {
- if(fis != null) {
- fis.close();
- }
- } catch(IOException foo) {
- }
- }
- // we won't ever get here
- }
-
/**
* Public only for LocalHTTPServer, not for general use
*/
@@ -1163,12 +1118,12 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
private static class OnTimeout implements Runnable {
- private Socket _socket;
- private OutputStream _out;
- private String _target;
- private boolean _usingProxy;
- private String _wwwProxy;
- private long _requestId;
+ private final Socket _socket;
+ private final OutputStream _out;
+ private final String _target;
+ private final boolean _usingProxy;
+ private final String _wwwProxy;
+ private final long _requestId;
public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) {
_socket = s;
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
index f14de68b4b..191a68adba 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
@@ -3,6 +3,9 @@
*/
package net.i2p.i2ptunnel;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.ArrayList;
@@ -13,9 +16,11 @@ import java.util.Locale;
import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.data.Base64;
+import net.i2p.data.DataHelper;
import net.i2p.util.EventDispatcher;
import net.i2p.util.InternalSocket;
import net.i2p.util.Log;
+import net.i2p.util.PasswordManager;
/**
* Common things for HTTPClient and ConnectClient
@@ -25,6 +30,12 @@ import net.i2p.util.Log;
*/
public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implements Runnable {
+ private static final int PROXYNONCE_BYTES = 8;
+ private static final int MD5_BYTES = 16;
+ /** 24 */
+ private static final int NONCE_BYTES = DataHelper.DATE_LENGTH + MD5_BYTES;
+ private static final long MAX_NONCE_AGE = 30*24*60*60*1000L;
+
protected final List _proxyList;
protected final static byte[] ERR_NO_OUTPROXY =
@@ -40,7 +51,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
/** used to assign unique IDs to the threads / clients. no logic or functionality */
protected static volatile long __clientId = 0;
- protected static final File _errorDir = new File(I2PAppContext.getGlobalContext().getBaseDir(), "docs");
+ private final byte[] _proxyNonce;
protected String getPrefix(long requestId) { return "Client[" + _clientId + "/" + requestId + "]: "; }
@@ -63,6 +74,8 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
I2PTunnel tunnel) throws IllegalArgumentException {
super(localPort, ownDest, l, notifyThis, handlerName, tunnel);
_proxyList = new ArrayList(4);
+ _proxyNonce = new byte[PROXYNONCE_BYTES];
+ _context.random().nextBytes(_proxyNonce);
}
/**
@@ -76,6 +89,8 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
throws IllegalArgumentException {
super(localPort, l, sktMgr, tunnel, notifyThis, clientId);
_proxyList = new ArrayList(4);
+ _proxyNonce = new byte[PROXYNONCE_BYTES];
+ _context.random().nextBytes(_proxyNonce);
}
/** all auth @since 0.8.2 */
@@ -91,22 +106,48 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
public static final String PROP_OUTPROXY_USER_PREFIX = PROP_OUTPROXY_USER + '.';
public static final String PROP_OUTPROXY_PW_PREFIX = PROP_OUTPROXY_PW + '.';
+ protected abstract String getRealm();
+
/**
- * @param authorization may be null
+ * @since 0.9.4
+ */
+ protected boolean isDigestAuthRequired() {
+ String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
+ if (authRequired == null)
+ return true;
+ return authRequired.toLowerCase(Locale.US).equals("digest");
+ }
+
+ /**
+ * Authorization
+ * Ref: RFC 2617
+ * If the socket is an InternalSocket, no auth required.
+ *
+ * @param authorization may be null, the full auth line e.g. "Basic lskjlksjf"
* @return success
*/
protected boolean authorize(Socket s, long requestId, String authorization) {
- // Authorization
- // Ref: RFC 2617
- // If the socket is an InternalSocket, no auth required.
String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
- if (Boolean.parseBoolean(authRequired) ||
- (authRequired != null && "basic".equals(authRequired.toLowerCase(Locale.US)))) {
- if (s instanceof InternalSocket) {
- if (_log.shouldLog(Log.INFO))
- _log.info(getPrefix(requestId) + "Internal access, no auth required");
- return true;
- } else if (authorization != null) {
+ if (authRequired == null)
+ return true;
+ authRequired = authRequired.toLowerCase(Locale.US);
+ if (authRequired.equals("false"))
+ return true;
+ if (s instanceof InternalSocket) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info(getPrefix(requestId) + "Internal access, no auth required");
+ return true;
+ }
+ if (authorization == null)
+ return false;
+ if (_log.shouldLog(Log.INFO))
+ _log.info(getPrefix(requestId) + "Auth: " + authorization);
+ String authLC = authorization.toLowerCase(Locale.US);
+ if (authRequired.equals("true") || authRequired.equals("basic")) {
+ if (!authLC.startsWith("basic "))
+ return false;
+ authorization = authorization.substring(6);
+
// hmm safeDecode(foo, true) to use standard alphabet is private in Base64
byte[] decoded = Base64.decode(authorization.replace("/", "~").replace("+", "="));
if (decoded != null) {
@@ -148,10 +189,136 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix(requestId) + "Bad auth B64: " + authorization);
}
- }
+
return false;
+ } else if (authRequired.equals("digest")) {
+ if (!authLC.startsWith("digest "))
+ return false;
+ authorization = authorization.substring(7);
+ _log.error("Digest unimplemented");
+ return true;
} else {
+ _log.error("Unknown proxy authorization type configured: " + authRequired);
return true;
}
}
+
+ /**
+ * The Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
+ * @since 0.9.4
+ */
+ private String getNonce() {
+ byte[] b = new byte[DataHelper.DATE_LENGTH + PROXYNONCE_BYTES];
+ byte[] n = new byte[NONCE_BYTES];
+ long now = _context.clock().now();
+ DataHelper.toLong(b, 0, DataHelper.DATE_LENGTH, now);
+ System.arraycopy(_proxyNonce, 0, b, DataHelper.DATE_LENGTH, PROXYNONCE_BYTES);
+ System.arraycopy(b, 0, n, 0, DataHelper.DATE_LENGTH);
+ byte[] md5 = PasswordManager.md5Sum(b);
+ System.arraycopy(md5, 0, n, DataHelper.DATE_LENGTH, MD5_BYTES);
+ return Base64.encode(n);
+ }
+
+ enum AuthResult {AUTH_BAD, AUTH_STALE, AUTH_GOOD}
+
+ /**
+ * Verify the Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
+ * @since 0.9.4
+ */
+ private AuthResult verifyNonce(String b64) {
+ byte[] n = Base64.decode(b64);
+ if (n == null || n.length != NONCE_BYTES)
+ return AuthResult.AUTH_BAD;
+ long now = _context.clock().now();
+ long stamp = DataHelper.fromLong(n, 0, DataHelper.DATE_LENGTH);
+ if (now - stamp > MAX_NONCE_AGE)
+ return AuthResult.AUTH_STALE;
+ byte[] b = new byte[DataHelper.DATE_LENGTH + PROXYNONCE_BYTES];
+ System.arraycopy(n, 0, b, 0, DataHelper.DATE_LENGTH);
+ System.arraycopy(_proxyNonce, 0, b, DataHelper.DATE_LENGTH, PROXYNONCE_BYTES);
+ byte[] md5 = PasswordManager.md5Sum(b);
+ if (!DataHelper.eq(md5, 0, n, DataHelper.DATE_LENGTH, MD5_BYTES))
+ return AuthResult.AUTH_BAD;
+ return AuthResult.AUTH_GOOD;
+ }
+
+ protected String getDigestHeader(boolean isStale) {
+ return
+ "Proxy-Authenticate: Digest realm=\"" + getRealm() + "\"" +
+ " nonce=\"" + getNonce() + "\"" +
+ " algorithm=MD5" +
+ " qop=\"auth\"" +
+ (isStale ? " stale=true" : "") +
+ "\r\n";
+ }
+
+ /**
+ * foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
+ * or the backup byte array on fail.
+ *
+ * .ht files must be UTF-8 encoded and use \r\n terminators so the
+ * HTTP headers are conformant.
+ * We can't use FileUtil.readFile() because it strips \r
+ *
+ * @return non-null
+ * @since 0.9.4 moved from I2PTunnelHTTPClient
+ */
+ protected byte[] getErrorPage(String base, byte[] backup) {
+ return getErrorPage(_context, base, backup);
+ }
+
+ /**
+ * foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
+ * or the backup byte array on fail.
+ *
+ * .ht files must be UTF-8 encoded and use \r\n terminators so the
+ * HTTP headers are conformant.
+ * We can't use FileUtil.readFile() because it strips \r
+ *
+ * @return non-null
+ * @since 0.9.4 moved from I2PTunnelHTTPClient
+ */
+ protected static byte[] getErrorPage(I2PAppContext ctx, String base, byte[] backup) {
+ File errorDir = new File(ctx.getBaseDir(), "docs");
+ String lang = ctx.getProperty("routerconsole.lang", Locale.getDefault().getLanguage());
+ if(lang != null && lang.length() > 0 && !lang.equals("en")) {
+ File file = new File(errorDir, base + "-header_" + lang + ".ht");
+ try {
+ return readFile(file);
+ } catch(IOException ioe) {
+ // try the english version now
+ }
+ }
+ File file = new File(errorDir, base + "-header.ht");
+ try {
+ return readFile(file);
+ } catch(IOException ioe) {
+ return backup;
+ }
+ }
+
+ /**
+ * @since 0.9.4 moved from I2PTunnelHTTPClient
+ */
+ private static byte[] readFile(File file) throws IOException {
+ FileInputStream fis = null;
+ byte[] buf = new byte[2048];
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
+ try {
+ int len = 0;
+ fis = new FileInputStream(file);
+ while((len = fis.read(buf)) > 0) {
+ baos.write(buf, 0, len);
+ }
+ return baos.toByteArray();
+ } finally {
+ try {
+ if(fis != null) {
+ fis.close();
+ }
+ } catch(IOException foo) {
+ }
+ }
+ // we won't ever get here
+ }
}