* I2PTunnel:
- Display destination even when stopped - Enable key generation, dest modification, and hashcash estimation in the GUI - Add new CONNECT client
This commit is contained in:
@ -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
|
||||
*
|
||||
|
@ -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) {}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -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.
|
||||
|
Reference in New Issue
Block a user