more work on proxy digest auth

This commit is contained in:
zzz
2012-10-15 21:04:49 +00:00
parent d01aae7860
commit 9b6d5daeef
5 changed files with 185 additions and 45 deletions

View File

@@ -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.<BR>") "Your browser is misconfigured. Do not use the proxy to access the router console or other localhost destinations.<BR>")
.getBytes(); .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"+
"<html><body><H1>I2P ERROR: PROXY AUTHENTICATION REQUIRED</H1>"+
"This proxy is configured to require authentication.<BR>")
.getBytes();
private final static byte[] SUCCESS_RESPONSE = private final static byte[] SUCCESS_RESPONSE =
("HTTP/1.1 200 Connection Established\r\n"+ ("HTTP/1.1 200 Connection Established\r\n"+
"Proxy-agent: I2P\r\n"+ "Proxy-agent: I2P\r\n"+
@@ -288,14 +277,14 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
} }
// Authorization // Authorization
if (!authorize(s, requestId, authorization)) { if (!authorize(s, requestId, method, authorization)) {
if (_log.shouldLog(Log.WARN)) { if (_log.shouldLog(Log.WARN)) {
if (authorization != null) if (authorization != null)
_log.warn(getPrefix(requestId) + "Auth failed, sending 407 again"); _log.warn(getPrefix(requestId) + "Auth failed, sending 407 again");
else else
_log.warn(getPrefix(requestId) + "Auth required, sending 407"); _log.warn(getPrefix(requestId) + "Auth required, sending 407");
} }
writeErrorMessage(ERR_AUTH, out); out.write(getAuthError(false).getBytes());
s.close(); s.close();
return; return;
} }

View File

@@ -170,16 +170,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
"<html><body><H1>I2P ERROR: REQUEST DENIED</H1>" + "<html><body><H1>I2P ERROR: REQUEST DENIED</H1>" +
"Your browser is misconfigured. Do not use the proxy to access the router console or other localhost destinations.<BR>").getBytes(); "Your browser is misconfigured. Do not use the proxy to access the router console or other localhost destinations.<BR>").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" +
"<html><body><H1>I2P ERROR: PROXY AUTHENTICATION REQUIRED</H1>" +
"This proxy is configured to require authentication.<BR>").getBytes();
/** /**
* This constructor always starts the tunnel (ignoring the i2cp.delayOpen option). * This constructor always starts the tunnel (ignoring the i2cp.delayOpen option).
* It is used to add a client to an existing socket manager. * It is used to add a client to an existing socket manager.
@@ -856,7 +846,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
} }
// Authorization // Authorization
if(!authorize(s, requestId, authorization)) { if(!authorize(s, requestId, method, authorization)) {
if(_log.shouldLog(Log.WARN)) { if(_log.shouldLog(Log.WARN)) {
if(authorization != null) { if(authorization != null) {
_log.warn(getPrefix(requestId) + "Auth failed, sending 407 again"); _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"); _log.warn(getPrefix(requestId) + "Auth required, sending 407");
} }
} }
if (isDigestAuthRequired()) { out.write(getAuthError(false).getBytes());
// weep
} else {
out.write(getErrorPage("auth", ERR_AUTH));
}
writeFooter(out); writeFooter(out);
s.close(); s.close();
return; return;

View File

@@ -10,8 +10,10 @@ import java.io.UnsupportedEncodingException;
import java.net.Socket; import java.net.Socket;
import java.util.ArrayList; import java.util.ArrayList;
import java.io.File; import java.io.File;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PSocketManager; 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 int NONCE_BYTES = DataHelper.DATE_LENGTH + MD5_BYTES;
private static final long MAX_NONCE_AGE = 30*24*60*60*1000L; 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" +
"<html><body><H1>I2P ERROR: PROXY AUTHENTICATION REQUIRED</H1>" +
"This proxy is configured to require authentication.";
protected final List<String> _proxyList; protected final List<String> _proxyList;
protected final static byte[] ERR_NO_OUTPROXY = protected final static byte[] ERR_NO_OUTPROXY =
@@ -93,6 +108,8 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
_context.random().nextBytes(_proxyNonce); _context.random().nextBytes(_proxyNonce);
} }
//////// Authorization stuff
/** all auth @since 0.8.2 */ /** all auth @since 0.8.2 */
public static final String PROP_AUTH = "proxyAuth"; public static final String PROP_AUTH = "proxyAuth";
public static final String PROP_USER = "proxyUsername"; public static final String PROP_USER = "proxyUsername";
@@ -114,7 +131,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
protected boolean isDigestAuthRequired() { protected boolean isDigestAuthRequired() {
String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH); String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
if (authRequired == null) if (authRequired == null)
return true; return false;
return authRequired.toLowerCase(Locale.US).equals("digest"); return authRequired.toLowerCase(Locale.US).equals("digest");
} }
@@ -123,10 +140,11 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
* Ref: RFC 2617 * Ref: RFC 2617
* If the socket is an InternalSocket, no auth required. * 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" * @param authorization may be null, the full auth line e.g. "Basic lskjlksjf"
* @return success * @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); String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
if (authRequired == null) if (authRequired == null)
return true; return true;
@@ -195,14 +213,65 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
if (!authLC.startsWith("digest ")) if (!authLC.startsWith("digest "))
return false; return false;
authorization = authorization.substring(7); authorization = authorization.substring(7);
_log.error("Digest unimplemented"); Map<String, String> args = parseArgs(authorization);
return true; AuthResult rv = validateDigest(method, args);
return rv == AuthResult.AUTH_GOOD;
} else { } else {
_log.error("Unknown proxy authorization type configured: " + authRequired); _log.error("Unknown proxy authorization type configured: " + authRequired);
return true; return true;
} }
} }
/**
* Verify all of it.
* Ref: RFC 2617
* @since 0.9.4
*/
private AuthResult validateDigest(String method, Map<String, String> 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)) * The Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
* @since 0.9.4 * @since 0.9.4
@@ -219,7 +288,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
return Base64.encode(n); 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)) * 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; 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 return
"Proxy-Authenticate: Digest realm=\"" + getRealm() + "\"" + ERR_AUTH1 +
" nonce=\"" + getNonce() + "\"" + (isDigest ? "Digest" : "Basic") +
" algorithm=MD5" + " realm=\"" + getRealm() + '"' +
(isDigest ? ", nonce=\"" + getNonce() + "\"," +
" algorithm=MD5," +
" qop=\"auth\"" + " qop=\"auth\"" +
(isStale ? " stale=true" : "") + (isStale ? ", stale=true" : "")
"\r\n"; : "") +
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<String, String> parseArgs(String args) {
Map<String, String> 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, * foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
* or the backup byte array on fail. * or the backup byte array on fail.

View File

@@ -367,6 +367,15 @@ public class DataHelper {
* (unless the options param is an OrderedProperties) * (unless the options param is an OrderedProperties)
*/ */
public static String toString(Properties options) { 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(); StringBuilder buf = new StringBuilder();
if (options != null) { if (options != null) {
for (Map.Entry entry : options.entrySet()) { 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 * Use toHexString(byte[]) to not get leading zeros
* @param buf may be null (returns "") * @param buf may be null (returns "")
* @return String of length 2*buf.length * @return String of length 2*buf.length
@@ -516,7 +525,7 @@ public class DataHelper {
private static final byte[] EMPTY_BUFFER = "".getBytes(); 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 * Use toHexString(byte[]) to not get leading zeros
* @param buf may be null * @param buf may be null
* @param len number of bytes. If greater than buf.length, additional zeros will be prepended * @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 * Use toString(byte[] to get leading zeros
* @param data may be null (returns "00") * @param data may be null (returns "00")
*/ */

View File

@@ -161,6 +161,18 @@ public class PasswordManager {
*/ */
public static String md5Hex(String subrealm, String user, String pw) { public static String md5Hex(String subrealm, String user, String pw) {
String fullpw = user + ':' + subrealm + ':' + 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 { try {
byte[] data = fullpw.getBytes("ISO-8859-1"); byte[] data = fullpw.getBytes("ISO-8859-1");
byte[] sum = md5Sum(data); byte[] sum = md5Sum(data);