/*
Tor Research Framework - easy to use tor client library/framework
Copyright (C) 2014 Dr Gareth Owen <drgowen@gmail.com>
www.ghowen.me / github.com/drgowen/tor-research-framework
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 3 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, see <http://www.gnu.org/licenses/>.
*/
package tor;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import tor.util.TorDocumentParser;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.PublicKey;
import java.util.HashSet;
public class OnionRouter {
public String identityhash;
public HashSet<String> flags = new HashSet<>();
public byte[] onionKeyRaw;
public byte[] signKeyRaw;
public String consensusIPv4ExitPortSummary = null;
public String[] descriptorIPv4ExitPolicy = null;
public String[] parsedIPv4ExitPortList = null;
public String version = null;
String name;
InetAddress ip;
int orport;
int dirport;
PublicKey onionKey = null;
public OnionRouter(String _nm, String _ident, String _ip, int _orport, int _dirport) throws UnknownHostException {
name = _nm;
ip = InetAddress.getByName(_ip);
orport = _orport;
dirport = _dirport;
identityhash = _ident;
}
public OnionRouter(String _nm, String _ident, String _ip, int _orport, int _dirport, String _version) throws UnknownHostException {
name = _nm;
ip = InetAddress.getByName(_ip);
orport = _orport;
dirport = _dirport;
identityhash = _ident;
version = _version;
}
public void fetchDescriptor() throws IOException {
TorDocumentParser rdr = new TorDocumentParser(Consensus.getConsensus().getRouterDescriptor(identityhash));
onionKeyRaw = Base64.decodeBase64(rdr.getItem("onion-key"));
onionKey = TorCrypto.asn1GetPublicKey(onionKeyRaw);
signKeyRaw = Base64.decodeBase64(rdr.getItem("signing-key"));
}
public PublicKey getOnionKey() throws IOException {
if (onionKey == null)
fetchDescriptor();
return onionKey;
}
public Boolean acceptsIPv4ExitPort(int exitPort) {
// ignore an exitPort of 0, and invalid exitPorts
// return true to short-circuit potentially expensive checks that will never succeed, through the entire router list
if (exitPort == 0)
return true;
else if (exitPort < 0 || exitPort > 65535)
return true;
if (parsedIPv4ExitPortList == null) {
// if we don't have the p line from the consensus, download the entire router descriptor
if (consensusIPv4ExitPortSummary == null && descriptorIPv4ExitPolicy == null) {
try {
TorDocumentParser rdr = new TorDocumentParser(Consensus.getConsensus().getRouterDescriptor(identityhash));
descriptorIPv4ExitPolicy = rdr.getArrayItem(TorDocumentParser.IPv4PolicyKey);
} catch (IOException e) {
System.out.println("acceptsIPv4ExitPort: failed to retrieve exit policy for: " + name
+ ", assuming reject. Parsing router descriptor failed with IOException: " + e.toString());
return false;
} catch (RuntimeException e) {
System.out.println("acceptsIPv4ExitPort: failed to retrieve exit policy for: " + name
+ ", assuming reject. Retrieving consensus failed with RuntimeException: " + e.toString());
return false;
}
}
// The algorithm for parsing exit policies is complex,
// and even tor (C) sometimes connects to exits that are not suitable
// (e.g. because the remote IP isn't known, and therefore tor only performs an approximate coverage check,
// or because tor is using the port summary from the consensus).
// See https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt for the gory details
// use p line in consensus if available
// https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt
// "p" SP ("accept" / "reject") SP PortList NL
// [At most once.]
// PortList = PortOrRange
// PortList = PortList "," PortOrRange
// PortOrRange = INT "-" INT / INT
// A list of those ports that this router supports (if 'accept')
// or does not support (if 'reject') for exit to "most addresses".
//System.out.println("acceptsIPv4ExitPort: checking for exit port " + String.valueOf(exitPort) + " in: "
// + consensusIPv4ExitPortSummary);
// Pre-process the exit summary into an array of:
// ("accept" / "reject") SP PortOrRange
if (consensusIPv4ExitPortSummary != null) {
String[] lineSplit = consensusIPv4ExitPortSummary.split(" ");
String acceptOrReject = lineSplit[0];
String[] portListSplit = lineSplit[1].split(",");
parsedIPv4ExitPortList = new String[portListSplit.length];
int i = 0;
for (String portOrRange : portListSplit) {
parsedIPv4ExitPortList[i] = acceptOrReject + " " + portOrRange;
i++;
}
}
//System.out.println("acceptsIPv4ExitPort: checking for exit port " + String.valueOf(exitPort) + " in:\n"
// + String.join("\n", descriptorIPv4ExitPolicy));
// We implement a simplified version which ignores IP addresses (skipping any that aren't "*")
// Each line of the policy consists of:
// ("accept" / "reject") SP IP ":" PortOrRange NL
// Pre-process the exit policy into an array of:
// ("accept" / "reject") SP PortOrRange
Boolean isParsedListEmpty = false;
if (parsedIPv4ExitPortList == null)
isParsedListEmpty = true;
else if (parsedIPv4ExitPortList.length == 0)
isParsedListEmpty = true;
if (isParsedListEmpty && descriptorIPv4ExitPolicy != null) {
// allow an extra item for the final "accept *"
parsedIPv4ExitPortList = new String[descriptorIPv4ExitPolicy.length + 1];
int i = 0;
for (String policy : descriptorIPv4ExitPolicy) {
String[] lineSplit = policy.split(" ");
String acceptOrReject = lineSplit[0];
String[] ipPortSplit = lineSplit[1].split(":");
String portOrRange = ipPortSplit[1];
// only process lines that apply to all IPs
if (ipPortSplit.equals("*")) {
// replace * ports with the full numeric port range
if (portOrRange.equals("*")) {
portOrRange = "1-65535";
}
parsedIPv4ExitPortList[i] = acceptOrReject + " " + portOrRange;
i++;
}
}
// "if no rule matches, the address will be accepted"
parsedIPv4ExitPortList[i] = "accept 1-65535";
i++;
}
}
// now compare the desired port to the parsed port policy
// The parsed policy is an ordered array of:
// ("accept" / "reject") SP PortOrRange
if (parsedIPv4ExitPortList != null) {
for (String portPolicy : parsedIPv4ExitPortList) {
// because we skip some lines when parsing descriptors, some lines may be empty
if (portPolicy != null) {
String[] lineSplit = portPolicy.split(" ");
Boolean accepted = lineSplit[0].equals("accept");
String portRange = lineSplit[1];
String[] portSplit = portRange.split("-");
if (portSplit.length == 1) {
// duplicate the single port to make a range
String port = portSplit[0];
portSplit = new String[2];
portSplit[0] = port;
portSplit[1] = port;
}
// portSplit is now a port range
try {
int lowPort = Integer.parseInt(portSplit[0]);
int highPort = Integer.parseInt(portSplit[1]);
if (exitPort >= lowPort && exitPort <= highPort)
// return the range match result
return accepted;
else
// no match, so check the next line
continue;
} catch (NumberFormatException e) {
// a line we don't understand - assume policy is too complex
System.out.println("acceptsIPv4ExitPort: failed to parse " + lineSplit[0]
+ " port numbers in exit policy for: " + name
+ ", assuming reject by: " + portPolicy);
return false;
}
}
}
}
// if no line matches the port,
// either we've used a consensus summary that doesn't contain the port,
// or we couldn't find both the consensus summary and the router descriptor
return false;
}
@Override
public String toString() {
return toString(false);
}
public String toString(boolean resolveHostname) {
if(resolveHostname)
ip.getHostName();
return "OnionRouter [name=" + name + ", ip=" + ip + ", orport="
+ orport + ", identityhash=" + identityhash + ", Flags=(" + StringUtils.join(flags.iterator(), ",") + ")]";
}
}