package net.i2p.i2ptunnel; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.I2PException; import net.i2p.client.I2PClient; import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; import net.i2p.client.I2PSessionException; import net.i2p.crypto.KeyGenerator; import net.i2p.crypto.SigType; import net.i2p.data.Destination; import net.i2p.data.KeyCertificate; import net.i2p.data.PrivateKey; import net.i2p.data.PrivateKeyFile; import net.i2p.data.PublicKey; import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPublicKey; import net.i2p.data.SimpleDataStructure; import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel; import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.RandomSource; import net.i2p.util.SecureFile; import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SystemVersion; /** * Coordinate the runtime operation and configuration of a single I2PTunnel. * An I2PTunnel tracks one or more I2PTunnelTasks and one or more I2PSessions. * Usually one of each. * * These objects are bundled together under a TunnelControllerGroup where the * entire group is stored / loaded from a single config file. * * This is the class used by several plugins to create tunnels, so * take care to maintain the public methods as a stable API. */ public class TunnelController implements Logging { private final Log _log; private Properties _config; private final I2PTunnel _tunnel; private final List<String> _messages; private List<I2PSession> _sessions; private volatile TunnelState _state; /** @since 0.9.19 */ private enum TunnelState { START_ON_LOAD, STARTING, RUNNING, STOPPING, STOPPED, DESTROYING, DESTROYED, } public static final String KEY_BACKUP_DIR = "i2ptunnel-keyBackup"; /** all of these @since 0.9.14 */ public static final String PROP_DESCR = "description"; public static final String PROP_DEST = "targetDestination"; public static final String PROP_I2CP_HOST = "i2cpHost"; public static final String PROP_I2CP_PORT = "i2cpPort"; public static final String PROP_INTFC = "interface"; public static final String PROP_FILE = "privKeyFile"; public static final String PROP_LISTEN_PORT = "listenPort"; public static final String PROP_NAME = "name"; public static final String PROP_PROXIES = "proxyList"; public static final String PROP_SHARED = "sharedClient"; public static final String PROP_SPOOFED_HOST = "spoofedHost"; public static final String PROP_START = "startOnLoad"; public static final String PROP_TARGET_HOST = "targetHost"; public static final String PROP_TARGET_PORT = "targetPort"; public static final String PROP_TYPE = "type"; /** @since 0.9.14 */ public static final String PFX_OPTION = "option."; private static final String OPT_PERSISTENT = PFX_OPTION + "persistentClientKey"; public static final String OPT_BUNDLE_REPLY = PFX_OPTION + "shouldBundleReplyInfo"; private static final String OPT_TAGS_SEND = PFX_OPTION + "crypto.tagsToSend"; private static final String OPT_LOW_TAGS = PFX_OPTION + "crypto.lowTagThreshold"; private static final String OPT_SIG_TYPE = PFX_OPTION + I2PClient.PROP_SIGTYPE; /** @since 0.9.30 */ private static final String OPT_ALT_PKF = PFX_OPTION + I2PTunnelServer.PROP_ALT_PKF; /** all of these @since 0.9.14 */ public static final String TYPE_CONNECT = "connectclient"; public static final String TYPE_HTTP_BIDIR_SERVER = "httpbidirserver"; public static final String TYPE_HTTP_CLIENT = "httpclient"; public static final String TYPE_HTTP_SERVER = "httpserver"; public static final String TYPE_IRC_CLIENT = "ircclient"; public static final String TYPE_IRC_SERVER = "ircserver"; public static final String TYPE_SOCKS = "sockstunnel"; public static final String TYPE_SOCKS_IRC = "socksirctunnel"; public static final String TYPE_STD_CLIENT = "client"; public static final String TYPE_STD_SERVER = "server"; /** Client in the UI and I2P side but a server on the localhost side */ public static final String TYPE_STREAMR_CLIENT = "streamrclient"; /** Server in the UI and I2P side but a client on the localhost side */ public static final String TYPE_STREAMR_SERVER = "streamrserver"; /** * This is guaranteed to be available. * @since 0.9.17 */ public static final SigType PREFERRED_SIGTYPE; static { if (SystemVersion.isGNU() || SystemVersion.isAndroid()) { if (SigType.ECDSA_SHA256_P256.isAvailable()) PREFERRED_SIGTYPE = SigType.ECDSA_SHA256_P256; else PREFERRED_SIGTYPE = SigType.DSA_SHA1; } else { PREFERRED_SIGTYPE = SigType.EdDSA_SHA512_Ed25519; } } /** * Create a new controller for a tunnel out of the specific config options. * The config may contain a large number of options - only ones that begin in * the prefix should be used (and, in turn, that prefix should be stripped off * before being interpreted by this controller) * * Defaults in config properties are not recommended, they may or may not be honored. * * @param config original key=value mapping non-null * @param prefix beginning of key values that are relevant to this tunnel */ public TunnelController(Properties config, String prefix) { this(config, prefix, true); } /** * Defaults in config properties are not recommended, they may or may not be honored. * * @param config original key=value mapping non-null * @param prefix beginning of key values that are relevant to this tunnel * @param createKey for servers, whether we want to create a brand new destination * with private keys at the location specified or not (does not * overwrite existing ones) */ public TunnelController(Properties config, String prefix, boolean createKey) { _tunnel = new I2PTunnel(); _log = I2PAppContext.getGlobalContext().logManager().getLog(TunnelController.class); setConfig(config, prefix); _messages = new ArrayList<String>(4); boolean keyOK = true; if (createKey && (!isClient() || getPersistentClientKey())) { keyOK = createPrivateKey(); if (keyOK && !isClient() && !getType().equals(TYPE_STREAMR_SERVER)) { // check rv? createAltPrivateKey(); } } _state = keyOK && getStartOnLoad() ? TunnelState.START_ON_LOAD : TunnelState.STOPPED; } /** * @return success */ private boolean createPrivateKey() { I2PClient client = I2PClientFactory.createClient(); File keyFile = getPrivateKeyFile(); if (keyFile == null) { log("No filename specified for the private key"); return false; } if (keyFile.exists()) { //log("Not overwriting existing private keys in " + keyFile.getAbsolutePath()); return true; } else { File parent = keyFile.getParentFile(); if ( (parent != null) && (!parent.exists()) ) parent.mkdirs(); } FileOutputStream fos = null; try { fos = new SecureFileOutputStream(keyFile); SigType stype = PREFERRED_SIGTYPE; String st = _config.getProperty(OPT_SIG_TYPE); if (st != null) { SigType type = SigType.parseSigType(st); if (type != null && type.isAvailable()) stype = type; else log("Unsupported sig type " + st + ", reverting to " + stype); } Destination dest = client.createDestination(fos, stype); String destStr = dest.toBase64(); log("Private key created and saved in " + keyFile.getAbsolutePath()); log("You should backup this file in a secure place."); log("New alternate destination: " + destStr); String b32 = dest.toBase32(); log("Base32: " + b32); File backupDir = new SecureFile(I2PAppContext.getGlobalContext().getConfigDir(), KEY_BACKUP_DIR); if (backupDir.isDirectory() || backupDir.mkdir()) { String name = b32 + '-' + I2PAppContext.getGlobalContext().clock().now() + ".dat"; File backup = new File(backupDir, name); if (FileUtil.copy(keyFile, backup, false, true)) { SecureFileOutputStream.setPerms(backup); log("Private key backup saved to " + backup.getAbsolutePath()); } } } catch (I2PException ie) { if (_log.shouldLog(Log.ERROR)) _log.error("Error creating new destination", ie); log("Error creating new destination: " + ie.getMessage()); return false; } catch (IOException ioe) { if (_log.shouldLog(Log.ERROR)) _log.error("Error creating writing the destination to " + keyFile.getAbsolutePath(), ioe); log("Error writing the keys to " + keyFile.getAbsolutePath()); return false; } finally { if (fos != null) try { fos.close(); } catch (IOException ioe) {} } return true; } /** * Creates alternate Destination with the same encryption keys as the primary Destination, * but a different signing key. * * Must have already called createPrivateKey() successfully. * Does nothing unless option OPT_ALT_PKF is set with the privkey file name. * Does nothing if the file already exists. * * @return success * @since 0.9.30 */ private boolean createAltPrivateKey() { if (PREFERRED_SIGTYPE == SigType.DSA_SHA1) return false; File keyFile = getPrivateKeyFile(); if (keyFile == null) return false; if (!keyFile.exists()) return false; File altFile = getAlternatePrivateKeyFile(); if (altFile == null) return false; if (altFile.equals(keyFile)) return false; if (altFile.exists()) return true; PrivateKeyFile pkf = new PrivateKeyFile(keyFile); FileOutputStream out = null; try { Destination dest = pkf.getDestination(); if (dest == null) return false; if (dest.getSigType() != SigType.DSA_SHA1) return false; PublicKey pub = dest.getPublicKey(); PrivateKey priv = pkf.getPrivKey(); SimpleDataStructure[] signingKeys = KeyGenerator.getInstance().generateSigningKeys(PREFERRED_SIGTYPE); SigningPublicKey signingPubKey = (SigningPublicKey) signingKeys[0]; SigningPrivateKey signingPrivKey = (SigningPrivateKey) signingKeys[1]; KeyCertificate cert = new KeyCertificate(signingPubKey); Destination d = new Destination(); d.setPublicKey(pub); d.setSigningPublicKey(signingPubKey); d.setCertificate(cert); int len = signingPubKey.length(); if (len < 128) { byte[] pad = new byte[128 - len]; RandomSource.getInstance().nextBytes(pad); d.setPadding(pad); } else if (len > 128) { // copy of excess data handled in KeyCertificate constructor } out = new SecureFileOutputStream(altFile); d.writeBytes(out); priv.writeBytes(out); signingPrivKey.writeBytes(out); try { out.close(); } catch (IOException ioe) {} String destStr = d.toBase64(); log("Alternate private key created and saved in " + altFile.getAbsolutePath()); log("You should backup this file in a secure place."); log("New destination: " + destStr); String b32 = d.toBase32(); log("Base32: " + b32); File backupDir = new SecureFile(I2PAppContext.getGlobalContext().getConfigDir(), KEY_BACKUP_DIR); if (backupDir.isDirectory() || backupDir.mkdir()) { String name = b32 + '-' + I2PAppContext.getGlobalContext().clock().now() + ".dat"; File backup = new File(backupDir, name); if (FileUtil.copy(altFile, backup, false, true)) { SecureFileOutputStream.setPerms(backup); log("Alternate private key backup saved to " + backup.getAbsolutePath()); } } return true; } catch (GeneralSecurityException e) { log("Error creating keys " + e); return false; } catch (I2PSessionException e) { log("Error creating keys " + e); return false; } catch (I2PException e) { log("Error creating keys " + e); return false; } catch (IOException e) { log("Error creating keys " + e); return false; } catch (RuntimeException e) { log("Error creating keys " + e); return false; } finally { if (out != null) try { out.close(); } catch (IOException ioe) {} } } public void startTunnelBackground() { synchronized (this) { if (_state != TunnelState.STOPPED && _state != TunnelState.START_ON_LOAD) return; } new I2PAppThread(new Runnable() { public void run() { startTunnel(); } }, "Tunnel Starter " + getName()).start(); } /** * Start up the tunnel (if it isn't already running) * */ public void startTunnel() { synchronized (this) { if (_state != TunnelState.STOPPED && _state != TunnelState.START_ON_LOAD) { if (_state == TunnelState.RUNNING) { if (_log.shouldLog(Log.INFO)) _log.info("Already running"); log("Tunnel " + getName() + " is already running"); } return; } changeState(TunnelState.STARTING); } try { doStartTunnel(); } catch (RuntimeException e) { _log.error("Error starting the tunnel " + getName(), e); log("Error starting the tunnel " + getName() + ": " + e.getMessage()); // if we don't acquire() then the release() in stopTunnel() won't work acquire(); stopTunnel(); } } /** * @throws IllegalArgumentException via methods in I2PTunnel */ private void doStartTunnel() { synchronized (this) { if (_state != TunnelState.STARTING) return; } String type = getType(); if ( (type == null) || (type.length() <= 0) ) { changeState(TunnelState.STOPPED); if (_log.shouldLog(Log.ERROR)) _log.error("Cannot start the tunnel - no type specified"); return; } // Config options may have changed since instantiation, so do this again. // Or should we take it out of the constructor completely? if (!isClient() || getPersistentClientKey()) { boolean ok = createPrivateKey(); if (!ok) { changeState(TunnelState.STOPPED); log("Failed to start tunnel " + getName() + " as the private key file could not be created"); return; } if (!isClient() && !getType().equals(TYPE_STREAMR_SERVER)) { // check rv? createAltPrivateKey(); } } setI2CPOptions(); setSessionOptions(); if (TYPE_HTTP_CLIENT.equals(type)) { startHttpClient(); } else if(TYPE_IRC_CLIENT.equals(type)) { startIrcClient(); } else if(TYPE_SOCKS.equals(type)) { startSocksClient(); } else if(TYPE_SOCKS_IRC.equals(type)) { startSocksIRCClient(); } else if(TYPE_CONNECT.equals(type)) { startConnectClient(); } else if (TYPE_STD_CLIENT.equals(type)) { startClient(); } else if (TYPE_STREAMR_CLIENT.equals(type)) { startStreamrClient(); } else if (TYPE_STD_SERVER.equals(type)) { startServer(); } else if (TYPE_HTTP_SERVER.equals(type)) { startHttpServer(); } else if (TYPE_HTTP_BIDIR_SERVER.equals(type)) { startHttpBidirServer(); } else if (TYPE_IRC_SERVER.equals(type)) { startIrcServer(); } else if (TYPE_STREAMR_SERVER.equals(type)) { startStreamrServer(); } else { changeState(TunnelState.STOPPED); if (_log.shouldLog(Log.ERROR)) _log.error("Cannot start tunnel - unknown type [" + type + "]"); return; } acquire(); changeState(TunnelState.RUNNING); } private void startHttpClient() { setListenOn(); String listenPort = getListenPort(); String proxyList = getProxyList(); String sharedClient = getSharedClient(); if (proxyList == null) _tunnel.runHttpClient(new String[] { listenPort, sharedClient }, this); else _tunnel.runHttpClient(new String[] { listenPort, sharedClient, proxyList }, this); } private void startConnectClient() { 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); } private void startIrcClient() { setListenOn(); String listenPort = getListenPort(); String dest = getTargetDestination(); String sharedClient = getSharedClient(); if (getPersistentClientKey()) { String privKeyFile = getPrivKeyFile(); _tunnel.runIrcClient(new String[] { listenPort, dest, sharedClient, privKeyFile }, this); } else { _tunnel.runIrcClient(new String[] { listenPort, dest, sharedClient }, this); } } private void startSocksClient() { setListenOn(); String listenPort = getListenPort(); String sharedClient = getSharedClient(); String proxyList = getProxyList(); if (proxyList != null) { // set the outproxy property the socks tunnel wants Properties props = _tunnel.getClientOptions(); if (!props.containsKey(I2PSOCKSTunnel.PROP_PROXY_DEFAULT)) props.setProperty(I2PSOCKSTunnel.PROP_PROXY_DEFAULT, proxyList); } if (getPersistentClientKey()) { String privKeyFile = getPrivKeyFile(); _tunnel.runSOCKSTunnel(new String[] { listenPort, "false", privKeyFile }, this); } else { _tunnel.runSOCKSTunnel(new String[] { listenPort, sharedClient }, this); } } /** @since 0.7.12 */ private void startSocksIRCClient() { setListenOn(); String listenPort = getListenPort(); String sharedClient = getSharedClient(); String proxyList = getProxyList(); if (proxyList != null) { // set the outproxy property the socks tunnel wants Properties props = _tunnel.getClientOptions(); if (!props.containsKey(I2PSOCKSTunnel.PROP_PROXY_DEFAULT)) props.setProperty(I2PSOCKSTunnel.PROP_PROXY_DEFAULT, proxyList); } if (getPersistentClientKey()) { String privKeyFile = getPrivKeyFile(); _tunnel.runSOCKSIRCTunnel(new String[] { listenPort, "false", privKeyFile }, this); } else { _tunnel.runSOCKSIRCTunnel(new String[] { listenPort, sharedClient }, this); } } /* * Streamr client is a UDP server, use the listenPort field for targetPort */ private void startStreamrClient() { String targetHost = getTargetHost(); String targetPort = getListenPort(); String dest = getTargetDestination(); _tunnel.runStreamrClient(new String[] { targetHost, targetPort, dest }, this); } /** * Streamr server is a UDP client, use the targetPort field for listenPort */ private void startStreamrServer() { String listenOn = getListenOnInterface(); if ( (listenOn != null) && (listenOn.length() > 0) ) { _tunnel.runListenOn(new String[] { listenOn }, this); } String listenPort = getTargetPort(); String privKeyFile = getPrivKeyFile(); _tunnel.runStreamrServer(new String[] { listenPort, privKeyFile }, this); } /** * Note the fact that we are using some sessions, so that they dont get * closed by some other tunnels */ private void acquire() { List<I2PSession> sessions = _tunnel.getSessions(); if (!sessions.isEmpty()) { for (int i = 0; i < sessions.size(); i++) { I2PSession session = sessions.get(i); if (_log.shouldLog(Log.INFO)) _log.info("Acquiring session " + session); TunnelControllerGroup group = TunnelControllerGroup.getInstance(); if (group != null) group.acquire(this, session); } _sessions = sessions; } else { if (_log.shouldLog(Log.WARN)) _log.warn("No sessions to acquire? for " + getName()); } } /** * Note the fact that we are no longer using some sessions, and if * no other tunnels are using them, close them. */ private void release(Collection<I2PSession> sessions) { if (!sessions.isEmpty()) { for (I2PSession s : sessions) { if (_log.shouldLog(Log.INFO)) _log.info("Releasing session " + s); TunnelControllerGroup group = TunnelControllerGroup.getInstance(); if (group != null) group.release(this, s); } // _sessions.clear() ???? } else { if (_log.shouldLog(Log.WARN)) _log.warn("No sessions to release? for " + getName()); } } /** * Get all the sessions we may be using. * * @return a copy, non-null * @since 0.9.15 */ private Collection<I2PSession> getAllSessions() { // We use _sessions AND the tunnel sessions as // _sessions will be null for delay-open tunnels - see acquire(). // We want the current sessions. Set<I2PSession> sessions = new HashSet<I2PSession>(_tunnel.getSessions()); if (_sessions != null) sessions.addAll(_sessions); return sessions; } private void startClient() { setListenOn(); String listenPort = getListenPort(); String dest = getTargetDestination(); String sharedClient = getSharedClient(); if (getPersistentClientKey()) { String privKeyFile = getPrivKeyFile(); _tunnel.runClient(new String[] { listenPort, dest, sharedClient, privKeyFile }, this); } else { _tunnel.runClient(new String[] { listenPort, dest, sharedClient }, this); } } private void startServer() { String targetHost = getTargetHost(); String targetPort = getTargetPort(); String privKeyFile = getPrivKeyFile(); _tunnel.runServer(new String[] { targetHost, targetPort, privKeyFile }, this); } private void startHttpServer() { String targetHost = getTargetHost(); String targetPort = getTargetPort(); String spoofedHost = getSpoofedHost(); String privKeyFile = getPrivKeyFile(); _tunnel.runHttpServer(new String[] { targetHost, targetPort, spoofedHost, privKeyFile }, this); } private void startHttpBidirServer() { setListenOn(); String targetHost = getTargetHost(); String targetPort = getTargetPort(); String listenPort = getListenPort(); String spoofedHost = getSpoofedHost(); String privKeyFile = getPrivKeyFile(); _tunnel.runHttpBidirServer(new String[] { targetHost, targetPort, listenPort, spoofedHost, privKeyFile }, this); } private void startIrcServer() { String targetHost = getTargetHost(); String targetPort = getTargetPort(); String privKeyFile = getPrivKeyFile(); _tunnel.runIrcServer(new String[] { targetHost, targetPort, privKeyFile }, this); } private void setListenOn() { String listenOn = getListenOnInterface(); if ( (listenOn != null) && (listenOn.length() > 0) ) { _tunnel.runListenOn(new String[] { listenOn }, this); } } /** * These are the ones stored with a prefix of "option." * Defaults in config properties are not honored. * * @return keys with the "option." prefix stripped, non-null * @since 0.9.1 Much better than getClientOptions() */ public Properties getClientOptionProps() { Properties opts = new Properties(); for (Map.Entry<Object, Object> e : _config.entrySet()) { String key = (String) e.getKey(); if (key.startsWith(PFX_OPTION)) { key = key.substring(PFX_OPTION.length()); String val = (String) e.getValue(); opts.setProperty(key, val); } } return opts; } private void setSessionOptions() { Properties opts = getClientOptionProps(); // targetDestination does NOT start with "option.", but we still want // to allow a change on the fly, so we pass it through this way, // as a "spoofed" option. Since 0.9.9. String target = getTargetDestination(); if (target != null) opts.setProperty(PROP_DEST, target); // Ditto outproxy list. Since 0.9.12. String proxies = getProxyList(); if (proxies != null) opts.setProperty(PROP_PROXIES, proxies); // Ditto spoof host. Since 0.9.15. String spoofhost = getSpoofedHost(); if (spoofhost != null) opts.setProperty(PROP_SPOOFED_HOST, spoofhost); // Ditto target host/port. Since 0.9.15. String targethost = getTargetHost(); if (targethost != null) opts.setProperty(PROP_TARGET_HOST, targethost); String targetport = getTargetPort(); if (targetport != null) opts.setProperty(PROP_TARGET_PORT, targetport); _tunnel.setClientOptions(opts); } private void setI2CPOptions() { String host = getI2CPHost(); if ( (host != null) && (host.length() > 0) ) _tunnel.host = host; // woohah, special casing for people with ipv6/etc if ("localhost".equals(_tunnel.host)) _tunnel.host = "127.0.0.1"; String port = getI2CPPort(); if ( (port != null) && (port.length() > 0) ) { try { int portNum = Integer.parseInt(port); _tunnel.port = String.valueOf(portNum); } catch (NumberFormatException nfe) { _tunnel.port = "7654"; } } else { _tunnel.port = "7654"; } } /** * May be restarted with restartTunnel() or startTunnel() later. * This may not release all resources. In particular, the I2PSocketManager remains * and it may have timer threads that continue running. */ public void stopTunnel() { synchronized (this) { if (_state != TunnelState.STARTING && _state != TunnelState.RUNNING) return; changeState(TunnelState.STOPPING); } // I2PTunnel removes the session in close(), // so save the sessions to pass to release() and TCG Collection<I2PSession> sessions = getAllSessions(); _tunnel.runClose(new String[] { "forced", "all" }, this); release(sessions); changeState(TunnelState.STOPPED); } /** * May NOT be restarted with restartTunnel() or startTunnel() later. * This should release all resources. * * @since 0.9.17 */ public void destroyTunnel() { synchronized (this) { if (_state != TunnelState.RUNNING) return; changeState(TunnelState.DESTROYING); } // I2PTunnel removes the session in close(), // so save the sessions to pass to release() and TCG Collection<I2PSession> sessions = getAllSessions(); _tunnel.runClose(new String[] { "destroy", "all" }, this); release(sessions); changeState(TunnelState.DESTROYED); } public void restartTunnel() { stopTunnel(); startTunnel(); } /** * As of 0.9.1, updates the options on an existing session */ public void setConfig(Properties config, String prefix) { Properties props = new Properties(); for (Map.Entry<Object, Object> e : config.entrySet()) { String key = (String) e.getKey(); if (key.startsWith(prefix)) { key = key.substring(prefix.length()); String val = (String) e.getValue(); props.setProperty(key, val); } } Properties oldConfig = _config; _config = props; // Set up some per-type defaults // This really isn't the best spot to do this but for servers in particular, // it's hard to override settings in the subclass since the session connect // is done in the I2PTunnelServer constructor. String type = getType(); if (type != null) { if (type.equals(TYPE_HTTP_SERVER) || type.equals(TYPE_STREAMR_SERVER)) { if (!_config.containsKey(OPT_BUNDLE_REPLY)) _config.setProperty(OPT_BUNDLE_REPLY, "false"); } else if (!isClient(type)) { // override UI that sets it to false _config.setProperty(OPT_BUNDLE_REPLY, "true"); } if (type.contains("irc") || type.equals(TYPE_STREAMR_CLIENT)) { // maybe a bad idea for ircclient if DCC is enabled if (!_config.containsKey(OPT_TAGS_SEND)) _config.setProperty(OPT_TAGS_SEND, "20"); if (!_config.containsKey(OPT_LOW_TAGS)) _config.setProperty(OPT_LOW_TAGS, "14"); } // same default logic as in EditBean.getSigType() if (!isClient(type) || type.equals(TYPE_IRC_CLIENT) || type.equals(TYPE_STD_CLIENT) || type.equals(TYPE_SOCKS) || type.equals(TYPE_SOCKS_IRC) || type.equals(TYPE_STREAMR_CLIENT) || (type.equals(TYPE_HTTP_CLIENT) && Boolean.valueOf(getSharedClient()))) { if (!_config.containsKey(OPT_SIG_TYPE)) _config.setProperty(OPT_SIG_TYPE, PREFERRED_SIGTYPE.name()); } } // tell i2ptunnel, who will tell the TunnelTask, who will tell the SocketManager setSessionOptions(); synchronized (this) { if (_state != TunnelState.RUNNING) { if (_log.shouldLog(Log.DEBUG)) { _log.debug("Not running, not updating sessions"); } return; } } if (oldConfig != null) { if (configChanged(_config, oldConfig, PROP_FILE) || configChanged(_config, oldConfig, OPT_ALT_PKF) || configChanged(_config, oldConfig, OPT_SIG_TYPE)) { log("Tunnel must be stopped and restarted for private key file changes to take effect"); } } // Running, so check sessions Collection<I2PSession> sessions = getAllSessions(); if (sessions.isEmpty()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Running but no sessions to update"); } for (I2PSession s : sessions) { // tell the router via the session if (!s.isClosed()) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Session is open, updating: " + s); s.updateOptions(_tunnel.getClientOptions()); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Session is closed, not updating: " + s); } } } /** * Is property p different in p1 and p2? * @since 0.9.30 */ private static boolean configChanged(Properties p1, Properties p2, String p) { String s1 = p1.getProperty(p); String s2 = p2.getProperty(p); return (s1 != null && !s1.equals(s2)) || (s1 == null && s2 != null); } /** * @return a copy */ public Properties getConfig(String prefix) { Properties rv = new Properties(); for (Map.Entry<Object, Object> e : _config.entrySet()) { String key = (String) e.getKey(); String val = (String) e.getValue(); rv.setProperty(prefix + key, val); } return rv; } public String getType() { return _config.getProperty(PROP_TYPE); } public String getName() { return _config.getProperty(PROP_NAME); } public String getDescription() { return _config.getProperty(PROP_DESCR); } public String getI2CPHost() { return _config.getProperty(PROP_I2CP_HOST); } public String getI2CPPort() { return _config.getProperty(PROP_I2CP_PORT); } /** * Is it a client or server in the UI and I2P side? * Note that a streamr client is a UI and I2P client but a server on the localhost side. * Note that a streamr server is a UI and I2P server but a client on the localhost side. * * @since 0.9.17 */ public boolean isClient() { return isClient(getType()); } /** * Is it a client or server in the UI and I2P side? * Note that a streamr client is a UI and I2P client but a server on the localhost side. * Note that a streamr server is a UI and I2P server but a client on the localhost side. * * @since 0.9.17 moved from IndexBean */ public static boolean isClient(String type) { return TYPE_STD_CLIENT.equals(type) || TYPE_HTTP_CLIENT.equals(type) || TYPE_SOCKS.equals(type) || TYPE_SOCKS_IRC.equals(type) || TYPE_CONNECT.equals(type) || TYPE_STREAMR_CLIENT.equals(type) || TYPE_IRC_CLIENT.equals(type); } /** * These are the ones with a prefix of "option." * * @return one big string of "key=val key=val ..." * @deprecated why would you want this? Use getClientOptionProps() instead */ @Deprecated public String getClientOptions() { StringBuilder opts = new StringBuilder(64); for (Map.Entry<Object, Object> e : _config.entrySet()) { String key = (String) e.getKey(); if (key.startsWith(PFX_OPTION)) { key = key.substring(PFX_OPTION.length()); String val = (String) e.getValue(); if (opts.length() > 0) opts.append(' '); opts.append(key).append('=').append(val); } } return opts.toString(); } public String getListenOnInterface() { return _config.getProperty(PROP_INTFC); } public String getTargetHost() { return _config.getProperty(PROP_TARGET_HOST); } public String getTargetPort() { return _config.getProperty(PROP_TARGET_PORT); } public String getSpoofedHost() { return _config.getProperty(PROP_SPOOFED_HOST); } /** * Probably not absolute. May be null. getPrivateKeyFile() recommended. */ public String getPrivKeyFile() { return _config.getProperty(PROP_FILE); } public String getListenPort() { return _config.getProperty(PROP_LISTEN_PORT); } public String getTargetDestination() { return _config.getProperty(PROP_DEST); } public String getProxyList() { return _config.getProperty(PROP_PROXIES); } /** default true */ public String getSharedClient() { return _config.getProperty(PROP_SHARED, "true"); } /** default true */ public boolean getStartOnLoad() { return Boolean.parseBoolean(_config.getProperty(PROP_START, "true")); } public boolean getPersistentClientKey() { return Boolean.parseBoolean(_config.getProperty(OPT_PERSISTENT)); } /** * Does not necessarily exist. * @return absolute path or null if unset * @since 0.9.17 */ public File getPrivateKeyFile() { return filenameToFile(getPrivKeyFile()); } /** * Does not necessarily exist. * @return absolute path or null if unset * @since 0.9.30 */ public File getAlternatePrivateKeyFile() { return filenameToFile(_config.getProperty(OPT_ALT_PKF)); } /** * Does not necessarily exist. * @param f relative or absolute path, may be null * @return absolute path or null * @since 0.9.30 */ static File filenameToFile(String f) { if (f == null) return null; f = f.trim(); if (f.length() == 0) return null; File rv = new File(f); if (!rv.isAbsolute()) rv = new File(I2PAppContext.getGlobalContext().getConfigDir(), f); return rv; } /** * Returns null if not running. * @return Base64 or null */ public String getMyDestination() { Destination dest = getDestination(); if (dest != null) return dest.toBase64(); return null; } /** * Returns null if not running. * @return "{52 chars}.b32.i2p" or null */ public String getMyDestHashBase32() { Destination dest = getDestination(); if (dest != null) return dest.toBase32(); return null; } /** * Returns null if not running. * @return Destination or null * @since 0.9.17 */ public Destination getDestination() { if (_tunnel != null) { List<I2PSession> sessions = _tunnel.getSessions(); for (int i = 0; i < sessions.size(); i++) { I2PSession session = sessions.get(i); Destination dest = session.getMyDestination(); if (dest != null) return dest; } } return null; } public boolean getIsRunning() { return _state == TunnelState.RUNNING; } public boolean getIsStarting() { return _state == TunnelState.START_ON_LOAD || _state == TunnelState.STARTING; } /** if running but no open sessions, we are in standby */ public boolean getIsStandby() { synchronized (this) { if (_state != TunnelState.RUNNING) return false; } for (I2PSession sess : _tunnel.getSessions()) { if (!sess.isClosed()) return false; } return true; } /** @since 0.9.19 */ private synchronized void changeState(TunnelState state) { _state = state; } /** * A text description of the tunnel. * @deprecated unused */ @Deprecated public void getSummary(StringBuilder buf) { String type = getType(); buf.append(type); /**** if ("httpclient".equals(type)) getHttpClientSummary(buf); else if ("client".equals(type)) getClientSummary(buf); else if ("server".equals(type)) getServerSummary(buf); else if ("httpserver".equals(type)) getHttpServerSummary(buf); else buf.append("Unknown type ").append(type); ****/ } /**** private void getHttpClientSummary(StringBuilder buf) { String description = getDescription(); if ( (description != null) && (description.trim().length() > 0) ) buf.append("<i>").append(description).append("</i><br />\n"); buf.append("HTTP proxy listening on port ").append(getListenPort()); String listenOn = getListenOnInterface(); if ("0.0.0.0".equals(listenOn)) buf.append(" (reachable by any machine)"); else if ("127.0.0.1".equals(listenOn)) buf.append(" (reachable locally only)"); else buf.append(" (reachable at the ").append(listenOn).append(" interface)"); buf.append("<br />\n"); String proxies = getProxyList(); if ( (proxies == null) || (proxies.trim().length() <= 0) ) buf.append("Outproxy: default [squid.i2p]<br />\n"); else buf.append("Outproxy: ").append(proxies).append("<br />\n"); getOptionSummary(buf); } private void getClientSummary(StringBuilder buf) { String description = getDescription(); if ( (description != null) && (description.trim().length() > 0) ) buf.append("<i>").append(description).append("</i><br />\n"); buf.append("Client tunnel listening on port ").append(getListenPort()); buf.append(" pointing at ").append(getTargetDestination()); String listenOn = getListenOnInterface(); if ("0.0.0.0".equals(listenOn)) buf.append(" (reachable by any machine)"); else if ("127.0.0.1".equals(listenOn)) buf.append(" (reachable locally only)"); else buf.append(" (reachable at the ").append(listenOn).append(" interface)"); buf.append("<br />\n"); getOptionSummary(buf); } private void getServerSummary(StringBuilder buf) { String description = getDescription(); if ( (description != null) && (description.trim().length() > 0) ) buf.append("<i>").append(description).append("</i><br />\n"); buf.append("Server tunnel pointing at port ").append(getTargetPort()); buf.append(" on ").append(getTargetHost()); buf.append("<br />\n"); buf.append("Private destination loaded from ").append(getPrivKeyFile()).append("<br />\n"); getOptionSummary(buf); } private void getHttpServerSummary(StringBuilder buf) { String description = getDescription(); if ( (description != null) && (description.trim().length() > 0) ) buf.append("<i>").append(description).append("</i><br />\n"); buf.append("Server tunnel pointing at port ").append(getTargetPort()); buf.append(" on ").append(getTargetHost()); buf.append(" for the site ").append(getSpoofedHost()); buf.append("<br />\n"); buf.append("Private destination loaded from ").append(getPrivKeyFile()).append("<br />\n"); getOptionSummary(buf); } private void getOptionSummary(StringBuilder buf) { String opts = getClientOptions(); if ( (opts != null) && (opts.length() > 0) ) buf.append("Network options: ").append(opts).append("<br />\n"); if (_running) { List<I2PSession> sessions = _tunnel.getSessions(); for (int i = 0; i < sessions.size(); i++) { I2PSession session = sessions.get(i); Destination dest = session.getMyDestination(); if (dest != null) { buf.append("Destination hash: ").append(dest.calculateHash().toBase64()).append("<br />\n"); if ( ("server".equals(getType())) || ("httpserver".equals(getType())) ) { buf.append("Full destination: "); buf.append("<input type=\"text\" size=\"10\" onclick=\"this.select();\" "); buf.append("value=\"").append(dest.toBase64()).append("\" />\n"); long val = new Random().nextLong(); if (val < 0) val = 0 - val; buf.append("<br />You can <a href=\"http://temp").append(val); buf.append(".i2p/?i2paddresshelper=").append(dest.toBase64()).append("\">view</a>"); buf.append(" it in a browser (only when you're using the eepProxy)\n"); buf.append("<br />If you are going to share this on IRC, you need to split it up:<br />\n"); String str = dest.toBase64(); buf.append(str.substring(0, str.length()/2)).append("<br />\n"); buf.append(str.substring(str.length()/2)).append("<br />\n"); buf.append("You can also post it to <a href=\"http://forum.i2p/viewforum.php?f=16\">Eepsite announcement forum</a><br />"); } } } } } ****/ /** * */ public void log(String s) { synchronized (_messages) { _messages.add(s); while (_messages.size() > 10) _messages.remove(0); } if (_log.shouldLog(Log.INFO)) _log.info(s); } /** * Pull off any messages that the I2PTunnel has produced * * @return list of messages pulled off (each is a String, earliest first) */ public List<String> clearMessages() { List<String> rv; synchronized (_messages) { rv = new ArrayList<String>(_messages); _messages.clear(); } return rv; } /** * @since 0.9.15 */ @Override public String toString() { return "TC " + getType() + ' ' + getName() + " for " + _tunnel + ' ' + _state; } }