I2PTunnelHTTPClientBase.java 31.90 KiB
/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
* (c) 2003 - 2004 mihi
*/
package net.i2p.i2ptunnel;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PSocketException;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.i2cp.MessageStatusMessage;
import net.i2p.util.EepGet;
import net.i2p.util.EventDispatcher;
import net.i2p.util.InternalSocket;
import net.i2p.util.Log;
import net.i2p.util.PasswordManager;
import net.i2p.util.PortMapper;
import net.i2p.util.Translate;
import net.i2p.util.TranslateReader;
/**
* Common things for HTTPClient and ConnectClient
* Retrofit over them in 0.8.2
*
* @since 0.8.2
*/
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 = 60*60*1000L;
private static final int MAX_NONCE_COUNT = 1024;
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" +
"Connection: close\r\n"+
"Proxy-Connection: close\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 static String ERR_NO_OUTPROXY =
"HTTP/1.1 503 Service Unavailable\r\n"+
"Content-Type: text/html; charset=iso-8859-1\r\n"+
"Cache-control: no-cache\r\n"+
"Connection: close\r\n"+
"Proxy-Connection: close\r\n"+
"\r\n"+
"<html><body><H1>I2P ERROR: No outproxy found</H1>"+
"Your request was for a site outside of I2P, but you have no "+
"HTTP outproxy configured. Please configure an outproxy in I2PTunnel";
protected final static String ERR_DESTINATION_UNKNOWN =
"HTTP/1.1 503 Service Unavailable\r\n" +
"Content-Type: text/html; charset=iso-8859-1\r\n" +
"Cache-control: no-cache\r\n" +
"Connection: close\r\n"+
"Proxy-Connection: close\r\n"+
"\r\n" +
"<html><body><H1>I2P ERROR: DESTINATION NOT FOUND</H1>" +
"That I2P Destination was not found. Perhaps you pasted in the " +
"wrong BASE64 I2P Destination or the link you are following is " +
"bad. The host (or the WWW proxy, if you're using one) could also " +
"be temporarily offline. You may want to <b>retry</b>. " +
"Could not find the following Destination:<BR><BR><div>";
protected final static String SUCCESS_RESPONSE =
"HTTP/1.1 200 Connection Established\r\n"+
"Proxy-agent: I2P\r\n"+
"\r\n";
private final byte[] _proxyNonce;
private final ConcurrentHashMap<String, NonceInfo> _nonces;
private final AtomicInteger _nonceCleanCounter = new AtomicInteger();
protected String getPrefix(long requestId) {
return "HTTPClient[" + _clientId + '/' + requestId + "]: ";
}
protected String selectProxy() {
synchronized (_proxyList) {
int size = _proxyList.size();
if (size <= 0)
return null;
int index = _context.random().nextInt(size);
return _proxyList.get(index);
}
}
protected static final int DEFAULT_READ_TIMEOUT = 5*60*1000;
protected static final AtomicLong __requestId = new AtomicLong();
public I2PTunnelHTTPClientBase(int localPort, boolean ownDest, Logging l,
EventDispatcher notifyThis, String handlerName,
I2PTunnel tunnel) throws IllegalArgumentException {
super(localPort, ownDest, l, notifyThis, handlerName, tunnel);
_proxyList = new ArrayList<String>(4);
_proxyNonce = new byte[PROXYNONCE_BYTES];
_context.random().nextBytes(_proxyNonce);
_nonces = new ConcurrentHashMap<String, NonceInfo>();
}
/**
* This constructor always starts the tunnel (ignoring the i2cp.delayOpen option).
* It is used to add a client to an existing socket manager.
*
* @param sktMgr the existing socket manager
*/
public I2PTunnelHTTPClientBase(int localPort, Logging l, I2PSocketManager sktMgr,
I2PTunnel tunnel, EventDispatcher notifyThis, long clientId )
throws IllegalArgumentException {
super(localPort, l, sktMgr, tunnel, notifyThis, clientId);
_proxyList = new ArrayList<String>(4);
_proxyNonce = new byte[PROXYNONCE_BYTES];
_context.random().nextBytes(_proxyNonce);
_nonces = new ConcurrentHashMap<String, NonceInfo>();
}
//////// Authorization stuff
/** all auth @since 0.8.2 */
public static final String PROP_AUTH = "proxyAuth";
public static final String PROP_USER = "proxyUsername";
public static final String PROP_PW = "proxyPassword";
/** additional users may be added with proxyPassword.user=pw */
public static final String PROP_PW_PREFIX = PROP_PW + '.';
public static final String PROP_OUTPROXY_AUTH = "outproxyAuth";
public static final String PROP_OUTPROXY_USER = "outproxyUsername";
public static final String PROP_OUTPROXY_PW = "outproxyPassword";
/** 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.6
*/
private static class NonceInfo {
private final long expires;
private final BitSet counts;
public NonceInfo(long exp) {
expires = exp;
counts = new BitSet(MAX_NONCE_COUNT);
}
public long getExpires() {
return expires;
}
public AuthResult isValid(int nc) {
if (nc <= 0)
return AuthResult.AUTH_BAD;
if (nc >= MAX_NONCE_COUNT)
return AuthResult.AUTH_STALE;
synchronized(counts) {
if (counts.get(nc))
return AuthResult.AUTH_BAD;
counts.set(nc);
}
return AuthResult.AUTH_GOOD;
}
}
/**
* Update the outproxy list then call super.
*
* @since 0.9.12
*/
@Override
public void optionsUpdated(I2PTunnel tunnel) {
if (getTunnel() != tunnel)
return;
Properties props = tunnel.getClientOptions();
// see TunnelController.setSessionOptions()
String proxies = props.getProperty("proxyList");
if (proxies != null) {
StringTokenizer tok = new StringTokenizer(proxies, ",; \r\n\t");
synchronized(_proxyList) {
_proxyList.clear();
while (tok.hasMoreTokens()) {
String p = tok.nextToken().trim();
if (p.length() > 0)
_proxyList.add(p);
}
}
} else {
synchronized(_proxyList) {
_proxyList.clear();
}
}
super.optionsUpdated(tunnel);
}
/**
* @since 0.9.4
*/
protected boolean isDigestAuthRequired() {
String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
if (authRequired == null)
return false;
return authRequired.toLowerCase(Locale.US).equals("digest");
}
/**
* Authorization
* Ref: RFC 2617
* 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"
* @return success
*/
protected AuthResult authorize(Socket s, long requestId, String method, String authorization) {
String authRequired = getTunnel().getClientOptions().getProperty(PROP_AUTH);
if (authRequired == null)
return AuthResult.AUTH_GOOD;
authRequired = authRequired.toLowerCase(Locale.US);
if (authRequired.equals("false"))
return AuthResult.AUTH_GOOD;
if (s instanceof InternalSocket) {
if (_log.shouldLog(Log.INFO))
_log.info(getPrefix(requestId) + "Internal access, no auth required");
return AuthResult.AUTH_GOOD;
}
if (authorization == null)
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_AUTH)) {
if (!authLC.startsWith("basic "))
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 = DataHelper.split(dec, ":");
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 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, aioobe);
return AuthResult.AUTH_BAD_REQ;
}
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 AuthResult.AUTH_BAD;
authorization = authorization.substring(7);
Map<String, String> args = parseArgs(authorization);
AuthResult rv = validateDigest(method, args);
return rv;
} else {
_log.error("Unknown proxy authorization type configured: " + authRequired);
return AuthResult.AUTH_BAD_REQ;
}
}
/**
* 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, nc);
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_PROXY_DIGEST_PREFIX + user +
PROP_PROXY_DIGEST_SUFFIX);
if (ha1 == null) {
_log.logAlways(Log.WARN, "PROXY AUTH FAILURE: 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)) {
_log.logAlways(Log.WARN, "PROXY AUTH FAILURE: user " + user);
if (_log.shouldLog(Log.INFO))
_log.info("Bad digest auth: " + DataHelper.toString(args));
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))
* @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);
String rv = Base64.encode(n);
_nonces.putIfAbsent(rv, new NonceInfo(now + MAX_NONCE_AGE));
return rv;
}
/**
* Verify the Base 64 of 24 bytes: (now, md5 of (now, proxy nonce))
* and the nonce count.
* @param b64 nonce non-null
* @param ncs nonce count string non-null
* @since 0.9.4
*/
private AuthResult verifyNonce(String b64, String ncs) {
if (_nonceCleanCounter.incrementAndGet() % 16 == 0)
cleanNonces();
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) {
_nonces.remove(b64);
return AuthResult.AUTH_STALE;
}
NonceInfo info = _nonces.get(b64);
if (info == null)
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;
try {
int nc = Integer.parseInt(ncs, 16);
return info.isValid(nc);
} catch (NumberFormatException nfe) {
return AuthResult.AUTH_BAD;
}
}
/**
* Remove expired nonces from map
* @since 0.9.6
*/
private void cleanNonces() {
long now = _context.clock().now();
for (Iterator<NonceInfo> iter = _nonces.values().iterator(); iter.hasNext(); ) {
NonceInfo info = iter.next();
if (info.getExpires() <= now)
iter.remove();
}
}
/**
* What to send if digest auth fails
* @since 0.9.4
*/
protected String getAuthError(boolean isStale) {
boolean isDigest = isDigestAuthRequired();
return
ERR_AUTH1 +
(isDigest ? "Digest" : "Basic") +
" realm=\"" + getRealm() + '"' +
(isDigest ? ", nonce=\"" + getNonce() + "\"," +
" algorithm=MD5," +
" charset=UTF-8," + // RFC 7616/7617
" qop=\"auth\"" +
(isStale ? ", stale=true" : "")
: "") +
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) {
// moved to EepGet, since it needs this too
return EepGet.parseAuthArgs(args);
}
//////// Error page stuff
/**
* 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 String getErrorPage(String base, String 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 String getErrorPage(I2PAppContext ctx, String base, String backup) {
File errorDir = new File(ctx.getBaseDir(), "docs");
File file = new File(errorDir, base + "-header.ht");
try {
return readFile(ctx, file);
} catch(IOException ioe) {
return backup;
}
}
/** these strings go in the jar, not the war */
private static final String BUNDLE_NAME = "net.i2p.i2ptunnel.proxy.messages";
/**
* @since 0.9.4 moved from I2PTunnelHTTPClient
*/
private static String readFile(I2PAppContext ctx, File file) throws IOException {
Reader reader = null;
char[] buf = new char[512];
StringBuilder out = new StringBuilder(2048);
try {
int len;
reader = new TranslateReader(ctx, BUNDLE_NAME, new FileInputStream(file));
while((len = reader.read(buf)) > 0) {
out.append(buf, 0, len);
}
String rv = out.toString();
// Do we need to replace http://127.0.0.1:7657 console links in the error page?
// Get the registered host and port from the PortMapper.
final String unset = "*unset*";
final String httpHost = ctx.portMapper().getActualHost(PortMapper.SVC_CONSOLE, unset);
final String httpsHost = ctx.portMapper().getActualHost(PortMapper.SVC_HTTPS_CONSOLE, unset);
final int httpPort = ctx.portMapper().getPort(PortMapper.SVC_CONSOLE, 7657);
final int httpsPort = ctx.portMapper().getPort(PortMapper.SVC_HTTPS_CONSOLE, -1);
final boolean httpsOnly = httpsPort > 0 && httpHost.equals(unset) && !httpsHost.equals(unset);
final int port = httpsOnly ? httpsPort : httpPort;
String host = httpsOnly ? httpsHost : httpHost;
if (host.equals(unset))
host = "127.0.0.1";
if (httpsOnly || port != 7657 || !host.equals("127.0.0.1")) {
String url = (httpsOnly ? "https://" : "http://") + host + ':' + port;
rv = rv.replace("http://127.0.0.1:7657", url);
}
return rv;
} finally {
try {
if(reader != null)
reader.close();
} catch(IOException foo) {}
}
// we won't ever get here
}
/**
* @since 0.9.14 moved from subclasses
*/
protected class OnTimeout implements I2PTunnelRunner.FailCallback {
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;
_target = target;
_usingProxy = usingProxy;
_wwwProxy = wwwProxy;
_requestId = id;
}
/**
* @param ex may be null
*/
public void onFail(Exception ex) {
Throwable cause = ex != null ? ex.getCause() : null;
if (cause != null && cause instanceof I2PSocketException) {
I2PSocketException ise = (I2PSocketException) cause;
handleI2PSocketException(ise, _out, _target, _usingProxy, _wwwProxy);
} else {
handleClientException(ex, _out, _target, _usingProxy, _wwwProxy, _requestId);
}
closeSocket(_socket);
}
}
/**
* @param ex may be null
* @since 0.9.14 moved from subclasses
*/
protected void handleClientException(Exception ex, OutputStream out, String targetRequest,
boolean usingWWWProxy, String wwwProxy, long requestId) {
if (out == null)
return;
String header;
if (usingWWWProxy)
header = getErrorPage(I2PAppContext.getGlobalContext(), "dnfp", ERR_DESTINATION_UNKNOWN);
else
header = getErrorPage(I2PAppContext.getGlobalContext(), "dnf", ERR_DESTINATION_UNKNOWN);
try {
writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy);
} catch (IOException ioe) {}
}
/**
* Generate an error page based on the status code
* in our custom exception.
*
* @param ise may be null
* @since 0.9.14
*/
protected void handleI2PSocketException(I2PSocketException ise, OutputStream out, String targetRequest,
boolean usingWWWProxy, String wwwProxy) {
if (out == null)
return;
int status = ise != null ? ise.getStatus() : -1;
String error;
if (status == MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET) {
// We won't get this one unless it is treated as a hard failure
// in streaming. See PacketQueue.java
error = usingWWWProxy ? "nolsp" : "nols";
} else if (status == MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION) {
error = usingWWWProxy ? "encp" : "enc";
} else if (status == I2PSocketException.STATUS_CONNECTION_RESET) {
error = usingWWWProxy ? "resetp" : "reset";
} else {
error = usingWWWProxy ? "dnfp" : "dnf";
}
String header = getErrorPage(error, ERR_DESTINATION_UNKNOWN);
String message = ise != null ? ise.getLocalizedMessage() : "unknown error";
try {
writeErrorMessage(header, message, out, targetRequest, usingWWWProxy, wwwProxy);
} catch(IOException ioe) {}
}
/**
* No jump servers or extra message
* @since 0.9.14
*/
protected void writeErrorMessage(String errMessage, OutputStream out, String targetRequest,
boolean usingWWWProxy, String wwwProxy) throws IOException {
writeErrorMessage(errMessage, null, out, targetRequest, usingWWWProxy, wwwProxy, null);
}
/**
* No extra message
* @param jumpServers comma- or space-separated list, or null
* @since 0.9.14 moved from subclasses
*/
protected void writeErrorMessage(String errMessage, OutputStream out, String targetRequest,
boolean usingWWWProxy, String wwwProxy, String jumpServers) throws IOException {
writeErrorMessage(errMessage, null, out, targetRequest, usingWWWProxy, wwwProxy, jumpServers);
}
/**
* No jump servers
* @param extraMessage extra message or null, will be HTML-escaped
* @since 0.9.14
*/
protected void writeErrorMessage(String errMessage, String extraMessage,
OutputStream out, String targetRequest,
boolean usingWWWProxy, String wwwProxy) throws IOException {
writeErrorMessage(errMessage, extraMessage, out, targetRequest, usingWWWProxy, wwwProxy, null);
}
/**
* @param jumpServers comma- or space-separated list, or null
* @param extraMessage extra message or null, will be HTML-escaped
* @since 0.9.14
*/
protected void writeErrorMessage(String errMessage, String extraMessage,
OutputStream outs, String targetRequest,
boolean usingWWWProxy, String wwwProxy,
String jumpServers) throws IOException {
if (outs == null)
return;
Writer out = new BufferedWriter(new OutputStreamWriter(outs, "UTF-8"));
out.write(errMessage);
if (targetRequest != null) {
String uri = DataHelper.escapeHTML(targetRequest);
out.write("<a href=\"");
out.write(uri);
out.write("\">");
if (targetRequest.length() > 80)
out.write(DataHelper.escapeHTML(targetRequest.substring(0, 75)) + "…");
else
out.write(uri);
out.write("</a>");
if (usingWWWProxy) {
out.write("<br><br><b>");
out.write(_t("HTTP Outproxy"));
out.write(":</b> " + wwwProxy);
}
if (extraMessage != null) {
out.write("<br><br><b>" + DataHelper.escapeHTML(extraMessage) + "</b>");
}
if (jumpServers != null && jumpServers.length() > 0) {
boolean first = true;
if(uri.startsWith("http://")) {
uri = uri.substring(7);
}
StringTokenizer tok = new StringTokenizer(jumpServers, ", ");
while(tok.hasMoreTokens()) {
String jurl = tok.nextToken();
String jumphost;
try {
URI jURI = new URI(jurl);
String proto = jURI.getScheme();
jumphost = jURI.getHost();
if (proto == null || jumphost == null ||
!proto.toLowerCase(Locale.US).equals("http"))
continue;
jumphost = jumphost.toLowerCase(Locale.US);
if (!jumphost.endsWith(".i2p"))
continue;
} catch(URISyntaxException use) {
continue;
}
// Skip jump servers we don't know
if (!jumphost.endsWith(".b32.i2p")) {
Destination dest = _context.namingService().lookup(jumphost);
if(dest == null) {
continue;
}
}
if (first) {
first = false;
out.write("<br><br><h3>");
out.write(_t("Click a link below for an address helper from a jump service"));
out.write("</h3>\n");
} else {
out.write("<br>");
}
out.write("<a href=\"");
out.write(jurl);
out.write(uri);
out.write("\">");
// Translators: parameter is a host name
out.write(_t("{0} jump service", jumphost));
out.write("</a>\n");
}
}
}
out.write("</div>");
writeFooter(out);
}
/**
* Flushes.
*
* Public only for LocalHTTPServer, not for general use
* @since 0.9.14 moved from I2PTunnelHTTPClient
*/
public static void writeFooter(OutputStream out) throws IOException {
out.write(getFooter().getBytes("UTF-8"));
out.flush();
}
/**
* Flushes.
*
* Public only for LocalHTTPServer, not for general use
* @since 0.9.19
*/
public static void writeFooter(Writer out) throws IOException {
out.write(getFooter());
out.flush();
}
private static String getFooter() {
// The css is hiding this div for now, but we'll keep it here anyway
// Tag the strings below for translation if we unhide it.
StringBuilder buf = new StringBuilder(128);
buf.append("<div class=\"proxyfooter\"><p><i>I2P HTTP Proxy Server<br>Generated on: ")
.append(new Date().toString())
.append("</i></div></body></html>\n");
return buf.toString();
}
/**
* Translate
* @since 0.9.14 moved from I2PTunnelHTTPClient
*/
protected String _t(String key) {
return Translate.getString(key, _context, BUNDLE_NAME);
}
/**
* Translate
* {0}
* @since 0.9.14 moved from I2PTunnelHTTPClient
*/
protected String _t(String key, Object o) {
return Translate.getString(key, o, _context, BUNDLE_NAME);
}
/**
* Translate
* {0} and {1}
* @since 0.9.14 moved from I2PTunnelHTTPClient
*/
protected String _t(String key, Object o, Object o2) {
return Translate.getString(key, o, o2, _context, BUNDLE_NAME);
}
}