package org.klomp.snark.dht;
/*
* From zzzot, modded and relicensed to GPLv2
*/
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.SimpleDataStructure;
import net.i2p.util.RandomSource;
/*
* A Node ID, Hash, and port, and an optional Destination.
* This is what DHTNodes remembers. The DHT tracker just stores Hashes.
* getData() returns the 54 byte compact info (NID, Hash, port).
*
* Things are a little tricky in KRPC since we exchange Hashes and don't
* always have the Destination.
* The conpact info is immutable. The Destination may be added later.
*
* @since 0.9.2
* @author zzz
*/
class NodeInfo extends SimpleDataStructure {
private final NID nID;
private final Hash hash;
private Destination dest;
private final int port;
public static final int LENGTH = NID.HASH_LENGTH + Hash.HASH_LENGTH + 2;
/**
* With a fake NID used for pings
*/
public NodeInfo(Destination dest, int port) {
super();
this.nID = KRPC.FAKE_NID;
this.dest = dest;
this.hash = dest.calculateHash();
this.port = port;
initialize();
}
/**
* Use this if we have the full destination
* @throws IllegalArgumentException
*/
public NodeInfo(NID nID, Destination dest, int port) {
super();
this.nID = nID;
this.dest = dest;
this.hash = dest.calculateHash();
this.port = port;
initialize();
verify();
}
/**
* No Destination yet available
* @throws IllegalArgumentException
*/
public NodeInfo(NID nID, Hash hash, int port) {
super();
this.nID = nID;
this.hash = hash;
this.port = port;
initialize();
verify();
}
/**
* No Destination yet available
* @param compactInfo 20 byte node ID, 32 byte destHash, 2 byte port
* @param offset starting at this offset in compactInfo
* @throws IllegalArgumentException
* @throws ArrayIndexOutOfBoundsException
*/
public NodeInfo(byte[] compactInfo, int offset) {
super();
byte[] d = new byte[LENGTH];
System.arraycopy(compactInfo, offset, d, 0, LENGTH);
setData(d);
byte[] ndata = new byte[NID.HASH_LENGTH];
System.arraycopy(d, 0, ndata, 0, NID.HASH_LENGTH);
this.nID = new NID(ndata);
this.hash = Hash.create(d, NID.HASH_LENGTH);
this.port = (int) DataHelper.fromLong(d, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2);
if (port <= 0 || port >= 65535)
throw new IllegalArgumentException("Bad port");
verify();
}
/**
* Create from persistent storage string.
* Format: NID:Hash:Destination:port
* First 3 in base 64; Destination may be empty string
* @throws IllegalArgumentException
*/
public NodeInfo(String s) throws DataFormatException {
super();
String[] parts = DataHelper.split(s, ":", 4);
if (parts.length != 4)
throw new DataFormatException("Bad format");
byte[] nid = Base64.decode(parts[0]);
if (nid == null || nid.length != NID.HASH_LENGTH)
throw new DataFormatException("Bad NID");
nID = new NID(nid);
byte[] h = Base64.decode(parts[1]);
if (h == null || h.length != Hash.HASH_LENGTH)
throw new DataFormatException("Bad hash");
//hash = new Hash(h);
hash = Hash.create(h);
if (parts[2].length() > 0)
dest = new Destination(parts[2]);
try {
port = Integer.parseInt(parts[3]);
} catch (NumberFormatException nfe) {
throw new DataFormatException("Bad port", nfe);
}
initialize();
}
/**
* Creates 54-byte compact info
* @throws IllegalArgumentException
*/
private void initialize() {
if (port <= 0 || port >= 65535)
throw new IllegalArgumentException("Bad port");
byte[] compactInfo = new byte[LENGTH];
System.arraycopy(nID.getData(), 0, compactInfo, 0, NID.HASH_LENGTH);
System.arraycopy(hash.getData(), 0, compactInfo, NID.HASH_LENGTH, Hash.HASH_LENGTH);
DataHelper.toLong(compactInfo, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2, port);
setData(compactInfo);
}
/**
* Generate a secure NID that matches the Hash and port.
* Rules: First 4 bytes must match Hash.
* Next 2 bytes must match Hash ^ port.
* Remaining bytes may be random.
*
* @throws IllegalArgumentException
*/
public static NID generateNID(Hash h, int p, RandomSource random) {
byte[] n = new byte[NID.HASH_LENGTH];
System.arraycopy(h.getData(), 0, n, 0, 6);
n[4] ^= (byte) (p >> 8);
n[5] ^= (byte) p;
random.nextBytes(n, 6, NID.HASH_LENGTH - 6);
return new NID(n);
}
/**
* Verify the NID matches the Hash.
* See generateNID() for requirements.
* @throws IllegalArgumentException on mismatch
*/
private void verify() {
if (!KRPC.SECURE_NID)
return;
byte[] nb = nID.getData();
byte[] hb = hash.getData();
if ((!DataHelper.eq(nb, 0, hb, 0, 4)) ||
((nb[4] ^ (port >> 8)) & 0xff) != (hb[4] & 0xff) ||
((nb[5] ^ port) & 0xff) != (hb[5] & 0xff))
throw new IllegalArgumentException("NID/Hash mismatch");
}
public int length() {
return LENGTH;
}
public NID getNID() {
return this.nID;
}
/** @return may be null if we don't have it */
public Destination getDestination() {
return this.dest;
}
public Hash getHash() {
return this.hash;
}
@Override
public Hash calculateHash() {
return this.hash;
}
/**
* This can come in later but the hash must match.
* @throws IllegalArgumentException if hash of dest doesn't match previous hash
*/
public void setDestination(Destination dest) throws IllegalArgumentException {
if (this.dest != null)
return;
if (!dest.calculateHash().equals(this.hash))
throw new IllegalArgumentException("Hash mismatch, was: " + this.hash + " new: " + dest.calculateHash());
this.dest = dest;
}
public int getPort() {
return this.port;
}
public long lastSeen() {
return nID.lastSeen();
}
@Override
public int hashCode() {
return super.hashCode() ^ nID.hashCode() ^ port;
}
@Override
public boolean equals(Object o) {
try {
NodeInfo ni = (NodeInfo) o;
// assume dest matches, ignore it
return this.hash.equals(ni.hash) && nID.equals(ni.nID) && port == ni.port;
} catch (RuntimeException e) {
return false;
}
}
@Override
public String toString() {
return "NodeInfo: " + nID + ' ' + hash + " port: " + port + (dest != null ? " known dest" : " null dest");
}
/**
* To persistent storage string.
* Format: NID:Hash:Destination:port
* First 3 in base 64; Destination may be empty string
*/
public String toPersistentString() {
StringBuilder buf = new StringBuilder(650);
buf.append(nID.toBase64()).append(':');
buf.append(hash.toBase64()).append(':');
if (dest != null)
buf.append(dest.toBase64());
buf.append(':').append(port);
return buf.toString();
}
}