/**
* OnionCoffee - Anonymous Communication through TOR Network
* Copyright (C) 2005-2007 RWTH Aachen University, Informatik IV
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
package TorJava;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bouncycastle.asn1.pkcs.RSAPrivateKeyStructure;
import org.bouncycastle.asn1.x509.RSAPublicKeyStructure;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import TorJava.Common.Encoding;
import TorJava.Common.Encryption;
import TorJava.Common.Parsing;
import TorJava.Common.TorException;
/**
* a compound data structure that keeps track of the static informations we have
* about a single Tor server.
*
* CG - 07/07/09 Removed GeoIP
*
* @author Lexi Pimenidis
* @author Andriy Panchenko
* @author Michael Koellejan
* @author Connell Gauld
* @version unstable
*/
public class Server {
Tor tor;
// The raw router descriptor which has been handed to us.
// In the normal case we just return this stored descriptor.
String routerDescriptor;
// Information extracted from the Router descriptor.
public String nickname;
String hostname; // ip or hostname
public InetAddress address; // the resolved hostname
public String countryCode; // country code where it is located
int orPort;
int socksPort;
int dirPort;
int bandwidthAvg;
int bandwidthBurst;
int bandwidthObserved;
String platform;
Date published;
byte[] fingerprint;
int uptime;
RSAPublicKeyStructure onionKey;
RSAPrivateKeyStructure onionKeyPrivate;
RSAPublicKeyStructure signingKey;
RSAPrivateKeyStructure signingKeyPrivate;
ExitPolicy[] exitpolicy;
byte[] router_signature;
String contact;
HashSet<String> family = new HashSet<String>();
// FIXME: read-history, write-history not implemented
static final String PUBLISHED_ITEM_SIMPLEDATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
static final int MAX_EXITPOLICY_ITEMS = 300;
private SimpleDateFormat dateFormat;
// Additional information for V2-Directories
Date lastUpdate;
boolean dirv2_authority = false;
boolean dirv2_exit = false; // v1 trusted
boolean dirv2_fast = false;
boolean dirv2_guard = false;
boolean dirv2_named = false;
boolean dirv2_stable = false;
boolean dirv2_running = false; // v1 alive
boolean dirv2_valid = false;
boolean dirv2_v2dir = false;
// Additional internal information
// removed - are replaced with dirv2_* stuff
//boolean alive = true;
//boolean trusted = false;
// TorJava Server-Ranking data
float rankingIndex;
static final int highBandwidth = 2097152; // see updateServerRanking()
static final float alpha = 0.6f; // see updateServerRanking()
//static final float rankingIndexEffect = 0.5f; // see getRefinedRankingIndex
static final float punishmentFactor = 0.75f; // coefficient to decrease
// server ranking if the // server fails to respo // in ti
/**
* compound data structure for storing exit policies
*/
class ExitPolicy {
boolean accept; // if false: reject
long ip;
long netmask;
int lo_port;
int hi_port;
}
/**
* takes a router descriptor as string
*
* @param routerDescriptor
* a router descriptor to initialize the object from
*/
Server(Tor tor,String routerDescriptor) throws TorException {
if (tor==null) throw new TorException("Server.<init>: tor is null");
this.tor = tor;
init();
update(routerDescriptor);
this.countryCode = "?";
}
/**
* Special constructor for hidden service: Faked server in connectToHidden().
* @param pk
* @throws TorException
*/
Server(Tor tor,RSAPublicKeyStructure pk) throws TorException {
if (tor==null) throw new TorException("Server.<init>: tor is null");
this.tor = tor;
init();
onionKey = pk;
this.countryCode = "?";
}
/**
* takes input data and initializes the server object with it. A router
* descriptor and a signature will be automatically generated.
*/
Server(Tor tor,String varNickname, InetAddress varAddress, int varOrPort, int varSocksPort,
int varDirPort, int varBandwidthAvg, int varBandwidthBurst,
int varBandwidthObserved, byte[] varfingerprint, int varInitialUptime,
RSAPublicKeyStructure varOnionKey, RSAPrivateKeyStructure varOnionKeyPrivate,
RSAPublicKeyStructure varSigningKey, RSAPrivateKeyStructure varSigningKeyPrivate,
ExitPolicy[] varExitpolicy, String varContact, HashSet<String> varFamily)
throws TorException
{
if (tor==null) throw new TorException("Server.<init>: tor is null");
// Set member variables.
this.tor = tor;
this.nickname = varNickname;
this.address = varAddress;
this.hostname = varAddress.getHostAddress();
this.orPort = varOrPort;
this.socksPort = varSocksPort;
this.dirPort = varDirPort;
this.bandwidthAvg = varBandwidthAvg;
this.bandwidthBurst = varBandwidthBurst;
this.bandwidthObserved = varBandwidthObserved;
this.platform = TorConfig.TORJAVA_VERSION_STRING + " on " + TorConfig.operatingSystem();
this.published = new Date(System.currentTimeMillis());
this.fingerprint = new byte[varfingerprint.length];
System.arraycopy(varfingerprint, 0, this.fingerprint, 0,
varfingerprint.length);
this.uptime = varInitialUptime;
this.onionKey = varOnionKey;
this.onionKeyPrivate = varOnionKeyPrivate;
this.signingKey = varSigningKey;
this.signingKeyPrivate = varSigningKeyPrivate;
this.exitpolicy = varExitpolicy;
this.contact = varContact;
this.family = varFamily;
// Render router descriptor
this.routerDescriptor = renderRouterDescriptor();
this.countryCode = "?";
}
/** Constructor-indepentent initialization **/
private void init() {
dateFormat = new SimpleDateFormat(PUBLISHED_ITEM_SIMPLEDATE_FORMAT);
// unknown/new
rankingIndex = -1;
}
/**
* updates the object from a router descriptor
*
* @param routerDescriptor
* string encoded router descriptor
*/
void update(String routerDescriptor) throws TorException {
parseRouterDescriptor(routerDescriptor);
updateServerRanking();
}
/**
* wrapper from server-flags of dir-spec v1 to dir-spec v2
*/
void updateServerStatus(boolean alive,boolean trusted) {
dirv2_running = alive;
dirv2_exit = trusted;
dirv2_guard = trusted;
dirv2_valid = trusted;
}
/**
* Update this server's status
*
* @param flags string containing flags
*/
void updateServerStatus(String flags) {
if (flags.indexOf("Running")>=0) dirv2_running = true;
if (flags.indexOf("Exit")>=0) dirv2_exit = true;
if (flags.indexOf("Authority")>=0) dirv2_authority = true;
if (flags.indexOf("Fast")>=0) dirv2_fast = true;
if (flags.indexOf("Guard")>=0) dirv2_guard = true;
if (flags.indexOf("Stable")>=0) dirv2_stable = true;
if (flags.indexOf("Named")>=0) dirv2_named = true;
if (flags.indexOf("V2Dir")>=0) dirv2_v2dir = true;
if (flags.indexOf("Valid")>=0) dirv2_valid = true;
}
/**
* @return the regular expression that can be evaluated by the
* initialisation function
*/
static String regularExpression() {
return "(router (\\w+) \\S+ \\d+ \\d+.*?END SIGNATURE-----\n)";
}
/**
* This function parses the exit policy items from the router descriptor.
*
* @param routerDescriptor
* a router descriptor with exit policy items.
* @return the complete exit policy
*/
private ExitPolicy[] parseExitPolicy(String routerDescriptor) {
ArrayList<ExitPolicy> epList = new ArrayList<ExitPolicy>(30);
ExitPolicy ep;
Pattern p = Pattern.compile("^(accept|reject) (.*?):(.*?)$",
Pattern.DOTALL + Pattern.MULTILINE + Pattern.CASE_INSENSITIVE
+ Pattern.UNIX_LINES);
Matcher m = p.matcher(routerDescriptor);
// extract all exit policies from description
int nr = 0;
while (m.find() && (nr < MAX_EXITPOLICY_ITEMS)) {
ep = new ExitPolicy();
ep.accept = m.group(1).equals("accept");
// parse network
String network = m.group(2);
ep.ip = 0;
ep.netmask = 0;
if (!network.equals("*")) {
int slash = network.indexOf("/");
if (slash >= 0) {
ep.ip = Encoding.dottedNotationToBinary(network.substring(0,
slash));
String netmask = network.substring(slash + 1);
if (netmask.indexOf(".")>-1)
ep.netmask = Encoding.dottedNotationToBinary(netmask);
else ep.netmask = (((0xffffffffL << (32-(Integer.parseInt(netmask))))) & 0xffffffffL);
} else {
ep.ip = Encoding.dottedNotationToBinary(network);
ep.netmask = 0xffffffff;
}
;
}
;
ep.ip = ep.ip & ep.netmask;
// parse port range
if (m.group(3).equals("*")) {
ep.lo_port = 0;
ep.hi_port = 65535;
} else {
int dash = m.group(3).indexOf("-");
if (dash > 0) {
ep.lo_port = Integer
.parseInt(m.group(3).substring(0, dash));
ep.hi_port = Integer.parseInt(m.group(3)
.substring(dash + 1));
} else {
ep.lo_port = Integer.parseInt(m.group(3));
ep.hi_port = ep.lo_port;
}
;
}
;
++nr;
epList.add(ep);
}
return (ExitPolicy[]) (epList.toArray(new ExitPolicy[1]));
}
/**
* extracts all relevant information from the router discriptor and saves it
* in the member variables.
*
* @param rd
* string encoded router descriptor
*/
private void parseRouterDescriptor(String rd)
throws TorException {
this.routerDescriptor = rd;
// Router item: nickname, hostname, onion-router-port, socks-port, dir-port
Pattern p = Pattern.compile(
"^router (\\w+) (\\S+) (\\d+) (\\d+) (\\d+)", Pattern.DOTALL
+ Pattern.MULTILINE + Pattern.CASE_INSENSITIVE
+ Pattern.UNIX_LINES);
Matcher m = p.matcher(rd);
m.find();
this.nickname = m.group(1);
this.hostname = m.group(2);
this.orPort = Integer.parseInt(m.group(3));
this.socksPort = Integer.parseInt(m.group(4));
this.dirPort = Integer.parseInt(m.group(5));
// secondary information
platform = Parsing.parseStringByRE(rd, "^platform (.*?)$", "unknown");
published = dateFormat.parse(Parsing.parseStringByRE(rd, "^published (.*?)$", ""), (new ParsePosition(0)));
uptime = Integer.parseInt(Parsing.parseStringByRE(rd, "^uptime (\\d+)", "0"));
fingerprint = Encoding.parseHex(Parsing.parseStringByRE(rd, "^opt fingerprint (.*?)$", ""));
contact = Parsing.parseStringByRE(rd, "^contact (.*?)$", "");
// make that IF description is from a trusted server, that fingerprint is correct
if (tor.config.trustedServers.containsKey(nickname)) {
String fingerprintFromConfig = (String) (tor.config.trustedServers.get(nickname)).get("fingerprint");
if (!Encoding.toHexString(fingerprint).equalsIgnoreCase(fingerprintFromConfig))
throw new TorException("Server " + nickname + " is trusted, but fingerprint check failed");
}
// bandwith
p = Pattern.compile("^bandwidth (\\d+) (\\d+) (\\d+)?", Pattern.DOTALL
+ Pattern.MULTILINE + Pattern.CASE_INSENSITIVE
+ Pattern.UNIX_LINES);
m = p.matcher(rd);
if (m.find()) {
bandwidthAvg = Integer.parseInt(m.group(1));
bandwidthBurst = Integer.parseInt(m.group(2));
bandwidthObserved = Integer.parseInt(m.group(3));
}
;
// onion key
String stringOnionKey = Parsing.parseStringByRE(rd,
"^onion-key\n(.*?END RSA PUBLIC KEY......)", "");
onionKey = Encryption.extractRSAKey(stringOnionKey);
// signing key
String stringSigningKey = Parsing.parseStringByRE(rd,
"^signing-key\n(.*?END RSA PUBLIC KEY-----\n)", "");
signingKey = Encryption.extractRSAKey(stringSigningKey);
SHA1Digest sha1 = new SHA1Digest();
// verify signing-key against fingerprint
try {
RSAPublicKeyStructure signingKey_asn = new RSAPublicKeyStructure(
signingKey.getModulus(), signingKey.getPublicExponent());
byte[] pkcs = Encryption
.getPKCS1EncodingFromRSAPublicKey(signingKey_asn);
byte[] key_hash = new byte[20];
sha1.update(pkcs, 0, pkcs.length);
sha1.doFinal(key_hash, 0);
if (!Encoding.arraysEqual(key_hash, fingerprint))
throw new TorException("Server " + nickname
+ " doesn't verify signature vs fingerprint");
} catch (Exception e) {
throw new TorException("Server " + nickname
+ " doesn't verify signature vs fingerprint");
}
// parse family
String stringFamily = Parsing.parseStringByRE(rd, "^family (.*?)$", "");
if (stringFamily == "")
stringFamily = Parsing.parseStringByRE(rd, "^opt family (.*?)$", "");
Pattern p_family = Pattern.compile("(\\S+)");
Matcher m_family = p_family.matcher(stringFamily);
while (m_family.find()) {
String host = m_family.group(1);
family.add(host);
}
// check the validity of the signature
router_signature = Encoding
.parseBase64(Parsing
.parseStringByRE(
rd,
"^router-signature\n-----BEGIN SIGNATURE-----(.*?)-----END SIGNATURE-----",
""));
byte[] sha1_input = (Parsing.parseStringByRE(rd,
"^(router .*?router-signature\n)", "")).getBytes();
if (!Encryption.verifySignature(router_signature, signingKey, sha1_input)) {
Logger.logCrypto(Logger.ERROR,
"Server -> router-signature check failed for " + nickname);
throw new TorException("Server " + nickname
+ ": description signature verification failed");
}
// exit policy
exitpolicy = parseExitPolicy(rd);
// usually in directory the hostname is already set to the IP
// so, following resolve just converts it to the InetAddress
try {
address = InetAddress.getByName(hostname);
} catch (UnknownHostException e) {
throw new TorException("Server.ParseRouterDescriptor: Unresolvable hostname " + hostname);
}
}
/**
* converts exit policy objects back into an item
*
* @param ep
* an array of exit-policy objects.
* @return an exit policy item.
*
*/
private String renderExitPolicy(ExitPolicy[] ep) {
StringBuffer raw_policy = new StringBuffer();
for (int i = 0; i < ep.length; i++) {
if (ep[i].accept)
raw_policy.append("accept ");
else
raw_policy.append("reject ");
if (ep[i].netmask == 0 && ep[i].ip == 0) {
raw_policy.append("*");
} else {
if (ep[i].netmask == 0xffffffff) {
raw_policy.append(Encoding.binaryToDottedNotation(ep[i].ip));
} else {
raw_policy.append(Encoding.binaryToDottedNotation(ep[i].ip));
raw_policy.append("/"
+ Encoding.netmaskToInt(ep[i].netmask));
// + Encoding.binaryToDottedNotation(ep[i].netmask)); // deprecated
}
}
raw_policy.append(":");
if (ep[i].lo_port == 0 && ep[i].hi_port == 65535) {
raw_policy.append("*");
} else {
if (ep[i].lo_port == ep[i].hi_port) {
raw_policy.append(ep[i].lo_port);
} else {
raw_policy.append(ep[i].lo_port + "-" + ep[i].hi_port);
}
}
raw_policy.append("\n");
}
return raw_policy.toString();
}
/**
* renders a router descriptor from member variables
*
* @return router descriptor in extensible information format
*/
String renderRouterDescriptor() {
StringBuffer rawServer = new StringBuffer();
rawServer.append("router " + nickname + " " + address.getHostAddress() + " " + orPort + " " + socksPort + " " + dirPort + "\n");
rawServer.append("platform " + platform + "\n");
rawServer.append("published " + dateFormat.format(published) + "\n");
rawServer.append("opt fingerprint " + Parsing.renderFingerprint(fingerprint, true) + "\n");
if (uptime != 0)
rawServer.append("uptime " + uptime + "\n");
rawServer.append("bandwidth " + bandwidthAvg + " " + bandwidthBurst + " " + bandwidthObserved + "\n");
rawServer.append("onion-key\n" + Encryption.getPEMStringFromRSAPublicKey(onionKey) + "\n");
rawServer.append("signing-key\n" + Encryption.getPEMStringFromRSAPublicKey(signingKey) + "\n");
String stringFamily = "";
Iterator<String> familyIterator = family.iterator();
while(familyIterator.hasNext()) {
stringFamily += " " + familyIterator.next();
}
rawServer.append("opt family" + stringFamily + "\n");
if (contact != "")
rawServer.append("contact " + contact + "\n");
rawServer.append(renderExitPolicy(exitpolicy));
// sign data
rawServer.append("router-signature\n");
rawServer.append("directory-signature " + tor.config.nickname + "\n");
byte[] data = rawServer.toString().getBytes();
rawServer.append(Encryption.binarySignatureToPEM(Encryption.signData(data,
new RSAKeyParameters(true, signingKeyPrivate.getModulus(),
signingKeyPrivate.getPrivateExponent()))));
return rawServer.toString();
}
/**
* updates the server ranking index
*
* Is supposed to be between 0 (undesirable) and 1 (very desirable). Two
* variables are taken as input:
* <ul>
* <li> the uptime
* <li> the bandwidth
* <li> if available: the previous ranking
* </ul>
*/
private void updateServerRanking() {
float rankingFromDirectory = (Math.min(1, uptime / 86400) + Math.min(1,
(bandwidthAvg * alpha + bandwidthObserved * (1 - alpha))
/ highBandwidth)) / 2; // 86400 is uptime of 24h
// build over-all ranking from old value (if available) and new
if (rankingIndex<0) {
rankingIndex = rankingFromDirectory;
} else {
rankingIndex = rankingFromDirectory *(1-TorConfig.rankingTransferPerServerUpdate) +
rankingIndex * TorConfig.rankingTransferPerServerUpdate;
}
Logger.logDirectory(Logger.VERBOSE,"Server.updateServerRanking: "+nickname+" is ranked "+rankingIndex);
}
/**
* returns ranking index taking into account user preference
*
* @param p
* user preference (importance) of considering ranking index
* <ul>
* <li> 0 select hosts completely randomly
* <li> 1 select hosts with good uptime/bandwidth with higher
* prob.
* </ul>
*/
float getRefinedRankingIndex(float p) {
// align all ranking values to 0.5, if the user wants to choose his
// servers
// from a uniform probability distribution
return (rankingIndex * p + TorConfig.rankingIndexEffect * (1 - p));
}
/**
* decreases ranking_index by the punishment_factor
*/
void punishRanking() {
rankingIndex *= punishmentFactor;
}
/**
* can be used to query the exit policies wether this server would allow
* outgoing connections to the host and port as given in the parameters.
* <b>IMPORTANT:</b> this routing must be able to work, even if <i>addr</i>
* is not given!
*
* @param addr
* the host that someone wants to connect to
* @param port
* the port that is to be connected to
* @return a boolean value wether the conenction would be allowed
*/
boolean exitPolicyAccepts(InetAddress addr, int port) { // used by
// create_new_route
long ip;
if (addr != null) { // set IP as given
byte[] temp1 = addr.getAddress();
long[] temp = new long[4];
for (int i = 0; i < 4; ++i) {
temp[i] = temp1[i];
if (temp[i] < 0)
temp[i] = 256 + temp[i];
}
;
ip = ((temp[0] << 24) | (temp[1] << 16) | (temp[2] << 8) | temp[3]);
} else {
// HACK: if no IP and port is given, always return true
if (port == 0)
return true;
// HACK: if no IP is given, use only exits that allow ALL ip-ranges
// this should possibly be replaced by some other way of checking it
ip = 0xffffffffL;
}
;
for (int i = 0; i < exitpolicy.length; ++i) {
if ((exitpolicy[i].lo_port <= port)
&& (exitpolicy[i].hi_port >= port)
&& (exitpolicy[i].ip == (ip & exitpolicy[i].netmask))) {
return exitpolicy[i].accept;
}
;
}
;
return false;
}
/**
* @return can this server be used as a directory-server?
*/
boolean isDirServer() {
return (dirPort > 0);
}
// DEBUG_START
/**
* used for debugging purposes
*
* @param b
* an array t be printed in hex
*/
private String print_array(byte[] b) {
String hex = "0123456789abcdef";
StringBuffer sb = new StringBuffer();
for (int i = 0; i < b.length; ++i) {
int x = b[i];
if (x < 0)
x = 256 + x; // why are there no unsigned bytes in java?
sb.append(hex.substring(x >> 4, (x >> 4) + 1));
sb.append(hex.substring(x % 16, (x % 16) + 1));
sb.append(" ");
}
;
return sb.toString();
}
/**
* used for debugging purposes
*/
String print() {
StringBuffer sb = new StringBuffer();
sb.append("---- " + nickname + " (" + contact + ")\n");
sb.append("hostname:" + hostname + "\n");
sb.append("or port:" + orPort + "\n");
sb.append("socks port:" + socksPort + "\n");
sb.append("dirserver port:" + dirPort + "\n");
sb.append("platform:" + platform + "\n");
sb.append("published:" + published + "\n");
sb.append("uptime:" + uptime + "\n");
sb.append("bandwidth: " + bandwidthAvg + " " + bandwidthBurst
+ " " + bandwidthObserved + "\n");
sb.append("fingerprint:" + print_array(fingerprint) + "\n");
sb.append("onion key:" + onionKey + "\n");
sb.append("signing key:" + signingKey + "\n");
sb.append("signature:" + print_array(router_signature) + "\n");
sb.append("exit policies:" + "\n");
for (int i = 0; i < exitpolicy.length; ++i)
sb.append(" " + exitpolicy[i].accept + " "
+ Encoding.toHex(exitpolicy[i].ip) + "/"
+ Encoding.toHex(exitpolicy[i].netmask) + ":"
+ exitpolicy[i].lo_port + "-" + exitpolicy[i].hi_port
+ "\n");
return sb.toString();
}
// DEBUG_END
}