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.

758 lines
24 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.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.regex.Pattern;
import lib.Localisation;
import lib.SimpleFile;
/**
*
* @author Alistair Neil <info@dazzleships.net>
*/
public class NodeList {
public static final int NODELIST_IDLE = 0;
public static final int NODELIST_BUILDING = 1;
public static final int NODELIST_BUILT = 2;
public static final int NODELIST_FAILED = 3;
public static final int NODELIST_TERMINATED = 4;
private static final String EMPTYSTRING = "";
private static final Localisation LOCAL = new Localisation("resources/MessagesBundle");
private ExitNodeTableModel entm;
private GuardNodeTableModel gntm;
private int intStatus = NODELIST_IDLE;
private HashMap<String, NodeItem> hmNode;
private final HashMap<String, NodeItem> hmBridges;
private final ArrayList<String> alCountries;
private TorController tc;
private final String exitFavouritesFile;
private final String guardFavouritesFile;
private final String filepath;
private volatile boolean boolAbortActions;
private final Pattern pattspace = Pattern.compile(" ");
private int nooffavs;
public NodeList(String filepath, String exitfavourites, String guardfavouritesfile) {
this.hmNode = new HashMap<>();
this.hmBridges = new HashMap<>();
this.filepath = filepath;
this.alCountries = new ArrayList<>();
this.exitFavouritesFile = exitfavourites;
this.guardFavouritesFile = guardfavouritesfile;
}
/**
* Get node item from its fingerprint
*
* @param key or fingerprint
* @return NodeItem
*/
public final NodeItem getNode(String key) {
String temp;
NodeItem ni = hmNode.get(key);
if (ni == null) {
// No bridge found so must be a new node not currently in nodelist
ArrayList<String> alRouterInfo = getRouterDesc(key);
if (alRouterInfo != null) {
alRouterInfo.addAll(getRouterStatus(key));
ni = new NodeItem();
temp = filterRouterInfo(alRouterInfo, "router ");
if (temp == null) {
return null;
}
String[] data = pattspace.split(temp);
ni.setFingerprint(key);
ni.setNickName(data[0]);
ni.setIPAddress(data[1]);
String isocode = tc.getCountryFromIP(ni.getIPAddress());
if (isocode == null || isocode.contentEquals("??")) {
isocode = "U1";
}
ni.setCountryCode(isocode);
ni.setCountryName(LOCAL.getDisplayCountry(isocode));
temp = filterRouterInfo(alRouterInfo, "bandwidth ");
data = pattspace.split(temp);
ni.setBandwidth(getLowestBandwidth(data));
temp = filterRouterInfo(alRouterInfo, "s ");
if (temp == null) {
// We probably have a bridge
ni.setType(NodeItem.TYPE_GUARD);
ni.setStable(LOCAL.getString("text_yes"));
return ni;
}
if (temp.contains("Guard")) {
ni.setType(NodeItem.TYPE_GUARD);
}
if (temp.contains("Stable")) {
ni.setStable(LOCAL.getString("text_yes"));
} else {
ni.setStable(LOCAL.getString("text_no"));
}
if (temp.contains("Exit") && !temp.contains("BadExit")) {
ni.setType(NodeItem.TYPE_EXIT);
temp = filterRouterInfo(alRouterInfo, "p ");
if (temp.startsWith("accept")) {
temp = temp.replace("accept ", "");
if (!containsPort(temp, 80) && !containsPort(temp, 443)) {
return ni;
}
} else {
temp = temp.replace("reject ", "");
if (containsPort(temp, 80) || containsPort(temp, 443)) {
return ni;
}
}
ni.setHttpSupported(true);
}
}
}
return ni;
}
public ArrayList<String> getRouterDesc(String finger) {
ArrayList<String> alResult = tc.getInfo("desc/id/" + finger);
if (alResult == null) {
return null;
}
alResult.remove("250 OK");
if (alResult.isEmpty()) {
return null;
}
return alResult;
}
public ArrayList<String> getRouterStatus(String finger) {
ArrayList<String> alResult = tc.getInfo("ns/id/" + finger);
if (alResult == null) {
return null;
}
alResult.remove("250 OK");
if (alResult.isEmpty()) {
return null;
}
return alResult;
}
public String filterRouterInfo(ArrayList<String> alInfo, String field) {
for (String s : alInfo) {
if (s.startsWith(field)) {
return s.replace(field, "");
}
}
return null;
}
/**
* Get the process status
*
* @return status
*/
public final int getCurrentStatus() {
return intStatus;
}
/**
* Generate the nodelist
*
* @param tc
*/
public final void refreshNodelist(TorController tc) {
boolAbortActions = false;
intStatus = NODELIST_BUILDING;
this.tc = tc;
boolean boolSuccess = populateNodeMap();
if (boolSuccess) {
boolSuccess = fetchFingerData();
}
if (boolSuccess) {
intStatus = NODELIST_BUILT;
} else if (boolAbortActions) {
intStatus = NODELIST_TERMINATED;
} else {
intStatus = NODELIST_FAILED;
}
}
/**
* Update internal node mappings
*
*/
private boolean populateNodeMap() {
String strFlags;
String data[];
String temp;
String strNick;
String strIP;
String strCountryName;
String strCountryCode;
String strPerms;
String strDirPort;
String strOrPort;
NodeItem ni;
SimpleFile sfConsensus = new SimpleFile(tc.getDataFolder() + SimpleFile.getSeparator() + "cached-consensus");
hmNode.clear();
alCountries.clear();
sfConsensus.openBufferedRead();
while (!boolAbortActions) {
temp = sfConsensus.readLine();
// Test for end of node info file
if (temp.startsWith("directory-footer")) {
break;
}
// Test for node info line
if (temp.startsWith("r ")) {
data = pattspace.split(temp);
strNick = data[1];
strIP = data[6];
strOrPort = data[7];
strDirPort = data[8];
// Read flags line
while (true) {
strFlags = sfConsensus.readLine();
if (strFlags.startsWith("s ")) {
break;
}
}
// Ignore certain types of node
if (!strFlags.contains("Running")
|| !strFlags.contains("Valid")) {
continue;
}
// Read port permissions line
while (true) {
strPerms = sfConsensus.readLine();
if (strPerms.startsWith("p ")) {
break;
}
}
// Check if Tor is fully active
if (tc.getStatus() < TorController.STATUS_IDLE) {
boolAbortActions = false;
alCountries.clear();
break;
}
// Get country code from ip address
strCountryCode = tc.getCountryFromIP(strIP);
if (strCountryCode == null || strCountryCode.contains("??")) {
strCountryCode = "U1";
}
// If we get GEOIP errors then abort
if (strCountryCode.startsWith("551 GEOIP")) {
alCountries.clear();
break;
}
// Get country name
strCountryName = LOCAL.getDisplayCountry(strCountryCode);
ni = new NodeItem();
// Update our node item fields now that we have validated our node
ni.setCountryCode(strCountryCode);
ni.setCountryName(strCountryName);
ni.setNickName(strNick);
ni.setIPAddress(strIP);
// Get stableflag
if (strFlags.contains("Stable")) {
ni.setStable(LOCAL.getString("text_yes"));
} else {
ni.setStable(LOCAL.getString("text_no"));
}
hmNode.put(strOrPort + strDirPort + ":" + strIP, ni);
// Check if a guard node
if (strFlags.contains("Guard")) {
ni.setType(NodeItem.TYPE_GUARD);
}
// Check if an exit node also exclude nodes flagged as bad exits
if (strFlags.contains("Exit") && !strFlags.contains("BadExit")) {
ni.setType(NodeItem.TYPE_EXIT);
// Simple test on port permissions, we will only allow node
// if it accepts connections on port 80 and 443 for web browsing
if (strPerms.startsWith("p accept")) {
strPerms = strPerms.replace("p accept ", "");
if (!containsPort(strPerms, 80)) {
continue;
}
if (!containsPort(strPerms, 443)) {
continue;
}
} else {
strPerms = strPerms.replace("p reject ", "");
if (containsPort(strPerms, 80)) {
continue;
}
if (containsPort(strPerms, 443)) {
continue;
}
}
ni.setHttpSupported(true);
// Ensure we only add exit country info in once to validated countries list
if (!alCountries.contains(strCountryCode + "," + strCountryName)) {
alCountries.add(strCountryCode + "," + strCountryName);
}
}
}
}
sfConsensus.closeFile();
// Sort our country validation list
if (alCountries.isEmpty()) {
return false;
} else {
Collections.sort(alCountries, Collator.getInstance());
}
return !boolAbortActions;
}
/**
* Update internal finger mappings
*
*/
private boolean fetchFingerData() {
int idx;
String line;
String data[];
String ipaddress;
String finger;
String strOrPort;
String strDirPort;
String key;
boolean result;
String guardwhitelist = getGuardFavouritesAsCSV();
NodeItem ni;
HashMap<String, NodeItem> hmNodeReplacement = new HashMap<>();
SimpleFile sfDescriptors = new SimpleFile(tc.getDataFolder() + SimpleFile.getSeparator() + "cached-descriptors");
SimpleFile sfDescriptorsNew = new SimpleFile(tc.getDataFolder() + SimpleFile.getSeparator() + "cached-descriptors.new");
if (!sfDescriptorsNew.exists() || sfDescriptorsNew.getFile().length() == 0) {
sfDescriptorsNew = null;
}
sfDescriptors.openBufferedRead();
try {
while (!boolAbortActions) {
line = sfDescriptors.readLine();
// If line is null we have then processed the main descriptors file
if (line == null) {
// Check if their is a descriptors new file
if (sfDescriptorsNew == null) {
break;
} else {
sfDescriptors.closeFile();
sfDescriptors = sfDescriptorsNew;
sfDescriptors.openBufferedRead();
sfDescriptorsNew = null;
continue;
}
}
if (line.startsWith("router ")) {
data = pattspace.split(line);
ipaddress = data[2];
strOrPort = data[3];
strDirPort = data[5];
key = strOrPort + strDirPort + ":";
if (!hmNode.containsKey(key + ipaddress)) {
continue;
}
// Get fingerprint
while (true) {
line = sfDescriptors.readLine();
idx = line.indexOf("fingerprint");
if (idx > -1) {
line = line.substring(idx + 12);
break;
}
}
finger = "$" + line.replace(" ", EMPTYSTRING);
// Get bandwidth
while (true) {
line = sfDescriptors.readLine();
idx = line.indexOf("bandwidth");
if (idx > -1) {
line = line.substring(idx + 10);
break;
}
}
data = pattspace.split(line);
ni = hmNode.get(key + ipaddress);
ni.setFingerprint(finger);
ni.setGuardEnabled(guardwhitelist.contains(finger));
ni.setBandwidth(getLowestBandwidth(data));
hmNodeReplacement.put(finger, ni);
}
}
result = !boolAbortActions;
} catch (Exception ex) {
result = false;
}
sfDescriptors.closeFile();
// Replace our original ipaddress keyed nodemap with our fingerprint keyed nodemap
hmNode = hmNodeReplacement;
return result;
}
/**
* Return the lowest bandwidth
*
* @param data
* @return bandwidth as float
*/
private float getLowestBandwidth(String[] data) {
float bw = 1000000;
float tmp;
for (String s : data) {
tmp = Float.parseFloat(s) / 1000000;
if (tmp < bw) {
bw = tmp;
}
}
return bw;
}
/**
* Add newly learned bridge to nodelist
*
* @param bridgedata
*/
public void addBridge(String bridgedata) {
Pattern pat = Pattern.compile(" ");
bridgedata = bridgedata.substring(bridgedata.indexOf('$'));
String[] data = pat.split(bridgedata);
int index = data[0].indexOf('~');
if (index < 0) {
index = data[0].indexOf('=');
}
NodeItem ni = new NodeItem();
ni.setFingerprint(data[0].substring(0, index));
ni.setIPAddress(data[2]);
ni.setNickName(data[0].substring(index + 1));
hmBridges.put(ni.getFingerprint(), ni);
}
/**
* Clear any learned bridges
*/
public void clearBridges() {
hmBridges.clear();
}
/**
* Test to check supplied data contains the specified port
*
* @param data
* @param port
* @return True if data contains specified port
*/
private boolean containsPort(String data, int port) {
String strRanges[];
String[] strPorts;
int rangemin;
int rangemax;
Pattern pattcomma = Pattern.compile(",");
Pattern pattminus = Pattern.compile("-");
strRanges = pattcomma.split(data);
for (String s : strRanges) {
// Test to see if its a range of ports
if (s.contains("-")) {
strPorts = pattminus.split(s);
rangemin = Integer.parseInt(strPorts[0]);
rangemax = Integer.parseInt(strPorts[1]);
if (port >= rangemin && port <= rangemax) {
return true;
}
} else {
// We have a single port
rangemin = Integer.parseInt(s);
if (port == rangemin) {
return true;
}
}
}
return false;
}
/**
* Gets validated countries, a valid country is any country with active exit
* nodes
*
* @return String array of countries
*/
public final String[] getValidatedCountries() {
return alCountries.toArray(new String[alCountries.size()]);
}
/**
* Gets validated country codes, a valid country is any country with active
* exit nodes
*
* @return String array of country abbreviations
*/
public final String[] getValidatedCountryCodes() {
String[] result = new String[alCountries.size()];
int idx = 0;
for (String s : alCountries) {
result[idx++] = s.substring(0, 2);
}
return result;
}
/**
* Set the guard node view table model
*
* @param gntm
*/
public final void setGuardNodeTableModel(GuardNodeTableModel gntm) {
this.gntm = gntm;
}
/**
* Set the exit node view table model
*
* @param entm
*/
public final void setExitNodeTableModel(ExitNodeTableModel entm) {
this.entm = entm;
}
/**
* Get a list of exitnodes, if all is false then it returns only favourited
* nodes, if omitfailednodes is true then don't include nodes that failed
* testing
*
* @param all
* @param omitfailednodes
* @return ArrayList of exit nodes
*/
public final ArrayList<String> getExitNodes(boolean all, boolean omitfailednodes) {
ArrayList<String> al = new ArrayList<>();
for (int i = 0; i < entm.getRowCount(); i++) {
int teststatus = (Integer) entm.getValueAt(i, 7);
if (omitfailednodes && teststatus == NodeItem.TESTSTATUS_FAILED) {
continue;
}
boolean fave = (Boolean) entm.getValueAt(i, 4) | all;
if (fave) {
al.add((String) entm.getValueAt(i, 5));
}
}
return al;
}
/**
* Get a string of comma separated exitnodes, if all is false then it
* returns only favourited nodes, if omitfailednodes is true then don't
* include nodes that failed testing
*
* @param all
* @param omitfailednodes
* @return String of exitnodes in csv format
*/
public final String getExitNodesAsString(boolean all, boolean omitfailednodes) {
String result = getExitNodes(all, omitfailednodes).toString();
result = result.replace("[", "").replace(" ", "").replace("]", "");
return result;
}
/**
* Get a list of all guard nodes guard nodes
*
* @return ArrayList of guard nodes
*/
public final ArrayList<String> getGuardNodes() {
ArrayList<String> al = new ArrayList<>();
if (gntm == null) {
return al;
}
for (int i = 0; i < gntm.getRowCount(); i++) {
al.add((String) gntm.getValueAt(i, 3));
}
return al;
}
/**
* Get guard favourites
*
* @return CSV string of guard entry fingerprints
*/
public final String getGuardFavouritesAsCSV() {
StringBuilder sbResult = new StringBuilder();
String line;
String sep = "";
if (guardFavouritesFile == null) {
return sbResult.toString();
}
SimpleFile sf = new SimpleFile(filepath + guardFavouritesFile);
if (!sf.exists()) {
return sbResult.toString();
}
sf.openBufferedRead();
while ((line = sf.readLine()) != null) {
sbResult.append(sep);
sbResult.append(line);
if (sep.isEmpty()) {
sep = ",";
}
}
sf.closeFile();
return sbResult.toString();
}
/**
* Update the guard node table model
*/
public final void refreshGuardTableModel() {
if (gntm == null) {
return;
}
NodeItem ni;
gntm.clear();
// Populate model
for (String s : hmNode.keySet()) {
ni = hmNode.get(s);
if (ni.isGuard()) {
Object[] rowData = new Object[]{ni};
gntm.addRow(rowData);
}
}
}
/**
* Update the table model based on supplied country
*
*
* @param isocountry in the format "GB,Great Britain"
*/
public final void refreshExitTableModel(String isocountry) {
if (entm == null) {
return;
}
NodeItem ni;
entm.clear();
String abbrv = isocountry;
String favourites = "";
if (exitFavouritesFile != null) {
SimpleFile sf = new SimpleFile(filepath + exitFavouritesFile);
if (sf.exists()) {
sf.openBufferedRead();
favourites = sf.readEntireFile().trim();
sf.closeFile();
}
}
// Populate model
nooffavs = 0;
for (String s : hmNode.keySet()) {
ni = hmNode.get(s);
if (ni.isExit() && ni.isHttpSupported() && ni.getCountryCode().contentEquals(abbrv)) {
ni.setLatency(9999);
ni.setTestingMessage(LOCAL.getString("textfield_unknown"));
// Whitelisting here
ni.setFavouriteEnabled(favourites.contains(ni.getIPAddress()));
if (ni.isFavourite()) {
nooffavs++;
}
Object[] rowData = new Object[]{ni};
entm.addRow(rowData);
}
}
}
/**
* Get the number of active favourites
*
* @return number of favs as int
*/
public int getNumberOfFavs() {
return nooffavs;
}
/**
* Ensures any threaded actions will terminate themselves
*/
public final void terminate() {
boolAbortActions = true;
}
/**
* Save exit node whitelist
*/
public void saveExitFavourites() {
if (exitFavouritesFile == null) {
return;
}
SimpleFile sf = new SimpleFile(filepath + exitFavouritesFile);
sf.openBufferedWrite();
NodeItem ni;
nooffavs = 0;
for (String s : hmNode.keySet()) {
ni = hmNode.get(s);
if (ni.isExit() && ni.isFavourite()) {
nooffavs++;
sf.writeFile(ni.getIPAddress(), 1);
}
}
sf.closeFile();
}
/**
* Save exit node blacklist
*
* @return number of active guards
*/
public int saveGuardWhitelist() {
int activeguards = 0;
if (guardFavouritesFile == null) {
return activeguards;
}
SimpleFile sf = new SimpleFile(filepath + guardFavouritesFile);
sf.openBufferedWrite();
NodeItem ni;
for (String s : hmNode.keySet()) {
ni = hmNode.get(s);
if (ni.isGuard() && ni.isGuardEnabled()) {
sf.writeFile(ni.getFingerprint(), 1);
activeguards++;
}
}
sf.closeFile();
return activeguards;
}
}