forked from I2P_Developers/i2p.i2p
903 lines
37 KiB
Java
903 lines
37 KiB
Java
/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
|
|
* (c) 2003 - 2004 mihi
|
|
*/
|
|
package net.i2p.i2ptunnel;
|
|
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InterruptedIOException;
|
|
import java.net.ConnectException;
|
|
import java.net.InetAddress;
|
|
import java.net.NoRouteToHostException;
|
|
import java.net.ServerSocket;
|
|
import java.net.Socket;
|
|
import java.net.UnknownHostException;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Properties;
|
|
import java.util.concurrent.RejectedExecutionException;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
|
|
import javax.net.ssl.SSLServerSocket;
|
|
import javax.net.ssl.SSLServerSocketFactory;
|
|
|
|
import net.i2p.I2PAppContext;
|
|
import net.i2p.I2PException;
|
|
import net.i2p.client.I2PClient;
|
|
import net.i2p.client.I2PSession;
|
|
import net.i2p.client.I2PSessionException;
|
|
import net.i2p.client.streaming.I2PSocket;
|
|
import net.i2p.client.streaming.I2PSocketManager;
|
|
import net.i2p.client.streaming.I2PSocketManagerFactory;
|
|
import net.i2p.client.streaming.I2PSocketOptions;
|
|
import net.i2p.crypto.SigType;
|
|
import net.i2p.data.Destination;
|
|
import net.i2p.util.EventDispatcher;
|
|
import net.i2p.util.I2PAppThread;
|
|
import net.i2p.util.I2PSSLSocketFactory;
|
|
import net.i2p.util.Log;
|
|
|
|
public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runnable {
|
|
|
|
protected final Log _log;
|
|
protected final I2PAppContext _context;
|
|
protected final Logging l;
|
|
|
|
static final long DEFAULT_CONNECT_TIMEOUT = 60 * 1000;
|
|
|
|
private static final AtomicLong __clientId = new AtomicLong();
|
|
protected long _clientId;
|
|
protected final Object sockLock = new Object(); // Guards sockMgr and mySockets
|
|
protected I2PSocketManager sockMgr; // should be final and use a factory. LINT
|
|
protected final List<I2PSocket> mySockets = new ArrayList<I2PSocket>();
|
|
protected boolean _ownDest;
|
|
|
|
protected Destination dest;
|
|
private volatile int localPort;
|
|
private final String _handlerName;
|
|
|
|
/**
|
|
* Protected for I2Ping since 0.9.11. Not for use outside package.
|
|
*/
|
|
protected boolean listenerReady;
|
|
|
|
protected ServerSocket ss;
|
|
|
|
private final Object startLock = new Object();
|
|
private boolean startRunning;
|
|
|
|
// private Object closeLock = new Object();
|
|
|
|
// private byte[] pubkey;
|
|
|
|
private String privKeyFile;
|
|
|
|
// true if we are chained from a server.
|
|
private boolean chained;
|
|
|
|
private volatile ThreadPoolExecutor _executor;
|
|
|
|
/** this is ONLY for shared clients */
|
|
private static I2PSocketManager socketManager;
|
|
|
|
/**
|
|
* Only destroy and replace a static shared client socket manager if it's been connected before
|
|
* @since 0.9.20
|
|
*/
|
|
private enum SocketManagerState { INIT, CONNECTED }
|
|
private static SocketManagerState _socketManagerState = SocketManagerState.INIT;
|
|
|
|
public static final String PROP_USE_SSL = I2PTunnelServer.PROP_USE_SSL;
|
|
|
|
/**
|
|
* This constructor is used to add a client to an existing socket manager.
|
|
* <p/>
|
|
* As of 0.9.21 this does NOT open the local socket. You MUST call
|
|
* {@link #startRunning()} for that. The local socket will be opened
|
|
* immediately (ignoring the <code>i2cp.delayOpen</code> option).
|
|
*
|
|
* @param localPort if 0, use any port, get actual port selected with getLocalPort()
|
|
* @param sktMgr the existing socket manager
|
|
*/
|
|
public I2PTunnelClientBase(int localPort, Logging l, I2PSocketManager sktMgr,
|
|
I2PTunnel tunnel, EventDispatcher notifyThis, long clientId )
|
|
throws IllegalArgumentException {
|
|
super(localPort + " (uninitialized)", notifyThis, tunnel);
|
|
chained = true;
|
|
sockMgr = sktMgr;
|
|
_clientId = clientId;
|
|
_handlerName = "chained";
|
|
this.localPort = localPort;
|
|
this.l = l;
|
|
_ownDest = true; // == ! shared client
|
|
_context = tunnel.getContext();
|
|
initStats();
|
|
_log = _context.logManager().getLog(getClass());
|
|
}
|
|
|
|
/**
|
|
* The main constructor.
|
|
* <p/>
|
|
* As of 0.9.21 this is fast, and does NOT connect the manager to the router,
|
|
* or open the local socket. You MUST call startRunning() for that.
|
|
* <p/>
|
|
* (0.9.20 claimed to be fast, but due to a bug it DID connect the manager
|
|
* to the router. It did NOT open the local socket however, so it was still
|
|
* necessary to call startRunning() for that.)
|
|
*
|
|
* @param localPort if 0, use any port, get actual port selected with getLocalPort()
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we can't create a socketManager
|
|
*/
|
|
public I2PTunnelClientBase(int localPort, boolean ownDest, Logging l,
|
|
EventDispatcher notifyThis, String handlerName,
|
|
I2PTunnel tunnel) throws IllegalArgumentException {
|
|
this(localPort, ownDest, l, notifyThis, handlerName, tunnel, null);
|
|
}
|
|
|
|
/**
|
|
* Use this to build a client with a persistent private key.
|
|
* <p/>
|
|
* As of 0.9.21 this is fast, and does NOT connect the manager to the router,
|
|
* or open the local socket. You MUST call startRunning() for that.
|
|
* <p/>
|
|
* (0.9.20 claimed to be fast, but due to a bug it DID connect the manager
|
|
* to the router. It did NOT open the local socket however, so it was still
|
|
* necessary to call startRunning() for that.)
|
|
*
|
|
* @param localPort if 0, use any port, get actual port selected with getLocalPort()
|
|
* @param pkf Path to the private key file, or null to generate a transient key
|
|
*
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we can't create a socketManager
|
|
*/
|
|
public I2PTunnelClientBase(int localPort, boolean ownDest, Logging l,
|
|
EventDispatcher notifyThis, String handlerName,
|
|
I2PTunnel tunnel, String pkf) throws IllegalArgumentException{
|
|
super(localPort + " (uninitialized)", notifyThis, tunnel);
|
|
_clientId = __clientId.incrementAndGet();
|
|
this.localPort = localPort;
|
|
this.l = l;
|
|
_ownDest = ownDest; // == ! shared client
|
|
_handlerName = handlerName;
|
|
|
|
_context = tunnel.getContext();
|
|
initStats();
|
|
_log = _context.logManager().getLog(getClass());
|
|
|
|
// normalize path so we can find it
|
|
if (pkf != null) {
|
|
File keyFile = new File(pkf);
|
|
if (!keyFile.isAbsolute())
|
|
keyFile = new File(_context.getConfigDir(), pkf);
|
|
this.privKeyFile = keyFile.getAbsolutePath();
|
|
}
|
|
|
|
// no need to load the netDb with leaseSets for destinations that will never
|
|
// be looked up
|
|
boolean dccEnabled = (this instanceof I2PTunnelIRCClient) &&
|
|
Boolean.parseBoolean(tunnel.getClientOptions().getProperty(I2PTunnelIRCClient.PROP_DCC));
|
|
if (!dccEnabled)
|
|
tunnel.getClientOptions().setProperty("i2cp.dontPublishLeaseSet", "true");
|
|
if (tunnel.getClientOptions().getProperty("i2p.streaming.answerPings") == null)
|
|
tunnel.getClientOptions().setProperty("i2p.streaming.answerPings", "false");
|
|
}
|
|
|
|
private void initStats() {
|
|
//_context.statManager().createRateStat("i2ptunnel.client.closeBacklog", "How many pending sockets remain when we close one due to backlog?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
|
//_context.statManager().createRateStat("i2ptunnel.client.closeNoBacklog", "How many pending sockets remain when it was removed prior to backlog timeout?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
|
//_context.statManager().createRateStat("i2ptunnel.client.manageTime", "How long it takes to accept a socket and fire it into an i2ptunnel runner (or queue it for the pool)?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
|
//_context.statManager().createRateStat("i2ptunnel.client.buildRunTime", "How long it takes to run a queued socket into an i2ptunnel runner?", "I2PTunnel", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
|
|
}
|
|
|
|
/**
|
|
* Create the manager if it doesn't exist, AND connect it to the router and
|
|
* build tunnels.
|
|
*
|
|
* Sets the this.sockMgr field if it is null, or if we want a new one.
|
|
* This may take a LONG time if building a new manager.
|
|
*
|
|
* We need a socket manager before getDefaultOptions() and most other things
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we cant create a socketManager
|
|
*/
|
|
protected void verifySocketManager() {
|
|
synchronized(sockLock) {
|
|
boolean newManager = false;
|
|
// other shared client could have destroyed it
|
|
if (this.sockMgr == null || this.sockMgr.isDestroyed()) {
|
|
newManager = true;
|
|
} else {
|
|
I2PSession sess = sockMgr.getSession();
|
|
if (sess.isClosed() &&
|
|
Boolean.parseBoolean(getTunnel().getClientOptions().getProperty("i2cp.closeOnIdle")) &&
|
|
Boolean.parseBoolean(getTunnel().getClientOptions().getProperty("i2cp.newDestOnResume"))) {
|
|
// build a new socket manager and a new dest if the session is closed.
|
|
getTunnel().removeSession(sess);
|
|
if (_log.shouldLog(Log.WARN))
|
|
_log.warn(getTunnel().getClientOptions().getProperty("inbound.nickname") + ": Built a new destination on resume");
|
|
// make sure the old one is closed
|
|
// if it's shared client, it will be destroyed in getSocketManager()
|
|
// with the correct locking
|
|
boolean shouldDestroy;
|
|
synchronized(I2PTunnelClientBase.class) {
|
|
shouldDestroy = sockMgr != socketManager;
|
|
}
|
|
if (shouldDestroy)
|
|
sockMgr.destroySocketManager();
|
|
newManager = true;
|
|
} // else the old socket manager will reconnect the old session if necessary
|
|
}
|
|
if (newManager) {
|
|
if (_ownDest)
|
|
this.sockMgr = buildSocketManager();
|
|
else
|
|
this.sockMgr = getSocketManager();
|
|
}
|
|
}
|
|
connectManager();
|
|
}
|
|
|
|
/**
|
|
* This is ONLY for shared clients.
|
|
* As of 0.9.20 this is fast, and does NOT connect the manager to the router.
|
|
* Call verifySocketManager() for that.
|
|
*
|
|
* @return non-null
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we cant create a socketManager
|
|
*/
|
|
protected I2PSocketManager getSocketManager() {
|
|
return getSocketManager(getTunnel(), this.privKeyFile);
|
|
}
|
|
|
|
/**
|
|
* This is ONLY for shared clients.
|
|
* As of 0.9.20 this is fast, and does NOT connect the manager to the router.
|
|
* Call verifySocketManager() for that.
|
|
*
|
|
* @return non-null
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we cant create a socketManager
|
|
*/
|
|
protected static I2PSocketManager getSocketManager(I2PTunnel tunnel) {
|
|
return getSocketManager(tunnel, null);
|
|
}
|
|
|
|
/**
|
|
* This is ONLY for shared clients.
|
|
* As of 0.9.20 this is fast, and does NOT connect the manager to the router.
|
|
* Call verifySocketManager() for that.
|
|
*
|
|
* @return non-null
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we cant create a socketManager
|
|
*/
|
|
protected static synchronized I2PSocketManager getSocketManager(I2PTunnel tunnel, String pkf) {
|
|
// shadows instance _log
|
|
Log _log = tunnel.getContext().logManager().getLog(I2PTunnelClientBase.class);
|
|
if (socketManager != null && !socketManager.isDestroyed()) {
|
|
I2PSession s = socketManager.getSession();
|
|
if (s.isClosed() && _socketManagerState != SocketManagerState.INIT) {
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info(tunnel.getClientOptions().getProperty("inbound.nickname") + ": Building a new socket manager since the old one closed [s=" + s + "]");
|
|
tunnel.removeSession(s);
|
|
// make sure the old one is closed
|
|
socketManager.destroySocketManager();
|
|
_socketManagerState = SocketManagerState.INIT;
|
|
// We could be here a LONG time, holding the lock
|
|
socketManager = buildSocketManager(tunnel, pkf);
|
|
// FIXME may not be the right place for this
|
|
I2PSession sub = addSubsession(tunnel);
|
|
if (sub != null && _log.shouldLog(Log.WARN))
|
|
_log.warn("Added subsession " + sub);
|
|
} else {
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info(tunnel.getClientOptions().getProperty("inbound.nickname") + ": Not building a new socket manager since the old one is open [s=" + s + "]");
|
|
// If some other tunnel created the session, we need to add it
|
|
// as our session too.
|
|
// It's a Set in I2PTunnel
|
|
tunnel.addSession(s);
|
|
}
|
|
} else {
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info(tunnel.getClientOptions().getProperty("inbound.nickname") + ": Building a new socket manager since there is no other one");
|
|
socketManager = buildSocketManager(tunnel, pkf);
|
|
I2PSession sub = addSubsession(tunnel);
|
|
if (sub != null && _log.shouldLog(Log.WARN))
|
|
_log.warn("Added subsession " + sub);
|
|
}
|
|
return socketManager;
|
|
}
|
|
|
|
/**
|
|
* Add a subsession to a shared client if necessary.
|
|
*
|
|
* @since 0.9.20
|
|
*/
|
|
protected static synchronized I2PSession addSubsession(I2PTunnel tunnel) {
|
|
I2PSession sess = socketManager.getSession();
|
|
if (sess.getMyDestination().getSigType() == SigType.DSA_SHA1)
|
|
return null;
|
|
Properties props = new Properties();
|
|
props.putAll(tunnel.getClientOptions());
|
|
String name = props.getProperty("inbound.nickname");
|
|
if (name != null)
|
|
props.setProperty("inbound.nickname", name + " (DSA)");
|
|
name = props.getProperty("outbound.nickname");
|
|
if (name != null)
|
|
props.setProperty("outbound.nickname", name + " (DSA)");
|
|
props.setProperty(I2PClient.PROP_SIGTYPE, "DSA_SHA1");
|
|
try {
|
|
return socketManager.addSubsession(null, props);
|
|
} catch (I2PSessionException ise) {
|
|
Log log = tunnel.getContext().logManager().getLog(I2PTunnelClientBase.class);
|
|
if (log.shouldLog(Log.WARN))
|
|
log.warn("Failed to add subssession", ise);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Kill the shared client, so that on restart in android
|
|
* we won't latch onto the old one
|
|
*
|
|
* @since 0.9.18
|
|
*/
|
|
protected static synchronized void killSharedClient() {
|
|
socketManager = null;
|
|
}
|
|
|
|
/**
|
|
* For NON-SHARED clients (ownDest = true).
|
|
*
|
|
* As of 0.9.20 this is fast, and does NOT connect the manager to the router.
|
|
* Call verifySocketManager() for that.
|
|
*
|
|
* @return non-null
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we cant create a socketManager
|
|
*/
|
|
protected I2PSocketManager buildSocketManager() {
|
|
return buildSocketManager(getTunnel(), this.privKeyFile, this.l);
|
|
}
|
|
|
|
/**
|
|
* As of 0.9.20 this is fast, and does NOT connect the manager to the router.
|
|
* Call verifySocketManager() for that.
|
|
*
|
|
* @return non-null
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we cant create a socketManager
|
|
*/
|
|
protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel) {
|
|
return buildSocketManager(tunnel, null);
|
|
}
|
|
|
|
private static final int RETRY_DELAY = 20*1000;
|
|
private static final int MAX_RETRIES = 4;
|
|
|
|
/**
|
|
* As of 0.9.20 this is fast, and does NOT connect the manager to the router.
|
|
* Call verifySocketManager() for that.
|
|
*
|
|
* @param pkf absolute path or null
|
|
* @return non-null
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we cant create a socketManager
|
|
*/
|
|
protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel, String pkf) {
|
|
return buildSocketManager(tunnel, pkf, null);
|
|
}
|
|
|
|
/**
|
|
* As of 0.9.20 this is fast, and does NOT connect the manager to the router.
|
|
* Call verifySocketManager() for that.
|
|
*
|
|
* @param pkf absolute path or null
|
|
* @return non-null
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we cant create a socketManager
|
|
*/
|
|
protected static I2PSocketManager buildSocketManager(I2PTunnel tunnel, String pkf, Logging log) {
|
|
// shadows instance _log
|
|
Log _log = tunnel.getContext().logManager().getLog(I2PTunnelClientBase.class);
|
|
Properties props = new Properties();
|
|
props.putAll(tunnel.getClientOptions());
|
|
int portNum = 7654;
|
|
if (tunnel.port != null) {
|
|
try {
|
|
portNum = Integer.parseInt(tunnel.port);
|
|
} catch (NumberFormatException nfe) {
|
|
throw new IllegalArgumentException("Invalid port specified [" + tunnel.port + "]", nfe);
|
|
}
|
|
}
|
|
|
|
I2PSocketManager sockManager = null;
|
|
FileInputStream fis = null;
|
|
try {
|
|
if (pkf != null) {
|
|
// Persistent client dest
|
|
fis = new FileInputStream(pkf);
|
|
sockManager = I2PSocketManagerFactory.createDisconnectedManager(fis, tunnel.host, portNum, props);
|
|
} else {
|
|
sockManager = I2PSocketManagerFactory.createDisconnectedManager(null, tunnel.host, portNum, props);
|
|
}
|
|
} catch (I2PSessionException ise) {
|
|
throw new IllegalArgumentException("Can't create socket manager", ise);
|
|
} catch (IOException ioe) {
|
|
if (log != null)
|
|
log.log("Error opening key file " + ioe);
|
|
_log.error("Error opening key file", ioe);
|
|
throw new IllegalArgumentException("Error opening key file", ioe);
|
|
} finally {
|
|
if (fis != null)
|
|
try { fis.close(); } catch (IOException ioe) {}
|
|
}
|
|
sockManager.setName("Client");
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info(tunnel.getClientOptions().getProperty("inbound.nickname") + ": Built a new socket manager [s=" + sockManager.getSession() + "]");
|
|
tunnel.addSession(sockManager.getSession());
|
|
return sockManager;
|
|
}
|
|
|
|
|
|
/**
|
|
* Warning, blocks while connecting to router and building tunnels;
|
|
* This may take a LONG time.
|
|
*
|
|
* @throws IllegalArgumentException if the I2CP configuration is b0rked so
|
|
* badly that we cant create a socketManager
|
|
* @since 0.9.20
|
|
*/
|
|
private void connectManager() {
|
|
int retries = 0;
|
|
while (sockMgr.getSession().isClosed()) {
|
|
try {
|
|
sockMgr.getSession().connect();
|
|
synchronized(I2PTunnelClientBase.class) {
|
|
if (sockMgr == socketManager)
|
|
_socketManagerState = SocketManagerState.CONNECTED;
|
|
}
|
|
} catch (I2PSessionException ise) {
|
|
// shadows instance _log
|
|
Log _log = getTunnel().getContext().logManager().getLog(I2PTunnelClientBase.class);
|
|
Logging log = this.l;
|
|
// try to make this error sensible as it will happen...
|
|
String portNum = getTunnel().port;
|
|
if (portNum == null)
|
|
portNum = "7654";
|
|
String msg;
|
|
if (getTunnel().getContext().isRouterContext())
|
|
msg = "Unable to build tunnels for the client";
|
|
else
|
|
msg = "Unable to connect to the router at " + getTunnel().host + ':' + portNum +
|
|
" and build tunnels for the client";
|
|
if (++retries < MAX_RETRIES) {
|
|
if (log != null)
|
|
log.log(msg + ", retrying in " + (RETRY_DELAY / 1000) + " seconds");
|
|
_log.error(msg + ", retrying in " + (RETRY_DELAY / 1000) + " seconds", ise);
|
|
} else {
|
|
if (log != null)
|
|
log.log(msg + ", giving up");
|
|
_log.log(Log.CRIT, msg + ", giving up", ise);
|
|
throw new IllegalArgumentException(msg, ise);
|
|
}
|
|
try { Thread.sleep(RETRY_DELAY); } catch (InterruptedException ie) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
public final int getLocalPort() {
|
|
return localPort;
|
|
}
|
|
|
|
protected final InetAddress getListenHost(Logging l) {
|
|
try {
|
|
return InetAddress.getByName(getTunnel().listenHost);
|
|
} catch (UnknownHostException uhe) {
|
|
l.log("Could not find listen host to bind to [" + getTunnel().host + "]");
|
|
_log.error("Error finding host to bind", uhe);
|
|
notifyEvent("openBaseClientResult", "error");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actually open the local socket and start working on incoming connections. *Must* be
|
|
* called by derived classes after initialization.
|
|
*
|
|
* (this wasn't actually true until 0.9.20)
|
|
*
|
|
* This will be fast if i2cp.delayOpen is true, but could take
|
|
* a LONG TIME if it is false, as it connects to the router and builds tunnels.
|
|
*
|
|
* Extending classes must check the value of boolean open after calling
|
|
* super.startRunning(), if false then something went wrong.
|
|
*
|
|
*/
|
|
public void startRunning() {
|
|
boolean openNow = !Boolean.parseBoolean(getTunnel().getClientOptions().getProperty("i2cp.delayOpen"));
|
|
if (openNow) {
|
|
while (sockMgr == null) {
|
|
verifySocketManager();
|
|
if (sockMgr == null) {
|
|
_log.error("Unable to connect to router and build tunnels for " + _handlerName);
|
|
// FIXME there is a loop in buildSocketManager(), do we really need another one here?
|
|
// no matter, buildSocketManager() now throws an IllegalArgumentException
|
|
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
|
|
} else {
|
|
l.log("Tunnels ready for client: " + _handlerName);
|
|
}
|
|
}
|
|
// can't be null unless we limit the loop above
|
|
//if (sockMgr == null) {
|
|
// l.log("Invalid I2CP configuration");
|
|
// throw new IllegalArgumentException("Socket manager could not be created");
|
|
//}
|
|
|
|
} // else delay creating session until createI2PSocket() is called
|
|
startup();
|
|
}
|
|
|
|
private void startup() {
|
|
if (_log.shouldLog(Log.DEBUG))
|
|
_log.debug("startup " + _clientId, new Exception("I did it"));
|
|
// prevent JVM exit when running outside the router
|
|
boolean isDaemon = getTunnel().getContext().isRouterContext();
|
|
open = true;
|
|
Thread t = new I2PAppThread(this, "I2PTunnel Client " + getTunnel().listenHost + ':' + localPort, isDaemon);
|
|
t.start();
|
|
synchronized (this) {
|
|
while (!listenerReady && open) {
|
|
try {
|
|
wait();
|
|
} catch (InterruptedException e) {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
if (open && listenerReady) {
|
|
boolean openNow = !Boolean.parseBoolean(getTunnel().getClientOptions().getProperty("i2cp.delayOpen"));
|
|
if (openNow || chained)
|
|
l.log("Client ready, listening on " + getTunnel().listenHost + ':' + localPort);
|
|
else
|
|
l.log("Client ready, listening on " + getTunnel().listenHost + ':' + localPort + ", delaying tunnel open until required");
|
|
notifyEvent("openBaseClientResult", "ok");
|
|
} else {
|
|
l.log("Client error for " + getTunnel().listenHost + ':' + localPort + ", check logs");
|
|
notifyEvent("openBaseClientResult", "error");
|
|
}
|
|
synchronized (startLock) {
|
|
startRunning = true;
|
|
startLock.notify();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the default options (using the default timeout, etc).
|
|
* Warning, this does not make a copy of I2PTunnel's client options,
|
|
* it modifies them directly.
|
|
*/
|
|
protected I2PSocketOptions getDefaultOptions() {
|
|
Properties defaultOpts = getTunnel().getClientOptions();
|
|
I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
|
|
if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
|
|
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
|
|
return opts;
|
|
}
|
|
|
|
/**
|
|
* Create the default options (using the default timeout, etc).
|
|
* Warning, this does not make a copy of I2PTunnel's client options,
|
|
* it modifies them directly.
|
|
* Do not use overrides for per-socket options.
|
|
*/
|
|
protected I2PSocketOptions getDefaultOptions(Properties overrides) {
|
|
Properties defaultOpts = getTunnel().getClientOptions();
|
|
defaultOpts.putAll(overrides);
|
|
I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts);
|
|
if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT))
|
|
opts.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
|
|
return opts;
|
|
}
|
|
|
|
/**
|
|
* Update the I2PSocketManager.
|
|
*
|
|
* @since 0.9.1
|
|
*/
|
|
@Override
|
|
public void optionsUpdated(I2PTunnel tunnel) {
|
|
if (getTunnel() != tunnel)
|
|
return;
|
|
I2PSocketManager sm = _ownDest ? sockMgr : socketManager;
|
|
if (sm == null)
|
|
return;
|
|
Properties props = tunnel.getClientOptions();
|
|
sm.setDefaultOptions(sm.buildOptions(props));
|
|
}
|
|
|
|
/**
|
|
* Create a new I2PSocket towards to the specified destination,
|
|
* adding it to the list of connections actually managed by this
|
|
* tunnel.
|
|
*
|
|
* @param dest The destination to connect to
|
|
* @return a new I2PSocket
|
|
*/
|
|
public I2PSocket createI2PSocket(Destination dest) throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException {
|
|
return createI2PSocket(dest, 0);
|
|
}
|
|
|
|
/**
|
|
* Create a new I2PSocket towards to the specified destination,
|
|
* adding it to the list of connections actually managed by this
|
|
* tunnel.
|
|
*
|
|
* @param dest The destination to connect to
|
|
* @param port The destination port to connect to 0 - 65535
|
|
* @return a new I2PSocket
|
|
* @since 0.9.9
|
|
*/
|
|
public I2PSocket createI2PSocket(Destination dest, int port)
|
|
throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException {
|
|
verifySocketManager();
|
|
I2PSocketOptions opts = getDefaultOptions();
|
|
opts.setPort(port);
|
|
return createI2PSocket(dest, opts);
|
|
}
|
|
|
|
/**
|
|
* Create a new I2PSocket towards to the specified destination,
|
|
* adding it to the list of connections actually managed by this
|
|
* tunnel.
|
|
*
|
|
* @param dest The destination to connect to
|
|
* @param opt Option to be used to open when opening the socket
|
|
* @return a new I2PSocket
|
|
*
|
|
* @throws ConnectException if the peer refuses the connection
|
|
* @throws NoRouteToHostException if the peer is not found or not reachable
|
|
* @throws InterruptedIOException if the connection timeouts
|
|
* @throws I2PException if there is some other I2P-related problem
|
|
*/
|
|
public I2PSocket createI2PSocket(Destination dest, I2PSocketOptions opt) throws I2PException, ConnectException, NoRouteToHostException, InterruptedIOException {
|
|
I2PSocket i2ps;
|
|
|
|
verifySocketManager();
|
|
i2ps = sockMgr.connect(dest, opt);
|
|
synchronized (sockLock) {
|
|
mySockets.add(i2ps);
|
|
}
|
|
|
|
return i2ps;
|
|
}
|
|
|
|
/**
|
|
* Non-final since 0.9.11.
|
|
* open will be true before being called.
|
|
* Any overrides must set listenerReady = true and then notifyAll() if setup is successful,
|
|
* and must call close() and then notifyAll() on failure or termination.
|
|
*/
|
|
public void run() {
|
|
InetAddress addr = getListenHost(l);
|
|
if (addr == null) {
|
|
close(true);
|
|
open = false;
|
|
synchronized (this) {
|
|
notifyAll();
|
|
}
|
|
return;
|
|
}
|
|
try {
|
|
Properties opts = getTunnel().getClientOptions();
|
|
boolean useSSL = Boolean.parseBoolean(opts.getProperty(PROP_USE_SSL));
|
|
if (useSSL) {
|
|
// was already done in web/IndexBean.java when saving the config
|
|
boolean wasCreated = SSLClientUtil.verifyKeyStore(opts);
|
|
if (wasCreated) {
|
|
// From here, we can't save the config.
|
|
// We shouldn't get here, as SSL isn't the default, so it would
|
|
// be enabled via the GUI only.
|
|
// If it was done manually, the keys will be regenerated at every startup,
|
|
// which is bad.
|
|
_log.logAlways(Log.WARN, "Created new i2ptunnel SSL keys but can't save the config, disable and enable via i2ptunnel GUI");
|
|
}
|
|
SSLServerSocketFactory fact = SSLClientUtil.initializeFactory(opts);
|
|
ss = fact.createServerSocket(localPort, 0, addr);
|
|
I2PSSLSocketFactory.setProtocolsAndCiphers((SSLServerSocket) ss);
|
|
} else {
|
|
ss = new ServerSocket(localPort, 0, addr);
|
|
}
|
|
|
|
// If a free port was requested, find out what we got
|
|
if (localPort == 0) {
|
|
localPort = ss.getLocalPort();
|
|
}
|
|
notifyEvent("clientLocalPort", Integer.valueOf(ss.getLocalPort()));
|
|
// duplicates message in constructor
|
|
//l.log("Listening for clients on port " + localPort + " of " + getTunnel().listenHost);
|
|
|
|
// Notify constructor that port is ready
|
|
synchronized (this) {
|
|
listenerReady = true;
|
|
notifyAll();
|
|
}
|
|
|
|
// Wait until we are authorized to process data
|
|
synchronized (startLock) {
|
|
while (!startRunning) {
|
|
try {
|
|
startLock.wait();
|
|
} catch (InterruptedException ie) {
|
|
}
|
|
}
|
|
}
|
|
|
|
TunnelControllerGroup tcg = TunnelControllerGroup.getInstance();
|
|
if (tcg != null) {
|
|
_executor = tcg.getClientExecutor();
|
|
} else {
|
|
// Fallback in case TCG.getInstance() is null, never instantiated
|
|
// and we were not started by TCG.
|
|
// Maybe a plugin loaded before TCG? Should be rare.
|
|
// Never shut down.
|
|
_executor = new TunnelControllerGroup.CustomThreadPoolExecutor();
|
|
}
|
|
while (open) {
|
|
Socket s = ss.accept();
|
|
manageConnection(s);
|
|
}
|
|
} catch (IOException ex) {
|
|
synchronized (sockLock) {
|
|
mySockets.clear();
|
|
}
|
|
if (open) {
|
|
_log.error("Error listening for connections on " + addr + " port " + localPort, ex);
|
|
l.log("Error listening for connections on " + addr + " port " + localPort + ": " + ex);
|
|
notifyEvent("openBaseClientResult", "error");
|
|
close(true);
|
|
}
|
|
synchronized (this) {
|
|
notifyAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Manage the connection just opened on the specified socket
|
|
*
|
|
* @param s Socket to take care of
|
|
*/
|
|
protected void manageConnection(Socket s) {
|
|
if (s == null) return;
|
|
ThreadPoolExecutor tpe = _executor;
|
|
if (tpe == null) {
|
|
_log.error("No executor for socket!");
|
|
try {
|
|
s.close();
|
|
} catch (IOException ioe) {}
|
|
return;
|
|
}
|
|
try {
|
|
tpe.execute(new BlockingRunner(s));
|
|
} catch (RejectedExecutionException ree) {
|
|
// should never happen, we have an unbounded pool and never stop the executor
|
|
try {
|
|
s.close();
|
|
} catch (IOException ioe) {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Blocking runner, used during the connection establishment
|
|
*/
|
|
private class BlockingRunner implements Runnable {
|
|
private final Socket _s;
|
|
public BlockingRunner(Socket s) { _s = s; }
|
|
public void run() {
|
|
try {
|
|
clientConnectionRun(_s);
|
|
} catch (Throwable t) {
|
|
// probably an IllegalArgumentException from
|
|
// connecting to the router in a delay-open or
|
|
// close-on-idle tunnel (in connectManager() above)
|
|
_log.error("Uncaught error in i2ptunnel client", t);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Note that the tunnel can be reopened after this by calling startRunning().
|
|
* This may not release all resources. In particular, the I2PSocketManager remains
|
|
* and it may have timer threads that continue running.
|
|
*
|
|
* To release all resources permanently, call destroy().
|
|
*
|
|
* Does nothing if open is already false.
|
|
* Sets open = false but does not notifyAll().
|
|
*
|
|
* @return success
|
|
*/
|
|
public boolean close(boolean forced) {
|
|
if (_log.shouldLog(Log.INFO))
|
|
_log.info("close() called: forced = " + forced + " open = " + open + " sockMgr = " + sockMgr);
|
|
if (!open) return true;
|
|
// FIXME: here we might have to wait quite a long time if
|
|
// there is a connection attempt atm. But without waiting we
|
|
// might risk to create an orphan socket. Would be better
|
|
// to return with an error in that situation quickly.
|
|
synchronized (sockLock) {
|
|
if (sockMgr != null) {
|
|
mySockets.retainAll(sockMgr.listSockets());
|
|
if ((!forced) && (!mySockets.isEmpty())) {
|
|
l.log("Not closing, there are still active connections!");
|
|
_log.debug("can't close: there are still active connections!");
|
|
for (I2PSocket s : mySockets) {
|
|
l.log(" -> " + s.toString());
|
|
}
|
|
return false;
|
|
}
|
|
if (!chained) {
|
|
I2PSession session = sockMgr.getSession();
|
|
getTunnel().removeSession(session);
|
|
if (_ownDest) {
|
|
try {
|
|
session.destroySession();
|
|
} catch (I2PException ex) {}
|
|
}
|
|
// TCG will try to destroy it too
|
|
} // else the app chaining to this one closes it!
|
|
}
|
|
l.log("Stopping client " + toString());
|
|
open = false;
|
|
try {
|
|
if (ss != null) ss.close();
|
|
} catch (IOException ex) {
|
|
if (_log.shouldDebug())
|
|
_log.debug("error closing", ex);
|
|
return false;
|
|
}
|
|
//l.log("Client closed.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Note that the tunnel cannot be reopened after this by calling startRunning(),
|
|
* as it will destroy the underlying socket manager.
|
|
* This releases all resources if not a shared client.
|
|
* For shared client, the router will kill all the remaining streaming timers at shutdown.
|
|
*
|
|
* @since 0.9.17
|
|
*/
|
|
@Override
|
|
public synchronized boolean destroy() {
|
|
close(true);
|
|
if (_ownDest)
|
|
sockMgr.destroySocketManager();
|
|
return true;
|
|
}
|
|
|
|
public static void closeSocket(Socket s) {
|
|
try {
|
|
s.close();
|
|
} catch (IOException ex) {
|
|
//_log.error("Could not close socket", ex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Manage a connection in a separate thread. This only works if
|
|
* you do not override manageConnection().
|
|
*
|
|
* This is run in a thread from an unlimited-size thread pool,
|
|
* so it may block or run indefinitely.
|
|
*/
|
|
protected abstract void clientConnectionRun(Socket s);
|
|
}
|