From 775ab9a7bf42b1c32009ba6aa32b4d5626fd2409 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Sun, 15 Feb 2009 05:17:18 +0000 Subject: [PATCH] * I2PTunnel: - Display destination even when stopped - Enable key generation, dest modification, and hashcash estimation in the GUI - Add new CONNECT client --- .../java/src/net/i2p/i2ptunnel/I2PTunnel.java | 62 ++- .../i2p/i2ptunnel/I2PTunnelConnectClient.java | 369 ++++++++++++++++++ .../net/i2p/i2ptunnel/TunnelController.java | 20 +- .../src/net/i2p/i2ptunnel/web/IndexBean.java | 135 ++++++- apps/i2ptunnel/jsp/editServer.jsp | 20 +- apps/i2ptunnel/jsp/index.jsp | 2 +- .../java/src/net/i2p/data/PrivateKeyFile.java | 170 +++++--- 7 files changed, 707 insertions(+), 71 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java index 3b279a679a..b72ae18b31 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java @@ -244,6 +244,8 @@ public class I2PTunnel implements Logging, EventDispatcher { runIrcClient(args, l); } else if ("sockstunnel".equals(cmdname)) { runSOCKSTunnel(args, l); + } else if ("connectclient".equals(cmdname)) { + runConnectClient(args, l); } else if ("config".equals(cmdname)) { runConfig(args, l); } else if ("listen_on".equals(cmdname)) { @@ -296,6 +298,7 @@ public class I2PTunnel implements Logging, EventDispatcher { l.log("client <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]"); l.log("ircclient <port> <pubkey>[,<pubkey,...]|file:<pubkeyfile> [<sharedClient>]"); l.log("httpclient <port> [<sharedClient>] [<proxy>]"); + l.log("connectclient <port> [<sharedClient>] [<proxy>]"); l.log("lookup <name>"); l.log("quit"); l.log("close [forced] <jobnumber>|all"); @@ -555,7 +558,7 @@ public class I2PTunnel implements Logging, EventDispatcher { return; } - String proxy = "squid.i2p"; + String proxy = ""; boolean isShared = true; if (args.length > 1) { if ("true".equalsIgnoreCase(args[1].trim())) { @@ -595,11 +598,66 @@ public class I2PTunnel implements Logging, EventDispatcher { l.log(" <sharedClient> (optional) indicates if this client shares tunnels with other clients (true of false)"); l.log(" <proxy> (optional) indicates a proxy server to be used"); l.log(" when trying to access an address out of the .i2p domain"); - l.log(" (the default proxy is squid.i2p)."); notifyEvent("httpclientTaskId", Integer.valueOf(-1)); } } + /** + * Run a CONNECT client on the given port number + * + * @param args {portNumber[, sharedClient][, proxy to be used for the WWW]} + * @param l logger to receive events and output + */ + public void runConnectClient(String args[], Logging l) { + if (args.length >= 1 && args.length <= 3) { + int port = -1; + try { + port = Integer.parseInt(args[0]); + } catch (NumberFormatException nfe) { + _log.error(getPrefix() + "Port specified is not valid: " + args[0], nfe); + return; + } + + String proxy = ""; + boolean isShared = true; + if (args.length > 1) { + if ("true".equalsIgnoreCase(args[1].trim())) { + isShared = true; + if (args.length == 3) + proxy = args[2]; + } else if ("false".equalsIgnoreCase(args[1].trim())) { + _log.warn("args[1] == [" + args[1] + "] and rejected explicitly"); + isShared = false; + if (args.length == 3) + proxy = args[2]; + } else if (args.length == 3) { + isShared = false; // not "true" + proxy = args[2]; + _log.warn("args[1] == [" + args[1] + "] but rejected"); + } else { + // isShared not specified, default to true + isShared = true; + proxy = args[1]; + } + } + + I2PTunnelTask task; + ownDest = !isShared; + try { + task = new I2PTunnelConnectClient(port, l, ownDest, proxy, (EventDispatcher) this, this); + addtask(task); + } catch (IllegalArgumentException iae) { + _log.error(getPrefix() + "Invalid I2PTunnel config to create an httpclient [" + host + ":"+ port + "]", iae); + } + } else { + l.log("connectclient <port> [<sharedClient>] [<proxy>]"); + l.log(" creates a client that for SSL/HTTPS requests."); + l.log(" <sharedClient> (optional) indicates if this client shares tunnels with other clients (true of false)"); + l.log(" <proxy> (optional) indicates a proxy server to be used"); + l.log(" when trying to access an address out of the .i2p domain"); + } + } + /** * Run an IRC client on the given port number * diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java new file mode 100644 index 0000000000..bf7ebf0a7a --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java @@ -0,0 +1,369 @@ +/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) + * (c) 2003 - 2004 mihi + */ +package net.i2p.i2ptunnel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.StringTokenizer; + +import net.i2p.I2PAppContext; +import net.i2p.I2PException; +import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketOptions; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.util.EventDispatcher; +import net.i2p.util.FileUtil; +import net.i2p.util.Log; + +/** + * Supports the following: + * (where protocol is generally HTTP/1.1 but is ignored) + * (where host is one of: + * example.i2p + * 52chars.b32.i2p + * 516+charsbase64 + * example.com (sent to one of the configured proxies) + * ) + * + * (port and protocol are ignored for i2p destinations) + * CONNECT host + * CONNECT host protocol + * CONNECT host:port + * CONNECT host:port protocol (this is the standard) + * + * Additional lines after the CONNECT line but before the blank line are ignored and stripped. + * The CONNECT line is removed for .i2p accesses + * but passed along for outproxy accesses. + * + * Ref: + * INTERNET-DRAFT Ari Luotonen + * Expires: September 26, 1997 Netscape Communications Corporation + * <draft-luotonen-ssl-tunneling-03.txt> March 26, 1997 + * Tunneling SSL Through a WWW Proxy + * + * @author zzz a stripped-down I2PTunnelHTTPClient + */ +public class I2PTunnelConnectClient extends I2PTunnelClientBase implements Runnable { + private static final Log _log = new Log(I2PTunnelConnectClient.class); + + private List<String> _proxyList; + + private final static byte[] ERR_DESTINATION_UNKNOWN = + ("HTTP/1.1 503 Service Unavailable\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: DESTINATION NOT FOUND</H1>"+ + "That I2P Destination was not found. "+ + "The host (or the outproxy, if you're using one) could also "+ + "be temporarily offline. You may want to <b>retry</b>. "+ + "Could not find the following Destination:<BR><BR><div>") + .getBytes(); + + private final static byte[] 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"+ + "\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") + .getBytes(); + + private final static byte[] ERR_BAD_PROTOCOL = + ("HTTP/1.1 405 Bad Method\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: METHOD NOT ALLOWED</H1>"+ + "The request uses a bad protocol. "+ + "The Connect Proxy supports CONNECT requests ONLY. Other methods such as GET are not allowed - Maybe you wanted the HTTP Proxy?.<BR>") + .getBytes(); + + private final static byte[] 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"+ + "\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>") + .getBytes(); + + private final static byte[] SUCCESS_RESPONSE = + ("HTTP/1.1 200 Connection Established\r\n"+ + "Proxy-agent: I2P\r\n"+ + "\r\n") + .getBytes(); + + /** used to assign unique IDs to the threads / clients. no logic or functionality */ + private static volatile long __clientId = 0; + + /** + * @throws IllegalArgumentException if the I2PTunnel does not contain + * valid config to contact the router + */ + public I2PTunnelConnectClient(int localPort, Logging l, boolean ownDest, + String wwwProxy, EventDispatcher notifyThis, + I2PTunnel tunnel) throws IllegalArgumentException { + super(localPort, ownDest, l, notifyThis, "HTTPHandler " + (++__clientId), tunnel); + + if (waitEventValue("openBaseClientResult").equals("error")) { + notifyEvent("openConnectClientResult", "error"); + return; + } + + _proxyList = new ArrayList(); + if (wwwProxy != null) { + StringTokenizer tok = new StringTokenizer(wwwProxy, ","); + while (tok.hasMoreTokens()) + _proxyList.add(tok.nextToken().trim()); + } + + setName(getLocalPort() + " -> ConnectClient [Outproxy list: " + wwwProxy + "]"); + + startRunning(); + } + + private String getPrefix(long requestId) { return "Client[" + _clientId + "/" + requestId + "]: "; } + + private String selectProxy() { + synchronized (_proxyList) { + int size = _proxyList.size(); + if (size <= 0) + return null; + int index = I2PAppContext.getGlobalContext().random().nextInt(size); + return _proxyList.get(index); + } + } + + private static final int DEFAULT_READ_TIMEOUT = 60*1000; + + /** + * create the default options (using the default timeout, etc) + * + */ + 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); + I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts); + if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT)) + opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT); + return opts; + } + + private static long __requestId = 0; + protected void clientConnectionRun(Socket s) { + InputStream in = null; + OutputStream out = null; + String targetRequest = null; + boolean usingWWWProxy = false; + String currentProxy = null; + long requestId = ++__requestId; + try { + out = s.getOutputStream(); + in = s.getInputStream(); + String line, method = null, host = null, destination = null, restofline = null; + StringBuffer newRequest = new StringBuffer(); + int ahelper = 0; + while (true) { + // Use this rather than BufferedReader because we can't have readahead, + // since we are passing the stream on to I2PTunnelRunner + line = DataHelper.readLine(in); + line = line.trim(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug(getPrefix(requestId) + "Line=[" + line + "]"); + + if (method == null) { // first line CONNECT blah.i2p:80 HTTP/1.1 + int pos = line.indexOf(" "); + if (pos == -1) break; // empty first line + method = line.substring(0, pos); + String request = line.substring(pos + 1); + + pos = request.indexOf(":"); + if (pos == -1) + pos = request.indexOf(" "); + if (pos == -1) { + host = request; + restofline = ""; + } else { + host = request.substring(0, pos); + restofline = request.substring(pos); // ":80 HTTP/1.1" or " HTTP/1.1" + } + + if (host.toLowerCase().endsWith(".i2p")) { + // Destination gets the host name + destination = host; + } else if (host.indexOf(".") != -1) { + // The request must be forwarded to a outproxy + currentProxy = selectProxy(); + if (currentProxy == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn(getPrefix(requestId) + "Host wants to be outproxied, but we dont have any!"); + writeErrorMessage(ERR_NO_OUTPROXY, out); + s.close(); + return; + } + destination = currentProxy; + usingWWWProxy = true; + newRequest.append("CONNECT ").append(host).append(restofline).append("\r\n\r\n"); // HTTP spec + } else if (host.toLowerCase().equals("localhost")) { + writeErrorMessage(ERR_LOCALHOST, out); + s.close(); + return; + } else { // full b64 address (hopefully) + destination = host; + } + targetRequest = host; + + if (_log.shouldLog(Log.DEBUG)) { + _log.debug(getPrefix(requestId) + "METHOD:" + method + ":"); + _log.debug(getPrefix(requestId) + "HOST :" + host + ":"); + _log.debug(getPrefix(requestId) + "REST :" + restofline + ":"); + _log.debug(getPrefix(requestId) + "DEST :" + destination + ":"); + } + + } else if (line.length() > 0) { + // Additional lines - shouldn't be too many. Firefox sends: + // User-Agent: blabla + // Proxy-Connection: keep-alive + // Host: blabla.i2p + // + // We could send these (filtered like in HTTPClient) on to the outproxy, + // but for now just chomp them all. + line = null; + } else { + // do it + break; + } + } + + if (destination == null || !"CONNECT".equalsIgnoreCase(method)) { + writeErrorMessage(ERR_BAD_PROTOCOL, out); + s.close(); + return; + } + + Destination dest = I2PTunnel.destFromName(destination); + if (dest == null) { + String str; + byte[] header; + if (usingWWWProxy) + str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true); + else + str = FileUtil.readTextFile("docs/dnfh-header.ht", 100, true); + if (str != null) + header = str.getBytes(); + else + header = ERR_DESTINATION_UNKNOWN; + writeErrorMessage(header, out, targetRequest, usingWWWProxy, destination); + s.close(); + return; + } + + I2PSocket i2ps = createI2PSocket(dest, getDefaultOptions()); + byte[] data = null; + byte[] response = null; + if (usingWWWProxy) + data = newRequest.toString().getBytes("ISO-8859-1"); + else + response = SUCCESS_RESPONSE; + Runnable onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId); + I2PTunnelRunner runner = new I2PTunnelRunner(s, i2ps, sockLock, data, response, mySockets, onTimeout); + } catch (SocketException ex) { + _log.info(getPrefix(requestId) + "Error trying to connect", ex); + handleConnectClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); + closeSocket(s); + } catch (IOException ex) { + _log.info(getPrefix(requestId) + "Error trying to connect", ex); + handleConnectClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); + closeSocket(s); + } catch (I2PException ex) { + _log.info("getPrefix(requestId) + Error trying to connect", ex); + handleConnectClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); + closeSocket(s); + } catch (OutOfMemoryError oom) { + IOException ex = new IOException("OOM"); + _log.info("getPrefix(requestId) + Error trying to connect", ex); + handleConnectClientException(ex, out, targetRequest, usingWWWProxy, currentProxy, requestId); + closeSocket(s); + } + } + + private static class OnTimeout implements Runnable { + private Socket _socket; + private OutputStream _out; + private String _target; + private boolean _usingProxy; + private String _wwwProxy; + private long _requestId; + public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) { + _socket = s; + _out = out; + _target = target; + _usingProxy = usingProxy; + _wwwProxy = wwwProxy; + _requestId = id; + } + public void run() { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Timeout occured requesting " + _target); + handleConnectClientException(new RuntimeException("Timeout"), _out, + _target, _usingProxy, _wwwProxy, _requestId); + closeSocket(_socket); + } + } + + private static void writeErrorMessage(byte[] errMessage, OutputStream out) throws IOException { + if (out == null) + return; + out.write(errMessage); + out.write("\n</body></html>\n".getBytes()); + out.flush(); + } + + private static void writeErrorMessage(byte[] errMessage, OutputStream out, String targetRequest, + boolean usingWWWProxy, String wwwProxy) throws IOException { + if (out != null) { + out.write(errMessage); + if (targetRequest != null) { + out.write(targetRequest.getBytes()); + if (usingWWWProxy) + out.write(("<br>WWW proxy: " + wwwProxy).getBytes()); + } + out.write("</div>".getBytes()); + out.write("\n</body></html>\n".getBytes()); + out.flush(); + } + } + + private static void handleConnectClientException(Exception ex, OutputStream out, String targetRequest, + boolean usingWWWProxy, String wwwProxy, long requestId) { + if (out == null) + return; + try { + String str; + byte[] header; + if (usingWWWProxy) + str = FileUtil.readTextFile("docs/dnfp-header.ht", 100, true); + else + str = FileUtil.readTextFile("docs/dnf-header.ht", 100, true); + if (str != null) + header = str.getBytes(); + else + header = ERR_DESTINATION_UNKNOWN; + writeErrorMessage(header, out, targetRequest, usingWWWProxy, wwwProxy); + } catch (IOException ioe) {} + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index f8592fcd39..3c9640ce5a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -73,7 +73,7 @@ public class TunnelController implements Logging { File keyFile = new File(getPrivKeyFile()); if (keyFile.exists()) { - log("Not overwriting existing private keys in " + keyFile.getAbsolutePath()); + //log("Not overwriting existing private keys in " + keyFile.getAbsolutePath()); return; } else { File parent = keyFile.getParentFile(); @@ -87,6 +87,7 @@ public class TunnelController implements Logging { String destStr = dest.toBase64(); log("Private key created and saved in " + keyFile.getAbsolutePath()); log("New destination: " + destStr); + log("Base32: " + Base32.encode(dest.calculateHash().getData()) + ".b32.i2p"); } catch (I2PException ie) { if (_log.shouldLog(Log.ERROR)) _log.error("Error creating new destination", ie); @@ -139,6 +140,8 @@ public class TunnelController implements Logging { startIrcClient(); } else if("sockstunnel".equals(type)) { startSocksClient(); + } else if("connectclient".equals(type)) { + startConnectClient(); } else if ("client".equals(type)) { startClient(); } else if ("server".equals(type)) { @@ -166,6 +169,21 @@ public class TunnelController implements Logging { _running = true; } + private void startConnectClient() { + setI2CPOptions(); + setSessionOptions(); + setListenOn(); + String listenPort = getListenPort(); + String proxyList = getProxyList(); + String sharedClient = getSharedClient(); + if (proxyList == null) + _tunnel.runConnectClient(new String[] { listenPort, sharedClient }, this); + else + _tunnel.runConnectClient(new String[] { listenPort, sharedClient, proxyList }, this); + acquire(); + _running = true; + } + private void startIrcClient() { setI2CPOptions(); setSessionOptions(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 1aca37bf53..46b5557729 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -18,6 +18,11 @@ import java.util.Set; import java.util.StringTokenizer; import net.i2p.I2PAppContext; +import net.i2p.data.Base32; +import net.i2p.data.Certificate; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKeyFile; +import net.i2p.data.SessionKey; import net.i2p.i2ptunnel.TunnelController; import net.i2p.i2ptunnel.TunnelControllerGroup; import net.i2p.util.ConcurrentHashSet; @@ -65,6 +70,9 @@ public class IndexBean { private boolean _removeConfirmed; private Set<String> _booleanOptions; private Map<String, String> _otherOptions; + private int _hashCashValue; + private int _certType; + private String _certSigner; public static final int RUNNING = 1; public static final int STARTING = 2; @@ -156,6 +164,12 @@ public class IndexBean { else if ("Delete this proxy".equals(_action) || // IE workaround: (_action.toLowerCase().indexOf("d</span>elete") >= 0)) return deleteTunnel(); + else if ("Estimate".equals(_action)) + return PrivateKeyFile.estimateHashCashTime(_hashCashValue); + else if ("Modify".equals(_action)) + return modifyDestination(); + else if ("Generate".equals(_action)) + return generateNewEncryptionKey(); else return "Action " + _action + " unknown"; } @@ -370,7 +384,7 @@ public class IndexBean { else if ("ircclient".equals(internalType)) return "IRC client"; else if ("server".equals(internalType)) return "Standard server"; else if ("httpserver".equals(internalType)) return "HTTP server"; - else if ("sockstunnel".equals(internalType)) return "SOCKS proxy"; + else if ("sockstunnel".equals(internalType)) return "SOCKS 5 proxy"; else if ("connectclient".equals(internalType)) return "CONNECT/SSL/HTTPS proxy"; else return internalType; } @@ -440,6 +454,16 @@ public class IndexBean { String rv = tun.getMyDestination(); if (rv != null) return rv; + // if not running, do this the hard way + String keyFile = tun.getPrivKeyFile(); + if (keyFile != null && keyFile.trim().length() > 0) { + PrivateKeyFile pkf = new PrivateKeyFile(keyFile); + try { + Destination d = pkf.getDestination(); + if (d != null) + return d.toBase64(); + } catch (Exception e) {} + } } return ""; } @@ -616,6 +640,115 @@ public class IndexBean { } } + /** params needed for hashcash and dest modification */ + public void setEffort(String val) { + if (val != null) { + try { + _hashCashValue = Integer.parseInt(val.trim()); + } catch (NumberFormatException nfe) {} + } + } + public void setCert(String val) { + if (val != null) { + try { + _certType = Integer.parseInt(val.trim()); + } catch (NumberFormatException nfe) {} + } + } + public void setSigner(String val) { + _certSigner = val; + } + + /** Modify or create a destination */ + private String modifyDestination() { + if (_privKeyFile == null || _privKeyFile.trim().length() <= 0) + return "Private Key File not specified"; + + TunnelController tun = getController(_tunnel); + Properties config = getConfig(); + if (config == null) + return "Invalid params"; + if (tun == null) { + // creating new + tun = new TunnelController(config, "", true); + _group.addController(tun); + saveChanges(); + } else if (tun.getIsRunning() || tun.getIsStarting()) { + return "Tunnel must be stopped before modifying destination"; + } + PrivateKeyFile pkf = new PrivateKeyFile(_privKeyFile); + try { + pkf.createIfAbsent(); + } catch (Exception e) { + return "Create private key file failed: " + e; + } + switch (_certType) { + case Certificate.CERTIFICATE_TYPE_NULL: + case Certificate.CERTIFICATE_TYPE_HIDDEN: + pkf.setCertType(_certType); + break; + case Certificate.CERTIFICATE_TYPE_HASHCASH: + pkf.setHashCashCert(_hashCashValue); + break; + case Certificate.CERTIFICATE_TYPE_SIGNED: + if (_certSigner == null || _certSigner.trim().length() <= 0) + return "No signing destination specified"; + // find the signer's key file... + String signerPKF = null; + for (int i = 0; i < getTunnelCount(); i++) { + TunnelController c = getController(i); + if (_certSigner.equals(c.getConfig("").getProperty("name")) || + _certSigner.equals(c.getConfig("").getProperty("spoofedHost"))) { + signerPKF = c.getConfig("").getProperty("privKeyFile"); + break; + } + } + if (signerPKF == null || signerPKF.length() <= 0) + return "Signing destination " + _certSigner + " not found"; + if (_privKeyFile.equals(signerPKF)) + return "Self-signed destinations not allowed"; + Certificate c = pkf.setSignedCert(new PrivateKeyFile(signerPKF)); + if (c == null) + return "Signing failed - does signer destination exist?"; + break; + default: + return "Unknown certificate type"; + } + Destination newdest; + try { + pkf.write(); + newdest = pkf.getDestination(); + } catch (Exception e) { + return "Modification failed: " + e; + } + return "Destination modified - " + + "New Base32 is " + Base32.encode(newdest.calculateHash().getData()) + ".b32.i2p " + + "New Destination is " + newdest.toBase64(); + } + + /** New key */ + private String generateNewEncryptionKey() { + TunnelController tun = getController(_tunnel); + Properties config = getConfig(); + if (config == null) + return "Invalid params"; + if (tun == null) { + // creating new + tun = new TunnelController(config, "", true); + _group.addController(tun); + saveChanges(); + } else if (tun.getIsRunning() || tun.getIsStarting()) { + return "Tunnel must be stopped before modifying leaseset encryption key"; + } + byte[] data = new byte[SessionKey.KEYSIZE_BYTES]; + _context.random().nextBytes(data); + SessionKey sk = new SessionKey(data); + setEncryptKey(sk.toBase64()); + setEncrypt(""); + saveChanges(); + return "New Leaseset Encryption Key: " + sk.toBase64(); + } + /** * Based on all provided data, create a set of configuration parameters * suitable for use in a TunnelController. This will replace (not add to) diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index 5968486178..82ac69dc5c 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -254,12 +254,18 @@ </label> <input value="1" type="checkbox" id="startOnLoad" name="encrypt" title="Encrypt LeaseSet"<%=(editBean.getEncrypt(curTunnel) ? " checked=\"checked\"" : "")%> class="tickbox" /> </div> - <div id="hostField" class="rowItem"> + <div id="portField" class="rowItem"> <label for="encrypt" accesskey="e"> Leaseset Encryption Key: </label> - <input type="text" id="hostField" name="encryptKey" size="60" title="Encrypt Key" value="<%=editBean.getEncryptKey(curTunnel)%>" class="freetext" /> - <span class="comment">(Users will require this key)</span> + <textarea rows="1" cols="44" id="portField" name="encryptKey" title="Encrypt Key" wrap="off"><%=editBean.getEncryptKey(curTunnel)%></textarea> + </div> + <div id="portField" class="rowItem"> + <label for="force" accesskey="c"> + Generate Key: + </label> + <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Generate" title="Generate New Key Now">Generate New Key</button> + <span class="comment">(Tunnel must be stopped first)</span> </div> <div class="subdivider"> @@ -319,7 +325,7 @@ <div id="tunnelOptionsField" class="rowItem"> <label for="cert" accesskey="c"> - <span class="accessKey">C</span>ertificate type: + New <span class="accessKey">C</span>ertificate type: </label> </div> <div id="hostField" class="rowItem"> @@ -331,14 +337,14 @@ <div id="portField" class="rowItem"> <label>Hashcash (effort)</label> <input value="1" type="radio" id="startOnLoad" name="cert" title="Hashcash Certificate"<%=(editBean.getCert(curTunnel)==1 ? " checked=\"checked\"" : "")%> class="tickbox" /> - <input type="text" id="port" name="effort" size="2" title="Hashcash Effort" value="<%=editBean.getEffort(curTunnel)%>" class="freetext" /> + <input type="text" id="port" name="effort" size="2" maxlength="2" title="Hashcash Effort" value="<%=editBean.getEffort(curTunnel)%>" class="freetext" /> </div> </div> <div id="portField" class="rowItem"> <label for="force" accesskey="c"> Estimate Hashcash Calc Time: </label> - <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Estimate Calculation Time" title="Estimate Calculation Time">Estimate</button> + <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Estimate" title="Estimate Calculation Time">Estimate</button> </div> <div id="hostField" class="rowItem"> <div id="portField" class="rowItem"> @@ -359,7 +365,7 @@ <label for="force" accesskey="c"> Modify Certificate: </label> - <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Modify Cert Now" title="Force New Cert Now">Modify</button> + <button id="controlSave" accesskey="S" class="control" type="submit" name="action" value="Modify" title="Force New Cert Now">Modify</button> <span class="comment">(Tunnel must be stopped first)</span> </div> diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index a06177dd47..b96236ae14 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -148,7 +148,7 @@ <option value="client">Standard</option> <option value="httpclient">HTTP</option> <option value="ircclient">IRC</option> - <option value="sockstunnel">SOCKS</option> + <option value="sockstunnel">SOCKS 5</option> <option value="connectclient">CONNECT</option> </select> <input class="control" type="submit" value="Create" /> diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java index 7680204d1a..d9e52aecc0 100644 --- a/core/java/src/net/i2p/data/PrivateKeyFile.java +++ b/core/java/src/net/i2p/data/PrivateKeyFile.java @@ -77,74 +77,25 @@ public class PrivateKeyFile { verifySignature(d); if (args.length == 1) return; - Certificate c = new Certificate(); if (args[0].equals("-n")) { // Cert constructor generates a null cert + pkf.setCertType(Certificate.CERTIFICATE_TYPE_NULL); } else if (args[0].equals("-u")) { - c.setCertificateType(99); + pkf.setCertType(99); } else if (args[0].equals("-x")) { - c.setCertificateType(Certificate.CERTIFICATE_TYPE_HIDDEN); + pkf.setCertType(Certificate.CERTIFICATE_TYPE_HIDDEN); } else if (args[0].equals("-h")) { int hashEffort = HASH_EFFORT; if (args.length == 3) hashEffort = Integer.parseInt(args[1]); System.out.println("Estimating hashcash generation time, stand by..."); - // takes a lot longer than the estimate usually... - // maybe because the resource string is much longer than used in the estimate? - long low = HashCash.estimateTime(hashEffort); - System.out.println("It is estimated this will take " + DataHelper.formatDuration(low) + - " to " + DataHelper.formatDuration(4*low)); - - long begin = System.currentTimeMillis(); - System.out.println("Starting hashcash generation now..."); - String resource = d.getPublicKey().toBase64() + d.getSigningPublicKey().toBase64(); - HashCash hc = HashCash.mintCash(resource, hashEffort); - System.out.println("Generation took: " + DataHelper.formatDuration(System.currentTimeMillis() - begin)); - System.out.println("Full Hashcash is: " + hc); - // Take the resource out of the stamp - String hcs = hc.toString(); - int end1 = 0; - for (int i = 0; i < 3; i++) { - end1 = 1 + hcs.indexOf(':', end1); - if (end1 < 0) { - System.out.println("Bad hashcash"); - return; - } - } - int start2 = hcs.indexOf(':', end1); - if (start2 < 0) { - System.out.println("Bad hashcash"); - return; - } - hcs = hcs.substring(0, end1) + hcs.substring(start2); - System.out.println("Short Hashcash is: " + hcs); - - c.setCertificateType(Certificate.CERTIFICATE_TYPE_HASHCASH); - c.setPayload(hcs.getBytes()); + System.out.println(estimateHashCashTime(hashEffort)); + pkf.setHashCashCert(hashEffort); } else if (args.length == 3 && args[0].equals("-s")) { // Sign dest1 with dest2's Signing Private Key - File f2 = new File(args[2]); - I2PClient client2 = I2PClientFactory.createClient(); - PrivateKeyFile pkf2 = new PrivateKeyFile(f2, client2); - Destination d2 = pkf2.getDestination(); - SigningPrivateKey spk2 = pkf2.getSigningPrivKey(); - System.out.println("Signing With Dest:"); - System.out.println(pkf2.toString()); - - int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert - byte[] data = new byte[len]; - System.arraycopy(d.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES); - System.arraycopy(d.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES); - byte[] payload = new byte[Hash.HASH_LENGTH + Signature.SIGNATURE_BYTES]; - byte[] sig = DSAEngine.getInstance().sign(new ByteArrayInputStream(data), spk2).getData(); - System.arraycopy(sig, 0, payload, 0, Signature.SIGNATURE_BYTES); - // Add dest2's Hash for reference - byte[] h2 = d2.calculateHash().getData(); - System.arraycopy(h2, 0, payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH); - c.setCertificateType(Certificate.CERTIFICATE_TYPE_SIGNED); - c.setPayload(payload); + PrivateKeyFile pkf2 = new PrivateKeyFile(args[2]); + pkf.setSignedCert(pkf2); } - d.setCertificate(c); // do this rather than just change the existing cert so the hash is recalculated System.out.println("New signed destination is:"); System.out.println(pkf); pkf.write(); @@ -154,7 +105,10 @@ public class PrivateKeyFile { } } - + public PrivateKeyFile(String file) { + this(new File(file), I2PClientFactory.createClient()); + } + public PrivateKeyFile(File file, I2PClient client) { this.file = file; this.client = client; @@ -176,7 +130,7 @@ public class PrivateKeyFile { return getDestination(); } - /** Also sets the local privKay and signingPrivKey */ + /** Also sets the local privKey and signingPrivKey */ public Destination getDestination() throws I2PSessionException, IOException, DataFormatException { if (dest == null) { I2PSession s = open(); @@ -188,6 +142,86 @@ public class PrivateKeyFile { } return this.dest; } + + public void setDestination(Destination d) { + this.dest = d; + } + + /** change cert type - caller must also call write() */ + public Certificate setCertType(int t) { + if (this.dest == null) + throw new IllegalArgumentException("Dest is null"); + Certificate c = new Certificate(); + c.setCertificateType(t); + this.dest.setCertificate(c); + return c; + } + + /** change to hashcash cert - caller must also call write() */ + public Certificate setHashCashCert(int effort) { + Certificate c = setCertType(Certificate.CERTIFICATE_TYPE_HASHCASH); + long begin = System.currentTimeMillis(); + System.out.println("Starting hashcash generation now..."); + String resource = this.dest.getPublicKey().toBase64() + this.dest.getSigningPublicKey().toBase64(); + HashCash hc; + try { + hc = HashCash.mintCash(resource, effort); + } catch (Exception e) { + return null; + } + System.out.println("Generation took: " + DataHelper.formatDuration(System.currentTimeMillis() - begin)); + System.out.println("Full Hashcash is: " + hc); + // Take the resource out of the stamp + String hcs = hc.toString(); + int end1 = 0; + for (int i = 0; i < 3; i++) { + end1 = 1 + hcs.indexOf(':', end1); + if (end1 < 0) { + System.out.println("Bad hashcash"); + return null; + } + } + int start2 = hcs.indexOf(':', end1); + if (start2 < 0) { + System.out.println("Bad hashcash"); + return null; + } + hcs = hcs.substring(0, end1) + hcs.substring(start2); + System.out.println("Short Hashcash is: " + hcs); + + c.setPayload(hcs.getBytes()); + return c; + } + + /** sign this dest by dest found in pkf2 - caller must also call write() */ + public Certificate setSignedCert(PrivateKeyFile pkf2) { + Certificate c = setCertType(Certificate.CERTIFICATE_TYPE_SIGNED); + Destination d2; + try { + d2 = pkf2.getDestination(); + } catch (Exception e) { + return null; + } + if (d2 == null) + return null; + SigningPrivateKey spk2 = pkf2.getSigningPrivKey(); + System.out.println("Signing With Dest:"); + System.out.println(pkf2.toString()); + + int len = PublicKey.KEYSIZE_BYTES + SigningPublicKey.KEYSIZE_BYTES; // no cert + byte[] data = new byte[len]; + System.arraycopy(this.dest.getPublicKey().getData(), 0, data, 0, PublicKey.KEYSIZE_BYTES); + System.arraycopy(this.dest.getSigningPublicKey().getData(), 0, data, PublicKey.KEYSIZE_BYTES, SigningPublicKey.KEYSIZE_BYTES); + byte[] payload = new byte[Hash.HASH_LENGTH + Signature.SIGNATURE_BYTES]; + byte[] sig = DSAEngine.getInstance().sign(new ByteArrayInputStream(data), spk2).getData(); + System.arraycopy(sig, 0, payload, 0, Signature.SIGNATURE_BYTES); + // Add dest2's Hash for reference + byte[] h2 = d2.calculateHash().getData(); + System.arraycopy(h2, 0, payload, Signature.SIGNATURE_BYTES, Hash.HASH_LENGTH); + c.setCertificateType(Certificate.CERTIFICATE_TYPE_SIGNED); + c.setPayload(payload); + return c; + } public PrivateKey getPrivKey() { return this.privKey; @@ -238,7 +272,25 @@ public class PrivateKeyFile { return s.toString(); } - + public static String estimateHashCashTime(int hashEffort) { + if (hashEffort <= 0 || hashEffort > 160) + return "Bad HashCash value: " + hashEffort; + long low = Long.MAX_VALUE; + try { + low = HashCash.estimateTime(hashEffort); + } catch (Exception e) {} + // takes a lot longer than the estimate usually... + // maybe because the resource string is much longer than used in the estimate? + return "It is estimated that generating a HashCash Certificate with value " + hashEffort + + " for the Destination will take " + + ((low < 1000l * 24l * 60l * 60l * 1000l) + ? + "approximately " + DataHelper.formatDuration(low) + + " to " + DataHelper.formatDuration(4*low) + : + "longer than three years!" + ); + } /** * Sample code to verify a 3rd party signature. -- GitLab