- Convert HTTP and CONNECT proxies to MD5 authentication

- Allow multiple users
  - Migrate passwords on first save
This commit is contained in:
zzz
2012-10-16 19:17:06 +00:00
parent 613dd77d2c
commit a9e18620b9
6 changed files with 154 additions and 93 deletions

View File

@ -58,7 +58,7 @@ import net.i2p.util.PortMapper;
*/
public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements Runnable {
private static final String AUTH_REALM = "I2P SSL Proxy";
public static final String AUTH_REALM = "I2P SSL Proxy";
private final static byte[] ERR_DESTINATION_UNKNOWN =
("HTTP/1.1 503 Service Unavailable\r\n"+
@ -277,14 +277,15 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
}
// Authorization
if (!authorize(s, requestId, method, authorization)) {
AuthResult result = authorize(s, requestId, method, authorization);
if (result != AuthResult.AUTH_GOOD) {
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");
}
out.write(getAuthError(false).getBytes());
out.write(getAuthError(result == AuthResult.AUTH_STALE).getBytes());
s.close();
return;
}

View File

@ -77,7 +77,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
*/
private final String _proxyNonce;
private static final String AUTH_REALM = "I2P HTTP Proxy";
public static final String AUTH_REALM = "I2P HTTP Proxy";
/**
* These are backups if the xxx.ht error page is missing.
@ -846,7 +846,8 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
}
// Authorization
if(!authorize(s, requestId, method, authorization)) {
AuthResult result = authorize(s, requestId, method, authorization);
if (result != AuthResult.AUTH_GOOD) {
if(_log.shouldLog(Log.WARN)) {
if(authorization != null) {
_log.warn(getPrefix(requestId) + "Auth failed, sending 407 again");
@ -854,7 +855,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
_log.warn(getPrefix(requestId) + "Auth required, sending 407");
}
}
out.write(getAuthError(false).getBytes());
out.write(getAuthError(result == AuthResult.AUTH_STALE).getBytes());
writeFooter(out);
s.close();
return;

View File

@ -122,9 +122,16 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
/** passwords for specific outproxies may be added with outproxyUsername.fooproxy.i2p=user and outproxyPassword.fooproxy.i2p=pw */
public static final String PROP_OUTPROXY_USER_PREFIX = PROP_OUTPROXY_USER + '.';
public static final String PROP_OUTPROXY_PW_PREFIX = PROP_OUTPROXY_PW + '.';
/** new style MD5 auth */
public static final String PROP_PROXY_DIGEST_PREFIX = "proxy.auth.";
public static final String PROP_PROXY_DIGEST_SUFFIX = ".md5";
public static final String BASIC_AUTH = "basic";
public static final String DIGEST_AUTH = "digest";
protected abstract String getRealm();
protected enum AuthResult {AUTH_BAD_REQ, AUTH_BAD, AUTH_STALE, AUTH_GOOD}
/**
* @since 0.9.4
*/
@ -144,81 +151,77 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
* @param authorization may be null, the full auth line e.g. "Basic lskjlksjf"
* @return success
*/
protected boolean authorize(Socket s, long requestId, String method, String authorization) {
protected AuthResult authorize(Socket s, long requestId, String method, String authorization) {
String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
if (authRequired == null)
return true;
return AuthResult.AUTH_GOOD;
authRequired = authRequired.toLowerCase(Locale.US);
if (authRequired.equals("false"))
return true;
return AuthResult.AUTH_GOOD;
if (s instanceof InternalSocket) {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix(requestId) + "Internal access, no auth required");
return true;
return AuthResult.AUTH_GOOD;
}
if (authorization == null)
return false;
return AuthResult.AUTH_BAD;
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 (authRequired.equals("true") || authRequired.equals(BASIC_AUTH)) {
if (!authLC.startsWith("basic "))
return false;
return AuthResult.AUTH_BAD;
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) {
// We send Accept-Charset: UTF-8 in the 407 so hopefully it comes back that way inside the B64 ?
try {
String dec = new String(decoded, "UTF-8");
String[] parts = dec.split(":");
String user = parts[0];
String pw = parts[1];
// first try pw for that user
String configPW = getTunnel().getClientOptions().getProperty(PROP_PW_PREFIX + user);
if (configPW == null) {
// if not, look at default user and pw
String configUser = getTunnel().getClientOptions().getProperty(PROP_USER);
if (user.equals(configUser))
configPW = getTunnel().getClientOptions().getProperty(PROP_PW);
}
if (configPW != null) {
if (pw.equals(configPW)) {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix(requestId) + "Good auth - user: " + user + " pw: " + pw);
return true;
} else {
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix(requestId) + "Bad auth, pw mismatch - user: " + user + " pw: " + pw + " expected: " + configPW);
}
} else {
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix(requestId) + "Bad auth, no stored pw for user: " + user + " pw: " + pw);
}
} catch (UnsupportedEncodingException uee) {
_log.error(getPrefix(requestId) + "No UTF-8 support? B64: " + authorization, uee);
} catch (ArrayIndexOutOfBoundsException aioobe) {
// no ':' in response
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix(requestId) + "Bad auth B64: " + authorization, aioobe);
// hmm safeDecode(foo, true) to use standard alphabet is private in Base64
byte[] decoded = Base64.decode(authorization.replace("/", "~").replace("+", "="));
if (decoded != null) {
// We send Accept-Charset: UTF-8 in the 407 so hopefully it comes back that way inside the B64 ?
try {
String dec = new String(decoded, "UTF-8");
String[] parts = dec.split(":");
String user = parts[0];
String pw = parts[1];
// first try pw for that user
String configPW = getTunnel().getClientOptions().getProperty(PROP_PW_PREFIX + user);
if (configPW == null) {
// if not, look at default user and pw
String configUser = getTunnel().getClientOptions().getProperty(PROP_USER);
if (user.equals(configUser))
configPW = getTunnel().getClientOptions().getProperty(PROP_PW);
}
} else {
if (configPW != null) {
if (pw.equals(configPW)) {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix(requestId) + "Good auth - user: " + user + " pw: " + pw);
return AuthResult.AUTH_GOOD;
}
}
_log.logAlways(Log.WARN, "PROXY AUTH FAILURE: user " + user);
} catch (UnsupportedEncodingException uee) {
_log.error(getPrefix(requestId) + "No UTF-8 support? B64: " + authorization, uee);
} catch (ArrayIndexOutOfBoundsException aioobe) {
// no ':' in response
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix(requestId) + "Bad auth B64: " + authorization);
_log.warn(getPrefix(requestId) + "Bad auth B64: " + authorization, aioobe);
return AuthResult.AUTH_BAD_REQ;
}
return false;
} else if (authRequired.equals("digest")) {
return AuthResult.AUTH_BAD;
} else {
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix(requestId) + "Bad auth B64: " + authorization);
return AuthResult.AUTH_BAD_REQ;
}
} else if (authRequired.equals(DIGEST_AUTH)) {
if (!authLC.startsWith("digest "))
return false;
return AuthResult.AUTH_BAD;
authorization = authorization.substring(7);
Map<String, String> args = parseArgs(authorization);
AuthResult rv = validateDigest(method, args);
return rv == AuthResult.AUTH_GOOD;
return rv;
} else {
_log.error("Unknown proxy authorization type configured: " + authRequired);
return true;
return AuthResult.AUTH_BAD_REQ;
}
}
@ -250,10 +253,10 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
return check;
}
// get H(A1) == stored password
String ha1 = getTunnel().getClientOptions().getProperty(PROP_PW_PREFIX + user);
String ha1 = getTunnel().getClientOptions().getProperty(PROP_PROXY_DIGEST_PREFIX + user +
PROP_PROXY_DIGEST_SUFFIX);
if (ha1 == null) {
if (_log.shouldLog(Log.INFO))
_log.info("Bad digest auth - no stored pw for user: " + user);
_log.logAlways(Log.WARN, "PROXY AUTH FAILURE: user " + user);
return AuthResult.AUTH_BAD;
}
// get H(A2)
@ -263,8 +266,9 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
String kd = ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2;
String hkd = PasswordManager.md5Hex(kd);
if (!response.equals(hkd)) {
_log.logAlways(Log.WARN, "PROXY AUTH FAILURE: user " + user);
if (_log.shouldLog(Log.INFO))
_log.info("Bad digest auth - user: " + user);
_log.info("Bad digest auth: " + DataHelper.toString(args));
return AuthResult.AUTH_BAD;
}
if (_log.shouldLog(Log.INFO))
@ -288,8 +292,6 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
return Base64.encode(n);
}
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))
* @since 0.9.4

View File

@ -221,19 +221,7 @@ public class EditBean extends IndexBean {
/** all proxy auth @since 0.8.2 */
public boolean getProxyAuth(int tunnel) {
return getBooleanProperty(tunnel, I2PTunnelHTTPClientBase.PROP_AUTH) &&
getProxyUsername(tunnel).length() > 0 &&
getProxyPassword(tunnel).length() > 0;
}
public String getProxyUsername(int tunnel) {
return getProperty(tunnel, I2PTunnelHTTPClientBase.PROP_USER, "");
}
public String getProxyPassword(int tunnel) {
if (getProxyUsername(tunnel).length() <= 0)
return "";
return getProperty(tunnel, I2PTunnelHTTPClientBase.PROP_PW, "");
return getProperty(tunnel, I2PTunnelHTTPClientBase.PROP_AUTH, "false") != "false";
}
public boolean getOutproxyAuth(int tunnel) {
@ -354,10 +342,17 @@ public class EditBean extends IndexBean {
if (opts == null) return "";
StringBuilder buf = new StringBuilder(64);
int i = 0;
boolean isMD5Proxy = "httpclient".equals(tun.getType()) ||
"connectclient".equals(tun.getType());
for (Iterator iter = opts.keySet().iterator(); iter.hasNext(); ) {
String key = (String)iter.next();
if (_noShowSet.contains(key))
continue;
// leave in for HTTP and Connect so it can get migrated to MD5
// hide for SOCKS until migrated to MD5
if ((!isMD5Proxy) &&
_nonProxyNoShowSet.contains(key))
continue;
String val = opts.getProperty(key);
if (i != 0) buf.append(' ');
buf.append(key).append('=').append(val);

View File

@ -27,6 +27,7 @@ import net.i2p.data.Certificate;
import net.i2p.data.Destination;
import net.i2p.data.PrivateKeyFile;
import net.i2p.data.SessionKey;
import net.i2p.i2ptunnel.I2PTunnelConnectClient;
import net.i2p.i2ptunnel.I2PTunnelHTTPClient;
import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase;
import net.i2p.i2ptunnel.I2PTunnelIRCClient;
@ -36,6 +37,7 @@ import net.i2p.util.Addresses;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.FileUtil;
import net.i2p.util.Log;
import net.i2p.util.PasswordManager;
/**
* Simple accessor for exposing tunnel info, but also an ugly form handler
@ -83,6 +85,8 @@ public class IndexBean {
private int _hashCashValue;
private int _certType;
private String _certSigner;
private String _newProxyUser;
private String _newProxyPW;
public static final int RUNNING = 1;
public static final int STARTING = 2;
@ -224,9 +228,9 @@ public class IndexBean {
private String start() {
if (_tunnel < 0) return "Invalid tunnel";
List controllers = _group.getControllers();
List<TunnelController> controllers = _group.getControllers();
if (_tunnel >= controllers.size()) return "Invalid tunnel";
TunnelController controller = (TunnelController)controllers.get(_tunnel);
TunnelController controller = controllers.get(_tunnel);
controller.startTunnelBackground();
// give the messages a chance to make it to the window
try { Thread.sleep(1000); } catch (InterruptedException ie) {}
@ -237,9 +241,9 @@ public class IndexBean {
private String stop() {
if (_tunnel < 0) return "Invalid tunnel";
List controllers = _group.getControllers();
List<TunnelController> controllers = _group.getControllers();
if (_tunnel >= controllers.size()) return "Invalid tunnel";
TunnelController controller = (TunnelController)controllers.get(_tunnel);
TunnelController controller = controllers.get(_tunnel);
controller.stopTunnel();
// give the messages a chance to make it to the window
try { Thread.sleep(1000); } catch (InterruptedException ie) {}
@ -268,10 +272,10 @@ public class IndexBean {
// if the current tunnel is shared, and of supported type
if (Boolean.parseBoolean(cur.getSharedClient()) && isClient(cur.getType())) {
// all clients use the same I2CP session, and as such, use the same I2CP options
List controllers = _group.getControllers();
List<TunnelController> controllers = _group.getControllers();
for (int i = 0; i < controllers.size(); i++) {
TunnelController c = (TunnelController)controllers.get(i);
TunnelController c = controllers.get(i);
// Current tunnel modified by user, skip
if (c == cur) continue;
@ -804,21 +808,22 @@ public class IndexBean {
/** all proxy auth @since 0.8.2 */
public void setProxyAuth(String s) {
_booleanOptions.add(I2PTunnelHTTPClientBase.PROP_AUTH);
if (s != null)
_otherOptions.put(I2PTunnelHTTPClientBase.PROP_AUTH, I2PTunnelHTTPClientBase.DIGEST_AUTH);
}
public void setProxyUsername(String s) {
if (s != null)
_otherOptions.put(I2PTunnelHTTPClientBase.PROP_USER, s.trim());
_newProxyUser = s.trim();
}
public void setProxyPassword(String s) {
if (s != null)
_otherOptions.put(I2PTunnelHTTPClientBase.PROP_PW, s.trim());
_newProxyPW = s.trim();
}
public void setOutproxyAuth(String s) {
_booleanOptions.add(I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH);
_otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH, I2PTunnelHTTPClientBase.DIGEST_AUTH);
}
public void setOutproxyUsername(String s) {
@ -1040,6 +1045,45 @@ public class IndexBean {
config.setProperty("proxyList", _proxyList);
}
// Proxy auth including migration to MD5
if ("httpclient".equals(_type) || "connectclient".equals(_type)) {
// Migrate even if auth is disabled
// go get the old from custom options that updateConfigGeneric() put in there
String puser = "option." + I2PTunnelHTTPClientBase.PROP_USER;
String user = config.getProperty(puser);
String ppw = "option." + I2PTunnelHTTPClientBase.PROP_PW;
String pw = config.getProperty(ppw);
if (user != null && pw != null && user.length() > 0 && pw.length() > 0) {
String pmd5 = "option." + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
user + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SUFFIX;
if (config.getProperty(pmd5) == null) {
// not in there, migrate
String realm = _type.equals("httpclient") ? I2PTunnelHTTPClient.AUTH_REALM
: I2PTunnelConnectClient.AUTH_REALM;
String hex = PasswordManager.md5Hex(realm, user, pw);
if (hex != null) {
config.setProperty(pmd5, hex);
config.remove(puser);
config.remove(ppw);
}
}
}
// New user/password
String auth = _otherOptions.get(I2PTunnelHTTPClientBase.PROP_AUTH);
if (auth != null && !auth.equals("false")) {
if (_newProxyUser != null && _newProxyPW != null &&
_newProxyUser.length() > 0 && _newProxyPW.length() > 0) {
String pmd5 = "option." + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
_newProxyUser + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SUFFIX;
String realm = _type.equals("httpclient") ? I2PTunnelHTTPClient.AUTH_REALM
: I2PTunnelConnectClient.AUTH_REALM;
String hex = PasswordManager.md5Hex(realm, _newProxyUser, _newProxyPW);
if (hex != null)
config.setProperty(pmd5, hex);
}
}
}
if ("ircclient".equals(_type) || "client".equals(_type) || "streamrclient".equals(_type)) {
if (_targetDestination != null)
config.setProperty("targetDestination", _targetDestination);
@ -1084,15 +1128,16 @@ public class IndexBean {
"i2cp.reduceOnIdle", "i2cp.closeOnIdle", "i2cp.newDestOnResume", "persistentClientKey", "i2cp.delayOpen"
};
private static final String _booleanProxyOpts[] = {
I2PTunnelHTTPClientBase.PROP_AUTH, I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH
I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH
};
private static final String _booleanServerOpts[] = {
"i2cp.reduceOnIdle", "i2cp.encryptLeaseSet", PROP_ENABLE_ACCESS_LIST, PROP_ENABLE_BLACKLIST
};
private static final String _otherClientOpts[] = {
"i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.closeIdleTime",
"proxyUsername", "proxyPassword", "outproxyUsername", "outproxyPassword",
I2PTunnelHTTPClient.PROP_JUMP_SERVERS
"outproxyUsername", "outproxyPassword",
I2PTunnelHTTPClient.PROP_JUMP_SERVERS,
I2PTunnelHTTPClientBase.PROP_AUTH
};
private static final String _otherServerOpts[] = {
"i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.leaseSetKey", "i2cp.accessList",
@ -1101,7 +1146,17 @@ public class IndexBean {
PROP_MAX_STREAMS
};
/**
* do NOT add these to noShoOpts, we must leave them in for HTTPClient and ConnectCLient
* so they will get migrated to MD5
* TODO migrate socks to MD5
*/
private static final String _otherProxyOpts[] = {
"proxyUsername", "proxyPassword"
};
protected static final Set _noShowSet = new HashSet(64);
protected static final Set _nonProxyNoShowSet = new HashSet(4);
static {
_noShowSet.addAll(Arrays.asList(_noShowOpts));
_noShowSet.addAll(Arrays.asList(_booleanClientOpts));
@ -1109,6 +1164,7 @@ public class IndexBean {
_noShowSet.addAll(Arrays.asList(_booleanServerOpts));
_noShowSet.addAll(Arrays.asList(_otherClientOpts));
_noShowSet.addAll(Arrays.asList(_otherServerOpts));
_nonProxyNoShowSet.addAll(Arrays.asList(_otherProxyOpts));
}
private void updateConfigGeneric(Properties config) {
@ -1139,6 +1195,12 @@ public class IndexBean {
String key = pair.substring(0, eq);
if (_noShowSet.contains(key))
continue;
// leave in for HTTP and Connect so it can get migrated to MD5
// hide for SOCKS until migrated to MD5
if ((!"httpclient".equals(_type)) &&
(! "connectclient".equals(_type)) &&
_nonProxyNoShowSet.contains(key))
continue;
String val = pair.substring(eq+1);
config.setProperty("option." + key, val);
}
@ -1190,9 +1252,9 @@ public class IndexBean {
protected TunnelController getController(int tunnel) {
if (tunnel < 0) return null;
if (_group == null) return null;
List controllers = _group.getControllers();
List<TunnelController> controllers = _group.getControllers();
if (controllers.size() > tunnel)
return (TunnelController)controllers.get(tunnel);
return controllers.get(tunnel);
else
return null;
}