HTTP Proxy:

- Move error page methods to base
 - Preliminary code for digest auth
This commit is contained in:
zzz
2012-10-15 15:37:13 +00:00
parent 50cb427377
commit d01aae7860
3 changed files with 230 additions and 109 deletions

View File

@ -13,6 +13,7 @@ import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;
import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketOptions;
@ -20,7 +21,6 @@ import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.util.EventDispatcher;
import net.i2p.util.FileUtil;
import net.i2p.util.Log;
import net.i2p.util.PortMapper;
@ -58,6 +58,8 @@ import net.i2p.util.PortMapper;
*/
public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements Runnable {
private static final String AUTH_REALM = "I2P SSL Proxy";
private final static byte[] ERR_DESTINATION_UNKNOWN =
("HTTP/1.1 503 Service Unavailable\r\n"+
"Content-Type: text/html; charset=iso-8859-1\r\n"+
@ -94,7 +96,7 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
"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=\"I2P SSL Proxy\"\r\n" +
"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>")
@ -165,6 +167,11 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
return super.close(forced);
}
/** @since 0.9.4 */
protected String getRealm() {
return AUTH_REALM;
}
protected void clientConnectionRun(Socket s) {
InputStream in = null;
OutputStream out = null;
@ -237,10 +244,10 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
_log.debug(getPrefix(requestId) + "REST :" + restofline + ":");
_log.debug(getPrefix(requestId) + "DEST :" + destination + ":");
}
} else if (line.toLowerCase(Locale.US).startsWith("proxy-authorization: basic ")) {
} else if (line.toLowerCase(Locale.US).startsWith("proxy-authorization: ")) {
// strip Proxy-Authenticate from the response in HTTPResponseOutputStream
// save for auth check below
authorization = line.substring(27); // "proxy-authorization: basic ".length()
authorization = line.substring(21); // "proxy-authorization: ".length()
line = null;
} else if (line.length() > 0) {
// Additional lines - shouldn't be too many. Firefox sends:
@ -295,16 +302,11 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
Destination clientDest = _context.namingService().lookup(destination);
if (clientDest == null) {
String str;
byte[] header;
if (usingWWWProxy)
str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
header = getErrorPage("dnfp-header.ht", ERR_DESTINATION_UNKNOWN);
else
str = FileUtil.readTextFile((new File(_errorDir, "dnfh-header.ht")).getAbsolutePath(), 100, true);
if (str != null)
header = str.getBytes();
else
header = ERR_DESTINATION_UNKNOWN;
header = getErrorPage("dnfh-header.ht", ERR_DESTINATION_UNKNOWN);
writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination);
s.close();
return;
@ -341,12 +343,13 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
}
private static class OnTimeout implements Runnable {
private Socket _socket;
private OutputStream _out;
private String _target;
private boolean _usingProxy;
private String _wwwProxy;
private long _requestId;
private final Socket _socket;
private final OutputStream _out;
private final String _target;
private final boolean _usingProxy;
private final String _wwwProxy;
private final long _requestId;
public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) {
_socket = s;
_out = out;
@ -355,6 +358,7 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
_wwwProxy = wwwProxy;
_requestId = id;
}
public void run() {
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("Timeout occured requesting " + _target);
@ -391,17 +395,12 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R
boolean usingWWWProxy, String wwwProxy, long requestId) {
if (out == null)
return;
byte[] header;
if (usingWWWProxy)
header = getErrorPage(I2PAppContext.getGlobalContext(), "dnfp-header.ht", ERR_DESTINATION_UNKNOWN);
else
header = getErrorPage(I2PAppContext.getGlobalContext(), "dnf-header.ht", ERR_DESTINATION_UNKNOWN);
try {
String str;
byte[] header;
if (usingWWWProxy)
str = FileUtil.readTextFile((new File(_errorDir, "dnfp-header.ht")).getAbsolutePath(), 100, true);
else
str = FileUtil.readTextFile((new File(_errorDir, "dnf-header.ht")).getAbsolutePath(), 100, true);
if (str != null)
header = str.getBytes();
else
header = ERR_DESTINATION_UNKNOWN;
writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy);
} catch (IOException ioe) {}
}

View File

@ -3,9 +3,7 @@
*/
package net.i2p.i2ptunnel;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -73,10 +71,14 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
* via address helper links
*/
private final ConcurrentHashMap<String, String> addressHelpers = new ConcurrentHashMap(8);
/**
* Used to protect actions via http://proxy.i2p/
*/
private final String _proxyNonce;
private static final String AUTH_REALM = "I2P HTTP Proxy";
/**
* These are backups if the xxx.ht error page is missing.
*/
@ -167,12 +169,13 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
"\r\n" +
"<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();
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=\"I2P HTTP Proxy\"\r\n" +
"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();
@ -300,6 +303,12 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
}
return rv;
}
/** @since 0.9.4 */
protected String getRealm() {
return AUTH_REALM;
}
private static final String HELPER_PARAM = "i2paddresshelper";
public static final String LOCAL_SERVER = "proxy.i2p";
private static final boolean DEFAULT_GZIP = true;
@ -769,10 +778,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
// hop-by-hop header, and we definitely want to block Windows NTLM after a far-end 407.
// Response to far-end shouldn't happen, as we
// strip Proxy-Authenticate from the response in HTTPResponseOutputStream
if(lowercaseLine.startsWith("proxy-authorization: basic ")) // save for auth check below
{
authorization = line.substring(27); // "proxy-authorization: basic ".length()
}
authorization = line.substring(21); // "proxy-authorization: ".length()
line = null;
continue;
} else if(lowercaseLine.startsWith("icy")) {
@ -858,7 +864,11 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
_log.warn(getPrefix(requestId) + "Auth required, sending 407");
}
}
out.write(getErrorPage("auth", ERR_AUTH));
if (isDigestAuthRequired()) {
// weep
} else {
out.write(getErrorPage("auth", ERR_AUTH));
}
writeFooter(out);
s.close();
return;
@ -1095,61 +1105,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
return Base32.encode(_dest.calculateHash().getData()) + ".b32.i2p";
}
/**
* foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
* or the backup byte array on fail.
*
* .ht files must be UTF-8 encoded and use \r\n terminators so the
* HTTP headers are conformant.
* We can't use FileUtil.readFile() because it strips \r
*
* @return non-null
*/
private byte[] getErrorPage(String base, byte[] backup) {
return getErrorPage(_context, base, backup);
}
private static byte[] getErrorPage(I2PAppContext ctx, String base, byte[] backup) {
File errorDir = new File(ctx.getBaseDir(), "docs");
String lang = ctx.getProperty("routerconsole.lang", Locale.getDefault().getLanguage());
if(lang != null && lang.length() > 0 && !lang.equals("en")) {
File file = new File(errorDir, base + "-header_" + lang + ".ht");
try {
return readFile(file);
} catch(IOException ioe) {
// try the english version now
}
}
File file = new File(errorDir, base + "-header.ht");
try {
return readFile(file);
} catch(IOException ioe) {
return backup;
}
}
private static byte[] readFile(File file) throws IOException {
FileInputStream fis = null;
byte[] buf = new byte[512];
ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
try {
int len = 0;
fis = new FileInputStream(file);
while((len = fis.read(buf)) > 0) {
baos.write(buf, 0, len);
}
return baos.toByteArray();
} finally {
try {
if(fis != null) {
fis.close();
}
} catch(IOException foo) {
}
}
// we won't ever get here
}
/**
* Public only for LocalHTTPServer, not for general use
*/
@ -1163,12 +1118,12 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn
private static class OnTimeout implements Runnable {
private Socket _socket;
private OutputStream _out;
private String _target;
private boolean _usingProxy;
private String _wwwProxy;
private long _requestId;
private final Socket _socket;
private final OutputStream _out;
private final String _target;
private final boolean _usingProxy;
private final String _wwwProxy;
private final long _requestId;
public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) {
_socket = s;

View File

@ -3,6 +3,9 @@
*/
package net.i2p.i2ptunnel;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.ArrayList;
@ -13,9 +16,11 @@ import java.util.Locale;
import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.EventDispatcher;
import net.i2p.util.InternalSocket;
import net.i2p.util.Log;
import net.i2p.util.PasswordManager;
/**
* Common things for HTTPClient and ConnectClient
@ -25,6 +30,12 @@ import net.i2p.util.Log;
*/
public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implements Runnable {
private static final int PROXYNONCE_BYTES = 8;
private static final int MD5_BYTES = 16;
/** 24 */
private static final int NONCE_BYTES = DataHelper.DATE_LENGTH + MD5_BYTES;
private static final long MAX_NONCE_AGE = 30*24*60*60*1000L;
protected final List<String> _proxyList;
protected final static byte[] ERR_NO_OUTPROXY =
@ -40,7 +51,7 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
/** used to assign unique IDs to the threads / clients. no logic or functionality */
protected static volatile long __clientId = 0;
protected static final File _errorDir = new File(I2PAppContext.getGlobalContext().getBaseDir(), "docs");
private final byte[] _proxyNonce;
protected String getPrefix(long requestId) { return "Client[" + _clientId + "/" + requestId + "]: "; }
@ -63,6 +74,8 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
I2PTunnel tunnel) throws IllegalArgumentException {
super(localPort, ownDest, l, notifyThis, handlerName, tunnel);
_proxyList = new ArrayList(4);
_proxyNonce = new byte[PROXYNONCE_BYTES];
_context.random().nextBytes(_proxyNonce);
}
/**
@ -76,6 +89,8 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
throws IllegalArgumentException {
super(localPort, l, sktMgr, tunnel, notifyThis, clientId);
_proxyList = new ArrayList(4);
_proxyNonce = new byte[PROXYNONCE_BYTES];
_context.random().nextBytes(_proxyNonce);
}
/** all auth @since 0.8.2 */
@ -91,22 +106,48 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
public static final String PROP_OUTPROXY_USER_PREFIX = PROP_OUTPROXY_USER + '.';
public static final String PROP_OUTPROXY_PW_PREFIX = PROP_OUTPROXY_PW + '.';
protected abstract String getRealm();
/**
* @param authorization may be null
* @since 0.9.4
*/
protected boolean isDigestAuthRequired() {
String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
if (authRequired == null)
return true;
return authRequired.toLowerCase(Locale.US).equals("digest");
}
/**
* Authorization
* Ref: RFC 2617
* If the socket is an InternalSocket, no auth required.
*
* @param authorization may be null, the full auth line e.g. "Basic lskjlksjf"
* @return success
*/
protected boolean authorize(Socket s, long requestId, String authorization) {
// Authorization
// Ref: RFC 2617
// If the socket is an InternalSocket, no auth required.
String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
if (Boolean.parseBoolean(authRequired) ||
(authRequired != null && "basic".equals(authRequired.toLowerCase(Locale.US)))) {
if (s instanceof InternalSocket) {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix(requestId) + "Internal access, no auth required");
return true;
} else if (authorization != null) {
if (authRequired == null)
return true;
authRequired = authRequired.toLowerCase(Locale.US);
if (authRequired.equals("false"))
return true;
if (s instanceof InternalSocket) {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix(requestId) + "Internal access, no auth required");
return true;
}
if (authorization == null)
return false;
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 (!authLC.startsWith("basic "))
return false;
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) {
@ -148,10 +189,136 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem
if (_log.shouldLog(Log.WARN))
_log.warn(getPrefix(requestId) + "Bad auth B64: " + authorization);
}
}
return false;
} else if (authRequired.equals("digest")) {
if (!authLC.startsWith("digest "))
return false;
authorization = authorization.substring(7);
_log.error("Digest unimplemented");
return true;
} else {
_log.error("Unknown proxy authorization type configured: " + authRequired);
return true;
}
}
/**
* The Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
* @since 0.9.4
*/
private String getNonce() {
byte[] b = new byte[DataHelper.DATE_LENGTH + PROXYNONCE_BYTES];
byte[] n = new byte[NONCE_BYTES];
long now = _context.clock().now();
DataHelper.toLong(b, 0, DataHelper.DATE_LENGTH, now);
System.arraycopy(_proxyNonce, 0, b, DataHelper.DATE_LENGTH, PROXYNONCE_BYTES);
System.arraycopy(b, 0, n, 0, DataHelper.DATE_LENGTH);
byte[] md5 = PasswordManager.md5Sum(b);
System.arraycopy(md5, 0, n, DataHelper.DATE_LENGTH, MD5_BYTES);
return Base64.encode(n);
}
enum AuthResult {AUTH_BAD, AUTH_STALE, AUTH_GOOD}
/**
* Verify the Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
* @since 0.9.4
*/
private AuthResult verifyNonce(String b64) {
byte[] n = Base64.decode(b64);
if (n == null || n.length != NONCE_BYTES)
return AuthResult.AUTH_BAD;
long now = _context.clock().now();
long stamp = DataHelper.fromLong(n, 0, DataHelper.DATE_LENGTH);
if (now - stamp > MAX_NONCE_AGE)
return AuthResult.AUTH_STALE;
byte[] b = new byte[DataHelper.DATE_LENGTH + PROXYNONCE_BYTES];
System.arraycopy(n, 0, b, 0, DataHelper.DATE_LENGTH);
System.arraycopy(_proxyNonce, 0, b, DataHelper.DATE_LENGTH, PROXYNONCE_BYTES);
byte[] md5 = PasswordManager.md5Sum(b);
if (!DataHelper.eq(md5, 0, n, DataHelper.DATE_LENGTH, MD5_BYTES))
return AuthResult.AUTH_BAD;
return AuthResult.AUTH_GOOD;
}
protected String getDigestHeader(boolean isStale) {
return
"Proxy-Authenticate: Digest realm=\"" + getRealm() + "\"" +
" nonce=\"" + getNonce() + "\"" +
" algorithm=MD5" +
" qop=\"auth\"" +
(isStale ? " stale=true" : "") +
"\r\n";
}
/**
* foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
* or the backup byte array on fail.
*
* .ht files must be UTF-8 encoded and use \r\n terminators so the
* HTTP headers are conformant.
* We can't use FileUtil.readFile() because it strips \r
*
* @return non-null
* @since 0.9.4 moved from I2PTunnelHTTPClient
*/
protected byte[] getErrorPage(String base, byte[] backup) {
return getErrorPage(_context, base, backup);
}
/**
* foo => errordir/foo-header_xx.ht for lang xx, or errordir/foo-header.ht,
* or the backup byte array on fail.
*
* .ht files must be UTF-8 encoded and use \r\n terminators so the
* HTTP headers are conformant.
* We can't use FileUtil.readFile() because it strips \r
*
* @return non-null
* @since 0.9.4 moved from I2PTunnelHTTPClient
*/
protected static byte[] getErrorPage(I2PAppContext ctx, String base, byte[] backup) {
File errorDir = new File(ctx.getBaseDir(), "docs");
String lang = ctx.getProperty("routerconsole.lang", Locale.getDefault().getLanguage());
if(lang != null && lang.length() > 0 && !lang.equals("en")) {
File file = new File(errorDir, base + "-header_" + lang + ".ht");
try {
return readFile(file);
} catch(IOException ioe) {
// try the english version now
}
}
File file = new File(errorDir, base + "-header.ht");
try {
return readFile(file);
} catch(IOException ioe) {
return backup;
}
}
/**
* @since 0.9.4 moved from I2PTunnelHTTPClient
*/
private static byte[] readFile(File file) throws IOException {
FileInputStream fis = null;
byte[] buf = new byte[2048];
ByteArrayOutputStream baos = new ByteArrayOutputStream(2048);
try {
int len = 0;
fis = new FileInputStream(file);
while((len = fis.read(buf)) > 0) {
baos.write(buf, 0, len);
}
return baos.toByteArray();
} finally {
try {
if(fis != null) {
fis.close();
}
} catch(IOException foo) {
}
}
// we won't ever get here
}
}