" +
"You can resolve the conflict by considering which key you trust, " +
"and either discarding the addresshelper link, " +
"discarding the host entry from your host database, " +
"or naming one of them differently.
";
private final static String ERR_AHELPER_NOTFOUND =
"HTTP/1.1 404 Not Found\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" +
"
\n"));
out.write(DataHelper.getUTF8(DataHelper.escapeHTML(msg)));
out.write(DataHelper.getASCII("
\n"));
}
out.write(DataHelper.getASCII("\n"));
writeFooter(out);
reader.drain();
} catch (IOException ioe) {
// ignore
}
return;
}
String protocolVersion = params[2];
protocol = requestURI.getScheme();
host = requestURI.getHost();
if(protocol == null || host == null) {
_log.warn("Null protocol or host: " + request + ' ' + protocol + ' ' + host);
method = null;
break;
}
int port = requestURI.getPort();
// Go through the various types of host names, set
// the host and destination variables accordingly,
// and transform the first line.
// For all i2p network hosts, ensure that the host is a
// Base 32 hostname so that we do not reveal our name for it
// in our addressbook (all naming is local),
// and it is removed from the request line.
hostLowerCase = host.toLowerCase(Locale.US);
if(hostLowerCase.equals(LOCAL_SERVER)) {
// so we don't do any naming service lookups
destination = host;
usingInternalServer = true;
internalPath = requestURI.getPath();
internalRawQuery = requestURI.getRawQuery();
} else if(hostLowerCase.equals("i2p")) {
// pull the b64 _dest out of the first path element
String oldPath = requestURI.getPath().substring(1);
int slash = oldPath.indexOf('/');
if(slash < 0) {
slash = oldPath.length();
oldPath += '/';
}
String _dest = oldPath.substring(0, slash);
if(slash >= 516 && !_dest.contains(".")) {
// possible alternative:
// redirect to b32
destination = _dest;
host = getHostName(destination);
targetRequest = requestURI.toASCIIString();
String newURI = oldPath.substring(slash);
String query = requestURI.getRawQuery();
if(query != null) {
newURI += '?' + query;
}
try {
requestURI = new URI(newURI);
} catch(URISyntaxException use) {
// shouldnt happen
_log.warn(request, use);
method = null;
break;
}
} else {
_log.warn("Bad http://i2p/b64dest " + request);
host = null;
break;
}
} else if(hostLowerCase.endsWith(".i2p")) {
// Destination gets the host name
destination = host;
// Host becomes the destination's "{b32}.b32.i2p" string, or "i2p" on lookup failure
host = getHostName(destination);
int rPort = requestURI.getPort();
if (rPort > 0) {
// Save it to put in the I2PSocketOptions,
remotePort = rPort;
/********
// but strip it from the URL
if(_log.shouldLog(Log.WARN)) {
_log.warn(getPrefix(requestId) + "Removing port from [" + request + "]");
}
try {
requestURI = changeURI(requestURI, null, -1, null);
} catch(URISyntaxException use) {
_log.warn(request, use);
method = null;
break;
}
******/
} else if ("https".equals(protocol) ||
method.toUpperCase(Locale.US).equals("CONNECT")) {
remotePort = 443;
} else {
remotePort = 80;
}
String query = requestURI.getRawQuery();
if(query != null) {
boolean ahelperConflict = false;
// Try to find an address helper in the query
String[] helperStrings = removeHelper(query);
if(helperStrings != null &&
!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER))) {
query = helperStrings[0];
if(query.equals("")) {
query = null;
}
try {
requestURI = replaceQuery(requestURI, query);
} catch(URISyntaxException use) {
// shouldn't happen
_log.warn(request, use);
method = null;
break;
}
ahelperKey = helperStrings[1];
// Key contains data, lets not ignore it
if(ahelperKey.length() > 0) {
if(ahelperKey.endsWith(".i2p")) {
// allow i2paddresshelper=.b32.i2p syntax.
/*
also i2paddresshelper=name.i2p for aliases
i.e. on your eepsite put
This is the name I want to be called.
*/
Destination _dest = _context.namingService().lookup(ahelperKey);
if(_dest == null) {
if(_log.shouldLog(Log.WARN)) {
_log.warn(getPrefix(requestId) + "Could not find destination for " + ahelperKey);
}
String header = getErrorPage("ahelper-notfound", ERR_AHELPER_NOTFOUND);
try {
out.write(header.getBytes("UTF-8"));
out.write(("" + _t("This seems to be a bad destination:") + " " + ahelperKey + " " +
_t("i2paddresshelper cannot help you with a destination like that!") +
"
").getBytes("UTF-8"));
writeFooter(out);
reader.drain();
} catch (IOException ioe) {
// ignore
}
return;
}
ahelperKey = _dest.toBase64();
}
ahelperPresent = true;
// ahelperKey will be validated later
if(host == null || "i2p".equals(host)) {
// Host lookup failed - resolvable only with addresshelper
// Store in local HashMap unless there is conflict
String old = addressHelpers.putIfAbsent(destination.toLowerCase(Locale.US), ahelperKey);
ahelperNew = old == null;
// inr address helper links without trailing '=', so omit from comparison
if ((!ahelperNew) && !old.replace("=", "").equals(ahelperKey.replace("=", ""))) {
// Conflict: handle when URL reconstruction done
ahelperConflict = true;
if(_log.shouldLog(Log.WARN)) {
_log.warn(getPrefix(requestId) + "Addresshelper key conflict for site [" + destination +
"], trusted key [" + old + "], specified key [" + ahelperKey + "].");
}
}
} else {
// If the host is resolvable from database, verify addresshelper key
// Silently bypass correct keys, otherwise alert
Destination hostDest = _context.namingService().lookup(destination);
if(hostDest != null) {
String destB64 = hostDest.toBase64();
if(destB64 != null && !destB64.equals(ahelperKey)) {
// Conflict: handle when URL reconstruction done
ahelperConflict = true;
if(_log.shouldLog(Log.WARN)) {
_log.warn(getPrefix(requestId) + "Addresshelper key conflict for site [" + destination +
"], trusted key [" + destB64 + "], specified key [" + ahelperKey + "].");
}
}
}
}
} // ahelperKey
} // helperstrings
// Did addresshelper key conflict?
if(ahelperConflict) {
try {
// convert ahelperKey to b32
String alias = getHostName(ahelperKey);
if(alias.equals("i2p")) {
// bad ahelperKey
String header = getErrorPage("dnfb", ERR_DESTINATION_UNKNOWN);
writeErrorMessage(header, out, targetRequest, false, destination);
} else {
String trustedURL = requestURI.toASCIIString();
URI conflictURI;
try {
conflictURI = changeURI(requestURI, alias, 0, null);
} catch(URISyntaxException use) {
// shouldn't happen
_log.warn(request, use);
method = null;
break;
}
String conflictURL = conflictURI.toASCIIString();
String header = getErrorPage("ahelper-conflict", ERR_AHELPER_CONFLICT);
out.write(header.getBytes("UTF-8"));
out.write("".getBytes("UTF-8"));
out.write(_t("To visit the destination in your address book, click here. To visit the conflicting addresshelper destination, click here.",
trustedURL, conflictURL).getBytes("UTF-8"));
out.write("
".getBytes("UTF-8"));
Hash h1 = ConvertToHash.getHash(requestURI.getHost());
Hash h2 = ConvertToHash.getHash(ahelperKey);
if (h1 != null && h2 != null) {
String conURL = _context.portMapper().getConsoleURL();
out.write(("\n".getBytes("UTF-8"));
}
out.write("".getBytes("UTF-8"));
writeFooter(out);
}
reader.drain();
} catch (IOException ioe) {
// ignore
}
return;
}
} // end query processing
String addressHelper = addressHelpers.get(destination);
if(addressHelper != null) {
host = getHostName(addressHelper);
}
// now strip everything but path and query from URI
targetRequest = requestURI.toASCIIString();
String newURI = requestURI.getRawPath();
if(query != null) {
newURI += '?' + query;
}
try {
requestURI = new URI(newURI);
} catch(URISyntaxException use) {
// shouldnt happen
_log.warn(request, use);
method = null;
break;
}
// end of (host endsWith(".i2p"))
} else if(hostLowerCase.equals("localhost") || host.equals("127.0.0.1") ||
host.startsWith("192.168.") || host.equals("[::1]")) {
// if somebody is trying to get to 192.168.example.com, oh well
try {
out.write(getErrorPage("localhost", ERR_LOCALHOST).getBytes("UTF-8"));
writeFooter(out);
reader.drain();
} catch (IOException ioe) {
// ignore
}
return;
} else if(host.contains(".") || host.startsWith("[")) {
if (Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_USE_OUTPROXY_PLUGIN, "true"))) {
ClientAppManager mgr = _context.clientAppManager();
if (mgr != null) {
ClientApp op = mgr.getRegisteredApp(Outproxy.NAME);
if (op != null) {
outproxy = (Outproxy) op;
int rPort = requestURI.getPort();
if (rPort > 0)
remotePort = rPort;
else if ("https".equals(protocol) ||
method.toUpperCase(Locale.US).equals("CONNECT"))
remotePort = 443;
else
remotePort = 80;
usingInternalOutproxy = true;
targetRequest = requestURI.toASCIIString();
if(_log.shouldLog(Log.DEBUG))
_log.debug(getPrefix(requestId) + " [" + host + "]: outproxy!");
}
}
}
if (!usingInternalOutproxy) {
if(port >= 0) {
host = host + ':' + port;
}
// The request must be forwarded to a WWW proxy
if(_log.shouldLog(Log.DEBUG)) {
_log.debug("Before selecting outproxy for " + host);
}
if ("https".equals(protocol) ||
method.toUpperCase(Locale.US).equals("CONNECT"))
currentProxy = selectSSLProxy(hostLowerCase);
else
currentProxy = selectProxy(hostLowerCase);
if(_log.shouldLog(Log.DEBUG)) {
_log.debug("After selecting outproxy for " + host + ": " + currentProxy);
}
if(currentProxy == null) {
if(_log.shouldLog(Log.WARN)) {
_log.warn("No outproxy configured for request: " + requestURI);
}
try {
out.write(getErrorPage("noproxy", ERR_NO_OUTPROXY).getBytes("UTF-8"));
writeFooter(out);
reader.drain();
} catch (IOException ioe) {
// ignore
}
return;
}
destination = currentProxy;
usingWWWProxy = true;
targetRequest = requestURI.toASCIIString();
if(_log.shouldLog(Log.DEBUG)) {
_log.debug(getPrefix(requestId) + " [" + host + "]: wwwProxy!");
}
}
} else {
// what is left for here? a hostname with no dots, and != "i2p"
// and not a destination ???
// Perhaps something in privatehosts.txt ...
// Rather than look it up, just bail out.
if(_log.shouldLog(Log.WARN)) {
_log.warn("NODOTS, NOI2P: " + request);
}
try {
out.write(getErrorPage("denied", ERR_REQUEST_DENIED).getBytes("UTF-8"));
writeFooter(out);
reader.drain();
} catch (IOException ioe) {
// ignore
}
return;
} // end host name processing
boolean isValid = usingInternalOutproxy || usingWWWProxy ||
usingInternalServer || isSupportedAddress(host, protocol);
if(!isValid) {
if(_log.shouldLog(Log.INFO)) {
_log.info(getPrefix(requestId) + "notValid(" + host + ")");
}
method = null;
destination = null;
break;
}
if (method.toUpperCase(Locale.US).equals("CONNECT")) {
// fix up the change to requestURI above to get back to the original host:port
line = method + ' ' + requestURI.getHost() + ':' + requestURI.getPort() + ' ' + protocolVersion;
} else {
line = method + ' ' + requestURI.toASCIIString() + ' ' + protocolVersion;
}
if(_log.shouldLog(Log.DEBUG)) {
_log.debug(getPrefix(requestId) + "NEWREQ: \"" + line + "\"");
_log.debug(getPrefix(requestId) + "HOST : \"" + host + "\"");
_log.debug(getPrefix(requestId) + "DEST : \"" + destination + "\"");
}
// end first line processing
} else {
if (lowercaseLine.startsWith("connection: ")) {
if (lowercaseLine.contains("upgrade")) {
// pass through for websocket
preserveConnectionHeader = true;
} else {
continue;
}
} else if (lowercaseLine.startsWith("keep-alive: ") ||
lowercaseLine.startsWith("proxy-connection: ")) {
continue;
} else if (lowercaseLine.startsWith("host: ") && !usingWWWProxy && !usingInternalOutproxy) {
// Note that we only pass the original Host: line through to the outproxy
// But we don't create a Host: line if it wasn't sent to us
line = "Host: " + host;
if (_log.shouldDebug()) {
_log.debug(getPrefix(requestId) + "Setting host = " + host);
}
} else if(lowercaseLine.startsWith("user-agent: ")) {
// save for deciding whether to offer address book form
userAgent = lowercaseLine.substring(12);
if(!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_USER_AGENT))) {
line = null;
continue;
}
} else if(lowercaseLine.startsWith("accept: ")) {
if (!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_ACCEPT))) {
// Replace with a standard one if possible
boolean html = lowercaseLine.indexOf("text/html") > 0;
boolean css = lowercaseLine.indexOf("text/css") > 0;
boolean img = lowercaseLine.indexOf("image") > 0;
if (html && !img && !css) {
// firefox, tor browser
line = "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
} else if (img && !html && !css) {
// chrome
line = "Accept: image/webp,image/apng,image/*,*/*;q=0.8";
} else if (css && !html && !img) {
// chrome, firefox
line = "Accept: text/css,*/*;q=0.1";
} // else allow as-is
}
} else if(lowercaseLine.startsWith("accept")) {
// strip the accept-blah headers, as they vary dramatically from
// browser to browser
// But allow Accept-Encoding: gzip, deflate
if(!lowercaseLine.startsWith("accept-encoding: ") &&
!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_ACCEPT))) {
line = null;
continue;
}
} else if (lowercaseLine.startsWith("referer: ")) {
// save for address helper form below
referer = line.substring(9);
if (!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_REFERER))) {
try {
// Either strip or rewrite the referer line
URI refererURI = new URI(referer);
String refererHost = refererURI.getHost();
if (refererHost != null) {
String origHost = origRequestURI.getHost();
if (!refererHost.equals(origHost) ||
refererURI.getPort() != origRequestURI.getPort() ||
!DataHelper.eq(refererURI.getScheme(), origRequestURI.getScheme())) {
line = null;
continue; // completely strip the line if everything doesn't match
}
// Strip to a relative URI, to hide the original host name
StringBuilder buf = new StringBuilder();
buf.append("Referer: ");
String refererPath = refererURI.getRawPath();
buf.append(refererPath != null ? refererPath : "/");
String refererQuery = refererURI.getRawQuery();
if (refererQuery != null)
buf.append('?').append(refererQuery);
line = buf.toString();
} // else relative URI, leave in
} catch (URISyntaxException use) {
line = null;
continue; // completely strip the line
}
} // else allow
} else if(lowercaseLine.startsWith("via: ") &&
!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_VIA))) {
//line = "Via: i2p";
line = null;
continue; // completely strip the line
} else if(lowercaseLine.startsWith("from: ")) {
//line = "From: i2p";
line = null;
continue; // completely strip the line
} else if(lowercaseLine.startsWith("authorization: ntlm ")) {
// Block Windows NTLM after 401
line = null;
continue;
} else if(lowercaseLine.startsWith("proxy-authorization: ")) {
// This should be for us. It is a
// 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
authorization = line.substring(21); // "proxy-authorization: ".length()
line = null;
continue;
} else if(lowercaseLine.startsWith("icy")) {
// icecast/shoutcast, We need to leave the user-agent alone.
shout = true;
}
}
if(line.length() == 0) {
// No more headers, add our own and break out of the loop
String ok = getTunnel().getClientOptions().getProperty("i2ptunnel.gzip");
boolean gzip = DEFAULT_GZIP;
if(ok != null) {
gzip = Boolean.parseBoolean(ok);
}
if(gzip && !usingInternalServer &&
!method.toUpperCase(Locale.US).equals("CONNECT")) {
// according to rfc2616 s14.3, this *should* force identity, even if
// an explicit q=0 for gzip doesn't. tested against orion.i2p, and it
// seems to work.
//if (!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_ACCEPT)))
// newRequest.append("Accept-Encoding: \r\n");
if (!usingInternalOutproxy)
newRequest.append("X-Accept-Encoding: x-i2p-gzip;q=1.0, identity;q=0.5, deflate;q=0, gzip;q=0, *;q=0\r\n");
}
if(!shout && !method.toUpperCase(Locale.US).equals("CONNECT")) {
if(!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_USER_AGENT))) {
// let's not advertise to external sites that we are from I2P
String ua;
if (usingWWWProxy || usingInternalOutproxy) {
ua = getTunnel().getClientOptions().getProperty(PROP_UA_CLEARNET);
if (ua != null)
ua = "User-Agent: " + ua + "\r\n";
else
ua = UA_CLEARNET;
} else {
ua = getTunnel().getClientOptions().getProperty(PROP_UA_I2P);
if (ua != null)
ua = "User-Agent: " + ua + "\r\n";
else
ua = UA_I2P;
}
newRequest.append(ua);
}
}
// Add Proxy-Authentication header for next hop (outproxy)
if(usingWWWProxy && Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_AUTH))) {
// specific for this proxy
String user = getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_USER_PREFIX + currentProxy);
String pw = getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_PW_PREFIX + currentProxy);
if(user == null || pw == null) {
// if not, look at default user and pw
user = getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_USER);
pw = getTunnel().getClientOptions().getProperty(PROP_OUTPROXY_PW);
}
if(user != null && pw != null) {
newRequest.append("Proxy-Authorization: Basic ")
.append(Base64.encode((user + ':' + pw).getBytes("UTF-8"), true)) // true = use standard alphabet
.append("\r\n");
}
}
if (preserveConnectionHeader)
newRequest.append("\r\n");
else
newRequest.append("Connection: close\r\n\r\n");
s.setSoTimeout(BROWSER_READ_TIMEOUT);
break;
} else {
newRequest.append(line).append("\r\n"); // HTTP spec
}
} // end header processing
if(_log.shouldLog(Log.DEBUG)) {
_log.debug(getPrefix(requestId) + "NewRequest header: [" + newRequest.toString() + "]");
}
if(method == null || (destination == null && !usingInternalOutproxy)) {
//l.log("No HTTP method found in the request.");
try {
if (protocol != null && "http".equals(protocol.toLowerCase(Locale.US))) {
out.write(getErrorPage("denied", ERR_REQUEST_DENIED).getBytes("UTF-8"));
} else {
out.write(getErrorPage("protocol", ERR_BAD_PROTOCOL).getBytes("UTF-8"));
}
writeFooter(out);
} catch (IOException ioe) {
// ignore
}
return;
}
if(_log.shouldLog(Log.DEBUG)) {
_log.debug(getPrefix(requestId) + "Destination: " + destination);
}
// 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");
}
}
try {
out.write(getAuthError(result == AuthResult.AUTH_STALE).getBytes("UTF-8"));
writeFooter(out);
} catch (IOException ioe) {
// ignore
}
return;
}
// Serve local proxy files (images, css linked from error pages)
// Ignore all the headers
if (usingInternalServer) {
try {
// disable the add form if address helper is disabled
if(internalPath.equals("/add") &&
Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER))) {
out.write(ERR_HELPER_DISABLED.getBytes("UTF-8"));
} else {
LocalHTTPServer.serveLocalFile(_context, sockMgr, out, method, internalPath, internalRawQuery, _proxyNonce);
}
} catch (IOException ioe) {
// ignore
}
return;
}
// no destination, going to outproxy plugin
if (usingInternalOutproxy) {
Socket outSocket = outproxy.connect(host, remotePort);
OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId);
byte[] data;
byte[] response;
if (method.toUpperCase(Locale.US).equals("CONNECT")) {
data = null;
response = SUCCESS_RESPONSE.getBytes("UTF-8");
} else {
data = newRequest.toString().getBytes("ISO-8859-1");
response = null;
}
Thread t = new I2PTunnelOutproxyRunner(s, outSocket, sockLock, data, response, onTimeout);
// we are called from an unlimited thread pool, so run inline
//t.start();
t.run();
return;
}
// LOOKUP
// If the host is "i2p", the getHostName() lookup failed, don't try to
// look it up again as the naming service does not do negative caching
// so it will be slow.
Destination clientDest = null;
String addressHelper = addressHelpers.get(destination.toLowerCase(Locale.US));
if(addressHelper != null) {
clientDest = _context.namingService().lookup(addressHelper);
if(clientDest == null) {
// remove bad entries
addressHelpers.remove(destination.toLowerCase(Locale.US));
if(_log.shouldLog(Log.WARN)) {
_log.warn(getPrefix(requestId) + "Could not find destination for " + addressHelper);
}
String header = getErrorPage("ahelper-notfound", ERR_AHELPER_NOTFOUND);
try {
writeErrorMessage(header, out, targetRequest, false, destination);
} catch (IOException ioe) {
// ignore
}
return;
}
} else if("i2p".equals(host)) {
clientDest = null;
} else if (destination.toLowerCase(Locale.US).endsWith(".b32.i2p")) {
int len = destination.length();
if (len < 60 || (len >= 61 && len <= 63)) {
// 8-59 or 61-63 chars, this won't work
String header = getErrorPage("b32", ERR_DESTINATION_UNKNOWN);
try {
writeErrorMessage(header, _t("Corrupt Base32 address"), out, targetRequest, false, destination);
} catch (IOException ioe) {}
return;
}
if (len >= 64) {
// catch b33 errors before session lookup
try {
BlindData bd = Blinding.decode(_context, destination);
if (_log.shouldWarn())
_log.warn("Resolved b33 " + bd);
// TESTING
//sess.sendBlindingInfo(bd, 24*60*60*1000);
} catch (IllegalArgumentException iae) {
if (_log.shouldWarn())
_log.warn("Unable to resolve b33 " + destination, iae);
// b33 error page
String header = getErrorPage("b32", ERR_DESTINATION_UNKNOWN);
try {
writeErrorMessage(header, iae.getMessage(), out, targetRequest, false, destination);
} catch (IOException ioe) {}
return;
}
}
// use existing session to look up for efficiency
verifySocketManager();
I2PSession sess = sockMgr.getSession();
if (!sess.isClosed()) {
if (len == 60) {
byte[] hData = Base32.decode(destination.substring(0, 52));
if (hData != null) {
if (_log.shouldInfo())
_log.info("lookup b32 in-session " + destination);
Hash hash = Hash.create(hData);
clientDest = sess.lookupDest(hash, 20*1000);
} else {
clientDest = null;
}
} else if (len >= 64) {
if (_log.shouldInfo())
_log.info("lookup b33 in-session " + destination);
LookupResult lresult = sess.lookupDest2(destination, 20*1000);
clientDest = lresult.getDestination();
int code = lresult.getResultCode();
if (code != LookupResult.RESULT_SUCCESS) {
if (_log.shouldWarn())
_log.warn("Unable to resolve b33 " + destination + " error code " + code);
if (code != LookupResult.RESULT_FAILURE) {
// form to supply missing data
writeB32SaveForm(out, destination, code, targetRequest);
return;
}
// fall through to standard destination unreachable error page
}
}
} else {
if (_log.shouldInfo())
_log.info("lookup b32 out of session " + destination);
// TODO can't get result code from here
clientDest = _context.namingService().lookup(destination);
}
} else {
if (_log.shouldInfo())
_log.info("lookup hostname " + destination);
clientDest = _context.namingService().lookup(destination);
}
if(clientDest == null) {
//l.log("Could not resolve " + destination + ".");
if(_log.shouldLog(Log.WARN)) {
_log.warn("Unable to resolve " + destination + " (proxy? " + usingWWWProxy + ", request: " + targetRequest);
}
String header;
String jumpServers = null;
String extraMessage = null;
if(usingWWWProxy) {
header = getErrorPage("dnfp", ERR_DESTINATION_UNKNOWN);
} else if(ahelperPresent) {
header = getErrorPage("dnfb", ERR_DESTINATION_UNKNOWN);
} else if(destination.length() >= 60 && destination.toLowerCase(Locale.US).endsWith(".b32.i2p")) {
header = getErrorPage("nols", ERR_DESTINATION_UNKNOWN);
extraMessage = _t("Destination lease set not found");
} else {
header = getErrorPage("dnfh", ERR_DESTINATION_UNKNOWN);
jumpServers = getTunnel().getClientOptions().getProperty(PROP_JUMP_SERVERS);
if(jumpServers == null) {
jumpServers = DEFAULT_JUMP_SERVERS;
}
int jumpDelay = 400 + _context.random().nextInt(256);
try {
Thread.sleep(jumpDelay);
} catch (InterruptedException ie) {}
}
try {
writeErrorMessage(header, extraMessage, out, targetRequest, usingWWWProxy, destination, jumpServers);
} catch (IOException ioe) {
// ignore
}
return;
}
// as of 0.9.35, allowInternalSSL defaults to true, and overridden to true unless PROP_SSL_SET is set
if (method.toUpperCase(Locale.US).equals("CONNECT") &&
!usingWWWProxy &&
getTunnel().getClientOptions().getProperty(PROP_SSL_SET) != null &&
!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_INTERNAL_SSL, "true"))) {
try {
writeErrorMessage(ERR_INTERNAL_SSL, out, targetRequest, false, destination);
} catch (IOException ioe) {
// ignore
}
if (_log.shouldLog(Log.WARN))
_log.warn("SSL to i2p destinations denied by configuration: " + targetRequest);
return;
}
// Address helper response form
// This will only load once - the second time it won't be "new"
// Don't do this for eepget, which uses a user-agent of "Wget"
if(ahelperNew && "GET".equals(method) &&
(userAgent == null || !userAgent.startsWith("Wget")) &&
!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER))) {
try {
writeHelperSaveForm(out, destination, ahelperKey, targetRequest, referer);
} catch (IOException ioe) {
// ignore
}
return;
}
// Redirect to non-addresshelper URL to not clog the browser address bar
// and not pass the parameter to the eepsite.
// This also prevents the not-found error page from looking bad
// Syndie can't handle a redirect of a POST
if (ahelperPresent && !"POST".equals(method) && !"PUT".equals(method)) {
String uri = targetRequest;
if(_log.shouldLog(Log.DEBUG)) {
_log.debug("Auto redirecting to " + uri);
}
try {
out.write(("HTTP/1.1 301 Address Helper Accepted\r\n" +
"Location: " + uri + "\r\n" +
"Connection: close\r\n"+
"Proxy-Connection: close\r\n"+
"\r\n").getBytes("UTF-8"));
} catch (IOException ioe) {
// ignore
}
return;
}
Properties opts = new Properties();
//opts.setProperty("i2p.streaming.inactivityTimeout", ""+120*1000);
// 1 == disconnect. see ConnectionOptions in the new streaming lib, which i
// dont want to hard link to here
//opts.setProperty("i2p.streaming.inactivityTimeoutAction", ""+1);
I2PSocketOptions sktOpts = getDefaultOptions(opts);
if (remotePort > 0)
sktOpts.setPort(remotePort);
i2ps = createI2PSocket(clientDest, sktOpts);
boolean isConnect = method.toUpperCase(Locale.US).equals("CONNECT");
OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy,
currentProxy, requestId, hostLowerCase, isConnect);
I2PTunnelRunner t;
if (isConnect) {
byte[] data;
byte[] response;
if (usingWWWProxy) {
data = newRequest.toString().getBytes("ISO-8859-1");
response = null;
} else {
data = null;
response = SUCCESS_RESPONSE.getBytes("UTF-8");
}
t = new I2PTunnelRunner(s, i2ps, sockLock, data, response, mySockets, onTimeout);
} else {
byte[] data = newRequest.toString().getBytes("ISO-8859-1");
t = new I2PTunnelHTTPClientRunner(s, i2ps, sockLock, data, mySockets, onTimeout);
}
if (usingWWWProxy) {
t.setSuccessCallback(new OnProxySuccess(currentProxy, hostLowerCase, isConnect));
}
// we are called from an unlimited thread pool, so run inline
//t.start();
t.run();
} catch(IOException ex) {
if(_log.shouldLog(Log.INFO)) {
_log.info(getPrefix(requestId) + "Error trying to connect", ex);
}
handleClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
} catch(I2PException ex) {
if(_log.shouldLog(Log.INFO)) {
_log.info(getPrefix(requestId) + "Error trying to connect", ex);
}
handleClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
} catch(OutOfMemoryError oom) {
IOException ex = new IOException("OOM");
_log.error(getPrefix(requestId) + "Error trying to connect", oom);
handleClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId);
} finally {
// only because we are running it inline
closeSocket(s);
if (i2ps != null) try { i2ps.close(); } catch (IOException ioe) {}
}
}
/** @since 0.8.7 */
private void writeHelperSaveForm(OutputStream outs, String destination, String ahelperKey,
String targetRequest, String referer) throws IOException {
if(outs == null)
return;
Writer out = new BufferedWriter(new OutputStreamWriter(outs, "UTF-8"));
String header = getErrorPage("ahelper-new", ERR_AHELPER_NEW);
out.write(header);
out.write("\n| " + _t("Host") +
" | " + destination + " |
\n");
try {
String b32 = Base32.encode(SHA256Generator.getInstance().calculateHash(Base64.decode(ahelperKey)).getData());
out.write("| " + _t("Base32") + " | " +
"" + b32 + ".b32.i2p |
");
} catch(Exception e) {
}
out.write("| " + _t("Destination") + " | " +
"" +
" |
\n
\n" + "
\n" +
// FIXME if there is a query remaining it is lost
"\n" +
"\n\n");
writeFooter(out);
}
/** @since 0.9.43 */
private void writeB32SaveForm(OutputStream outs, String destination, int code,
String targetRequest) throws IOException {
if(outs == null)
return;
Writer out = new BufferedWriter(new OutputStreamWriter(outs, "UTF-8"));
String header = getErrorPage("b32-auth", ERR_DESTINATION_UNKNOWN);
out.write(header);
out.write("\n" +
"| " + _t("Base32") + " | " +
"" + destination + " |
" +
"\n
\n" + "
");
String msg;
if (code == LookupResult.RESULT_SECRET_REQUIRED)
msg = _t("Base32 address requires lookup password");
else if (code == LookupResult.RESULT_KEY_REQUIRED)
msg = _t("Base32 address requires encryption key");
else if (code == LookupResult.RESULT_SECRET_AND_KEY_REQUIRED)
msg = _t("Base32 address requires encryption key and lookup password");
else if (code == LookupResult.RESULT_DECRYPTION_FAILURE)
msg = _t("Base32 address decryption failure, check encryption key");
else
msg = "lookup failure code " + code;
out.write("" + msg + "
");
out.write("\n\n");
writeFooter(out);
}
/**
* Read the first line unbuffered.
* After that, switch to a BufferedReader, unless the method is "POST".
* We can't use BufferedReader for POST because we can't have readahead,
* since we are passing the stream on to I2PTunnelRunner for the POST data.
*
* Warning - BufferedReader removes \r, DataHelper does not
* Warning - DataHelper limits line length, BufferedReader does not
* Todo: Limit line length for buffered reads, or go back to unbuffered for all
*/
private static class InputReader {
InputStream _s;
public InputReader(InputStream s) {
_s = s;
}
String readLine(String method) throws IOException {
// Use unbuffered until we can find a BufferedReader that limits line length
//if (method == null || "POST".equals(method))
return DataHelper.readLine(_s);
//if (_br == null)
// _br = new BufferedReader(new InputStreamReader(_s, "ISO-8859-1"));
//return _br.readLine();
}
/**
* Read the rest of the headers, which keeps firefox
* from complaining about connection reset after
* an error on the first line.
* @since 0.9.14
*/
public void drain() {
try {
String line;
do {
line = DataHelper.readLine(_s);
// \r not stripped so length == 1 is empty
} while (line != null && line.length() > 1);
} catch (IOException ioe) {}
}
}
/**
* @return b32hash.b32.i2p, or "i2p" on lookup failure.
* Prior to 0.7.12, returned b64 key
*/
private final String getHostName(String host) {
if(host == null) {
return null;
}
if (host.toLowerCase(Locale.US).endsWith(".b32.i2p")) {
return host;
}
Destination dest = _context.namingService().lookup(host);
if (dest == null)
return "i2p";
return dest.toBase32();
}
public static final String DEFAULT_JUMP_SERVERS =
//"http://i2host.i2p/cgi-bin/i2hostjump?," +
"http://stats.i2p/cgi-bin/jump.cgi?a=," +
"http://no.i2p/jump/," +
"http://i2pjump.i2p/jump/";
//"http://i2jump.i2p/";
/** @param host ignored */
private static boolean isSupportedAddress(String host, String protocol) {
if((host == null) || (protocol == null)) {
return false;
}
/****
* Let's not look up the name _again_
* and now that host is a b32, this was failing
*
boolean found = false;
String lcHost = host.toLowerCase();
for (int i = 0; i < SUPPORTED_HOSTS.length; i++) {
if (SUPPORTED_HOSTS[i].equals(lcHost)) {
found = true;
break;
}
}
if (!found) {
try {
Destination d = _context.namingService().lookup(host);
if (d == null) return false;
} catch (DataFormatException dfe) {
}
}
****/
String lc = protocol.toLowerCase(Locale.US);
return lc.equals("http") || lc.equals("https");
}
private final static String ERR_HELPER_DISABLED =
"HTTP/1.1 403 Disabled\r\n" +
"Content-Type: text/plain\r\n" +
"Connection: close\r\n"+
"Proxy-Connection: close\r\n"+
"\r\n" +
"Address helpers disabled";
/**
* Change various parts of the URI.
* String parameters are all non-encoded.
*
* Scheme always preserved.
* Userinfo always cleared.
* Host changed if non-null.
* Port changed if non-zero.
* Path changed if non-null.
* Query always preserved.
* Fragment always cleared.
*
* @since 0.9
*/
private static URI changeURI(URI uri, String host, int port, String path) throws URISyntaxException {
return new URI(uri.getScheme(),
null,
host != null ? host : uri.getHost(),
port != 0 ? port : uri.getPort(),
path != null ? path : uri.getPath(),
// FIXME this breaks encoded =, &
uri.getQuery(),
null);
}
/**
* Replace query in the URI.
* Userinfo cleared if uri contained a query.
* Fragment cleared if uri contained a query.
*
* @param query an ENCODED query, removed if null
* @since 0.9
*/
private static URI replaceQuery(URI uri, String query) throws URISyntaxException {
URI rv = uri;
if(rv.getRawQuery() != null) {
rv = new URI(rv.getScheme(),
null,
uri.getHost(),
uri.getPort(),
uri.getPath(),
null,
null);
}
if(query != null) {
String newURI = rv.toASCIIString() + '?' + query;
rv = new URI(newURI);
}
return rv;
}
/**
* Remove the address helper from an encoded query.
*
* @param query an ENCODED query, removed if null
* @return rv[0] is ENCODED query with helper removed, non-null but possibly empty;
* rv[1] is DECODED helper value, non-null but possibly empty;
* rv null if no helper present
* @since 0.9
*/
private static String[] removeHelper(String query) {
int keystart = 0;
int valstart = -1;
String key = null;
for(int i = 0; i <= query.length(); i++) {
char c = i < query.length() ? query.charAt(i) : '&';
if(c == ';' || c == '&') {
// end of key or value
if(valstart < 0) {
key = query.substring(keystart, i);
}
String decodedKey = LocalHTTPServer.decode(key);
if(decodedKey.equals(HELPER_PARAM)) {
String newQuery = keystart > 0 ? query.substring(0, keystart - 1) : "";
if(i < query.length() - 1) {
if(keystart > 0) {
newQuery += query.substring(i);
} else {
newQuery += query.substring(i + 1);
}
}
String value = valstart >= 0 ? query.substring(valstart, i) : "";
String helperValue = LocalHTTPServer.decode(value);
return new String[] {newQuery, helperValue};
}
keystart = i + 1;
valstart = -1;
} else if (c == '=' && valstart < 0) {
// end of key
key = query.substring(keystart, i);
valstart = i + 1;
}
}
return null;
}
/****
private static String[] tests = {
"", "foo", "foo=bar", "&", "&=&", "===", "&&",
"i2paddresshelper=foo",
"i2paddresshelpe=foo",
"2paddresshelper=foo",
"i2paddresshelper=%66oo",
"%692paddresshelper=foo",
"i2paddresshelper=foo&a=b",
"a=b&i2paddresshelper=foo",
"a=b&i2paddresshelper&c=d",
"a=b&i2paddresshelper=foo&c=d",
"a=b;i2paddresshelper=foo;c=d",
"a=b&i2paddresshelper=foo&c",
"a=b&i2paddresshelper=foo==&c",
"a=b&i2paddresshelper=foo%3d%3d&c",
"a=b&i2paddresshelper=f%6f%6F==&c",
"a=b&i2paddresshelper=foo&i2paddresshelper=bar&c",
"a=b&i2paddresshelper=foo&c%3F%3f%26%3b%3B%3d%3Dc=x%3F%3f%26%3b%3B%3d%3Dx"
};
public static void main(String[] args) {
for (int i = 0; i < tests.length; i++) {
String[] s = removeHelper(tests[i]);
if (s != null)
System.out.println("Test \"" + tests[i] + "\" q=\"" + s[0] + "\" h=\"" + s[1] + "\"");
else
System.out.println("Test \"" + tests[i] + "\" no match");
}
}
****/
}