* EepGet:

- Support Digest proxy authentication(ticket #1173)
   - Move authentication parsing method from I2PTunnelHTTPClientBase
This commit is contained in:
zzz
2014-02-06 01:29:46 +00:00
parent 4998f86efe
commit 0d028122a6
2 changed files with 294 additions and 65 deletions

View File

@@ -24,6 +24,7 @@ import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.EepGet;
import net.i2p.util.EventDispatcher;
import net.i2p.util.InternalSocket;
import net.i2p.util.Log;
@@ -409,60 +410,8 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
* @since 0.9.4
*/
private static Map<String, String> parseArgs(String args) {
Map<String, String> rv = new HashMap<String, String>(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;
// moved to EepGet, since it needs this too
return EepGet.parseAuthArgs(args);
}
//////// Error page stuff

View File

@@ -19,12 +19,15 @@ import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import gnu.getopt.Getopt;
import net.i2p.I2PAppContext;
import net.i2p.data.Base32;
import net.i2p.data.Base64;
import net.i2p.data.ByteArray;
import net.i2p.data.DataHelper;
@@ -87,6 +90,10 @@ public class EepGet {
protected boolean _isGzippedResponse;
protected IOException _decompressException;
// following for proxy digest auth
// only created via addAuthorization()
private AuthState _authState;
/** this will be replaced by the HTTP Proxy if we are using it */
protected static final String USER_AGENT = "Wget/1.11.4";
protected static final long CONNECT_TIMEOUT = 45*1000;
@@ -662,6 +669,7 @@ public class EepGet {
}
if (_redirectLocation != null) {
// we also are here after a 407
//try {
if (_redirectLocation.startsWith("http://")) {
_actualURL = _redirectLocation;
@@ -680,10 +688,27 @@ public class EepGet {
//} catch (MalformedURLException mue) {
// throw new IOException("Redirected from an invalid URL");
//}
_redirects++;
if (_redirects > 5)
throw new IOException("Too many redirects: to " + _redirectLocation);
if (_log.shouldLog(Log.INFO)) _log.info("Redirecting to " + _redirectLocation);
AuthState as = _authState;
if (_responseCode == 407) {
if (!_shouldProxy)
throw new IOException("Proxy auth response from non-proxy");
if (as == null)
throw new IOException("Proxy requires authentication");
if (as.authSent)
throw new IOException("Proxy authentication failed"); // ignore stale
if (as.authChallenge == null)
throw new IOException("Bad proxy auth response");
if (_log.shouldLog(Log.INFO)) _log.info("Adding auth");
// actually happens in getRequest()
} else {
_redirects++;
if (_redirects > 5)
throw new IOException("Too many redirects: to " + _redirectLocation);
if (_log.shouldLog(Log.INFO)) _log.info("Redirecting to " + _redirectLocation);
if (as != null)
as.authSent = false;
}
// reset some important variables, we don't want to save the values from the redirect
_bytesRemaining = -1;
@@ -871,6 +896,7 @@ public class EepGet {
_keepFetching = false;
_notModified = true;
return;
case 401: // server auth
case 403: // bad req
case 404: // not found
case 409: // bad addr helper
@@ -889,6 +915,17 @@ public class EepGet {
_out = new FileOutputStream(_outputFile, true);
}
break;
case 407: // proxy auth
// we will treat this is a redirect if we haven't sent auth yet
//_redirectLocation will be set to _actualURL below
_alreadyTransferred = 0;
if (_authState != null)
rcOk = !_authState.authSent;
else
rcOk = false;
redirect = rcOk;
_keepFetching = rcOk;
break;
case 416: // completed (or range out of reach)
_bytesRemaining = 0;
if (_alreadyTransferred > 0 || !_shouldWriteErrorToOutput) {
@@ -970,11 +1007,14 @@ public class EepGet {
increment(lookahead, cur);
if (isEndOfHeaders(lookahead)) {
if (!rcOk)
throw new IOException("Invalid HTTP response code: " + _responseCode);
throw new IOException("Invalid HTTP response code: " + _responseCode + ' ' + _responseText);
if (_encodingChunked) {
_bytesRemaining = readChunkLength();
}
if (!redirect) _redirectLocation = null;
if (!redirect)
_redirectLocation = null;
else if (_responseCode == 407)
_redirectLocation = _actualURL;
return;
}
break;
@@ -1081,6 +1121,8 @@ public class EepGet {
_contentType=val;
} else if (key.equals("location")) {
_redirectLocation=val;
} else if (key.equals("proxy-authenticate") && _responseCode == 407 && _authState != null && _shouldProxy) {
_authState.setAuthChallenge(val);
} else {
// ignore the rest
}
@@ -1192,10 +1234,11 @@ public class EepGet {
urlToSend += '?' + query;
}
if (post) {
buf.append("POST ").append(urlToSend).append(" HTTP/1.1\r\n");
buf.append("POST ");
} else {
buf.append("GET ").append(urlToSend).append(" HTTP/1.1\r\n");
buf.append("GET ");
}
buf.append(urlToSend).append(" HTTP/1.1\r\n");
// RFC 2616 sec 5.1.2 - host + port (NOT authority, which includes userinfo)
buf.append("Host: ").append(host);
if (port >= 0)
@@ -1240,6 +1283,12 @@ public class EepGet {
}
if(!uaOverridden)
buf.append("User-Agent: " + USER_AGENT + "\r\n");
if (_authState != null && _shouldProxy && _authState.authMode != AUTH_MODE.NONE) {
buf.append("Proxy-Authorization: ");
String method = post ? "POST" : "GET";
buf.append(_authState.getAuthHeader(method, urlToSend));
buf.append("\r\n");
}
buf.append("Connection: close\r\n\r\n");
if (post)
buf.append(_postData);
@@ -1335,9 +1384,240 @@ public class EepGet {
* @since 0.8.9
*/
public void addAuthorization(String userName, String password) {
if (_shouldProxy)
addHeader("Proxy-Authorization",
"Basic " + Base64.encode(DataHelper.getUTF8(userName + ':' + password), true)); // true = use standard alphabet
if (_shouldProxy) {
// Could only do this for Basic
// Now we always wait for the 407, in the hope we can use Digest
//addHeader("Proxy-Authorization",
// "Basic " + Base64.encode(DataHelper.getUTF8(userName + ':' + password), true)); // true = use standard alphabet
if (_authState != null)
throw new IllegalStateException();
_authState = new AuthState(userName, password);
}
}
/**
* Parse the args in an authentication header.
*
* Modified from LoadClientAppsJob.
* All keys are mapped to lower case.
* Double quotes around values are stripped.
* Ref: RFC 2617
*
* Public for I2PTunnelHTTPClientBase; use outside of tree at own risk, subject to change or removal
*
* @param args non-null, starting after "Digest " or "Basic "
* @since 0.9.4, moved from I2PTunnelHTTPClientBase in 0.9.12
*/
public static Map<String, String> parseAuthArgs(String args) {
Map<String, String> rv = new HashMap<String, String>(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;
}
/**
* @since 0.9.12
*/
private enum AUTH_MODE {NONE, BASIC, DIGEST, UNKNOWN}
/**
* Manage the authentication parameters
* Ref: RFC 2617
* Supports both Basic and Digest, however i2ptunnel HTTP proxy
* has migrated all previous Basic support to Digest.
*
* @since 0.9.12
*/
private class AuthState {
private final String username;
private final String password;
// as recvd in 407
public AUTH_MODE authMode = AUTH_MODE.NONE;
// as recvd in 407, after the mode string
private String authChallenge;
public boolean authSent;
private int nonceCount;
private String cnonce;
// as parsed from authChallenge
private Map<String, String> args;
public AuthState(String user, String pw) {
username = user;
password = pw;
}
/**
* May be called multiple times, save the best one
*/
public void setAuthChallenge(String auth) {
String authLC = auth.toLowerCase(Locale.US);
if (authLC.startsWith("basic ")) {
// better than anything but DIGEST
if (authMode != AUTH_MODE.DIGEST) {
// use standard alphabet
authMode = AUTH_MODE.BASIC;
authChallenge = auth.substring(6);
}
} else if (authLC.startsWith("digest ")) {
// better than anything
authMode = AUTH_MODE.DIGEST;
authChallenge = auth.substring(7);
} else {
// better than NONE only
if (authMode == AUTH_MODE.NONE) {
authMode = AUTH_MODE.UNKNOWN;
authChallenge = "";
}
}
nonceCount = 0;
args = null;
}
public String getAuthHeader(String method, String uri) throws IOException {
switch (authMode) {
case BASIC:
authSent = true;
// use standard alphabet
return "Basic " +
Base64.encode(DataHelper.getUTF8(username + ':' + password), true);
case DIGEST:
if (args == null)
args = parseAuthArgs(authChallenge);
Map<String, String> outArgs = generateAuthArgs(method, uri);
if (outArgs == null)
throw new IOException("Bad proxy auth response");
StringBuilder buf = new StringBuilder(256);
buf.append("Digest");
for (Map.Entry<String, String> e : outArgs.entrySet()) {
buf.append(' ').append(e.getKey()).append('=').append(e.getValue());
}
authSent = true;
return buf.toString();
default:
throw new IOException("Unknown proxy auth type " + authChallenge);
}
}
/**
* Generate the digest authentication parameters
* Ref: RFC 2617
*
* @since 0.9.12 modified from I2PTunnelHTTPClientBase.validateDigest()
*/
public Map<String, String> generateAuthArgs(String method, String uri) throws IOException {
Map<String, String> rv = new HashMap<String, String>(12);
String realm = args.get("realm");
String nonce = args.get("nonce");
String qop = args.get("qop");
String opaque = args.get("opaque");
//String algorithm = args.get("algorithm");
//String stale = args.get("stale");
if (realm == null || nonce == null) {
if (_log.shouldLog(Log.INFO))
_log.info("Bad digest request: " + DataHelper.toString(args));
throw new IOException("Bad auth response");
}
rv.put("username", '"' + username + '"');
rv.put("realm", '"' + realm + '"');
rv.put("nonce", '"' + nonce + '"');
rv.put("uri", '"' + uri + '"');
if (opaque != null)
rv.put("opaque", '"' + opaque + '"');
String kdMiddle;
if ("auth".equals(qop)) {
rv.put("qop", "\"auth\"");
if (cnonce == null) {
byte[] rand = new byte[5];
_context.random().nextBytes(rand);
cnonce = Base32.encode(rand);
} // else reuse on redirect
rv.put("cnonce", '"' + cnonce + '"');
String nc = lc8hex(++nonceCount);
rv.put("nc", nc);
kdMiddle = ':' + nc + ':' + cnonce + ':' + qop;
} else {
kdMiddle = "";
}
// get H(A1)
String ha1 = PasswordManager.md5Hex(username + ':' + realm + ':' + password);
// get H(A2)
String a2 = method + ':' + uri;
String ha2 = PasswordManager.md5Hex(a2);
// response
String kd = ha1 + ':' + nonce + kdMiddle + ':' + ha2;
rv.put("response", '"' + PasswordManager.md5Hex(kd) + '"');
return rv;
}
}
/**
* @return 8 hex chars, lower case, e.g. 00000001
* @since 0.8.10
*/
private static String lc8hex(int nc) {
StringBuilder buf = new StringBuilder(8);
for (int i = 28; i >= 0; i -= 4) {
int v = (nc >> i) & 0xf;
if (v < 10)
buf.append((char) (v + '0'));
else
buf.append((char) (v + 'a' - 10));
}
return buf.toString();
}
/**