- 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
342 lines
13 KiB
Java
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());
|
|
}
|
|
|
|
}
|
|
|