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.
1166 lines
36 KiB
Java
1166 lines
36 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.DataInputStream;
|
|
import java.io.DataOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStreamReader;
|
|
import java.io.PrintWriter;
|
|
import java.net.Socket;
|
|
import java.net.URI;
|
|
import java.net.URISyntaxException;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import java.util.regex.Pattern;
|
|
import javax.swing.SwingUtilities;
|
|
import javax.swing.SwingWorker;
|
|
import lib.SimpleFile;
|
|
|
|
/**
|
|
*
|
|
* @author Alistair Neil <info@dazzleships.net>
|
|
*/
|
|
public class TorController extends TorProcess {
|
|
|
|
// Event constants, which supplement the ones provide by TorProcess
|
|
public static final int EVENT_CIRCUITS_BUILT = 15;
|
|
public static final int EVENT_CIRCUITS_FAILED = 16;
|
|
public static final int EVENT_CIRCUIT_BUILT = 17;
|
|
public static final int EVENT_CIRCUIT_FAILED = 18;
|
|
public static final int EVENT_LATENCY_DONE = 19;
|
|
public static final int EVENT_TESTING_DONE = 20;
|
|
public static final int EVENT_CIRCUIT_CHANGED = 21;
|
|
public static final int EVENT_ABORTED = 22;
|
|
private static final String LOCALHOST = "127.0.0.1";
|
|
private static final String[] EVENTMESSAGES = new String[]{
|
|
"EVENT_CIRCUITS_BUILT", "EVENT_CIRCUITS_FAILED", "EVENT_CIRCUIT_BUILT",
|
|
"EVENT_CIRCUIT_FAILED", "EVENT_LATENCY_DONE", "EVENT_TESTING_DONE",
|
|
"EVENT_CIRCUIT_CHANGED", "EVENT_ABORTED"
|
|
};
|
|
|
|
// Status constants
|
|
public static final int STATUS_DEAD = 0;
|
|
public static final int STATUS_BOOTING = 1;
|
|
public static final int STATUS_IDLE = 2;
|
|
public static final int STATUS_CIRCUIT_CREATION = 3;
|
|
public static final int STATUS_LATENCY_CHECKING = 4;
|
|
|
|
// Other constants
|
|
public static final long LATENCY_FAIL = 9999;
|
|
public static final int STREAM_IP = 5;
|
|
public static final int NODE_GUARD = 0;
|
|
public static final int NODE_MIDDLE = 1;
|
|
public static final int NODE_EXIT = 2;
|
|
private static final int DEFBUILDTIME = 60;
|
|
|
|
private volatile Socket sockControl;
|
|
private volatile BufferedReader brSocket;
|
|
private volatile PrintWriter pwSocket;
|
|
private String strLatencyURL;
|
|
private final ConcurrentHashMap<String, TorCircuit> chmUseableCircuits;
|
|
private ArrayList<String> alActiveStreams;
|
|
private int intStatus = STATUS_DEAD;
|
|
private volatile Socket sockProxy;
|
|
private long lngLatency;
|
|
private String strBestHops;
|
|
private long lngBestLatency;
|
|
private String entrynodes = "";
|
|
private Thread tActive;
|
|
private boolean haveEntryNode;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param clientpath Path to Tor client
|
|
* @param configfolder Location of configuration file torrc
|
|
*/
|
|
public TorController(String clientpath, String configfolder) {
|
|
super(clientpath, configfolder);
|
|
this.chmUseableCircuits = new ConcurrentHashMap<>();
|
|
this.alActiveStreams = new ArrayList<>();
|
|
}
|
|
|
|
/**
|
|
* Tor process event
|
|
*
|
|
* @param event
|
|
* @param data
|
|
*/
|
|
@Override
|
|
public final void torProcessEventFired(int event, String data) {
|
|
|
|
Logger.getGlobal().logp(Level.FINE, TorProcess.class.getName(),
|
|
"torProcessEventFired() on Port=" + getListenPort(), getEventMessage(event) + ", Data=" + data);
|
|
|
|
switch (event) {
|
|
case TOR_BRIDGE:
|
|
haveEntryNode = true;
|
|
break;
|
|
case TOR_STOPPED:
|
|
setStatus(STATUS_DEAD);
|
|
break;
|
|
case TOR_BOOTED:
|
|
case TOR_RESTARTED:
|
|
openControlSocket();
|
|
// Authenticate our control
|
|
authenticateTor(getSecret());
|
|
takeOwnership();
|
|
waitForBridgeNodes(DEFBUILDTIME);
|
|
setStatus(STATUS_IDLE);
|
|
break;
|
|
}
|
|
controllerEventFired(event, data);
|
|
}
|
|
|
|
@Override
|
|
public String getEventMessage(int event) {
|
|
if (event < EVENT_CIRCUITS_BUILT) {
|
|
return super.getEventMessage(event);
|
|
} else {
|
|
return EVENTMESSAGES[event - EVENT_CIRCUITS_BUILT];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle for controllerEventFired should be overridden by parent class
|
|
*
|
|
* @param event
|
|
* @param data
|
|
*/
|
|
public void controllerEventFired(int event, Object data) {
|
|
}
|
|
|
|
/**
|
|
* Set status flag
|
|
*
|
|
* @param status
|
|
*/
|
|
public synchronized void setStatus(int status) {
|
|
intStatus = status;
|
|
}
|
|
|
|
/**
|
|
* Get current status
|
|
*
|
|
* @return status integer constant, see defined status constants
|
|
*/
|
|
public synchronized int getStatus() {
|
|
return intStatus;
|
|
}
|
|
|
|
/**
|
|
* Convenience test for idle status
|
|
*
|
|
* @return true if idle
|
|
*/
|
|
public boolean isIdle() {
|
|
return (getStatus() <= STATUS_IDLE);
|
|
}
|
|
|
|
/**
|
|
* Stop the tor controller process completely
|
|
*
|
|
*/
|
|
public final void stop() {
|
|
haveEntryNode = false;
|
|
abortActions();
|
|
Logger.getGlobal().logp(Level.INFO, TorController.class.getName(),
|
|
"stop() on Port=" + getListenPort(), "Stop requested");
|
|
sendCommand("QUIT");
|
|
closeControlSocket();
|
|
stopProcess();
|
|
setStatus(STATUS_DEAD);
|
|
}
|
|
|
|
/**
|
|
* Start tor controller process and issue the TOR_BOOTED event
|
|
*
|
|
*/
|
|
public final void start() {
|
|
createDefaultConfig();
|
|
start(TOR_BOOTED);
|
|
}
|
|
|
|
/**
|
|
* Start tor controller process and issue the given event
|
|
*
|
|
* @param bootevent
|
|
*/
|
|
public final void start(int bootevent) {
|
|
if (getStatus() == STATUS_DEAD) {
|
|
setInitialBootEvent(bootevent);
|
|
setStatus(STATUS_BOOTING);
|
|
startProcess();
|
|
return;
|
|
}
|
|
if (getStatus() > STATUS_BOOTING) {
|
|
setStatus(STATUS_IDLE);
|
|
controllerEventFired(bootevent, null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abort all current actions
|
|
*/
|
|
public final void abortActions() {
|
|
if (getStatus() < STATUS_IDLE) {
|
|
return;
|
|
}
|
|
setStatus(STATUS_IDLE);
|
|
// Abort latency checking
|
|
abortLatencyCheck();
|
|
// Interrupt sleep thread if active
|
|
Thread t = getActiveThread();
|
|
if (t != null) {
|
|
t.interrupt();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abort latency checking
|
|
*/
|
|
public final void abortLatencyCheck() {
|
|
// Kill off latency checking proxy socket
|
|
if (sockProxy != null) {
|
|
try {
|
|
sockProxy.close();
|
|
} catch (IOException ex) {
|
|
}
|
|
}
|
|
sockProxy = null;
|
|
}
|
|
|
|
/**
|
|
* Save configuration file
|
|
*/
|
|
public void saveConf() {
|
|
SimpleFile sf = new SimpleFile(getConfigFilePath());
|
|
sf.delete();
|
|
sendCommand("SAVECONF");
|
|
}
|
|
|
|
/**
|
|
* Load configuration file
|
|
*/
|
|
public void loadConf() {
|
|
SimpleFile sf = new SimpleFile(getConfigFilePath());
|
|
sf.openBufferedRead();
|
|
String text = sf.readEntireFile();
|
|
sf.closeFile();
|
|
sendCommand("+loadconf\r\n" + text + "\r\n.");
|
|
}
|
|
|
|
/**
|
|
* Attempts to return the Country associated with an ip address
|
|
*
|
|
* @param ip
|
|
* @return Country or null if not found
|
|
*/
|
|
public String getCountryFromIP(String ip) {
|
|
// Get country from ip address
|
|
String cmd = "ip-to-country/" + ip;
|
|
ArrayList<String> infoList = getInfo(cmd);
|
|
try {
|
|
if (infoList != null) {
|
|
return infoList.get(0).toUpperCase();
|
|
}
|
|
} catch (Exception ex) {
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Set the url used for latency checking
|
|
*
|
|
* @param url
|
|
*/
|
|
public void setTestingURL(String url) {
|
|
strLatencyURL = url;
|
|
}
|
|
|
|
/**
|
|
* Take ownership of the Tor client process so that it shuts down if the
|
|
* process is destroyed, particular useful for linux desktops that dont
|
|
* issue proper terminations.
|
|
*/
|
|
private void takeOwnership() {
|
|
sendCommand("TAKEOWNERSHIP");
|
|
sendCommand("RESETCONF __OwningControllerProcess");
|
|
}
|
|
|
|
/**
|
|
* Authenticate a password protected tor control socket
|
|
*
|
|
* @param password
|
|
*/
|
|
private void authenticateTor(String password) {
|
|
if (password == null) {
|
|
sendCommand("AUTHENTICATE");
|
|
} else {
|
|
sendCommand("AUTHENTICATE " + "\"" + password + "\"");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable/Disable predictive circuit building
|
|
*
|
|
* @param enabled
|
|
*/
|
|
public void enablePredictiveCircuits(boolean enabled) {
|
|
if (enabled) {
|
|
setConf("__DisablePredictedCircuits=0");
|
|
} else {
|
|
setConf("__DisablePredictedCircuits=1");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get tor status information for a given property. See TOR control-spec
|
|
* documentation for valid properties
|
|
*
|
|
* @param property
|
|
* @return Arraylist containg the result of the command
|
|
*/
|
|
public final ArrayList<String> getInfo(String property) {
|
|
return sendCommand("GETINFO " + property);
|
|
}
|
|
|
|
/**
|
|
* Close a circuit with ID
|
|
*
|
|
* @param id
|
|
*/
|
|
public final void closeCircuit(String id) {
|
|
sendCommand("CLOSECIRCUIT " + id);
|
|
}
|
|
|
|
/**
|
|
* Set a tor configuration property
|
|
*
|
|
* @param property
|
|
* @return Arraylist containg the result of the command
|
|
*/
|
|
public final ArrayList<String> setConf(String property) {
|
|
return sendCommand("SETCONF " + property);
|
|
}
|
|
|
|
/**
|
|
* reset a tor configuration property
|
|
*
|
|
* @param property
|
|
* @return Arraylist containing the result of the command
|
|
*/
|
|
public final ArrayList<String> resetConf(String property) {
|
|
return sendCommand("RESETCONF " + property);
|
|
}
|
|
|
|
/**
|
|
* Send signal to tor
|
|
*
|
|
* @param cmd
|
|
* @return Arraylist containing the result of the command
|
|
*/
|
|
public final ArrayList<String> signal(String cmd) {
|
|
return sendCommand("SIGNAL " + cmd);
|
|
}
|
|
|
|
/**
|
|
* Get entry guards chosen by tor client
|
|
*
|
|
* @return entry guards as comma separated fingerprints
|
|
*/
|
|
public String getEntryGuardsAsCSV() {
|
|
ArrayList<String> al = getInfo("entry-guards");
|
|
StringBuilder sbResult = new StringBuilder();
|
|
if (!al.contains("250 OK")) {
|
|
return sbResult.toString();
|
|
}
|
|
// Remove 250 OK entry
|
|
al.remove("250 OK");
|
|
String sep = "";
|
|
for (String s : al) {
|
|
if (s.contains("~")) {
|
|
sbResult.append(sep);
|
|
sbResult.append(s.substring(0, s.indexOf('~')));
|
|
if (sep.isEmpty()) {
|
|
sep = ",";
|
|
}
|
|
}
|
|
}
|
|
return sbResult.toString();
|
|
}
|
|
|
|
/**
|
|
* Triggers a socks latency check, EVENT_LATENCY_CHECK is fired on
|
|
* completion
|
|
*
|
|
* @param timeout
|
|
*/
|
|
public final void doLatencyCheck(final int timeout) {
|
|
|
|
Thread t = new Thread(new java.lang.Runnable() {
|
|
long latency;
|
|
|
|
@Override
|
|
public void run() {
|
|
latency = getTorLatency(timeout);
|
|
SwingUtilities.invokeLater(new java.lang.Runnable() {
|
|
@Override
|
|
public void run() {
|
|
lngLatency = latency;
|
|
//? controllerEventFired(EVENT_LATENCY_DONE, null);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
if (getStatus() < STATUS_IDLE) {
|
|
return;
|
|
}
|
|
alActiveStreams = getInfo("stream-status");
|
|
alActiveStreams.remove("250 OK");
|
|
t.start();
|
|
}
|
|
|
|
/**
|
|
* Get current latency
|
|
*
|
|
* @return latency in ms as long
|
|
*/
|
|
public final long getLatency() {
|
|
return lngLatency;
|
|
}
|
|
|
|
/**
|
|
* Returns measured latency for the active circuit without creating a
|
|
* stream, this blocks so be careful
|
|
*
|
|
* @param timeout
|
|
* @return latency
|
|
*/
|
|
public long getTorLatency(int timeout) {
|
|
long lngResult = LATENCY_FAIL;
|
|
DataInputStream dis = null;
|
|
try {
|
|
sockProxy = createTorSocketToURL(strLatencyURL, true);
|
|
if (sockProxy != null) {
|
|
sockProxy.setSoTimeout(timeout);
|
|
dis = new DataInputStream(sockProxy.getInputStream());
|
|
long lngStart = System.currentTimeMillis();
|
|
dis.skipBytes(1);
|
|
lngResult = System.currentTimeMillis() - lngStart;
|
|
}
|
|
} catch (IOException ex) {
|
|
Logger.getGlobal().logp(Level.INFO, TorController.class.getName(),
|
|
"getTorLatency Exception " + getListenPort(), ex.getMessage());
|
|
}
|
|
try {
|
|
if (!sockProxy.isClosed()) {
|
|
sockProxy.close();
|
|
}
|
|
sockProxy = null;
|
|
} catch (Exception ex) {
|
|
}
|
|
try {
|
|
if (dis != null) {
|
|
dis.close();
|
|
}
|
|
} catch (Exception ex) {
|
|
}
|
|
return lngResult;
|
|
}
|
|
|
|
/**
|
|
* Close open circuits except circuit specified by id
|
|
*
|
|
* @param id
|
|
* @param filtered
|
|
*/
|
|
public void closeCircuitsExcept(String id, boolean filtered) {
|
|
Logger.getGlobal().logp(Level.FINEST, TorController.class.getName(),
|
|
"closeCircuitsExcept() Port=" + getListenPort(), "");
|
|
Set keys = getBuiltCircuits(filtered).keySet();
|
|
Iterator i = keys.iterator();
|
|
String hopid;
|
|
while (i.hasNext()) {
|
|
hopid = (String) i.next();
|
|
if (!hopid.contentEquals(id)) {
|
|
closeCircuit(hopid);
|
|
}
|
|
}
|
|
}
|
|
|
|
private String getCircuitIdFromStream(String stream) {
|
|
String data[];
|
|
Pattern pat = Pattern.compile(" ");
|
|
data = pat.split(stream);
|
|
return data[2];
|
|
}
|
|
|
|
/**
|
|
* Get list of active streams
|
|
*
|
|
* @return active streams as a list
|
|
*/
|
|
public final ArrayList<String> getActiveStreams() {
|
|
return alActiveStreams;
|
|
}
|
|
|
|
/**
|
|
* Set the csv list of exit node fingers to be used by tor, a single exit
|
|
* node may also be specified, this does not block
|
|
*
|
|
* @param fingers
|
|
* @param nocircs
|
|
*/
|
|
public final void activateNodes(final String fingers, final int nocircs) {
|
|
|
|
Thread t = new Thread(new java.lang.Runnable() {
|
|
|
|
@Override
|
|
public void run() {
|
|
activateNodesBlocking(fingers, nocircs);
|
|
SwingUtilities.invokeLater(new java.lang.Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (isIdle()) {
|
|
controllerEventFired(EVENT_ABORTED, null);
|
|
return;
|
|
}
|
|
setStatus(STATUS_IDLE);
|
|
if (chmUseableCircuits.isEmpty()) {
|
|
controllerEventFired(EVENT_CIRCUITS_FAILED, 0);
|
|
} else {
|
|
controllerEventFired(EVENT_CIRCUITS_BUILT, chmUseableCircuits.size());
|
|
}
|
|
}
|
|
});
|
|
|
|
}
|
|
});
|
|
if (getStatus() < STATUS_IDLE) {
|
|
return;
|
|
}
|
|
abortActions();
|
|
setStatus(STATUS_CIRCUIT_CREATION);
|
|
t.start();
|
|
}
|
|
|
|
/**
|
|
* Activate given circuit, does not block
|
|
*
|
|
* @param hops
|
|
*/
|
|
public final void activateCircuit(final String hops) {
|
|
|
|
Thread t = new Thread(new java.lang.Runnable() {
|
|
|
|
@Override
|
|
public void run() {
|
|
activateCircuitBlocking(hops);
|
|
SwingUtilities.invokeLater(new java.lang.Runnable() {
|
|
@Override
|
|
public void run() {
|
|
if (isIdle()) {
|
|
controllerEventFired(EVENT_ABORTED, null);
|
|
return;
|
|
}
|
|
setStatus(STATUS_IDLE);
|
|
if (chmUseableCircuits.isEmpty()) {
|
|
controllerEventFired(EVENT_CIRCUIT_FAILED, null);
|
|
} else {
|
|
controllerEventFired(EVENT_CIRCUIT_BUILT, null);
|
|
}
|
|
}
|
|
});
|
|
|
|
}
|
|
});
|
|
if (getStatus() < STATUS_IDLE) {
|
|
return;
|
|
}
|
|
setStatus(STATUS_CIRCUIT_CREATION);
|
|
t.start();
|
|
}
|
|
|
|
/**
|
|
* Set entry nodes
|
|
*
|
|
* @param fingers
|
|
*/
|
|
public void setEntryNodes(String fingers) {
|
|
entrynodes = "";
|
|
if (fingers == null || !getBridges().isEmpty()) {
|
|
return;
|
|
}
|
|
entrynodes = fingers;
|
|
}
|
|
|
|
/**
|
|
* Get configured entry nodes
|
|
*
|
|
* @return entry nodes in CSV format
|
|
*/
|
|
public String getEntryNodes() {
|
|
return entrynodes;
|
|
}
|
|
|
|
/**
|
|
* Set the csv list of exit node fingers to be used by tor, a single exit
|
|
* node may also be specified, this blocks
|
|
*
|
|
* @param fingers
|
|
* @param nocircs
|
|
*/
|
|
public void activateNodesBlocking(String fingers, int nocircs) {
|
|
chmUseableCircuits.clear();
|
|
enablePredictiveCircuits(true);
|
|
setConf("EntryNodes=" + entrynodes);
|
|
setConf("ExitNodes=" + fingers);
|
|
if (fingers.isEmpty() || fingers.contains("{")) {
|
|
fingers = null;
|
|
}
|
|
waitForCircuits(DEFBUILDTIME, nocircs, fingers);
|
|
enablePredictiveCircuits(false);
|
|
}
|
|
|
|
/**
|
|
* Activate given circuit, blocks
|
|
*
|
|
* @param hops
|
|
*/
|
|
private void activateCircuitBlocking(String hops) {
|
|
chmUseableCircuits.clear();
|
|
sendCommand("EXTENDCIRCUIT 0 " + hops + " PURPOSE=GENERAL");
|
|
waitForCircuit(DEFBUILDTIME, hops);
|
|
}
|
|
|
|
/**
|
|
* Wait so many seconds for valid bridges to appear if bridges are set
|
|
*
|
|
* @param secs
|
|
*/
|
|
private void waitForBridgeNodes(int secs) {
|
|
|
|
long timeout = System.currentTimeMillis() + (secs * 1000);
|
|
while (!haveValidEntryNode()) {
|
|
try {
|
|
Thread.sleep(250);
|
|
if (System.currentTimeMillis() > timeout) {
|
|
break;
|
|
}
|
|
} catch (InterruptedException ex) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean haveValidEntryNode() {
|
|
if (getBridges().isEmpty()) {
|
|
return true;
|
|
}
|
|
return haveEntryNode;
|
|
}
|
|
|
|
private void waitForCircuits(long secs, int nocircs, String reqFinger) {
|
|
|
|
long timeout = System.currentTimeMillis() + (secs * nocircs * 1000);
|
|
setActiveThread(Thread.currentThread());
|
|
while (chmUseableCircuits.size() < nocircs) {
|
|
try {
|
|
Thread.sleep(250);
|
|
} catch (InterruptedException ex) {
|
|
break;
|
|
}
|
|
if (System.currentTimeMillis() > timeout) {
|
|
Logger.getGlobal().logp(Level.INFO, TorController.class
|
|
.getName(),
|
|
"waitForCircuits() on Port=" + getListenPort(), "Timed Out");
|
|
break;
|
|
}
|
|
if (isIdle()) {
|
|
Logger.getGlobal().logp(Level.INFO, TorController.class
|
|
.getName(),
|
|
"waitForCircuits() on Port=" + getListenPort(), "Aborting");
|
|
chmUseableCircuits.clear();
|
|
break;
|
|
}
|
|
TorCircuit tc = getLatestCircuit();
|
|
if (tc != null) {
|
|
// If we have a node request then ensure that the circuit contains the requested node fingerprint
|
|
if (reqFinger != null && !reqFinger.contains(tc.getExit(TorCircuit.FINGER))) {
|
|
continue;
|
|
}
|
|
// If we have custom guards, then ensure circuit contains guard
|
|
if (entrynodes.isEmpty()) {
|
|
chmUseableCircuits.put(tc.getID(), tc);
|
|
} else if (entrynodes.contains(tc.getGuard(TorCircuit.FINGER))) {
|
|
chmUseableCircuits.put(tc.getID(), tc);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
private void waitForCircuit(long secs, String reqHops) {
|
|
|
|
long timeout = System.currentTimeMillis() + (secs * 1000);
|
|
setActiveThread(Thread.currentThread());
|
|
while (chmUseableCircuits.size() < 1) {
|
|
try {
|
|
Thread.sleep(250);
|
|
} catch (InterruptedException ex) {
|
|
break;
|
|
}
|
|
if (System.currentTimeMillis() > timeout) {
|
|
Logger.getGlobal().logp(Level.INFO, TorController.class
|
|
.getName(),
|
|
"waitForCircuit() on Port=" + getListenPort(), "Timed Out");
|
|
break;
|
|
}
|
|
if (isIdle()) {
|
|
Logger.getGlobal().logp(Level.INFO, TorController.class
|
|
.getName(),
|
|
"waitForCircuit() on Port=" + getListenPort(), "Aborting");
|
|
chmUseableCircuits.clear();
|
|
break;
|
|
}
|
|
TorCircuit tcActive = getLatestCircuit();
|
|
if (tcActive != null) {
|
|
String hops = tcActive.getHops();
|
|
if (hops.contains(reqHops)) {
|
|
chmUseableCircuits.put(tcActive.getID(), tcActive);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private synchronized void setActiveThread(Thread t) {
|
|
tActive = t;
|
|
}
|
|
|
|
private synchronized Thread getActiveThread() {
|
|
return tActive;
|
|
}
|
|
|
|
/**
|
|
* Test specified node, non blocking
|
|
*
|
|
* @param finger
|
|
*/
|
|
public void testNode(final String finger) {
|
|
|
|
SwingWorker<Void, Integer> sw = new SwingWorker<Void, Integer>() {
|
|
private long bestLatency = LATENCY_FAIL;
|
|
private String bestHops = null;
|
|
|
|
@Override
|
|
protected Void doInBackground() {
|
|
closeCircuitsExcept("", true);
|
|
activateNodesBlocking(finger, 1);
|
|
if (isIdle()) {
|
|
return null;
|
|
}
|
|
if (chmUseableCircuits.size() > 0) {
|
|
String id = chmUseableCircuits.keys().nextElement();
|
|
TorCircuit tc = chmUseableCircuits.get(id);
|
|
closeCircuitsExcept("", true);
|
|
activateCircuitBlocking(tc.getHops());
|
|
publish(EVENT_CIRCUIT_BUILT);
|
|
long latency = getTorLatency(5000);
|
|
if (latency < bestLatency) {
|
|
bestLatency = latency;
|
|
bestHops = tc.getHops();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
protected void process(List<Integer> chunks) {
|
|
for (Integer i : chunks) {
|
|
controllerEventFired(i, chmUseableCircuits.size());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void done() {
|
|
if (isIdle()) {
|
|
lngBestLatency = LATENCY_FAIL;
|
|
strBestHops = null;
|
|
controllerEventFired(EVENT_ABORTED, null);
|
|
} else {
|
|
setStatus(STATUS_IDLE);
|
|
lngBestLatency = bestLatency;
|
|
strBestHops = bestHops;
|
|
controllerEventFired(EVENT_TESTING_DONE, chmUseableCircuits.size());
|
|
}
|
|
}
|
|
|
|
};
|
|
setStatus(STATUS_CIRCUIT_CREATION);
|
|
strBestHops = null;
|
|
lngBestLatency = LATENCY_FAIL;
|
|
sw.execute();
|
|
}
|
|
|
|
/**
|
|
* Get best latency
|
|
*
|
|
* @return best latency value in ms as long
|
|
*/
|
|
public final long getBestLatency() {
|
|
return lngBestLatency;
|
|
}
|
|
|
|
/**
|
|
* Get best hops
|
|
*
|
|
* @return hops info as string
|
|
*/
|
|
public final String getBestHops() {
|
|
return strBestHops;
|
|
}
|
|
|
|
/**
|
|
* Get built circuits
|
|
*
|
|
* @param filtered Filter out unwanted circuits
|
|
* @return HashMap of built circuits keyed to their circuit id
|
|
*/
|
|
public final HashMap<String, TorCircuit> getBuiltCircuits(boolean filtered) {
|
|
HashMap<String, TorCircuit> hm = new HashMap<>();
|
|
ArrayList<String> circuits = getInfo("circuit-status");
|
|
circuits.remove("250 OK");
|
|
for (String circuit : circuits) {
|
|
if (circuit.contains("BUILT")) {
|
|
if (filtered) {
|
|
if (circuit.contains("ONEHOP_TUNNEL")) {
|
|
continue;
|
|
}
|
|
if (circuit.contains("IS_INTERNAL")) {
|
|
continue;
|
|
}
|
|
if (!circuit.contains("PURPOSE=GENERAL")) {
|
|
continue;
|
|
}
|
|
}
|
|
TorCircuit tc = new TorCircuit(circuit);
|
|
hm.put(tc.getID(), tc);
|
|
// Debug purposes
|
|
// if (getListenPort() == 9054) {
|
|
// System.out.println("Circuit=" + circuit);
|
|
// }
|
|
}
|
|
}
|
|
return hm;
|
|
}
|
|
|
|
/**
|
|
* Verify we have comms on the control socket
|
|
*
|
|
* @return true if its good
|
|
*/
|
|
public final boolean verifyControlComms() {
|
|
ArrayList<String> circuits = getInfo("circuit-status");
|
|
return !circuits.isEmpty();
|
|
}
|
|
|
|
/**
|
|
* Get the latest circuit
|
|
*
|
|
* @return circuit
|
|
*/
|
|
public final TorCircuit getLatestCircuit() {
|
|
|
|
HashMap<String, TorCircuit> hm = getBuiltCircuits(true);
|
|
Iterator i = getBuiltCircuits(true).keySet().iterator();
|
|
String strId;
|
|
TorCircuit tcRecent = null;
|
|
int hid = 0;
|
|
int id;
|
|
while (i.hasNext()) {
|
|
strId = (String) i.next();
|
|
id = Integer.parseInt(strId);
|
|
if (id > hid) {
|
|
hid = id;
|
|
tcRecent = hm.get(strId);
|
|
}
|
|
}
|
|
return tcRecent;
|
|
}
|
|
|
|
private void openControlSocket() {
|
|
try {
|
|
sockControl = new Socket(LOCALHOST, getControlPort());
|
|
sockControl.setKeepAlive(true);
|
|
sockControl.setSoTimeout(2000);
|
|
pwSocket = new PrintWriter(sockControl.getOutputStream());
|
|
brSocket = new BufferedReader(new InputStreamReader(sockControl.getInputStream()), 1024);
|
|
} catch (IOException ex) {
|
|
Logger.getGlobal().throwing(TorController.class
|
|
.getName(),
|
|
"openControlSocket() on Port=" + getListenPort(), ex);
|
|
}
|
|
}
|
|
|
|
private void closeControlSocket() {
|
|
try {
|
|
if (sockControl != null) {
|
|
sockControl.setKeepAlive(false);
|
|
sockControl.close();
|
|
sockControl = null;
|
|
pwSocket.close();
|
|
brSocket.close();
|
|
pwSocket = null;
|
|
brSocket = null;
|
|
|
|
}
|
|
} catch (IOException ex) {
|
|
Logger.getGlobal().throwing(TorController.class
|
|
.getName(),
|
|
"closeControlSocket() on Port=" + getListenPort(), ex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a command to tor client
|
|
*
|
|
* @param command
|
|
* @return Arraylist containing the result of the command
|
|
*/
|
|
public final synchronized ArrayList<String> sendCommand(String command) {
|
|
|
|
ArrayList<String> result = new ArrayList<>();
|
|
|
|
if (sockControl == null) {
|
|
Logger.getGlobal().logp(Level.WARNING, TorController.class
|
|
.getName(),
|
|
"sendCommand() Port=" + getListenPort(), "Cmd=" + command
|
|
+ ", Non-existent socket");
|
|
return result;
|
|
}
|
|
|
|
if (sockControl.isClosed()) {
|
|
Logger.getGlobal().logp(Level.WARNING, TorController.class
|
|
.getName(),
|
|
"sendCommand() Port=" + getListenPort(), "Cmd=" + command
|
|
+ ", Socket is closed.");
|
|
return result;
|
|
}
|
|
|
|
flushReadBuffer();
|
|
|
|
// Write out the command
|
|
if (pwSocket.checkError()) {
|
|
Logger.getGlobal().logp(Level.FINEST, TorController.class
|
|
.getName(),
|
|
"sendCommand() Port=" + getListenPort(), "Cmd=" + command
|
|
+ ", Write Socket Error");
|
|
return result;
|
|
}
|
|
|
|
pwSocket.write(command + "\r\n");
|
|
pwSocket.flush();
|
|
|
|
// Dont wait for command response if quitting
|
|
if (command.contentEquals("QUIT")) {
|
|
return result;
|
|
}
|
|
|
|
result = getCommandResponse();
|
|
|
|
if (!result.isEmpty()) {
|
|
Logger.getGlobal().logp(Level.FINEST, TorController.class
|
|
.getName(),
|
|
"sendCommand() Port=" + getListenPort(), "Cmd=" + command
|
|
+ ", Response=" + result.toString());
|
|
} else {
|
|
Logger.getGlobal().logp(Level.WARNING, TorController.class
|
|
.getName(),
|
|
"sendCommand() Port=" + getListenPort(), "Cmd=" + command
|
|
+ ", Response=Timed out");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private ArrayList<String> getCommandResponse() {
|
|
|
|
boolean boolMultiReply = false;
|
|
String response;
|
|
ArrayList<String> result = new ArrayList<>();
|
|
try {
|
|
while (true) {
|
|
response = brSocket.readLine();
|
|
if (response == null) {
|
|
break;
|
|
}
|
|
// Check to see if we are in the middle of a multi line reply
|
|
if (boolMultiReply) {
|
|
// A multiline extended reply is part terminated by a fullstop
|
|
if (response.startsWith(".")) {
|
|
boolMultiReply = false;
|
|
} else {
|
|
result.add(response);
|
|
}
|
|
continue;
|
|
}
|
|
// Check for the start of a multi line reply
|
|
if (response.startsWith("250+")) {
|
|
boolMultiReply = true;
|
|
response = response.substring(response.indexOf('=') + 1).trim();
|
|
if (!response.isEmpty()) {
|
|
result.add(response);
|
|
}
|
|
continue;
|
|
}
|
|
// Check for single line reply
|
|
if (response.startsWith("250-")) {
|
|
response = response.substring(response.indexOf('=') + 1).trim();
|
|
if (!response.isEmpty()) {
|
|
result.add(response);
|
|
}
|
|
continue;
|
|
}
|
|
result.add(response);
|
|
if (response.startsWith("250 ") || response.startsWith("251 ")
|
|
|| response.startsWith("4") || response.startsWith("5")) {
|
|
break;
|
|
}
|
|
}
|
|
} catch (IOException ex) {
|
|
result.clear();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private void flushReadBuffer() {
|
|
try {
|
|
while (brSocket.ready()) {
|
|
brSocket.readLine();
|
|
}
|
|
} catch (IOException ex) {
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a socks4a socket to this URL on this tor connection, if nowww is
|
|
* true then remove www. from domain
|
|
*
|
|
* @param url
|
|
* @param nowww
|
|
* @return socket
|
|
*/
|
|
public Socket createTorSocketToURL(String url, boolean nowww) {
|
|
|
|
try {
|
|
URI uri = new URI(url);
|
|
String host = uri.getHost().toLowerCase();
|
|
if (nowww) {
|
|
host = host.replace("www.", "");
|
|
}
|
|
String protocol = uri.getScheme();
|
|
int port = uri.getPort();
|
|
if (port == -1) {
|
|
switch (protocol) {
|
|
default:
|
|
port = 80;
|
|
break;
|
|
case "https":
|
|
port = 443;
|
|
break;
|
|
}
|
|
}
|
|
return createSocks4aSocket(LOCALHOST, getListenPort(), host, port);
|
|
|
|
} catch (URISyntaxException ex) {
|
|
Logger.getLogger(TorController.class
|
|
.getName()).log(Level.SEVERE, null, ex);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Create a Socks4a socket Taken from Wikipedia SOCKS4a is a simple
|
|
* extension to SOCKS4 protocol that allows a client that cannot resolve the
|
|
* destination host's domain name to specify it.
|
|
*
|
|
* The client should set the first three bytes of DSTIP to NULL and the last
|
|
* byte to a non-zero value. (This corresponds to IP address 0.0.0.x, with x
|
|
* nonzero, an inadmissible destination address and thus should never occur
|
|
* if the client can resolve the domain name.) Following the NULL byte
|
|
* terminating USERID, the client must send the destination domain name and
|
|
* terminate it with another NULL byte. This is used for both "connect" and
|
|
* "bind" requests.
|
|
*
|
|
* Client to SOCKS server: field 1: SOCKS version number, 1 byte, must be
|
|
* 0x04 for this version field 2: command code, 1 byte: 0x01 = establish a
|
|
* TCP/IP stream connection 0x02 = establish a TCP/IP port binding field 3:
|
|
* network byte order port number, 2 bytes field 4: deliberate invalid IP
|
|
* address, 4 bytes, first three must be 0x00 and the last one must not be
|
|
* 0x00 field 5: the user ID string, variable length, terminated with a null
|
|
* (0x00) field 6: the domain name of the host we want to contact, variable
|
|
* length, terminated with a null (0x00)
|
|
*
|
|
*
|
|
* Server to SOCKS client: field 1: null byte field 2: status, 1 byte: 0x5a
|
|
* = request granted 0x5b = request rejected or failed 0x5c = request failed
|
|
* because client is not running identd (or not reachable from the server)
|
|
* 0x5d = request failed because client's identd could not confirm the user
|
|
* ID string in the request field 3: network byte order port number, 2 bytes
|
|
* field 4: network byte order IP address, 4 bytes
|
|
*
|
|
* A server using protocol SOCKS4A must check the DSTIP in the request
|
|
* packet. If it represents address 0.0.0.x with nonzero x, the server must
|
|
* read in the domain name that the client sends in the packet. The server
|
|
* should resolve the domain name and make connection to the destination
|
|
* host if it can.
|
|
*
|
|
* @param socksaddr Socks ip address
|
|
* @param socksport Socks port
|
|
* @param remotehost Remote host
|
|
* @param remoteport Remote port
|
|
* @return Socket
|
|
*/
|
|
public Socket createSocks4aSocket(String socksaddr, int socksport, String remotehost, int remoteport) {
|
|
try {
|
|
Socket s = new Socket(socksaddr, socksport);
|
|
DataOutputStream dos = new DataOutputStream(s.getOutputStream());
|
|
dos.writeByte(0x04); // Version 4 Socks
|
|
dos.writeByte(0x01); // Connect command code
|
|
dos.writeShort(remoteport); // Remote Port number
|
|
dos.writeInt(0x01); // IP address of 0.0.0.1 means use Socks 4a
|
|
dos.writeByte(0x00); // Null terminator
|
|
dos.writeBytes(remotehost); // Remote host IP address
|
|
dos.writeByte(0x00); // Null terminator
|
|
return s;
|
|
} catch (IOException ex) {
|
|
Logger.getGlobal().logp(Level.FINE, this.getClass().getName(), "createSocks4aSocket", "", ex);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
}
|