From 624a67221340ebc1af4cf842ecf70c468ed1081f Mon Sep 17 00:00:00 2001 From: zzz Date: Sat, 28 Apr 2018 13:21:42 +0000 Subject: [PATCH] i2ptunnel: Initial SSL setup wizard Includes Jetty XML configuration parser Work in progress, doesn't work, not linked from anywhere --- apps/i2ptunnel/jsp/ssl.jsp | 406 ++++++++++++++++++ apps/i2ptunnel/jsp/web.xml | 5 + .../jetty/JettyXmlConfigurationParser.java | 134 ++++++ 3 files changed, 545 insertions(+) create mode 100644 apps/i2ptunnel/jsp/ssl.jsp create mode 100644 apps/jetty/java/src/net/i2p/jetty/JettyXmlConfigurationParser.java diff --git a/apps/i2ptunnel/jsp/ssl.jsp b/apps/i2ptunnel/jsp/ssl.jsp new file mode 100644 index 000000000..5d10a42cf --- /dev/null +++ b/apps/i2ptunnel/jsp/ssl.jsp @@ -0,0 +1,406 @@ +<% + // NOTE: Do the header carefully so there is no whitespace before the <%@page pageEncoding="UTF-8" +%><%@page contentType="text/html" import="java.io.File,net.i2p.crypto.KeyStoreUtil,net.i2p.data.DataHelper,net.i2p.jetty.JettyXmlConfigurationParser" +%><%@page +%> + +<% + /* right now using EditBean instead of IndexBean for getSpoofedHost() */ + /* but might want to POST to it anyway ??? */ +%> + + +<% + String tun = request.getParameter("tunnel"); + int curTunnel = -1; + if (tun != null) { + try { + curTunnel = Integer.parseInt(tun); + } catch (NumberFormatException nfe) { + curTunnel = -1; + } + } +%> + + + <%=intl._t("Hidden Services Manager")%> - <%=intl._t("SSL Helper")%> + + + <% if (editBean.allowCSS()) { + %> + + <% } + %> + + + +<% + + net.i2p.I2PAppContext ctx = net.i2p.I2PAppContext.getGlobalContext(); + if (!ctx.isRouterContext()) { + %>Unsupported in app context<% + } else if (editBean.isInitialized()) { + +%> +
+<% + String tunnelTypeName; + String tunnelType; + boolean valid = false; + if (curTunnel >= 0) { + tunnelTypeName = editBean.getTunnelType(curTunnel); + tunnelType = editBean.getInternalType(curTunnel); + %>

<%=intl._t("SSL Wizard")%> (<%=editBean.getTunnelName(curTunnel)%>)

<% + } else { + tunnelTypeName = "new"; + tunnelType = "new"; + %>

Fail

Tunnel not found

<% + } + + // set a bunch of variables for the current configuration + String b64 = editBean.getDestinationBase64(curTunnel); + String b32 = editBean.getDestHashBase32(curTunnel); + // todo + String altb32 = editBean.getAltDestHashBase32(curTunnel); + String name = editBean.getSpoofedHost(curTunnel); + String targetHost = editBean.getTargetHost(curTunnel); + if (targetHost != null && targetHost.indexOf(':') >= 0) + targetHost = '[' + targetHost + ']'; + String targetPort = editBean.getTargetPort(curTunnel); + int intPort = 0; + try { + intPort = Integer.parseInt(targetPort); + } catch (NumberFormatException nfe) {} + String clientTgt = targetHost + ':' + targetPort; + boolean sslToTarget = editBean.isSSLEnabled(curTunnel); + String targetLink = clientTgt; + boolean shouldLinkify = true; + if (shouldLinkify) { + String url = "://" + clientTgt + "\">" + clientTgt + ""; + if (sslToTarget) + targetLink = " + + + + +<% + if (!"new".equals(tunnelType)) { +%> + +
+ + + +<% + if (("httpserver".equals(tunnelType)) || ("httpbidirserver".equals(tunnelType))) { +%> + +<% + } + if (b64 == null || b64.length() < 516) { + %><% + } else if (name == null || name.equals("") || name.contains(" ") || !name.endsWith(".i2p")) { + if (("httpserver".equals(tunnelType)) || ("httpbidirserver".equals(tunnelType))) { + %><% + } else { + %><% + } + } else { + valid = true; +%> + +<% + if (altb32 != null && altb32.length() > 0) { +%> + +<% + } // altb32 +%> + + + +<% + // build tables for vhost and targets + java.util.TreeSet ports = new java.util.TreeSet(); + java.util.Map tgts = new java.util.HashMap(4); + java.util.Map spoofs = new java.util.HashMap(4); + String custom = editBean.getCustomOptions(curTunnel); + String[] opts = DataHelper.split(custom, "[, ]"); + for (int i = 0; i < opts.length; i++) { + String opt = opts[i]; + boolean isTgt = false; + if (opt.startsWith("targetForPort.")) { + opt = opt.substring("targetForPort.".length()); + isTgt = true; + } else if (opt.startsWith("spoofedHost.")) { + opt = opt.substring("spoofedHost.".length()); + } else { + continue; + } + int eq = opt.indexOf('='); + if (eq <= 0) + continue; + int port; + try { + port = Integer.parseInt(opt.substring(0, eq)); + } catch (NumberFormatException nfe) { + continue; + } + String tgt = opt.substring(eq + 1); + Integer iport = Integer.valueOf(port); + ports.add(iport); + if (isTgt) + tgts.put(iport, tgt); + else + spoofs.put(iport, tgt); + } + // output vhost and targets + for (Integer port : ports) { + boolean ssl = sslToTarget; + String spoof = spoofs.get(port); + if (spoof == null) + spoof = name; + // can't spoof for HTTPS + if (port.intValue() == 443) { + spoof = b32; + if (altb32 != null && altb32.length() > 0) + spoof += "
" + altb32; + ssl = true; + } + String tgt = tgts.get(port); + if (tgt != null) { + if (shouldLinkify) { + String url = "://" + tgt + "\">" + tgt + ""; + if (ssl) + tgt = " +
+<% + } +%> + + + + +<% + // Now try to find the Jetty server in clients.config + File configDir = ctx.getConfigDir(); + File clientsConfig = new File(configDir, "clients.config"); + java.util.Properties clientProps = new java.util.Properties(); + try { + DataHelper.loadProps(clientProps, clientsConfig); + for (int i = 0; i < 100; i++) { + String prop = "clientApp." + i + ".main"; + String cls = clientProps.getProperty(prop); + if (cls == null) + break; + if (!cls.equals("net.i2p.jetty.JettyStart")) + continue; + prop = "clientApp." + i + ".args"; + String clArgs = clientProps.getProperty(prop); + if (clArgs == null) + continue; + prop = "clientApp." + i + ".name"; + String clName = clientProps.getProperty(prop); + if (clName == null) + clName = intl._t("I2P webserver (eepsite)"); + prop = "clientApp." + i + ".startOnLoad"; + String clStart = clientProps.getProperty(prop); + boolean start = true; + if (clStart != null) + start = Boolean.parseBoolean(clStart); + // sample args + // clientApp.3.args="/home/xxx/.i2p/eepsite/jetty.xml" "/home/xxx/.i2p/eepsite/jetty-ssl.xml" "/home/xxx/.i2p/eepsite/jetty-rewrite.xml" + boolean ssl = clArgs.contains("jetty-ssl.xml"); + + boolean jettySSLFileInArgs = false; + boolean jettySSLFileExists = false; + boolean jettySSLFilePWSet = false; + File jettyFile = null, jettySSLFile = null; + String ksPW = null, kmPW = null, tsPW = null; + String ksPath = null, tsPath = null; + String host = null, port = null; + String sslHost = null, sslPort = null; + String error = ""; + java.util.List argList = net.i2p.i2ptunnel.web.SSLHelper.parseArgs(clArgs); + for (String arg : argList) { + if (arg.endsWith("jetty.xml")) { + jettyFile = new File(arg); + if (!jettyFile.isAbsolute()) + jettyFile = new File(ctx.getConfigDir(), arg); + jettySSLFileInArgs = true; + } else if (arg.endsWith("jetty-ssl.xml")) { + jettySSLFile = new File(arg); + if (!jettySSLFile.isAbsolute()) + jettySSLFile = new File(ctx.getConfigDir(), arg); + jettySSLFileInArgs = true; + } + } // for arg in argList + if (jettySSLFile == null && !argList.isEmpty()) { + String arg = argList.get(0); + File f = new File(arg); + if (!f.isAbsolute()) + f = new File(ctx.getConfigDir(), arg); + File p = f.getParentFile(); + if (p != null) + jettySSLFile = new File(p, "jetty-ssl.xml"); + } + boolean ksDflt = false; + boolean kmDflt = false; + boolean tsDflt = false; + boolean ksExists = false; + if (jettyFile != null && jettyFile.exists()) { + try { + org.eclipse.jetty.xml.XmlParser.Node root; + root = net.i2p.jetty.JettyXmlConfigurationParser.parse(jettyFile); + host = JettyXmlConfigurationParser.getValue(root, "host"); + port = JettyXmlConfigurationParser.getValue(root, "port"); + } catch (org.xml.sax.SAXException saxe) { + saxe.printStackTrace(); + error = DataHelper.escapeHTML(saxe.getMessage()); + } + } + if (jettySSLFile.exists()) { + try { + org.eclipse.jetty.xml.XmlParser.Node root; + root = net.i2p.jetty.JettyXmlConfigurationParser.parse(jettySSLFile); + ksPW = JettyXmlConfigurationParser.getValue(root, "KeyStorePassword"); + kmPW = JettyXmlConfigurationParser.getValue(root, "KeyManagerPassword"); + tsPW = JettyXmlConfigurationParser.getValue(root, "TrustStorePassword"); + ksPath = JettyXmlConfigurationParser.getValue(root, "KeyStorePath"); + tsPath = JettyXmlConfigurationParser.getValue(root, "TrustStorePath"); + sslHost = JettyXmlConfigurationParser.getValue(root, "host"); + sslPort = JettyXmlConfigurationParser.getValue(root, "port"); + // we can't proceed unless they are there + // tsPW may be null + File ksFile = null; + boolean tsIsKs = true; + boolean ksArgs = ksPW != null && kmPW != null && ksPath != null; + /** 2015+ installs */ + final String DEFAULT_KSPW_1 = KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD; + final String DEFAULT_KMPW_1 = "myKeyPassword"; + /** earlier */ + final String DEFAULT_KSPW_2 = "OBF:1vny1zlo1x8e1vnw1vn61x8g1zlu1vn4"; + final String DEFAULT_KMPW_2 = "OBF:1u2u1wml1z7s1z7a1wnl1u2g"; + if (ksArgs) { + jettySSLFileExists = true; + ksDflt = ksPW.equals(DEFAULT_KSPW_1) || ksPW.equals(DEFAULT_KSPW_2); + kmDflt = kmPW.equals(DEFAULT_KMPW_1) || kmPW.equals(DEFAULT_KMPW_2); + ksFile = new File(ksPath); + if (!ksFile.isAbsolute()) + ksFile = new File(ctx.getConfigDir(), ksPath); + ksExists = ksFile.exists(); + tsIsKs = tsPath == null || ksPath.equals(tsPath); + } + if (tsPW != null) { + tsDflt = tsPW.equals(DEFAULT_KSPW_1) || tsPW.equals(DEFAULT_KSPW_2); + } + } catch (org.xml.sax.SAXException saxe) { + saxe.printStackTrace(); + error = DataHelper.escapeHTML(saxe.getMessage()); + } + } + boolean canConfigure = jettySSLFileExists; + boolean isEnabled = canConfigure && jettySSLFileInArgs; + boolean isPWDefault = kmDflt || !ksExists; + + // now start the output for this client + +%> + +<% + if (!canConfigure) { +%> + +<% + } else { + if (isEnabled) { +%> + +<% + } else { +%> + +<% + } // isEnabled + if (isPWDefault) { +%> + +<% + } else { +%> + +<% + } // isPWDefault + } // canConfigure + } // for client + } catch (java.io.IOException ioe) { ioe.printStackTrace(); } +%> + +
<%=intl._t("Experts only!")%> Beta!
<%=intl._t("Tunnel name")%>: <%=editBean.getTunnelName(curTunnel)%>
<%=intl._t("Website name")%>: <%=editBean.getSpoofedHost(curTunnel)%>
<%=intl._t("Local destination is not available. Start the tunnel.")%>
<%=intl._t("To enable registration verification, edit tunnel and set name (or website name) to a valid host name ending in '.i2p'")%>
<%=intl._t("To enable registration verification, edit tunnel and set name to a valid host name ending in '.i2p'")%>
<%=intl._t("Base 32")%>: <%=b32%>
<%=intl._t("Alt Base 32")%>: <%=altb32%>
<%=intl._t("Incoming I2P Port Routing")%>
<%=intl._t("Route From I2P Port")%><%=intl._t("With Virtual Host")%><%=intl._t("Via SSL?")%><%=intl._t("To Server Host:Port")%>
<%=intl._t("Default")%><%=name%><%=sslToTarget%><%=targetLink%>
<%=port%><%=spoof%><%=ssl%><%=tgt%>
<%=intl._t("Add Port Routing")%>
+ " value="" class="freetext port" placeholder="required" /> + + " value="<%=name%>" class="freetext" /> + + + + " value="<%=targetHost%>" class="freetext host" /> : + " value="" class="freetext port" placeholder="required" /> +
<%=intl._t("Jetty Clients")%>
<%=intl._t("Client")%><%=intl._t("Configuration Files")%><%=intl._t("Enabled?")%><%=intl._t("SSL Enabled?")%><%=intl._t("KS Exists?")%><%=intl._t("KS Dflt PW?")%><%=intl._t("Privkey Dflt PW?")%>
<%=DataHelper.escapeHTML(clName)%> +<% + for (String arg : argList) { + %><%=DataHelper.escapeHTML(arg)%>
<% + } +%> +
<%=start%><%=ssl%><%=ksExists%> <%=error%><%=ksDflt%><%=kmDflt%>
Cannot configure, no Jetty SSL configuration template exists
Jetty SSL is enabled
Jetty SSL is not enabled
Jetty SSL cert passwords are the default
Jetty SSL cert passwords are not the default
+
">
+
+ +<% + } // valid b64 and name + } // !"new".equals(tunnelType) + if (!valid && curTunnel >= 0) { +%> + + +
<%=intl._t("Go back and edit the tunnel")%>
+<% + } // !valid +%> +
+<% + } else { +%> +
<%=intl._t("Tunnels are not initialized yet, please reload in two minutes.")%>
+<% + } // isInitialized() +%> + + diff --git a/apps/i2ptunnel/jsp/web.xml b/apps/i2ptunnel/jsp/web.xml index 4d370e2c5..e7d0bfb24 100644 --- a/apps/i2ptunnel/jsp/web.xml +++ b/apps/i2ptunnel/jsp/web.xml @@ -67,6 +67,11 @@ /register + + net.i2p.i2ptunnel.jsp.ssl_jsp + /ssl + + net.i2p.servlet.ErrorServlet /error diff --git a/apps/jetty/java/src/net/i2p/jetty/JettyXmlConfigurationParser.java b/apps/jetty/java/src/net/i2p/jetty/JettyXmlConfigurationParser.java new file mode 100644 index 000000000..3dd5a0104 --- /dev/null +++ b/apps/jetty/java/src/net/i2p/jetty/JettyXmlConfigurationParser.java @@ -0,0 +1,134 @@ +package net.i2p.jetty; + +// +// ======================================================================== +// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +import java.io.IOException; +import java.io.File; +import java.net.URL; +import java.util.Locale; + +import org.eclipse.jetty.util.Loader; +import org.eclipse.jetty.xml.XmlConfiguration; +import org.eclipse.jetty.xml.XmlParser; +import org.eclipse.jetty.xml.XmlParser.Node; +import org.xml.sax.SAXException; + +/** + * Parses a Jetty XML configuration file. + * Copied from Jetty XmlConfiguration.java, where the parser is private. + * + * @since 0.9.35 + */ +public class JettyXmlConfigurationParser +{ + private static XmlParser initParser() + { + XmlParser parser = new XmlParser(); + URL config60 = Loader.getResource(XmlConfiguration.class, "org/eclipse/jetty/xml/configure_6_0.dtd"); + URL config76 = Loader.getResource(XmlConfiguration.class,"org/eclipse/jetty/xml/configure_7_6.dtd"); + URL config90 = Loader.getResource(XmlConfiguration.class,"org/eclipse/jetty/xml/configure_9_0.dtd"); + parser.redirectEntity("configure.dtd",config90); + parser.redirectEntity("configure_1_0.dtd",config60); + parser.redirectEntity("configure_1_1.dtd",config60); + parser.redirectEntity("configure_1_2.dtd",config60); + parser.redirectEntity("configure_1_3.dtd",config60); + parser.redirectEntity("configure_6_0.dtd",config60); + parser.redirectEntity("configure_7_6.dtd",config76); + parser.redirectEntity("configure_9_0.dtd",config90); + + parser.redirectEntity("http://jetty.mortbay.org/configure.dtd",config90); + parser.redirectEntity("http://jetty.eclipse.org/configure.dtd",config90); + parser.redirectEntity("http://www.eclipse.org/jetty/configure.dtd",config90); + + parser.redirectEntity("-//Mort Bay Consulting//DTD Configure//EN",config90); + parser.redirectEntity("-//Jetty//Configure//EN",config90); + + return parser; + } + + /** + * Reads and parses the XML configuration file. + * + * @param f an XML configuration file + * @throws IOException if the configuration could not be read + * @throws SAXException if the configuration could not be parsed + */ + public static XmlParser.Node parse(File f) throws SAXException, IOException { + // we don't expect to need this very often, + // so just make a new parser every time + return initParser().parse(f); + } + + /** + * Recursively go through the entire tree starting at node. + * Return the value for the first node with the name set, + * e.g. [Set name="name"]value[/Set] + * @param name case insensitive + */ + public static String getValue(Node node, String name) { + String nameLC = name.toLowerCase(Locale.US); + for (Object o : node) { + if (!(o instanceof Node)) + continue; + Node n = (Node) o; + String tag = n.getTag(); + if (tag != null && "set".equals(tag.toLowerCase(Locale.US))) { + String aname = n.getAttribute("name"); + if (aname != null && aname.toLowerCase(Locale.US).equals(nameLC)) + return n.toString(false); + } else { + String rv = getValue(n, name); + if (rv != null) + return rv; + } + } + return null; + } + + /** + * Recursively go through the entire tree starting at node. + * Return the value for the first node with the name set, + * e.g. [Set name="name"]value[/Set] + * @param name case insensitive + * @return success + */ + public static boolean setValue(Node node, String name, String value) { + String nameLC = name.toLowerCase(Locale.US); + for (Object o : node) { + if (!(o instanceof Node)) + continue; + Node n = (Node) o; + String tag = n.getTag(); + if (tag != null && "set".equals(tag.toLowerCase(Locale.US))) { + String aname = n.getAttribute("name"); + if (aname != null && aname.toLowerCase(Locale.US).equals(nameLC)) { + // Node doesn't support set() or remove() but it does have clear() + n.clear(); + n.add(value); + return true; + } + } else { + boolean rv = setValue(n, name, value); + if (rv) + return rv; + } + } + return false; + } +}