/* 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.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.Socket; import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.util.Locale; import java.util.Properties; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.app.ClientApp; import net.i2p.app.ClientAppManager; import net.i2p.app.Outproxy; import net.i2p.client.I2PSession; import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocketManager; import net.i2p.client.streaming.I2PSocketOptions; import net.i2p.crypto.SHA256Generator; import net.i2p.data.Base32; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.i2ptunnel.localServer.LocalHTTPServer; import net.i2p.util.ConvertToHash; import net.i2p.util.EventDispatcher; import net.i2p.util.Log; import net.i2p.util.PortMapper; /** * Act as a mini HTTP proxy, handling various different types of requests, * forwarding them through I2P appropriately, and displaying the reply. Supported * request formats are: <pre> * $method http://$site[$port]/$path $protocolVersion * or * $method $path $protocolVersion\nHost: $site * or * $method http://i2p/$b64key/$path $protocolVersion * or * $method /$site/$path $protocolVersion * or (deprecated) * $method /eepproxy/$site/$path $protocolVersion * </pre> * * CONNECT (https) supported as of release 0.9.11. * * Note that http://i2p/$b64key/... and /eepproxy/$site/... are not recommended * in browsers or other user-visible applications, as relative links will not * resolve correctly, cookies won't work, etc. * * Note that http://$b64key/... and http://$b64key.i2p/... are NOT supported, as * a b64 key may contain '=' and '~', both of which are illegal host name characters. * Rewrite as http://i2p/$b64key/... * * If the $site resolves with the I2P naming service, then it is directed towards * that eepsite, otherwise it is directed towards this client's outproxy (typically * "squid.i2p"). Only HTTP and HTTPS are supported (no ftp, mailto, etc). Both GET * and POST have been tested, though other $methods should work. * */ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runnable { /** * Map of host name to base64 destination for destinations collected * via address helper links */ private final ConcurrentHashMap<String, String> addressHelpers = new ConcurrentHashMap<String, String>(8); /** * Used to protect actions via http://proxy.i2p/ */ private final String _proxyNonce; public static final String AUTH_REALM = "I2P HTTP Proxy"; /** * These are backups if the xxx.ht error page is missing. */ private final static String ERR_REQUEST_DENIED = "HTTP/1.1 403 Access Denied\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: REQUEST DENIED</H1>" + "You attempted to connect to a non-I2P website or location.<BR>"; /***** private final static byte[] ERR_TIMEOUT = ("HTTP/1.1 504 Gateway Timeout\r\n"+ "Content-Type: text/html; charset=iso-8859-1\r\n"+ "Cache-control: no-cache\r\n\r\n"+ "<html><body><H1>I2P ERROR: TIMEOUT</H1>"+ "That Destination was reachable, but timed out getting a "+ "response. This is likely a temporary error, so you should simply "+ "try to refresh, though if the problem persists, the remote "+ "destination may have issues. Could not get a response from "+ "the following Destination:<BR><BR>") .getBytes(); *****/ private 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"; private final static String ERR_AHELPER_CONFLICT = "HTTP/1.1 409 Conflict\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 key conflict</H1>" + "The addresshelper link you followed specifies a different destination key " + "than a host entry in your host database. " + "Someone could be trying to impersonate another website, " + "or people have given two websites identical names.<p>" + "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.<p>"; 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" + "<html><body><H1>I2P ERROR: Helper key not resolvable.</H1>" + "The helper key you put for i2paddresshelper= is not resolvable. " + "It seems to be garbage data, or a mistyped b32. Check your URL " + "to try and fix the helper key to be either a b32 or a base64."; private final static String ERR_AHELPER_NEW = "HTTP/1.1 409 New Address\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>New Host Name with Address Helper</H1>" + "The address helper link you followed is for a new host name that is not in your address book. " + "You may either save the destination for this host name to your address book, or remember it only until your router restarts. " + "If you save it to your address book, you will not see this message again. " + "If you do not wish to visit this host, click the \"back\" button on your browser."; private final static String ERR_BAD_PROTOCOL = "HTTP/1.1 403 Bad Protocol\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: NON-HTTP PROTOCOL</H1>" + "The request uses a bad protocol. " + "The I2P HTTP Proxy supports HTTP and HTTPS requests only. Other protocols such as FTP are not allowed.<BR>"; private final static String ERR_BAD_URI = "HTTP/1.1 403 Bad URI\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: INVALID REQUEST URI</H1>" + "The request URI is invalid, and probably contains illegal characters. " + "If you clicked e.g. a forum link, check the end of the URI for any characters the browser has mistakenly added on.<BR>"; private final static String ERR_LOCALHOST = "HTTP/1.1 403 Access Denied\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: REQUEST DENIED</H1>" + "Your browser is misconfigured. Do not use the proxy to access the router console or other localhost destinations.<BR>"; private final static String ERR_INTERNAL_SSL = "HTTP/1.1 403 SSL Rejected\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: SSL to I2P address rejected</H1>" + "SSL to .i2p addresses denied by configuration." + "You may change the configuration in I2PTunnel"; /** * This constructor always starts the tunnel (ignoring the i2cp.delayOpen option). * It is used to add a client to an existing socket manager. * * As of 0.9.20 this is fast, and does NOT connect the manager to the router, * or open the local socket. You MUST call startRunning() for that. * * @param sockMgr the existing socket manager */ public I2PTunnelHTTPClient(int localPort, Logging l, I2PSocketManager sockMgr, I2PTunnel tunnel, EventDispatcher notifyThis, long clientId) { super(localPort, l, sockMgr, tunnel, notifyThis, clientId); _proxyNonce = Long.toString(_context.random().nextLong()); // proxyList = new ArrayList(); if (tunnel.getClientOptions().getProperty("i2p.streaming.connectDelay") == null) tunnel.getClientOptions().setProperty("i2p.streaming.connectDelay", "1000"); setName("HTTP Proxy on " + getTunnel().listenHost + ':' + localPort); notifyEvent("openHTTPClientResult", "ok"); } /** * As of 0.9.20 this is fast, and does NOT connect the manager to the router, * or open the local socket. You MUST call startRunning() for that. * * @throws IllegalArgumentException if the I2PTunnel does not contain * valid config to contact the router */ public I2PTunnelHTTPClient(int localPort, Logging l, boolean ownDest, String wwwProxy, EventDispatcher notifyThis, I2PTunnel tunnel) throws IllegalArgumentException { super(localPort, ownDest, l, notifyThis, "HTTP Proxy on " + tunnel.listenHost + ':' + localPort, tunnel); _proxyNonce = Long.toString(_context.random().nextLong()); //proxyList = new ArrayList(); // We won't use outside of i2p if(wwwProxy != null) { StringTokenizer tok = new StringTokenizer(wwwProxy, ", "); while(tok.hasMoreTokens()) { _proxyList.add(tok.nextToken().trim()); } } if (tunnel.getClientOptions().getProperty("i2p.streaming.connectDelay") == null) tunnel.getClientOptions().setProperty("i2p.streaming.connectDelay", "1000"); setName("HTTP Proxy on " + tunnel.listenHost + ':' + localPort); notifyEvent("openHTTPClientResult", "ok"); } /** * Create the default options (using the default timeout, etc). * Warning, this does not make a copy of I2PTunnel's client options, * it modifies them directly. * unused? */ @Override protected I2PSocketOptions getDefaultOptions() { Properties defaultOpts = getTunnel().getClientOptions(); if(!defaultOpts.contains(I2PSocketOptions.PROP_READ_TIMEOUT)) { defaultOpts.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, "" + DEFAULT_READ_TIMEOUT); } //if (!defaultOpts.contains("i2p.streaming.inactivityTimeout")) // defaultOpts.setProperty("i2p.streaming.inactivityTimeout", ""+DEFAULT_READ_TIMEOUT); // delayed start verifySocketManager(); I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts); if(!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT)) { opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT); } return opts; } /** * Create the default options (using the default timeout, etc). * Warning, this does not make a copy of I2PTunnel's client options, * it modifies them directly. * Do not use overrides for per-socket options. */ @Override protected I2PSocketOptions getDefaultOptions(Properties overrides) { Properties defaultOpts = getTunnel().getClientOptions(); defaultOpts.putAll(overrides); if(!defaultOpts.contains(I2PSocketOptions.PROP_READ_TIMEOUT)) { defaultOpts.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, "" + DEFAULT_READ_TIMEOUT); } if(!defaultOpts.contains("i2p.streaming.inactivityTimeout")) { defaultOpts.setProperty("i2p.streaming.inactivityTimeout", "" + DEFAULT_READ_TIMEOUT); } // delayed start verifySocketManager(); I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts); if(!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT)) { opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT); } return opts; } private InternalSocketRunner isr; /** * Actually start working on incoming connections. * Overridden to start an internal socket too. * */ @Override public void startRunning() { // following are for HTTPResponseOutputStream //_context.statManager().createRateStat("i2ptunnel.httpCompressionRatio", "ratio of compressed size to decompressed size after transfer", "I2PTunnel", new long[] { 60*60*1000 }); //_context.statManager().createRateStat("i2ptunnel.httpCompressed", "compressed size transferred", "I2PTunnel", new long[] { 60*60*1000 }); //_context.statManager().createRateStat("i2ptunnel.httpExpanded", "size transferred after expansion", "I2PTunnel", new long[] { 60*60*1000 }); super.startRunning(); if (open) { this.isr = new InternalSocketRunner(this); this.isr.start(); int port = getLocalPort(); _context.portMapper().register(PortMapper.SVC_HTTP_PROXY, getTunnel().listenHost, port); _context.portMapper().register(PortMapper.SVC_HTTPS_PROXY, getTunnel().listenHost, port); } } /** * Overridden to close internal socket too. */ @Override public boolean close(boolean forced) { int port = getLocalPort(); int reg = _context.portMapper().getPort(PortMapper.SVC_HTTP_PROXY); if (reg == port) { _context.portMapper().unregister(PortMapper.SVC_HTTP_PROXY); } reg = _context.portMapper().getPort(PortMapper.SVC_HTTPS_PROXY); if (reg == port) { _context.portMapper().unregister(PortMapper.SVC_HTTPS_PROXY); } boolean rv = super.close(forced); if(this.isr != null) { this.isr.stopRunning(); } 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; /** all default to false */ public static final String PROP_REFERER = "i2ptunnel.httpclient.sendReferer"; public static final String PROP_USER_AGENT = "i2ptunnel.httpclient.sendUserAgent"; public static final String PROP_VIA = "i2ptunnel.httpclient.sendVia"; public static final String PROP_JUMP_SERVERS = "i2ptunnel.httpclient.jumpServers"; public static final String PROP_DISABLE_HELPER = "i2ptunnel.httpclient.disableAddressHelper"; /** @since 0.9.11 */ public static final String PROP_SSL_OUTPROXIES = "i2ptunnel.httpclient.SSLOutproxies"; /** @since 0.9.14 */ public static final String PROP_ACCEPT = "i2ptunnel.httpclient.sendAccept"; /** @since 0.9.14 */ public static final String PROP_INTERNAL_SSL = "i2ptunnel.httpclient.allowInternalSSL"; /** * * Note: This does not handle RFC 2616 header line splitting, * which is obsoleted in RFC 7230. */ protected void clientConnectionRun(Socket s) { OutputStream out = null; /** * The URL after fixup, always starting with http:// or https:// */ String targetRequest = null; // in-net outproxy boolean usingWWWProxy = false; // local outproxy plugin boolean usingInternalOutproxy = false; Outproxy outproxy = null; boolean usingInternalServer = false; String internalPath = null; String internalRawQuery = null; String currentProxy = null; long requestId = __requestId.incrementAndGet(); boolean shout = false; try { out = s.getOutputStream(); InputReader reader = new InputReader(s.getInputStream()); String line, method = null, protocol = null, host = null, destination = null; StringBuilder newRequest = new StringBuilder(); boolean ahelperPresent = false; boolean ahelperNew = false; String ahelperKey = null; String userAgent = null; String authorization = null; int remotePort = 0; String referer = null; URI origRequestURI = null; while((line = reader.readLine(method)) != null) { line = line.trim(); if(_log.shouldLog(Log.DEBUG)) { _log.debug(getPrefix(requestId) + "Line=[" + line + "]"); } String lowercaseLine = line.toLowerCase(Locale.US); if(lowercaseLine.startsWith("connection: ") || lowercaseLine.startsWith("keep-alive: ") || lowercaseLine.startsWith("proxy-connection: ")) { continue; } if(method == null) { // first line (GET /base64/realaddr) if(_log.shouldLog(Log.DEBUG)) { _log.debug(getPrefix(requestId) + "First line [" + line + "]"); } String[] params = DataHelper.split(line, " ", 3); if(params.length != 3) { break; } String request = params[1]; // various obscure fixups if(request.startsWith("/") && getTunnel().getClientOptions().getProperty("i2ptunnel.noproxy") != null) { // what is this for ??? request = "http://i2p" + request; } else if(request.startsWith("/eepproxy/")) { // Deprecated // /eepproxy/foo.i2p/bar/baz.html String subRequest = request.substring("/eepproxy/".length()); if(subRequest.indexOf('/') == -1) { subRequest += '/'; } request = "http://" + subRequest; /**** } else if (request.toLowerCase(Locale.US).startsWith("http://i2p/")) { // http://i2p/b64key/bar/baz.html // we can't do this now by setting the URI host to the b64key, as // it probably contains '=' and '~' which are illegal, // and a host may not include escaped octets // This will get undone below. String subRequest = request.substring("http://i2p/".length()); if (subRequest.indexOf("/") == -1) subRequest += "/"; "http://" + "b64key/bar/baz.html" request = "http://" + subRequest; } else if (request.toLowerCase(Locale.US).startsWith("http://")) { // Unsupported // http://$b64key/... // This probably used to work, rewrite it so that // we can create a URI without illegal characters // This will get undone below. String oldPath = request.substring(7); int slash = oldPath.indexOf("/"); if (slash < 0) slash = oldPath.length(); if (slash >= 516 && !oldPath.substring(0, slash).contains(".")) request = "http://i2p/" + oldPath; ****/ } method = params[0]; if (method.toUpperCase(Locale.US).equals("CONNECT")) { // this makes things easier later, by spoofing a // protocol so the URI parser find the host and port // For in-net outproxy, will be fixed up below request = "https://" + request + '/'; } // Now use the Java URI parser // This will be the incoming URI but will then get modified // to be the outgoing URI (with http:// if going to outproxy, otherwise without) URI requestURI; try { origRequestURI = requestURI = new URI(request); if(requestURI.getRawUserInfo() != null || requestURI.getRawFragment() != null) { // these should never be sent to the proxy in the request line if(_log.shouldLog(Log.WARN)) { _log.warn(getPrefix(requestId) + "Removing userinfo or fragment [" + request + "]"); } requestURI = changeURI(requestURI, null, 0, null); } if(requestURI.getPath() == null || requestURI.getPath().length() <= 0) { // Add a path if(_log.shouldLog(Log.WARN)) { _log.warn(getPrefix(requestId) + "Adding / path to [" + request + "]"); } requestURI = changeURI(requestURI, null, 0, "/"); } } catch(URISyntaxException use) { if(_log.shouldLog(Log.WARN)) { _log.warn(getPrefix(requestId) + "Bad request [" + request + "]", use); } try { out.write(getErrorPage("baduri", ERR_BAD_URI).getBytes("UTF-8")); writeFooter(out); reader.drain(); } catch (IOException ioe) { // ignore } finally { closeSocket(s); } 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. String 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>.b32.i2p syntax. /* also i2paddresshelper=name.i2p for aliases i.e. on your eepsite put <a href="?i2paddresshelper=name.i2p">This is the name I want to be called.</a> */ 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(("<p>" + _t("This seems to be a bad destination:") + " " + ahelperKey + " " + _t("i2paddresshelper cannot help you with a destination like that!") + "</p>").getBytes("UTF-8")); writeFooter(out); reader.drain(); // XXX: should closeSocket(s) be in a finally block? } catch (IOException ioe) { // ignore } finally { closeSocket(s); } 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("<p>".getBytes("UTF-8")); out.write(_t("To visit the destination in your address book, click <a href=\"{0}\">here</a>. To visit the conflicting addresshelper destination, click <a href=\"{1}\">here</a>.", trustedURL, conflictURL).getBytes("UTF-8")); out.write("</p>".getBytes("UTF-8")); Hash h1 = ConvertToHash.getHash(requestURI.getHost()); Hash h2 = ConvertToHash.getHash(ahelperKey); if (h1 != null && h2 != null) { // Do we need to replace http://127.0.0.1:7657 // Get the registered host and port from the PortMapper. final String unset = "*unset*"; final String httpHost = _context.portMapper().getActualHost(PortMapper.SVC_CONSOLE, unset); final String httpsHost = _context.portMapper().getActualHost(PortMapper.SVC_HTTPS_CONSOLE, unset); final int httpPort = _context.portMapper().getPort(PortMapper.SVC_CONSOLE, 7657); final int httpsPort = _context.portMapper().getPort(PortMapper.SVC_HTTPS_CONSOLE, -1); final boolean httpsOnly = httpsPort > 0 && httpHost.equals(unset) && !httpsHost.equals(unset); final int cport = httpsOnly ? httpsPort : httpPort; String chost = httpsOnly ? httpsHost : httpHost; if (chost.equals(unset)) chost = "127.0.0.1"; String chostport; if (httpsOnly || cport != 7657 || !chost.equals("127.0.0.1")) chostport = (httpsOnly ? "https://" : "http://") + chost + ':' + cport; else chostport = "http://127.0.0.1:7657"; out.write(("\n<table class=\"conflict\"><tr><th align=\"center\">" + "<a href=\"" + trustedURL + "\">").getBytes("UTF-8")); out.write(_t("Destination for {0} in address book", requestURI.getHost()).getBytes("UTF-8")); out.write(("</a></th>\n<th align=\"center\">" + "<a href=\"" + conflictURL + "\">").getBytes("UTF-8")); out.write(_t("Conflicting address helper destination").getBytes("UTF-8")); out.write(("</a></th></tr>\n<tr><td align=\"center\">" + "<a href=\"" + trustedURL + "\">" + "<img src=\"" + chostport + "/imagegen/id?s=160&c=" + h1.toBase64().replace("=", "%3d") + "\" width=\"160\" height=\"160\"></a>\n").getBytes("UTF-8")); out.write(("</td>\n<td align=\"center\">" + "<a href=\"" + conflictURL + "\">" + "<img src=\"" + chostport + "/imagegen/id?s=160&c=" + h2.toBase64().replace("=", "%3d") + "\" width=\"160\" height=\"160\"></a>\n").getBytes("UTF-8")); out.write("</td></tr></table>".getBytes("UTF-8")); } out.write("</div>".getBytes("UTF-8")); writeFooter(out); } reader.drain(); } catch (IOException ioe) { // ignore } finally { closeSocket(s); } 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 } finally { closeSocket(s); } 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(); else currentProxy = selectProxy(); if(_log.shouldLog(Log.DEBUG)) { _log.debug("After selecting outproxy for " + host + ": " + currentProxy); } if(currentProxy == null) { if(_log.shouldLog(Log.WARN)) { _log.warn(getPrefix(requestId) + "Host wants to be outproxied, but we dont have any!"); } l.log("No outproxy found for the request."); try { out.write(getErrorPage("noproxy", ERR_NO_OUTPROXY).getBytes("UTF-8")); writeFooter(out); reader.drain(); } catch (IOException ioe) { // ignore } finally { closeSocket(s); } 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 } finally { closeSocket(s); } 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("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.shouldLog(Log.INFO)) { _log.info(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")) { // 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 if(usingWWWProxy || usingInternalOutproxy) { newRequest.append("User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:24.0) Gecko/20100101 Firefox/24.0\r\n"); } else { newRequest.append("User-Agent: MYOB/6.66 (AN/ON)\r\n"); } } } // 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"); } } newRequest.append("Connection: close\r\n\r\n"); 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 } finally { closeSocket(s); } 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 } finally { closeSocket(s); } 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(out, method, internalPath, internalRawQuery, _proxyNonce); } } catch (IOException ioe) { // ignore } finally { closeSocket(s); } 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 } finally { closeSocket(s); } return; } } else if("i2p".equals(host)) { clientDest = null; } else if(destination.length() == 60 && destination.toLowerCase(Locale.US).endsWith(".b32.i2p")) { // use existing session to look up for efficiency verifySocketManager(); I2PSession sess = sockMgr.getSession(); if(!sess.isClosed()) { byte[] hData = Base32.decode(destination.substring(0, 52)); if(hData != null) { if(_log.shouldLog(Log.INFO)) { _log.info("lookup in-session " + destination); } Hash hash = Hash.create(hData); clientDest = sess.lookupDest(hash, 20 * 1000); } } else { clientDest = _context.namingService().lookup(destination); } } else { 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 } finally { closeSocket(s); } return; } if (method.toUpperCase(Locale.US).equals("CONNECT") && !usingWWWProxy && !Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_INTERNAL_SSL))) { try { writeErrorMessage(ERR_INTERNAL_SSL, out, targetRequest, false, destination); } catch (IOException ioe) { // ignore } finally { closeSocket(s); } 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 } finally { closeSocket(s); } 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)) { 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 } finally { closeSocket(s); } 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); I2PSocket i2ps = createI2PSocket(clientDest, sktOpts); OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId); Thread t; if (method.toUpperCase(Locale.US).equals("CONNECT")) { 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); } // 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); } //l.log("Error connecting: " + ex.getMessage()); handleClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); closeSocket(s); } catch(I2PException ex) { if(_log.shouldLog(Log.INFO)) { _log.info("getPrefix(requestId) + Error trying to connect", ex); } //l.log("Error connecting: " + ex.getMessage()); handleClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); closeSocket(s); } catch(OutOfMemoryError oom) { IOException ex = new IOException("OOM"); _log.error("getPrefix(requestId) + Error trying to connect", oom); //l.log("Error connecting: " + ex.getMessage()); handleClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); closeSocket(s); } } /** * Unlike selectProxy(), we parse the option on the fly so it * can be changed. selectProxy() requires restart... * @return null if none * @since 0.9.11 */ private String selectSSLProxy() { String s = getTunnel().getClientOptions().getProperty(PROP_SSL_OUTPROXIES); if (s == null) return null; String[] p = DataHelper.split(s, "[,; \r\n\t]"); if (p.length == 0) return null; // todo doesn't check for "" if (p.length == 1) return p[0]; int i = _context.random().nextInt(p.length); return p[i]; } /** @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("<table><tr><td class=\"mediumtags\" align=\"right\">" + _t("Host") + "</td><td class=\"mediumtags\">" + destination + "</td></tr>\n"); try { String b32 = Base32.encode(SHA256Generator.getInstance().calculateHash(Base64.decode(ahelperKey)).getData()); out.write("<tr><td class=\"mediumtags\" align=\"right\">" + _t("Base 32") + "</td>" + "<td><a href=\"http://" + b32 + ".b32.i2p/\">" + b32 + ".b32.i2p</a></td></tr>"); } catch(Exception e) { } out.write("<tr><td class=\"mediumtags\" align=\"right\">" + _t("Destination") + "</td><td>" + "<textarea rows=\"1\" style=\"height: 4em; min-width: 0; min-height: 0;\" cols=\"70\" wrap=\"off\" readonly=\"readonly\" >" + ahelperKey + "</textarea></td></tr></table>\n" + "<hr><div class=\"formaction\">" + // FIXME if there is a query remaining it is lost "<form method=\"GET\" action=\"" + targetRequest + "\">" + "<button type=\"submit\" class=\"go\">" + _t("Continue to {0} without saving", destination) + "</button>" + "</form>\n<form method=\"GET\" action=\"http://" + LOCAL_SERVER + "/add\">" + "<input type=\"hidden\" name=\"host\" value=\"" + destination + "\">\n" + "<input type=\"hidden\" name=\"dest\" value=\"" + ahelperKey + "\">\n" + "<input type=\"hidden\" name=\"nonce\" value=\"" + _proxyNonce + "\">\n" + "<button type=\"submit\" class=\"accept\" name=\"router\" value=\"router\">" + _t("Save {0} to router address book and continue to website", destination) + "</button><br>\n"); if(_context.namingService().getName().equals("BlockfileNamingService")) { // only blockfile supports multiple books out.write("<br><button type=\"submit\" name=\"master\" value=\"master\">" + _t("Save {0} to master address book and continue to website", destination) + "</button><br>\n"); out.write("<button type=\"submit\" name=\"private\" value=\"private\">" + _t("Save {0} to private address book and continue to website", destination) + "</button>\n"); } // Firefox (and others?) don't send referer to meta refresh target, which is // what the jump servers use, so this isn't that useful. if (referer != null) out.write("<input type=\"hidden\" name=\"referer\" value=\"" + referer + "\">\n"); out.write("<input type=\"hidden\" name=\"url\" value=\"" + targetRequest + "\">\n" + "</form></div></div>"); 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.length() == 60 && 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"); } } ****/ }