forked from I2P_Developers/i2p.i2p
* EepGet:
- Support Digest proxy authentication(ticket #1173) - Move authentication parsing method from I2PTunnelHTTPClientBase
This commit is contained in:
@@ -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
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user