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);