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(); } }