/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) * (c) 2003 - 2004 mihi */ package net.i2p.i2ptunnel.localServer; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import net.i2p.I2PAppContext; import net.i2p.client.naming.NamingService; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.i2ptunnel.I2PTunnelHTTPClient; import net.i2p.util.FileUtil; import net.i2p.util.Translate; /** * Very simple web server. * * Serve local files in the docs/ directory, for CSS and images in * error pages, using the reserved address proxy.i2p * (similar to p.p in privoxy). * This solves the problems with including links to the router console, * as assuming the router console is at 127.0.0.1 leads to broken * links if it isn't. * * @since 0.7.6, moved from I2PTunnelHTTPClient in 0.9 */ public abstract class LocalHTTPServer { private final static String ERR_404 = "HTTP/1.1 404 Not Found\r\n"+ "Content-Type: text/plain\r\n"+ "Connection: close\r\n"+ "Proxy-Connection: close\r\n"+ "\r\n"+ "HTTP Proxy local file not found"; private final static String ERR_ADD = "HTTP/1.1 409 Bad\r\n"+ "Content-Type: text/plain\r\n"+ "Connection: close\r\n"+ "Proxy-Connection: close\r\n"+ "\r\n"+ "Add to addressbook failed - bad parameters"; private final static String OK = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/plain\r\n" + "Cache-Control: max-age=86400\r\n" + "Connection: close\r\n"+ "Proxy-Connection: close\r\n"+ "\r\n"+ "I2P HTTP proxy OK"; /** * Very simple web server. * * Serve local files in the docs/ directory, for CSS and images in * error pages, using the reserved address proxy.i2p * (similar to p.p in privoxy). * This solves the problems with including links to the router console, * as assuming the router console is at 127.0.0.1 leads to broken * links if it isn't. * * Ignore all request headers (If-Modified-Since, etc.) * * There is basic protection here - * FileUtil.readFile() prevents traversal above the base directory - * but inproxy/gateway ops would be wise to block proxy.i2p to prevent * exposing the docs/ directory or perhaps other issues through * uncaught vulnerabilities. * Restrict to the /themes/ directory for now. * * @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 query, String proxyNonce) throws IOException { //System.err.println("targetRequest: \"" + targetRequest + "\""); // a home page message for the curious... if (targetRequest.equals("/")) { out.write(OK.getBytes("UTF-8")); out.flush(); return; } if ((method.equals("GET") || method.equals("HEAD")) && targetRequest.startsWith("/themes/") && !targetRequest.contains("..")) { String filename = null; try { filename = targetRequest.substring(8); // "/themes/".length } catch (IndexOutOfBoundsException ioobe) { return; } // theme hack if (filename.startsWith("console/default/")) filename = filename.replaceFirst("default", I2PAppContext.getGlobalContext().getProperty("routerconsole.theme", "light")); File themesDir = new File(I2PAppContext.getGlobalContext().getBaseDir(), "docs/themes"); File file = new File(themesDir, filename); if (file.exists() && !file.isDirectory()) { String type; if (filename.endsWith(".css")) type = "text/css"; else if (filename.endsWith(".ico")) type = "image/x-icon"; else if (filename.endsWith(".png")) type = "image/png"; else if (filename.endsWith(".jpg")) type = "image/jpeg"; else type = "text/html"; out.write("HTTP/1.1 200 OK\r\nContent-Type: ".getBytes("UTF-8")); out.write(type.getBytes("UTF-8")); out.write("\r\nCache-Control: max-age=86400\r\nConnection: close\r\nProxy-Connection: close\r\n\r\n".getBytes("UTF-8")); FileUtil.readFile(filename, themesDir.getAbsolutePath(), out); return; } } // Add to addressbook (form submit) // Parameters are url, host, dest, nonce, and master | router | private. // Do the add and redirect. if (targetRequest.equals("/add")) { if (query == null) { out.write(ERR_ADD.getBytes("UTF-8")); return; } Map<String, String> opts = new HashMap<String, String>(8); // this only works if all keys are followed by =value StringTokenizer tok = new StringTokenizer(query, "=&;"); while (tok.hasMoreTokens()) { String k = tok.nextToken(); if (!tok.hasMoreTokens()) break; String v = tok.nextToken(); opts.put(decode(k), decode(v)); } String url = opts.get("url"); String host = opts.get("host"); String b64Dest = opts.get("dest"); String nonce = opts.get("nonce"); String referer = opts.get("referer"); String book = "privatehosts.txt"; if (opts.get("master") != null) book = "userhosts.txt"; else if (opts.get("router") != null) book = "hosts.txt"; Destination dest = null; if (b64Dest != null) { try { dest = new Destination(b64Dest); } catch (DataFormatException dfe) { System.err.println("Bad dest to save?" + b64Dest); } } //System.err.println("url : \"" + url + "\""); //System.err.println("host : \"" + host + "\""); //System.err.println("b64dest : \"" + b64Dest + "\""); //System.err.println("book : \"" + book + "\""); //System.err.println("nonce : \"" + nonce + "\""); if (proxyNonce.equals(nonce) && url != null && host != null && dest != null) { NamingService ns = I2PAppContext.getGlobalContext().namingService(); Properties nsOptions = new Properties(); nsOptions.setProperty("list", book); if (referer != null && referer.startsWith("http")) { String ref = DataHelper.escapeHTML(referer); String from = "<a href=\"" + ref + "\">" + ref + "</a>"; nsOptions.setProperty("s", _t("Added via address helper from {0}", from)); } else { nsOptions.setProperty("s", _t("Added via address helper")); } boolean success = ns.put(host, dest, nsOptions); writeRedirectPage(out, success, host, book, url); return; } out.write(ERR_ADD.getBytes("UTF-8")); } else { out.write(ERR_404.getBytes("UTF-8")); } out.flush(); } /** @since 0.8.7 */ private static void writeRedirectPage(OutputStream out, boolean success, String host, String book, String url) throws IOException { String tbook; if ("hosts.txt".equals(book)) tbook = _t("router"); else if ("userhosts.txt".equals(book)) tbook = _t("master"); else if ("privatehosts.txt".equals(book)) tbook = _t("private"); else tbook = book; out.write(("HTTP/1.1 200 OK\r\n"+ "Content-Type: text/html; charset=UTF-8\r\n"+ "Referrer-Policy: no-referrer\r\n"+ "Connection: close\r\n"+ "Proxy-Connection: close\r\n"+ "\r\n"+ "<html><head>"+ "<title>" + _t("Redirecting to {0}", host) + "</title>\n" + "<link rel=\"shortcut icon\" href=\"http://proxy.i2p/themes/console/images/favicon.ico\" >\n" + "<link href=\"http://proxy.i2p/themes/console/default/console.css\" rel=\"stylesheet\" type=\"text/css\" >\n" + "<meta http-equiv=\"Refresh\" content=\"1; url=" + url + "\">\n" + "</head><body>\n" + "<div class=logo>\n" + "<a href=\"http://127.0.0.1:7657/\" title=\"" + _t("Router Console") + "\"><img src=\"http://proxy.i2p/themes/console/images/i2plogo.png\" alt=\"I2P Router Console\" border=\"0\"></a><hr>\n" + "<a href=\"http://127.0.0.1:7657/config\">" + _t("Configuration") + "</a> <a href=\"http://127.0.0.1:7657/help.jsp\">" + _t("Help") + "</a> <a href=\"http://127.0.0.1:7657/susidns/index\">" + _t("Addressbook") + "</a>\n" + "</div>" + "<div class=warning id=warning>\n" + "<h3>" + (success ? _t("Saved {0} to the {1} addressbook, redirecting now.", host, tbook) : _t("Failed to save {0} to the {1} addressbook, redirecting now.", host, tbook)) + "</h3>\n<p><a href=\"" + url + "\">" + _t("Click here if you are not redirected automatically.") + "</a></p></div>").getBytes("UTF-8")); I2PTunnelHTTPClient.writeFooter(out); out.flush(); } /** * Decode %xx encoding * @since 0.8.7 */ public static String decode(String s) { if (!s.contains("%")) return s; StringBuilder buf = new StringBuilder(s.length()); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c != '%') { buf.append(c); } else { try { buf.append((char) Integer.parseInt(s.substring(++i, (++i) + 1), 16)); } catch (IndexOutOfBoundsException ioobe) { break; } catch (NumberFormatException nfe) { break; } } } return buf.toString(); } /** these strings go in the jar, not the war */ private static final String BUNDLE_NAME = "net.i2p.i2ptunnel.proxy.messages"; /** lang in routerconsole.lang property, else current locale */ protected static String _t(String key) { return Translate.getString(key, I2PAppContext.getGlobalContext(), BUNDLE_NAME); } /** {0} */ protected static String _t(String key, Object o) { return Translate.getString(key, o, I2PAppContext.getGlobalContext(), BUNDLE_NAME); } /** {0} and {1} */ protected static String _t(String key, Object o, Object o2) { return Translate.getString(key, o, o2, I2PAppContext.getGlobalContext(), BUNDLE_NAME); } }