diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
index 884795ce7..fde47077f 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java
@@ -91,17 +91,6 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
"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=\"" + AUTH_REALM + "\"\r\n" +
- "\r\n"+
- "
I2P ERROR: PROXY AUTHENTICATION REQUIRED
"+
- "This proxy is configured to require authentication.
")
- .getBytes();
-
private final static byte[] SUCCESS_RESPONSE =
("HTTP/1.1 200 Connection Established\r\n"+
"Proxy-agent: I2P\r\n"+
@@ -288,14 +277,14 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
}
// Authorization
- if (!authorize(s, requestId, authorization)) {
+ if (!authorize(s, requestId, method, authorization)) {
if (_log.shouldLog(Log.WARN)) {
if (authorization != null)
_log.warn(getPrefix(requestId) + "Auth failed, sending 407 again");
else
_log.warn(getPrefix(requestId) + "Auth required, sending 407");
}
- writeErrorMessage(ERR_AUTH, out);
+ out.write(getAuthError(false).getBytes());
s.close();
return;
}
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
index e7d7ecea7..594e64c3a 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java
@@ -170,16 +170,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
"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=\"" + AUTH_REALM + "\"\r\n" +
- "\r\n" +
- "I2P ERROR: PROXY AUTHENTICATION REQUIRED
" +
- "This proxy is configured to require authentication.
").getBytes();
-
/**
* This constructor always starts the tunnel (ignoring the i2cp.delayOpen option).
* It is used to add a client to an existing socket manager.
@@ -856,7 +846,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
}
// Authorization
- if(!authorize(s, requestId, authorization)) {
+ if(!authorize(s, requestId, method, authorization)) {
if(_log.shouldLog(Log.WARN)) {
if(authorization != null) {
_log.warn(getPrefix(requestId) + "Auth failed, sending 407 again");
@@ -864,11 +854,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
_log.warn(getPrefix(requestId) + "Auth required, sending 407");
}
}
- if (isDigestAuthRequired()) {
- // weep
- } else {
- out.write(getErrorPage("auth", ERR_AUTH));
- }
+ out.write(getAuthError(false).getBytes());
writeFooter(out);
s.close();
return;
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
index 191a68adb..4b53e0399 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java
@@ -10,8 +10,10 @@ import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.ArrayList;
import java.io.File;
+import java.util.HashMap;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PSocketManager;
@@ -36,6 +38,19 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
private static final int NONCE_BYTES = DataHelper.DATE_LENGTH + MD5_BYTES;
private static final long MAX_NONCE_AGE = 30*24*60*60*1000L;
+ private static final String ERR_AUTH1 =
+ "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: ";
+ // put the auth type and realm in between
+ private static final String ERR_AUTH2 =
+ "\r\n" +
+ "\r\n" +
+ "I2P ERROR: PROXY AUTHENTICATION REQUIRED
" +
+ "This proxy is configured to require authentication.";
+
protected final List _proxyList;
protected final static byte[] ERR_NO_OUTPROXY =
@@ -93,6 +108,8 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
_context.random().nextBytes(_proxyNonce);
}
+ //////// Authorization stuff
+
/** all auth @since 0.8.2 */
public static final String PROP_AUTH = "proxyAuth";
public static final String PROP_USER = "proxyUsername";
@@ -114,7 +131,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
protected boolean isDigestAuthRequired() {
String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
if (authRequired == null)
- return true;
+ return false;
return authRequired.toLowerCase(Locale.US).equals("digest");
}
@@ -123,10 +140,11 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
* Ref: RFC 2617
* If the socket is an InternalSocket, no auth required.
*
+ * @param method GET, POST, etc.
* @param authorization may be null, the full auth line e.g. "Basic lskjlksjf"
* @return success
*/
- protected boolean authorize(Socket s, long requestId, String authorization) {
+ protected boolean authorize(Socket s, long requestId, String method, String authorization) {
String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
if (authRequired == null)
return true;
@@ -195,14 +213,65 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
if (!authLC.startsWith("digest "))
return false;
authorization = authorization.substring(7);
- _log.error("Digest unimplemented");
- return true;
+ Map args = parseArgs(authorization);
+ AuthResult rv = validateDigest(method, args);
+ return rv == AuthResult.AUTH_GOOD;
} else {
_log.error("Unknown proxy authorization type configured: " + authRequired);
return true;
}
}
+ /**
+ * Verify all of it.
+ * Ref: RFC 2617
+ * @since 0.9.4
+ */
+ private AuthResult validateDigest(String method, Map args) {
+ String user = args.get("username");
+ String realm = args.get("realm");
+ String nonce = args.get("nonce");
+ String qop = args.get("qop");
+ String uri = args.get("uri");
+ String cnonce = args.get("cnonce");
+ String nc = args.get("nc");
+ String response = args.get("response");
+ if (user == null || realm == null || nonce == null || qop == null ||
+ uri == null || cnonce == null || nc == null || response == null) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Bad digest request: " + DataHelper.toString(args));
+ return AuthResult.AUTH_BAD_REQ;
+ }
+ // nonce check
+ AuthResult check = verifyNonce(nonce);
+ if (check != AuthResult.AUTH_GOOD) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Bad digest nonce: " + check + ' ' + DataHelper.toString(args));
+ return check;
+ }
+ // get H(A1) == stored password
+ String ha1 = getTunnel().getClientOptions().getProperty(PROP_PW_PREFIX + user);
+ if (ha1 == null) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Bad digest auth - no stored pw for user: " + user);
+ return AuthResult.AUTH_BAD;
+ }
+ // get H(A2)
+ String a2 = method + ':' + uri;
+ String ha2 = PasswordManager.md5Hex(a2);
+ // response check
+ String kd = ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2;
+ String hkd = PasswordManager.md5Hex(kd);
+ if (!response.equals(hkd)) {
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Bad digest auth - user: " + user);
+ return AuthResult.AUTH_BAD;
+ }
+ if (_log.shouldLog(Log.INFO))
+ _log.info("Good digest auth - user: " + user);
+ return AuthResult.AUTH_GOOD;
+ }
+
/**
* The Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
* @since 0.9.4
@@ -219,7 +288,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
return Base64.encode(n);
}
- enum AuthResult {AUTH_BAD, AUTH_STALE, AUTH_GOOD}
+ protected enum AuthResult {AUTH_BAD_REQ, AUTH_BAD, AUTH_STALE, AUTH_GOOD}
/**
* Verify the Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
@@ -242,16 +311,91 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
return AuthResult.AUTH_GOOD;
}
- protected String getDigestHeader(boolean isStale) {
+ /**
+ * What to send if digest auth fails
+ * @since 0.9.4
+ */
+ protected String getAuthError(boolean isStale) {
+ boolean isDigest = isDigestAuthRequired();
return
- "Proxy-Authenticate: Digest realm=\"" + getRealm() + "\"" +
- " nonce=\"" + getNonce() + "\"" +
- " algorithm=MD5" +
- " qop=\"auth\"" +
- (isStale ? " stale=true" : "") +
- "\r\n";
+ ERR_AUTH1 +
+ (isDigest ? "Digest" : "Basic") +
+ " realm=\"" + getRealm() + '"' +
+ (isDigest ? ", nonce=\"" + getNonce() + "\"," +
+ " algorithm=MD5," +
+ " qop=\"auth\"" +
+ (isStale ? ", stale=true" : "")
+ : "") +
+ ERR_AUTH2;
}
+ /**
+ * Modified from LoadClientAppsJob.
+ * All keys are mapped to lower case.
+ * Ref: RFC 2617
+ *
+ * @param args non-null
+ * @since 0.9.4
+ */
+ private static Map parseArgs(String args) {
+ Map rv = new HashMap(8);
+ char data[] = args.toCharArray();
+ StringBuilder buf = new StringBuilder(32);
+ boolean isQuoted = false;
+ String key = null;
+ for (int i = 0; i < data.length; i++) {
+ switch (data[i]) {
+ case '\"':
+ if (isQuoted) {
+ // keys never quoted
+ if (key != null) {
+ rv.put(key, buf.toString().trim());
+ key = null;
+ }
+ buf.setLength(0);
+ }
+ isQuoted = !isQuoted;
+ break;
+
+ case ' ':
+ case '\r':
+ case '\n':
+ case '\t':
+ case ',':
+ // whitespace - if we're in a quoted section, keep this as part of the quote,
+ // otherwise use it as a delim
+ if (isQuoted) {
+ buf.append(data[i]);
+ } else {
+ if (key != null) {
+ rv.put(key, buf.toString().trim());
+ key = null;
+ }
+ buf.setLength(0);
+ }
+ break;
+
+ case '=':
+ if (isQuoted) {
+ buf.append(data[i]);
+ } else {
+ key = buf.toString().trim().toLowerCase(Locale.US);
+ buf.setLength(0);
+ }
+ break;
+
+ default:
+ buf.append(data[i]);
+ break;
+ }
+ }
+ if (key != null)
+ rv.put(key, buf.toString().trim());
+ return rv;
+ }
+
+ //////// Error page stuff
+
/**
* foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
* or the backup byte array on fail.
diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java
index 04ec793fd..97d6ce501 100644
--- a/core/java/src/net/i2p/data/DataHelper.java
+++ b/core/java/src/net/i2p/data/DataHelper.java
@@ -361,12 +361,21 @@ public class DataHelper {
throw new RuntimeException("IO error writing to memory?! " + ioe.getMessage());
}
}
-
+
/**
* Pretty print the mapping, unsorted
* (unless the options param is an OrderedProperties)
*/
public static String toString(Properties options) {
+ return toString((Map) options);
+ }
+
+ /**
+ * Pretty print the mapping, unsorted
+ * (unless the options param is an OrderedProperties)
+ * @since 0.9.4
+ */
+ public static String toString(Map, ?> options) {
StringBuilder buf = new StringBuilder();
if (options != null) {
for (Map.Entry entry : options.entrySet()) {
@@ -502,7 +511,7 @@ public class DataHelper {
}
/**
- * Hex with leading zeros.
+ * Lower-case hex with leading zeros.
* Use toHexString(byte[]) to not get leading zeros
* @param buf may be null (returns "")
* @return String of length 2*buf.length
@@ -516,7 +525,7 @@ public class DataHelper {
private static final byte[] EMPTY_BUFFER = "".getBytes();
/**
- * Hex with leading zeros.
+ * Lower-case hex with leading zeros.
* Use toHexString(byte[]) to not get leading zeros
* @param buf may be null
* @param len number of bytes. If greater than buf.length, additional zeros will be prepended
@@ -546,7 +555,7 @@ public class DataHelper {
}
/**
- * Hex without leading zeros.
+ * Lower-case hex without leading zeros.
* Use toString(byte[] to get leading zeros
* @param data may be null (returns "00")
*/
diff --git a/core/java/src/net/i2p/util/PasswordManager.java b/core/java/src/net/i2p/util/PasswordManager.java
index 83176f7cc..e0f75a095 100644
--- a/core/java/src/net/i2p/util/PasswordManager.java
+++ b/core/java/src/net/i2p/util/PasswordManager.java
@@ -161,6 +161,18 @@ public class PasswordManager {
*/
public static String md5Hex(String subrealm, String user, String pw) {
String fullpw = user + ':' + subrealm + ':' + pw;
+ return md5Hex(fullpw);
+ }
+
+ /**
+ * Straight MD5, no salt
+ * Will return the MD5 sum of the data, compatible with Jetty
+ * and RFC 2617.
+ *
+ * @param fullpw non-null, plain text, already trimmed
+ * @return lower-case hex with leading zeros, 32 chars, or null on error
+ */
+ public static String md5Hex(String fullpw) {
try {
byte[] data = fullpw.getBytes("ISO-8859-1");
byte[] sum = md5Sum(data);