/* 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);
}
}