You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

846 lines
25 KiB
Java

/*
* Copyright (C) 2009-2017 Alistair Neil <info@dazzleships.net>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package client;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.swing.JTextArea;
import javax.swing.text.BadLocationException;
import lib.ClientProcess;
import lib.SimpleFile;
/**
*
* @author Alistair Neil <info@dazzleships.net>
*/
public class TorProcess extends ClientProcess {
public static final int LOG_DEBUG = 0;
public static final int LOG_INFO = 1;
public static final int LOG_NOTICE = 2;
private static final String TORCONFIGFILE = "torrc";
public static final String EMPTYSTRING = "";
// Event constants
public static final int TOR_MESSAGE = 0;
public static final int TOR_BOOT_TIMEOUT = 1;
public static final int TOR_BOOT_FATAL = 2;
public static final int TOR_BOOT_ERROR = 3;
public static final int TOR_CLOCK_ERROR = 4;
public static final int TOR_NOROUTE = 5;
public static final int TOR_BOOTED = 6;
public static final int TOR_RESTARTED = 7;
public static final int TOR_NOEXITS = 8;
public static final int TOR_STOPPED = 9;
public static final int TOR_BRIDGE = 10;
public static final int TOR_NEWCIRC = 11;
public static final int TOR_DIRINFO_STALE = 12;
public static final int TOR_NOHOP0 = 13;
public static final int TOR_NONET_ACTIVITY = 14;
private static final String[] EVENTMESSAGES = new String[]{
"TOR_MESSAGE", "TOR_BOOT_TIMEOUT", "TOR_BOOT_FATAL", "TOR_BOOT_ERROR",
"TOR_CLOCK_ERROR", "TOR_NOROUTE", "TOR_BOOTED", "TOR_RESTARTED",
"TOR_NOEXITS", "TOR_STOPPED", "TOR_BRIDGE", "TOR_NEWCIRC",
"TOR_DIRINFO_STALE", "TOR_NOHOP0", "TOR_NONET_ACTIVITY"
};
private final LinkedHashMap<String, String> lhmCLIOptions;
private final LinkedHashMap<String, String> lhmTorrcOptions;
private final String strClientLocation;
private final String strConfigFolder;
private String strSecret;
private String strCachedDataFolder;
private String invCommas = "\"";
private String strExternalArgs = "";
private String strBridges = "";
private int intListenPort;
private int intInitialBootEvent;
private int loglev = LOG_NOTICE;
private float version = 9999;
private int maxlines = 50;
private int nolines;
private JTextArea jtxtstdout;
private boolean boolSilentBoot;
/**
* Constructor
*
* @param clientpath Path to Tor client
* @param configfolder Filepath to torrc
*/
public TorProcess(String clientpath, String configfolder) {
setSilentBootEnabled(false);
strClientLocation = clientpath;
this.intInitialBootEvent = TOR_BOOTED;
this.strConfigFolder = configfolder;
this.lhmCLIOptions = new LinkedHashMap<>();
this.lhmTorrcOptions = new LinkedHashMap<>();
// All our initialisation
if (SimpleFile.getSeparator().compareTo("/") == 0) {
this.invCommas = "";
}
}
/**
* Starts Tor process, and issues booted event on completion
*
*/
public final void startProcess() {
setStartupTimeout(60);
// Cache age check
float age = getCacheAge();
Logger.getGlobal().logp(Level.INFO, TorProcess.class.getName(),
"start() on Port=" + getListenPort(), "Cache age = " + (int) age + " minutes.");
if (age > 180) {
deleteCacheData();
Logger.getGlobal().logp(Level.INFO, TorProcess.class.getName(),
"start() on Port=" + getListenPort(), "Cache stale, deleting old cache data.");
}
// Some essential initialisation
String strConfig = strClientLocation
+ " -f " + invCommas
+ getConfigFilePath()
+ invCommas
+ " " + getCLIOptionsAsString()
+ " " + strExternalArgs;
Logger.getGlobal().logp(Level.INFO, TorProcess.class.getName(),
"start() on Port=" + getListenPort(),
"INFO: starting with " + strConfig );
super.start(strConfig);
}
/**
* Set the Tor stdout log level
*
* @param lev
*/
public final void setLogLevel(int lev) {
loglev = lev;
}
/**
* Set external user provided startup arguments
*
* @param torargs
*/
public final void setExternalArgs(String torargs) {
strExternalArgs = torargs;
}
/**
* Set the listening port
*
* @param port
*/
public final void setListenPort(int port) {
// Setup listening port and control port
if (intListenPort == port) {
return;
}
intListenPort = port;
setCLIOption("SocksPort", "127.0.0.1:" + String.valueOf(port));
setCLIOption("ControlSocket", "/run/tor/control");
setCLIOption("ControlPort", "127.0.0.1:" + String.valueOf(port + 1));
// Finish of by creating the default config file and data folders
createDataFolder();
}
/**
* Set the event that is issued on process boot completion
*
* @param event
*/
public final void setInitialBootEvent(int event) {
intInitialBootEvent = event;
}
/**
* Client event handler, can be overriden by parent class
*
* @param data line data from standard output of Tor client
*/
@Override
public final void clientProcessEventFired(String data) {
Logger.getGlobal().logp(Level.FINER, TorProcess.class.getName(),
"clientProcessEventFired() on Port=" + getListenPort(), data);
if (!data.isEmpty()) {
appendStdout(data);
}
// Check for process stopped
if (getClientStatus() == CLIENT_STOPPED) {
torProcessEventFired(TOR_STOPPED, null);
return;
}
// Check for timeout
if (getClientStatus() == CLIENT_TIMEDOUT) {
torProcessEventFired(TOR_BOOT_TIMEOUT, null);
return;
}
if (getClientStatus() == CLIENT_RUNNING) {
// Check to see if we need to copy cache
if (strCachedDataFolder != null) {
SimpleFile.copyFolderContents(strCachedDataFolder, getDataFolder() + SimpleFile.getSeparator(), "torrc");
strCachedDataFolder = null;
}
// Check for fatal tor process startup errors
if (data.contains("[warn] Our clock")) {
torProcessEventFired(TOR_CLOCK_ERROR, data);
return;
}
// Check for errors
if (data.contains("exception") || data.contains("[err]")) {
torProcessEventFired(TOR_BOOT_FATAL, data);
return;
}
// Check for network unreachable
if (data.contains("NOROUTE")) {
torProcessEventFired(TOR_NOROUTE, data);
return;
}
// Check new tor bridge
if (data.contains("[notice] new bridge")) {
torProcessEventFired(TOR_BRIDGE, data);
return;
}
// Check for conditions that may prevent circuit building
if (data.contains("directory information is no longer up-to-date")) {
torProcessEventFired(TOR_DIRINFO_STALE, data);
return;
}
// Check for conditions that will prevent circuit building
if (data.contains("All routers are down")) {
torProcessEventFired(TOR_NOEXITS, data);
return;
}
// Check for Tor retrying on new circuit
if (data.contains("Retrying on a new circuit")) {
torProcessEventFired(TOR_NEWCIRC, data);
return;
}
// Check for Tor failed to find hop zero
if (data.contains("Failed to find node for hop 0")) {
torProcessEventFired(TOR_NOHOP0, data);
return;
}
// Check for no net activity
if (data.contains("Tor has not observed any network activity")) {
torProcessEventFired(TOR_NONET_ACTIVITY, data);
return;
}
// Check for a bootstrap message
if (data.contains("Bootstrapped")) {
// If silent then dont fire bootstrapping messages
if (!boolSilentBoot) {
torProcessEventFired(TOR_MESSAGE, data.substring(data.indexOf(']') + 2));
}
int percent = getPercentage(data);
// Set startup timeout based on percentage of Tor progression
if (percent >= 15) {
setStartupTimeout(60);
}
if (percent >= 40) {
setStartupTimeout(120);
}
if (percent >= 80) {
setStartupTimeout(30);
}
// Check for initial bootup completion
if (percent >= 100) {
setStartupTimeout(-1);
torProcessEventFired(intInitialBootEvent, null);
intInitialBootEvent = TOR_BOOTED;
setSilentBootEnabled(false);
}
}
}
}
/**
* Get percentage value from bootstrap message
*
* @param data
* @return percentage as and int
*/
private int getPercentage(String data) {
int result = -1;
int idx1 = data.indexOf("Bootstrapped ");
if (idx1 > -1) {
idx1 += 13;
int idx2 = data.indexOf('%', idx1);
if (idx2 > -1) {
String temp = data.substring(idx1, idx2);
result = Integer.parseInt(temp);
}
}
return result;
}
/**
* Enable/Disable bootstrap message events on startup
*
* @param enabled
*/
public final void setSilentBootEnabled(boolean enabled) {
boolSilentBoot = enabled;
}
/**
* Get textual representation on an event
*
* @param event
* @return Event as text
*/
public String getEventMessage(int event) {
return EVENTMESSAGES[event];
}
/**
* Called if an event was fired, will be overidden by sub class
*
* @param event
* @param data
*/
public void torProcessEventFired(int event, String data) {
}
public final void setControlPassword(String secret, String hashpass) {
strSecret = secret;
setCLIOption("hashedcontrolpassword", hashpass);
}
/**
* Get the currently set authentification secret
*
* @return String
*/
public final String getSecret() {
return strSecret;
}
/**
* Set Tor bridges, supports multiple addresses
*
* @param bridges
*/
public final void setBridges(String bridges) {
clearCLIOption("UseBridges");
clearCLIOption("Bridge");
clearCLIOption("UpdateBridgesFromAuthority");
strBridges = "";
if (bridges == null || bridges.isEmpty()) {
return;
}
StringBuilder sbBridgesOption = new StringBuilder();
String sep = "";
String[] arrBridges = Pattern.compile(",").split(bridges);
for (String s : arrBridges) {
sbBridgesOption.append(sep);
sbBridgesOption.append(s);
if (sep.isEmpty()) {
sep = " --Bridge ";
}
}
strBridges = bridges;
setCLIOption("UseBridges", "1");
setCLIOption("UpdateBridgesFromAuthority", "1");
setCLIOption("Bridge", sbBridgesOption.toString());
}
/**
* Validate bridge addresses
*
* @param bridges
* @return true if valid
*/
public boolean validateBridges(String bridges) {
if (bridges.isEmpty()) {
return true;
}
String[] arrBridges = Pattern.compile(",").split(bridges);
for (String s : arrBridges) {
if (!validateHostPort(s)) {
return false;
}
}
return true;
}
/**
* Validate a host:port ipv4 address
*
* @param hostport
* @return true if valid
*/
public final boolean validateHostPort(String hostport) {
try {
URI uri = new URI("my://" + hostport); // may throw URISyntaxException
if (uri.getHost() == null || uri.getPort() == -1) {
return false;
}
} catch (Exception ex) {
return false;
}
return true;
}
/**
* Get bridges
*
* @return bridges as csv string
*/
public final String getBridges() {
return strBridges;
}
/**
* Set Ownership process id, useful for proper process termination in event
* of a crash
*
* @param processid
*/
public void setOwnershipID(String processid) {
this.setCLIOption("__OwningControllerProcess", processid);
}
/**
* Return the currently set process ownership ID
*
* @return String process id
*/
public String getOwnershipID() {
return getCLIOptions("__OwningControllerProcess");
}
/**
* Set the path to the geoip file
*
* @param filepath File location
*/
public final void setGeoIP4(String filepath) {
if (filepath != null) {
if (SimpleFile.exists(filepath)) {
setTorrcOption("GeoIPFile", invCommas + filepath + invCommas);
} else {
clearTorrcOption("GeoIPFile");
}
} else {
clearTorrcOption("GeoIPFile");
}
}
/**
* Set the path to the geoip file
*
* @param filepath File location
*/
public final void setGeoIP6(String filepath) {
if (filepath != null) {
if (SimpleFile.exists(filepath)) {
setTorrcOption("GeoIPv6File", invCommas + filepath + invCommas);
} else {
clearTorrcOption("GeoIPv6File");
}
} else {
clearTorrcOption("GeoIPv6File");
}
}
/**
* Get Tor client location as a filepath
*
* @return filepath as string
*/
public final String getClientLocation() {
return strClientLocation;
}
/**
* Get the path to the configuration file
*
* @return String Path to configuration file
*/
public final String getConfigFilePath() {
return getDataFolder() + File.separator + TORCONFIGFILE;
}
/**
* Get a previously added tor option string
*
* @param option Tor option key
* @return String Tor option value
*/
public final String getCLIOptions(String option) {
return lhmCLIOptions.get(option);
}
/**
* Add a tor option string which is passed to the tor client on startup. See
* Tor documentation for valid options.
*
* @param option
* @param value
*/
public final void setCLIOption(String option, String value) {
lhmCLIOptions.put(option, value);
}
/**
* Add a tor option boolean value
*
* @param option
* @param value
*/
public final void setBoolTorOption(String option, boolean value) {
lhmCLIOptions.remove(option);
if (value) {
lhmCLIOptions.put(option, "1");
} else {
lhmCLIOptions.put(option, "0");
}
}
/**
* Get a previously added tor option string
*
* @param option Tor option key
* @return String Tor option value
*/
public final String getTorrcOption(String option) {
return lhmTorrcOptions.get(option);
}
/**
* Add a torrc option string See Tor documentation for valid options.
*
* @param option
* @param value
*/
public final void setTorrcOption(String option, String value) {
if (value.startsWith("\"")) {
value = value.replace('\\', '/');
}
lhmTorrcOptions.put(option, value);
}
/**
* Gets all the currently set torrc options as single String
*
* @return String Tor client formatted cli arguments
*/
public final String getTorrcOptionsAsString() {
Iterator<String> iterator = lhmTorrcOptions.keySet().iterator();
String key;
StringBuilder sbResult = new StringBuilder();
while (iterator.hasNext()) {
key = iterator.next();
sbResult.append(key);
sbResult.append(" ");
sbResult.append(lhmTorrcOptions.get(key));
sbResult.append("\r\n");
}
return sbResult.toString();
}
/**
* Remove previously add torrc option
*
* @param option
*/
public final void clearTorrcOption(String option) {
lhmTorrcOptions.remove(option);
}
/**
* Get a previously added tor option boolean value
*
* @param option Tor option key
* @return Boolean value
*/
public final boolean getCLIOptionBool(String option) {
return lhmCLIOptions.get(option).contentEquals("1");
}
/**
* Remove previously add tor option
*
* @param option
*/
public final void clearCLIOption(String option) {
lhmCLIOptions.remove(option);
}
/**
* Gets all the currently set tor options as single String for use as
* arguments passed to tor client
*
* @return String Tor cleint formatted cli arguments
*/
public final String getCLIOptionsAsString() {
Iterator<String> iterator = lhmCLIOptions.keySet().iterator();
String key;
String value;
StringBuilder sbResult = new StringBuilder();
while (iterator.hasNext()) {
key = iterator.next();
sbResult.append("--");
sbResult.append(key);
sbResult.append(" ");
value = lhmCLIOptions.get(key);
if (!value.isEmpty()) {
sbResult.append(value);
sbResult.append(" ");
}
}
return sbResult.toString().trim();
}
/**
* Get the listening port
*
* @return port
*/
public final int getListenPort() {
return intListenPort;
}
/**
* Get the control port
*
* @return port
*/
public final int getControlPort() {
return intListenPort + 1;
}
/**
* Creates the default Tor config file
*/
public final void createDefaultConfig() {
SimpleFile sfTorrc = new SimpleFile(getConfigFilePath());
sfTorrc.openBufferedWrite();
sfTorrc.writeFile(getTorrcOptionsAsString(), 0);
switch (loglev) {
case LOG_DEBUG:
sfTorrc.writeFile("log debug stdout", 1);
break;
case LOG_INFO:
sfTorrc.writeFile("log info stdout", 1);
break;
case LOG_NOTICE:
sfTorrc.writeFile("log notice stdout", 1);
break;
}
sfTorrc.writeFile(EMPTYSTRING, 1);
sfTorrc.closeFile();
}
/**
* Delete the configuration file
*/
public final void deleteConfigFile() {
SimpleFile.delete(getConfigFilePath());
}
/**
* Creates a data folder for the Tor client to put its cache data
*/
public final void createDataFolder() {
String folder = getDataFolder();
if (folder != null) {
setTorrcOption("DataDirectory", invCommas + getDataFolder() + invCommas);
SimpleFile.createFolder(getDataFolder());
}
}
/**
* Get the datafolder being used by tor client
*
* @return Path to datafolder
*/
public final String getDataFolder() {
if (strConfigFolder == null) {
return null;
}
return strConfigFolder + String.valueOf(intListenPort);
}
/**
* Get the age of the file cache in minutes
*
* @return age of cache in minutes
*/
public float getCacheAge() {
String path = getDataFolder() + SimpleFile.getSeparator()
+ "cached-consensus";
if (SimpleFile.exists(path)) {
return SimpleFile.getAgeOfFile(path, SimpleFile.PERIOD_MINUTES);
} else {
return -1;
}
}
/**
* Delete Tor cache data
*/
public final void deleteCacheData() {
SimpleFile.secureWipe(getDataFolder() + SimpleFile.getSeparator() + "cached-consensus");
SimpleFile.secureWipe(getDataFolder() + SimpleFile.getSeparator() + "cached-certs");
SimpleFile.secureWipe(getDataFolder() + SimpleFile.getSeparator() + "cached-descriptors");
SimpleFile.secureWipe(getDataFolder() + SimpleFile.getSeparator() + "cached-descriptors.new");
SimpleFile.secureWipe(getDataFolder() + SimpleFile.getSeparator() + "lock");
SimpleFile.secureWipe(getDataFolder() + SimpleFile.getSeparator() + "state");
}
/**
* This populates the the current folder whose name is derived from the
* listening port with data from the given source folder derived by the
* given port number. This effectively allows each Tor client spawned to
* have its Tor cache data copied from the first Tor client launched instead
* of having to go to the net and fetch it and thus start up is a lot
* faster. I was actually mildly suprised that this actually works.
*
* @param port
*/
public final void setCachedDataFolder(int port) {
if (port < 0) {
strCachedDataFolder = null;
return;
}
strCachedDataFolder = strConfigFolder + String.valueOf(port);
}
/**
* Get the currently set cached data folder
*
* @return path to cached data folder as String
*/
public final String getCachedDataFolder() {
return strCachedDataFolder;
}
/**
* Set the text area that will receive Stdout output
*
* @param jta
*/
public void setStdoutTextArea(JTextArea jta) {
jtxtstdout = jta;
}
/**
* Set the maximum no of lines to display in the Stdout output
*
* @param lines
*/
public void setMaxHistory(int lines) {
maxlines = lines;
}
/**
* Clear the Stdout text area
*/
public void clearStdout() {
if (jtxtstdout != null) {
jtxtstdout.setText("");
}
nolines = 0;
}
/**
* Append text to the StdOut text area
*
* @param text
*/
private void appendStdout(String text) {
if (jtxtstdout == null) {
return;
}
jtxtstdout.append(text + "\n");
if (++nolines > maxlines) {
try {
int end = jtxtstdout.getLineEndOffset(0);
jtxtstdout.replaceRange("", 0, end);
} catch (BadLocationException ex) {
}
}
jtxtstdout.setCaretPosition(jtxtstdout.getText().length());
}
/**
* Get Tor version as float
*
* @return Tor version as String
*/
public final float getVersion() {
if (version == 9999) {
BufferedReader br;
String strVer = "";
Process proc;
try {
proc = Runtime.getRuntime().exec(strClientLocation + " --version");
br = new BufferedReader(new InputStreamReader(proc.getInputStream()), 256);
String line;
while (true) {
line = br.readLine();
if (line == null) {
break;
}
strVer = line;
}
br.close();
proc.destroy();
proc.waitFor();
} catch (IOException | InterruptedException ex) {
Logger.getLogger(TorProcess.class.getName()).log(Level.SEVERE, null, ex);
}
int idx = strVer.indexOf("ion");
if (idx > -1) {
strVer = strVer.substring(idx + 4).replace(".", "");
idx = strVer.indexOf(' ');
if (idx > -1) {
strVer = strVer.substring(0, idx);
}
try {
version = Float.parseFloat("0." + strVer) * 10;
} catch (Exception ex) {
Logger.getGlobal().logp(Level.SEVERE, this.getClass().getName(), "getVersion() Port=" + intListenPort, "", ex);
}
}
}
return version;
}
}