Files
i2p.plugins.orchid/src/java/net/i2p/orchid/OrchidController.java
zzz c4038916c4 Selected and modified portions of patch from drz3d:
- Overhaul front-end to use full html output and a default light theme
 - Enable GeoIP lookups for nodes to permit display of flags
 - Add country code lookups for nodes and display on tooltip
 - Mitigate logging output issues caused by html output
 - Set useMicroDescriptors to FALSE so additional node information can
   be presented in the UI, if enough memory available
 - Allow 2nd parameter in extra-info-digest when pulling full descriptors
 - Display platform, observed bandwidth and uptime on circuit node tooltips
 - Add hints and notes to config section, and include missing options
 - Change maxCircuitDirtiness to Tor default of 10 minutes

The modifications to the Orchid source code are released under the same license as the parent application.
--
dr|z3d - z3d@mail.i2p

Thanks to George Hodan for the orchid image
http://www.publicdomainpictures.net/view-image.php?image=35307
See LICENSE-FatCowIcons.txt & LICENSE-Fugue-Icons.txt in i2p/licenses/ for icon licences

Conversion from new MaxMind format to v.1 GeoIP.dat format courtesy of:
https://github.com/sherpya/geolite2legacy
Usage:
wget https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz
./geolite2legacy.py -i GeoLite2-Country-CSV.zip -f geoname2fips.csv -o GeoIP.dat
For GeoIP licensing information, See the MaxMind license in the I2P application directory:
i2p/licenses/LICENSE-GeoIP.txt
2019-08-17 14:35:17 +00:00

342 lines
13 KiB
Java

package net.i2p.orchid;
/*
* Copyright 2014 zzz (zzz@mail.i2p)
*
* BSD License
*/
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import com.subgraph.orchid.TorClient;
import com.subgraph.orchid.TorConfig;
// import static com.subgraph.orchid.TorConfig.ConfigVarType.*;
import com.subgraph.orchid.TorConfig.AutoBoolValue;
import com.subgraph.orchid.TorInitializationListener;
import com.subgraph.orchid.config.TorConfigBridgeLine;
import com.subgraph.orchid.config.TorConfigInterval;
import com.subgraph.orchid.config.TorConfigParser;
import com.subgraph.orchid.dashboard.Dashboard;
import com.subgraph.orchid.geoip.CountryCodeService;
import net.i2p.I2PAppContext;
import net.i2p.app.*;
import static net.i2p.app.ClientAppState.*;
import net.i2p.data.Base32;
import net.i2p.data.DataHelper;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
/**
* This handles the starting and stopping of Orchid itself.
*
* This is instantiated and started by the servlet to avoid class loader issues later.
* Starting from client.config caused class loader problems.
*
* We implement ClientApp so we may register with the ClientAppManager,
* as this is how i2ptunnel finds us.
*
* @author zzz
*/
public class OrchidController implements ClientApp, TorInitializationListener, Outproxy {
private final Log _log;
private volatile ClientAppState _state;
private final I2PAppContext _context;
private final ClientAppManager _mgr;
private final File _configDir;
private final File _configFile;
private TorClient _client;
private OrchidLogHandler _logger;
private static final String DEFAULT_CONFIG_DIR = ".orchid";
private static final String REGISTERED_NAME = Outproxy.NAME;
private static final String CONFIG_FILE = "orchid.config";
/**
* Instantiation only. Caller must call startup().
* Config file problems will not throw exception until startup().
*
* @param mgr may be null
* @param args one arg, the config file, if not absolute will be relative to the context's config dir,
* if empty or null, the default is i2ptunnel.config
*/
public OrchidController(I2PAppContext context, ClientAppManager mgr, String[] args) {
_state = UNINITIALIZED;
_context = context;
_mgr = mgr;
_log = _context.logManager().getLog(OrchidController.class);
if (args == null || args.length <= 0)
_configDir = new File(System.getProperty("user.home"), DEFAULT_CONFIG_DIR);
else if (args.length >= 1)
_configDir = new File(args[0], "data");
else
throw new IllegalArgumentException("Usage: OrchidController [configDir]");
_configFile = new File(_configDir.getParentFile(), CONFIG_FILE);
_state = INITIALIZED;
}
public void initializationProgress(String message, int percent) {
if (_log.shouldLog(Log.INFO))
_log.info(message + ' ' + percent + '%');
}
public void initializationCompleted() {
changeState(RUNNING);
if (_mgr != null)
_mgr.register(this);
if (_log.shouldLog(Log.INFO))
_log.info("Orchid plugin ready");
}
/**
* ClientApp interface
* @throws IllegalArgumentException if unable to load config from file
*/
public synchronized void startup() {
if (_state != INITIALIZED && _state != STOPPED)
throw new IllegalStateException();
changeState(STARTING);
if (_log.shouldLog(Log.INFO))
_log.info("Starting Orchid plugin...");
// TODO config dir
try {
_logger = new OrchidLogHandler(_context);
_client = new TorClient();
_client.getConfig().setDataDirectory(_configDir);
loadConfig(_client.getConfig());
_client.addInitializationListener(this);
CountryCodeService.getInstance();
_client.start();
} catch (RuntimeException t) {
// TorException extends RuntimeException,
// unlimited strength policy files not installed
changeState(START_FAILED, t);
throw t;
}
if (_mgr != null) {
// Don't register until initializationCompleted()
//_mgr.register(this);
// RouterAppManager registers its own shutdown hook
} else {
_context.addShutdownTask(new Shutdown());
}
}
/**
* ClientApp interface
*/
public ClientAppState getState() {
return _state;
}
/**
* ClientApp interface
*/
public String getName() {
return REGISTERED_NAME;
}
/**
* ClientApp interface
*/
public String getDisplayName() {
return REGISTERED_NAME;
}
/**
*
*/
private void changeState(ClientAppState state) {
changeState(state, null);
}
/**
*
*/
private synchronized void changeState(ClientAppState state, Exception e) {
_state = state;
if (_mgr != null)
_mgr.notify(this, state, null, e);
}
/**
*
*/
private class Shutdown implements Runnable {
public void run() {
shutdown();
}
}
/**
* ClientApp interface
*/
public void shutdown(String[] args) {
shutdown();
}
/**
* Stop everything
*/
public synchronized void shutdown() {
if (_state != STARTING && _state != RUNNING)
return;
changeState(STOPPING);
if (_log.shouldLog(Log.INFO))
_log.info("Stopping Orchid plugin...");
if (_mgr != null)
_mgr.unregister(this);
if (_client != null) {
_client.stop();
_client = null;
}
if (_logger != null) {
_logger.close();
_logger = null;
}
changeState(STOPPED);
if (_log.shouldLog(Log.INFO))
_log.info("Orchid plugin stopped");
}
public Socket connect(String host, int port) throws IOException {
if (host.equals("127.0.0.1") || host.equals("localhost") ||
host.toLowerCase(Locale.US).endsWith(".i2p"))
throw new IOException("unsupported host: " + host);
ClientAppState state = _state;
if (state != RUNNING)
throw new IOException("Cannot connect in state " + state);
if (_log.shouldLog(Log.INFO))
_log.info("Connecting to " + host + ':' + port);
try {
return new TorStreamSocket(_client, host, port);
} catch (IOException ioe) {
throw ioe;
} catch (Exception e) {
IOException ioe = new IOException("connect error");
ioe.initCause(e);
if (_log.shouldLog(Log.DEBUG))
_log.debug("Error connecting to " + host + ':' + port + "\n* Reason:" + ioe.getMessage());
throw ioe;
}
}
public synchronized TorConfig getConfig() {
if (_client != null)
return _client.getConfig();
// else load from file
return null;
}
public synchronized void saveConfig() {
// ...
}
public synchronized void loadConfig(TorConfig tc) {
Properties p = new Properties();
try {
DataHelper.loadProps(p, _configFile);
} catch (IOException ioe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error loading Orchid config file \n* Reason: " + ioe.getMessage());
return;
}
if (_log.shouldLog(Log.INFO))
_log.info("Loading " + p.size() + " Orchid configuration options");
TorConfigParser tcp = new TorConfigParser();
for (Map.Entry e : p.entrySet()) {
String k = (String) e.getKey();
String v = (String) e.getValue();
if (k.equals("bridges")) {
// unimplemented in parser, will throw IAE
List<TorConfigBridgeLine> list = (List<TorConfigBridgeLine>) tcp.parseValue(v, "BRIDGE_LINE");
for (TorConfigBridgeLine tcbl : list) {
tc.addBridge(tcbl.getAddress(), tcbl.getPort(), tcbl.getFingerprint());
}
} else if (k.equals("circuitBuildTimeout")) {
TorConfigInterval tci = (TorConfigInterval) tcp.parseValue(v, "INTERVAL");
tc.setCircuitBuildTimeout(tci.getMilliseconds(), TimeUnit.MILLISECONDS);
} else if (k.equals("circuitIdleTimeout")) {
TorConfigInterval tci = (TorConfigInterval) tcp.parseValue(v, "INTERVAL");
tc.setCircuitIdleTimeout(tci.getMilliseconds(), TimeUnit.MILLISECONDS);
} else if (k.equals("circuitStreamTimeout")) {
TorConfigInterval tci = (TorConfigInterval) tcp.parseValue(v, "INTERVAL");
tc.setCircuitStreamTimeout(tci.getMilliseconds(), TimeUnit.MILLISECONDS);
} else if (k.equals("clientRejectInternalAddress")) {
tc.setClientRejectInternalAddress((Boolean) tcp.parseValue(v, "BOOLEAN"));
} else if (k.equals("enforceDistinctSubnets")) {
tc.setEnforceDistinctSubnets((Boolean) tcp.parseValue(v, "INTEGER"));
} else if (k.equals("numEntryGuards")) {
tc.setNumEntryGuards((Integer) tcp.parseValue(v, "INTEGER"));
} else if (k.equals("entryNodes")) {
tc.setEntryNodes((List<String>) tcp.parseValue(v, "STRINGLIST"));
} else if (k.equals("excludeExitNodes")) {
tc.setExcludeExitNodes((List<String>) tcp.parseValue(v, "STRINGLIST"));
} else if (k.equals("excludeNodes")) {
tc.setExcludeNodes((List<String>) tcp.parseValue(v, "STRINGLIST"));
} else if (k.equals("exitNodes")) {
tc.setExitNodes((List<String>) tcp.parseValue(v, "STRINGLIST"));
} else if (k.equals("fascistFirewall")) {
tc.setFascistFirewall((Boolean) tcp.parseValue(v, "INTEGER"));
} else if (k.equals("firewallPorts")) {
tc.setFirewallPorts((List<Integer>) tcp.parseValue(v, "PORTLIST"));
} else if (k.equals("handshakeV2Enabled")) {
tc.setHandshakeV2Enabled((Boolean) tcp.parseValue(v, "BOOLEAN"));
} else if (k.equals("handshakeV3Enabled")) {
tc.setHandshakeV3Enabled((Boolean) tcp.parseValue(v, "BOOLEAN"));
} else if (k.equals("longLivedPorts")) {
tc.setLongLivedPorts((List<Integer>) tcp.parseValue(v, "PORTLIST"));
} else if (k.equals("maxCircuitDirtiness")) {
TorConfigInterval tci = (TorConfigInterval) tcp.parseValue(v, "INTERVAL");
tc.setMaxCircuitDirtiness(tci.getMilliseconds(), TimeUnit.MILLISECONDS);
} else if (k.equals("maxClientCircuitsPending")) {
tc.setMaxClientCircuitsPending((Integer) tcp.parseValue(v, "INTEGER"));
} else if (k.equals("newCircuitPeriod")) {
TorConfigInterval tci = (TorConfigInterval) tcp.parseValue(v, "INTERVAL");
tc.setNewCircuitPeriod(tci.getMilliseconds(), TimeUnit.MILLISECONDS);
} else if (k.equals("safeLogging")) {
tc.setSafeLogging((Boolean) tcp.parseValue(v, "BOOLEAN"));
} else if (k.equals("safeSocks")) {
tc.setSafeSocks((Boolean) tcp.parseValue(v, "BOOLEAN"));
} else if (k.equals("strictNodes")) {
tc.setStrictNodes((Boolean) tcp.parseValue(v, "BOOLEAN"));
} else if (k.equals("useBridges")) {
tc.setUseBridges((Boolean) tcp.parseValue(v, "BOOLEAN"));
} else if (k.equals("useMicrodescriptors")) {
tc.setUseMicrodescriptors((AutoBoolValue) tcp.parseValue(v, "AUTOBOOL"));
} else if (k.equals("useNTorHandshake")) {
tc.setUseNTorHandshake((AutoBoolValue) tcp.parseValue(v, "AUTOBOOL"));
} else if (k.equals("warnUnsafeSocks")) {
tc.setWarnUnsafeSocks((Boolean) tcp.parseValue(v, "BOOLEAN"));
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("Invalid Orchid config entry: " + k + " = " + v);
}
}
}
public synchronized void renderStatusHTML(PrintWriter out) throws IOException {
if (_client == null)
return;
// can't get to TorConfig's Dashboard from here so make a new one
StringWriter sw = new StringWriter(1024);
PrintWriter pw = new PrintWriter(sw);
(new Dashboard()).renderComponent(pw, 0xff, _client.getCircuitManager());
pw.close();
out.write(sw.toString());
}
}