diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index 4980425c691a40fd9f767af582d9c35bde670fc9..408c736f9463bf617a18eba88e100679ed5b2704 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -13,6 +13,8 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; import java.net.SocketException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -61,6 +63,10 @@ import net.i2p.util.Translate; * 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 is supported (no HTTPS, ftp, mailto, etc). Both GET @@ -309,6 +315,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn return rv; } + 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 */ @@ -321,11 +328,19 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn protected void clientConnectionRun(Socket s) { InputStream in = null; OutputStream out = null; + + /** + * The URL after fixup, always starting with http:// + */ String targetRequest = null; + boolean usingWWWProxy = false; boolean usingInternalServer = false; + String internalPath = null; + String internalRawQuery = null; String currentProxy = null; long requestId = ++__requestId; + try { out = s.getOutputStream(); InputReader reader = new InputReader(s.getInputStream()); @@ -351,79 +366,84 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix(requestId) + "First line [" + line + "]"); - int pos = line.indexOf(" "); - if (pos == -1) break; - method = line.substring(0, pos); - // TODO use Java URL class to make all this simpler and more robust - // That will also fix IPV6 [a:b:c] - String request = line.substring(pos + 1); + String[] params = line.split(" ", 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/")) { - // /eepproxy/foo.i2p/bar/baz.html HTTP/1.0 + // Deprecated + // /eepproxy/foo.i2p/bar/baz.html String subRequest = request.substring("/eepproxy/".length()); - int protopos = subRequest.indexOf(" "); - String uri = subRequest.substring(0, protopos); - if (uri.indexOf("/") == -1) { - uri = uri + "/"; - } - // "http://" + "foo.i2p/bar/baz.html" + " HTTP/1.0" - request = "http://" + uri + subRequest.substring(protopos); + if (subRequest.indexOf("/") == -1) + subRequest += "/"; + request = "http://" + subRequest; + /**** } else if (request.toLowerCase(Locale.US).startsWith("http://i2p/")) { - // http://i2p/b64key/bar/baz.html HTTP/1.0 + // 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()); - int protopos = subRequest.indexOf(" "); - String uri = subRequest.substring(0, protopos); - if (uri.indexOf("/") == -1) { - uri = uri + "/"; - } - // "http://" + "b64key/bar/baz.html" + " HTTP/1.0" - request = "http://" + uri + subRequest.substring(protopos); + 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; + ****/ } - pos = request.indexOf("//"); - if (pos == -1) { - method = null; + // 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 { + 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); break; } - protocol = request.substring(0, pos + 2); - request = request.substring(pos + 2); - - // "foo.i2p/bar/baz HTTP/1.1", with any i2paddresshelper parameter removed - targetRequest = request; + method = params[0]; + String protocolVersion = params[2]; - // pos is the start of the path - pos = request.indexOf("/"); - if (pos == -1) { - //pos = request.length(); + protocol = requestURI.getScheme(); + host = requestURI.getHost(); + if (protocol == null || host == null) { + _log.warn(request); method = null; break; } - host = request.substring(0, pos); - // parse port - int posPort = host.indexOf(":"); - int port = 80; - if(posPort != -1) { - String[] parts = host.split(":"); - try { - host = parts[0]; - } catch (ArrayIndexOutOfBoundsException ex) { - if (out != null) { - out.write(getErrorPage("denied", ERR_REQUEST_DENIED)); - writeFooter(out); - } - s.close(); - return; - - } - try { - port = Integer.parseInt(parts[1]); - } catch(Exception exc) { - // TODO: log this - } - } + int port = requestURI.getPort(); // Go through the various types of host names, set // the host and destination variables accordingly, @@ -433,115 +453,141 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn // in our addressbook (all naming is local), // and it is removed from the request line. - if (host.length() >= 516 && host.indexOf(".") < 0) { - // http://b64key/bar/baz.html - destination = host; - host = getHostName(destination); - line = method + ' ' + request.substring(pos); - } else if (host.toLowerCase(Locale.US).equals(LOCAL_SERVER)) { + String hostLowerCase = host.toLowerCase(Locale.US); + if (hostLowerCase.equals(LOCAL_SERVER)) { // so we don't do any naming service lookups destination = host; usingInternalServer = true; - } else if (host.toLowerCase(Locale.US).endsWith(".i2p")) { + 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 newPath = dest.substring(slash); + String newURI = requestURI.getRawPath(); + 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(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 pos2; - if ((pos2 = request.indexOf("?")) != -1) { - // Try to find an address helper in the fragments - // and split the request into it's component parts for rebuilding later + if (requestURI.getPort() >= 0) { + // TODO support I2P ports someday + 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; + } + } + + String query = requestURI.getRawQuery(); + if (query != null) { boolean ahelperConflict = false; - String fragments = request.substring(pos2 + 1); - String uriPath = request.substring(0, pos2); - pos2 = fragments.indexOf(" "); - String protocolVersion = fragments.substring(pos2 + 1); - String urlEncoding = ""; - fragments = fragments.substring(0, pos2); - String initialFragments = fragments; - // FIXME split on ';' also - fragments = fragments + "&"; - String fragment; - while(fragments.length() > 0) { - pos2 = fragments.indexOf("&"); - fragment = fragments.substring(0, pos2); - fragments = fragments.substring(pos2 + 1); - - // Fragment looks like addresshelper key - if (fragment.startsWith("i2paddresshelper=") && - !Boolean.valueOf(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER)).booleanValue()) { - pos2 = fragment.indexOf("="); - ahelperKey = fragment.substring(pos2 + 1); - // Key contains data, lets not ignore it - if (ahelperKey != null) { - 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); - byte[] header = getErrorPage("ahelper-notfound", ERR_AHELPER_NOTFOUND); - out.write(header); - out.write(("<p>" + _("This seems to be a bad destination:") + " " + ahelperKey + " " + _("i2paddresshelper cannot help you with a destination like that!") + "</p>").getBytes("UTF-8")); - writeFooter(out); - // XXX: should closeSocket(s) be in a finally block? - 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; - if ((!ahelperNew) && !old.equals(ahelperKey)) { + // Try to find an address helper in the query + String[] helperStrings = removeHelper(query); + if (helperStrings != null && + !Boolean.valueOf(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER)).booleanValue()) { + 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); + byte[] header = getErrorPage("ahelper-notfound", ERR_AHELPER_NOTFOUND); + out.write(header); + out.write(("<p>" + _("This seems to be a bad destination:") + " " + ahelperKey + " " + _("i2paddresshelper cannot help you with a destination like that!") + "</p>").getBytes("UTF-8")); + writeFooter(out); + // XXX: should closeSocket(s) be in a finally block? + 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; + if ((!ahelperNew) && !old.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 [" + 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 [" + 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 + "]."); - - } + "], trusted key [" + destB64 + "], specified key [" + ahelperKey + "]."); + } } - } // ahelperKey - } else { - // Other fragments, just pass along - // Append each fragment to urlEncoding - if ("".equals(urlEncoding)) { - urlEncoding = "?" + fragment; - } else { - urlEncoding = urlEncoding + "&" + fragment; } - } - } - // Reconstruct the request minus the i2paddresshelper GET var - request = uriPath + urlEncoding + " " + protocolVersion; - targetRequest = request; + } // ahelperKey + } // helperstrings // Did addresshelper key conflict? if (ahelperConflict) { @@ -553,9 +599,17 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn byte[] header = getErrorPage("dnfb", ERR_DESTINATION_UNKNOWN); writeErrorMessage(header, out, targetRequest, false, destination, null); } else { - String trustedURL = protocol + uriPath + urlEncoding; - // Fixme - any path is lost - String conflictURL = protocol + alias + '/' + urlEncoding; + 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(); byte[] header = getErrorPage("ahelper-conflict", ERR_AHELPER_CONFLICT); out.write(header); out.write(_("To visit the destination in your host database, click <a href=\"{0}\">here</a>. To visit the conflicting addresshelper destination, click <a href=\"{1}\">here</a>.", trustedURL, conflictURL).getBytes("UTF-8")); @@ -572,11 +626,24 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn if (addressHelper != null) host = getHostName(addressHelper); - line = method + " " + request.substring(pos); + // 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 (host.toLowerCase(Locale.US).equals("localhost") || host.equals("127.0.0.1") || - host.startsWith("192.168.")) { + } 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 if (out != null) { out.write(getErrorPage("localhost", ERR_LOCALHOST)); @@ -585,7 +652,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn s.close(); return; } else if (host.indexOf(".") != -1) { - // rebuild host host = host + ":" + port; // The request must be forwarded to a WWW proxy if (_log.shouldLog(Log.DEBUG)) @@ -606,37 +672,21 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } destination = currentProxy; usingWWWProxy = true; + targetRequest = requestURI.toASCIIString(); if (_log.shouldLog(Log.DEBUG)) - _log.debug(getPrefix(requestId) + "Host doesnt end with .i2p and it contains a period [" + host + "]: wwwProxy!"); + _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 ... - request = request.substring(pos + 1); - pos = request.indexOf("/"); - if (pos < 0) { - l.log("Invalid request url [" + request + "]"); - if (out != null) { - out.write(getErrorPage("denied", ERR_REQUEST_DENIED)); - writeFooter(out); - } - s.close(); - return; - } - destination = request.substring(0, pos); + if (_log.shouldLog(Log.WARN)) + _log.warn("NODOTS, NOI2P: " + request); + destination = requestURI.getHost(); host = getHostName(destination); - line = method + " " + request.substring(pos); + targetRequest = requestURI.toASCIIString(); + // FIXME treat as I2P or not??? } // end host name processing - if (port != 80 && !usingWWWProxy) { - if (out != null) { - out.write(getErrorPage("denied", ERR_REQUEST_DENIED)); - writeFooter(out); - } - s.close(); - return; - } - boolean isValid = usingWWWProxy || usingInternalServer || isSupportedAddress(host, protocol); if (!isValid) { if (_log.shouldLog(Log.INFO)) _log.info(getPrefix(requestId) + "notValid(" + host + ")"); @@ -645,18 +695,10 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn break; } - // don't do this, it forces yet another hostname lookup, - // and in all cases host was already set above - //if ((!usingWWWProxy) && (!usingInternalServer)) { - // String oldhost = host; - // host = getHostName(destination); // hide original host - // if (_log.shouldLog(Log.INFO)) - // _log.info(getPrefix(requestId) + " oldhost " + oldhost + " newhost " + host + " dest " + destination); - //} + line = method + ' ' + requestURI.toASCIIString() + ' ' + protocolVersion; if (_log.shouldLog(Log.DEBUG)) { - _log.debug(getPrefix(requestId) + "METHOD: \"" + method + "\""); - _log.debug(getPrefix(requestId) + "PROTOC: \"" + protocol + "\""); + _log.debug(getPrefix(requestId) + "NEWREQ: \"" + line + "\""); _log.debug(getPrefix(requestId) + "HOST : \"" + host + "\""); _log.debug(getPrefix(requestId) + "DEST : \"" + destination + "\""); } @@ -763,7 +805,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn if (method == null || destination == null) { //l.log("No HTTP method found in the request."); if (out != null) { - if (protocol != null && "http://".equals(protocol.toLowerCase(Locale.US))) + if (protocol != null && "http".equals(protocol.toLowerCase(Locale.US))) out.write(getErrorPage("denied", ERR_REQUEST_DENIED)); else out.write(getErrorPage("protocol", ERR_BAD_PROTOCOL)); @@ -794,11 +836,11 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn // Ignore all the headers if (usingInternalServer) { // disable the add form if address helper is disabled - if (targetRequest.startsWith(LOCAL_SERVER + "/add?") && + if (internalPath.equals("/add") && Boolean.valueOf(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER)).booleanValue()) { out.write(ERR_HELPER_DISABLED); } else { - LocalHTTPServer.serveLocalFile(out, method, targetRequest, _proxyNonce); + LocalHTTPServer.serveLocalFile(out, method, internalPath, internalRawQuery, _proxyNonce); } s.close(); return; @@ -865,7 +907,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn if (ahelperNew && "GET".equals(method) && (userAgent == null || !userAgent.startsWith("Wget")) && !Boolean.valueOf(getTunnel().getClientOptions().getProperty(PROP_DISABLE_HELPER)).booleanValue()) { - writeHelperSaveForm(out, destination, ahelperKey, protocol + targetRequest); + writeHelperSaveForm(out, destination, ahelperKey, targetRequest); s.close(); return; } @@ -875,10 +917,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn // 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 = protocol + targetRequest; - int spc = uri.indexOf(" "); - if (spc >= 0) - uri = uri.substring(0, spc); + String uri = targetRequest; if (_log.shouldLog(Log.DEBUG)) _log.debug("Auto redirecting to " + uri); out.write(("HTTP/1.1 301 Address Helper Accepted\r\n"+ @@ -928,10 +967,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn private void writeHelperSaveForm(OutputStream out, String destination, String ahelperKey, String targetRequest) throws IOException { if (out == null) return; - // strip HTTP/1.1 - int protopos = targetRequest.indexOf(" "); - if (protopos >= 0) - targetRequest = targetRequest.substring(0, protopos); byte[] header = getErrorPage("ahelper-new", ERR_AHELPER_NEW); out.write(header); out.write(("<table><tr><td class=\"mediumtags\" align=\"right\">" + _("Host") + "</td><td class=\"mediumtags\">" + destination + "</td></tr>\n" + @@ -939,6 +974,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn "<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\">" + _("Continue to {0} without saving", destination) + "</button>" + "</form>\n<form method=\"GET\" action=\"http://" + LOCAL_SERVER + "/add\">" + @@ -1094,15 +1130,10 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn if (out != null) { out.write(errMessage); if (targetRequest != null) { - int protopos = targetRequest.indexOf(" "); - String uri; - if (protopos >= 0) - uri = targetRequest.substring(0, protopos); - else - uri = targetRequest; - out.write("<a href=\"http://".getBytes()); + String uri = targetRequest; + out.write("<a href=\"".getBytes()); out.write(uri.getBytes()); - out.write("\">http://".getBytes()); + out.write("\">".getBytes()); out.write(uri.getBytes()); out.write("</a>".getBytes()); if (usingWWWProxy) { @@ -1196,7 +1227,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } } ****/ - return protocol.toLowerCase(Locale.US).equals("http://"); + return protocol.toLowerCase(Locale.US).equals("http"); } private final static byte[] ERR_HELPER_DISABLED = @@ -1206,6 +1237,128 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn "Address helpers disabled") .getBytes(); + /** + * 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 == '=') { + // 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" + }; + + 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"); + } + } +****/ + + /** */ private static final String BUNDLE_NAME = "net.i2p.i2ptunnel.web.messages"; /** lang in routerconsole.lang property, else current locale */ diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java index 0140b609cd4c3a30c20a3f23e3f471d52616aeb2..7c68c4e4062525dbc55a5b1780943cf4a51b6c3d 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java @@ -67,12 +67,13 @@ public abstract class LocalHTTPServer { * uncaught vulnerabilities. * Restrict to the /themes/ directory for now. * - * @param targetRequest "proxy.i2p/themes/foo.png HTTP/1.1" + * @param targetRequest decoded path only, non-null + * @param query raw (encoded), may be null */ - public static void serveLocalFile(OutputStream out, String method, String targetRequest, String proxyNonce) { + public static void serveLocalFile(OutputStream out, String method, String targetRequest, String query, String proxyNonce) { //System.err.println("targetRequest: \"" + targetRequest + "\""); // a home page message for the curious... - if (targetRequest.startsWith(I2PTunnelHTTPClient.LOCAL_SERVER + "/ ")) { + if (targetRequest.equals("/")) { try { out.write(("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nCache-Control: max-age=86400\r\n\r\nI2P HTTP proxy OK").getBytes()); out.flush(); @@ -80,12 +81,11 @@ public abstract class LocalHTTPServer { return; } if ((method.equals("GET") || method.equals("HEAD")) && - targetRequest.startsWith(I2PTunnelHTTPClient.LOCAL_SERVER + "/themes/") && + targetRequest.startsWith("/themes/") && !targetRequest.contains("..")) { - int space = targetRequest.indexOf(' '); String filename = null; try { - filename = targetRequest.substring(I2PTunnelHTTPClient.LOCAL_SERVER.length() + 8, space); // "/themes/".length + filename = targetRequest.substring(8); // "/themes/".length } catch (IndexOutOfBoundsException ioobe) { return; } @@ -118,10 +118,9 @@ public abstract class LocalHTTPServer { // Add to addressbook (form submit) // Parameters are url, host, dest, nonce, and master | router | private. // Do the add and redirect. - if (targetRequest.startsWith(I2PTunnelHTTPClient.LOCAL_SERVER + "/add?")) { - int spc = targetRequest.indexOf(' '); - String query = targetRequest.substring(I2PTunnelHTTPClient.LOCAL_SERVER.length() + 5, spc); // "/add?".length() + if (targetRequest.equals("/add")) { Map<String, String> opts = new HashMap(8); + // this only works if all keys are followed by =value StringTokenizer tok = new StringTokenizer(query, "=&;"); while (tok.hasMoreTokens()) { String k = tok.nextToken(); @@ -207,7 +206,7 @@ public abstract class LocalHTTPServer { * Decode %xx encoding * @since 0.8.7 */ - private static String decode(String s) { + public static String decode(String s) { if (!s.contains("%")) return s; StringBuilder buf = new StringBuilder(s.length());