Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
5af780b935 | |||
3a4b2fe14f |
484
Leenkx.js
Normal file
484
Leenkx.js
Normal file
@ -0,0 +1,484 @@
|
||||
var RAWCHANNEL = "";
|
||||
module.exports = Leenkx;
|
||||
|
||||
var debug = console.log;
|
||||
var WebTorrent = require("webtorrent");
|
||||
var bencode = require("bencode");
|
||||
var nacl = require("tweetnacl");
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
var inherits = require("inherits");
|
||||
var bs58 = require("bs58");
|
||||
var bs58check = require("bs58check");
|
||||
var ripemd160 = require("ripemd160");
|
||||
|
||||
inherits(Leenkx, EventEmitter);
|
||||
|
||||
var EXT = "lx_channel";
|
||||
var PEERTIMEOUT = 5 * 60 * 1000;
|
||||
var SEEDPREFIX = "490a";
|
||||
var ADDRESSPREFIX = "55";
|
||||
|
||||
/**
|
||||
* Multi-party data channels on WebTorrent extension.
|
||||
*/
|
||||
function Leenkx(identifier, opts) {
|
||||
if (identifier && typeof identifier == "object") {
|
||||
opts = identifier;
|
||||
identifier = null;
|
||||
}
|
||||
var opts = opts || {};
|
||||
if (!(this instanceof Leenkx)) return new Leenkx(identifier, opts);
|
||||
|
||||
var trackeropts = opts.tracker || {};
|
||||
trackeropts.getAnnounceOpts =
|
||||
trackeropts.getAnnounceOpts ||
|
||||
function() {
|
||||
return { numwant: 4 };
|
||||
};
|
||||
if (opts.iceServers) {
|
||||
trackeropts.rtcConfig = { iceServers: opts.iceServers };
|
||||
}
|
||||
this.announce = opts.announce || [
|
||||
"wss://ws1.leenkx.com",
|
||||
"wss://tracker.openwebtorrent.com"
|
||||
];
|
||||
this.wt = opts.wt || new WebTorrent({ tracker: trackeropts });
|
||||
this.nacl = nacl;
|
||||
|
||||
if (opts["seed"]) {
|
||||
this.seed = opts["seed"];
|
||||
} else {
|
||||
//random seed
|
||||
this.seed = this.encodeseed(nacl.randomBytes(32));
|
||||
}
|
||||
|
||||
this.timeout = opts["timeout"] || PEERTIMEOUT; //5 minutes
|
||||
this.keyPair =
|
||||
opts["keyPair"] ||
|
||||
nacl.sign.keyPair.fromSeed(
|
||||
Uint8Array.from(bs58check.decode(this.seed)).slice(2)
|
||||
);
|
||||
|
||||
this.pk = bs58.encode(Buffer.from(this.keyPair.publicKey));
|
||||
|
||||
this.identifier = identifier || this.address();
|
||||
this.peers = {}; // list of peers seen recently: address -> pk, timestamp
|
||||
this.seen = {}; // messages we've seen recently: hash -> timestamp
|
||||
this.lastwirecount = null;
|
||||
|
||||
// pending callback functions
|
||||
this.callbacks = {};
|
||||
this.serveraddress = null;
|
||||
this.heartbeattimer = null;
|
||||
|
||||
debug("address", this.address());
|
||||
debug("identifier", this.identifier);
|
||||
debug("public key", this.pk);
|
||||
|
||||
if (typeof File == "object") {
|
||||
var blob = new File([this.identifier], this.identifier);
|
||||
} else {
|
||||
var blob = new Buffer.from(this.identifier);
|
||||
blob.name = this.identifier;
|
||||
}
|
||||
|
||||
//seeding the webtorrent is where the magic happens
|
||||
var torrent = this.wt.seed(
|
||||
blob,
|
||||
{ name: this.identifier, announce: this.announce },
|
||||
//function onseed(torrent)
|
||||
partial(function(leenkx, torrent) {
|
||||
// debug("torrent", leenkx.identifier, torrent);
|
||||
debug("torrent", "torrent.name", torrent.name);
|
||||
debug(
|
||||
"torrent.infoHash",
|
||||
torrent.infoHash,
|
||||
"torrent.magnetURI",
|
||||
torrent.magnetURI
|
||||
);
|
||||
leenkx.emit("torrent", leenkx.identifier, torrent);
|
||||
//using torrent discovery API
|
||||
if (torrent.discovery.tracker) {
|
||||
torrent.discovery.tracker.on("update", function(update) {
|
||||
leenkx.emit("tracker", leenkx.identifier, update);
|
||||
});
|
||||
}
|
||||
torrent.discovery.on("trackerAnnounce", function() {
|
||||
leenkx.emit("announce", leenkx);
|
||||
leenkx.connections();
|
||||
});
|
||||
}, this)
|
||||
);
|
||||
// Emitted whenever a new peer is connected for this torrent.
|
||||
torrent.on("wire", partial(attach, this, this.identifier));
|
||||
console.log("connected to peer with identifier " + this.identifier);
|
||||
this.torrent = torrent;
|
||||
|
||||
if (opts.heartbeat) {
|
||||
this.heartbeat(opts.heartbeat);
|
||||
}
|
||||
}
|
||||
|
||||
Leenkx.prototype.WebTorrent = WebTorrent;
|
||||
|
||||
//I wonder why he is encoding the seed, I don't think it was needed
|
||||
Leenkx.encodeseed = Leenkx.prototype.encodeseed = function(material) {
|
||||
return bs58check.encode(
|
||||
Buffer.concat([Buffer.from(SEEDPREFIX, "hex"), Buffer.from(material)])
|
||||
);
|
||||
};
|
||||
|
||||
//I also don't understand why he is encoding the heartbeat
|
||||
Leenkx.encodeaddress = Leenkx.prototype.encodeaddress = function(material) {
|
||||
return bs58check.encode(
|
||||
Buffer.concat([
|
||||
Buffer.from(ADDRESSPREFIX, "hex"),
|
||||
new ripemd160().update(Buffer.from(nacl.hash(material))).digest()
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
// smart way of removing old peers
|
||||
// start a heartbeat and expire old "seen" peers who don't send us a heartbeat
|
||||
Leenkx.prototype.heartbeat = function(interval) {
|
||||
var interval = interval || 30000;
|
||||
this.heartbeattimer = setInterval(
|
||||
partial(function(leenkx) {
|
||||
// broadcast a 'ping' message
|
||||
leenkx.ping();
|
||||
var t = now();
|
||||
// remove any 'peers' entries with timestamps older than timeout
|
||||
for (var p in leenkx.peers) {
|
||||
var pk = leenkx.peers[p].pk;
|
||||
var address = leenkx.address(pk);
|
||||
var last = leenkx.peers[p].last;
|
||||
if (last + leenkx.timeout < t) {
|
||||
delete leenkx.peers[p];
|
||||
leenkx.emit("timeout", address);
|
||||
leenkx.emit("left", address);
|
||||
}
|
||||
}
|
||||
}, this),
|
||||
interval
|
||||
);
|
||||
};
|
||||
|
||||
// cleaning up means removing the torrent
|
||||
// clean up this leenkx instance
|
||||
Leenkx.prototype.destroy = function(cb) {
|
||||
clearInterval(this.heartbeattimer);
|
||||
var packet = makePacket(this, { y: "x" });
|
||||
sendRaw(this, packet);
|
||||
this.wt.remove(this.torrent, cb);
|
||||
};
|
||||
|
||||
Leenkx.prototype.close = Leenkx.prototype.destroy;
|
||||
|
||||
Leenkx.prototype.connections = function() {
|
||||
if (this.torrent.wires.length != this.lastwirecount) {
|
||||
this.lastwirecount = this.torrent.wires.length;
|
||||
this.emit("connections", this.torrent.wires.length);
|
||||
}
|
||||
return this.lastwirecount;
|
||||
};
|
||||
|
||||
// This is where this.address() goes
|
||||
// So it encodes your public key, which is also your address?
|
||||
Leenkx.prototype.address = function(pk) {
|
||||
if (pk && typeof pk == "string") {
|
||||
pk = bs58.decode(pk);
|
||||
} else if (pk && pk.length == 32) {
|
||||
pk = pk;
|
||||
} else {
|
||||
pk = this.keyPair.publicKey;
|
||||
}
|
||||
return this.encodeaddress(pk);
|
||||
};
|
||||
|
||||
Leenkx.address = Leenkx.prototype.address;
|
||||
|
||||
Leenkx.prototype.ping = function() {
|
||||
// send a ping out so they know about us too
|
||||
var packet = makePacket(this, { y: "p" });
|
||||
sendRaw(this, packet);
|
||||
};
|
||||
|
||||
Leenkx.prototype.send = function(address, message) {
|
||||
if (!message) {
|
||||
var message = address;
|
||||
var address = null;
|
||||
}
|
||||
var packet = makePacket(this, { y: "m", v: JSON.stringify(message) });
|
||||
sendRaw(this, packet);
|
||||
};
|
||||
|
||||
// outgoing
|
||||
|
||||
function makePacket(leenkx, params) {
|
||||
var p = {
|
||||
t: now(),
|
||||
i: leenkx.identifier,
|
||||
pk: leenkx.pk,
|
||||
n: nacl.randomBytes(8)
|
||||
};
|
||||
for (var k in params) {
|
||||
p[k] = params[k];
|
||||
}
|
||||
pe = bencode.encode(p);
|
||||
return bencode.encode({
|
||||
s: nacl.sign.detached(pe, leenkx.keyPair.secretKey),
|
||||
p: pe
|
||||
});
|
||||
}
|
||||
|
||||
//So you need to send a message over the wires
|
||||
//*And* use the extension, called lx_channel
|
||||
function sendRaw(leenkx, message) {
|
||||
var wires = leenkx.torrent.wires;
|
||||
//for each wire
|
||||
for (var w = 0; w < wires.length; w++) {
|
||||
//get the key "peerExtendedHankshake"
|
||||
var extendedhandshake = wires[w]["peerExtendedHandshake"];
|
||||
if (extendedhandshake && extendedhandshake.m && extendedhandshake.m[EXT]) {
|
||||
//This is where the magic happens
|
||||
//See github.com/webtorrent/bittorrent-protocol and http://www.bittorrent.org/beps/bep_0010.html
|
||||
//The explanation is a bit confusing though
|
||||
wires[w].extended(EXT, message);
|
||||
}
|
||||
}
|
||||
var hash = toHex(nacl.hash(message).slice(16)); //pure debug value
|
||||
debug("sent", hash, "to", wires.length, "wires"); //for this log
|
||||
}
|
||||
|
||||
// incoming -- this is where message unpacking happens and where you can see his message packing scheme the best
|
||||
// message types: (m)essage, (r)pc, (r)pc (r)esponse, (p)ing, (x)rossed out/leave/split/kruisje
|
||||
function onMessage(leenkx, identifier, wire, message) {
|
||||
// hash to reference incoming message
|
||||
var hash = toHex(nacl.hash(message).slice(16));
|
||||
var t = now();
|
||||
debug("raw message", identifier, message.length, hash);
|
||||
if (!leenkx.seen[hash]) {
|
||||
var unpacked = bencode.decode(message); //he needs to decode bencode, because that is how the bittorrent protocol communicates, I think...
|
||||
if (unpacked && unpacked.p) {
|
||||
debug(
|
||||
"unpacked message"
|
||||
// unpacked
|
||||
);
|
||||
var packet = bencode.decode(unpacked.p);
|
||||
var pk = packet.pk.toString();
|
||||
var id = packet.i.toString();
|
||||
var checksig = nacl.sign.detached.verify(
|
||||
unpacked.p,
|
||||
unpacked.s,
|
||||
bs58.decode(pk)
|
||||
);
|
||||
var checkid = id == identifier;
|
||||
var checktime = packet.t + leenkx.timeout > t;
|
||||
debug(
|
||||
"packet"
|
||||
// packet
|
||||
);
|
||||
if (checksig && checkid && checktime) {
|
||||
//note that this means the sender is pinged back
|
||||
sawPeer(leenkx, pk, identifier);
|
||||
// check packet types
|
||||
// m stands for message
|
||||
if (packet.y == "m") {
|
||||
debug(
|
||||
"message",
|
||||
identifier
|
||||
// packet
|
||||
);
|
||||
var messagestring = packet.v.toString();
|
||||
var messagejson = null;
|
||||
try {
|
||||
var messagejson = JSON.parse(messagestring);
|
||||
} catch (e) {
|
||||
debug("Malformed message JSON: " + messagestring);
|
||||
}
|
||||
if (messagejson) {
|
||||
leenkx.emit("message", leenkx.address(pk), messagejson, packet);
|
||||
}
|
||||
}
|
||||
// p stands for ping
|
||||
else if (packet.y == "p") {
|
||||
var address = leenkx.address(pk);
|
||||
debug("ping from", address);
|
||||
leenkx.emit("ping", address);
|
||||
}
|
||||
// x stands for split/leave
|
||||
else if (packet.y == "x") {
|
||||
var address = leenkx.address(pk);
|
||||
debug("got left from", address);
|
||||
delete leenkx.peers[address];
|
||||
leenkx.emit("left", address);
|
||||
} else {
|
||||
// TODO: handle ping/keep-alive message
|
||||
debug("unknown packet type");
|
||||
}
|
||||
} else {
|
||||
debug("dropping bad packet", hash, checksig, checkid, checktime);
|
||||
}
|
||||
} else {
|
||||
debug("skipping packet with no payload", hash, unpacked);
|
||||
}
|
||||
// forward first-seen message to all connected wires
|
||||
// TODO: block flooders
|
||||
sendRaw(leenkx, message);
|
||||
} else {
|
||||
debug("already seen", hash);
|
||||
}
|
||||
// refresh last-seen timestamp on this message
|
||||
leenkx.seen[hash] = now();
|
||||
}
|
||||
|
||||
// network functions
|
||||
|
||||
function sawPeer(leenkx, pk, identifier) {
|
||||
debug("sawPeer", leenkx.address(pk));
|
||||
var t = now();
|
||||
var address = leenkx.address(pk);
|
||||
// ignore ourself
|
||||
if (address != leenkx.address()) {
|
||||
// if we haven't seen this peer for a while
|
||||
if (
|
||||
!leenkx.peers[address] ||
|
||||
leenkx.peers[address].last + leenkx.timeout < t
|
||||
) {
|
||||
leenkx.peers[address] = {
|
||||
pk: pk,
|
||||
last: t
|
||||
};
|
||||
debug("seen", leenkx.address(pk));
|
||||
leenkx.emit("seen", leenkx.address(pk));
|
||||
if (leenkx.address(pk) == leenkx.identifier) {
|
||||
leenkx.serveraddress = address;
|
||||
debug("seen server", leenkx.address(pk));
|
||||
leenkx.emit("server", leenkx.address(pk));
|
||||
}
|
||||
// send a ping out so they know about us too
|
||||
var packet = makePacket(leenkx, { y: "p" });
|
||||
sendRaw(leenkx, packet);
|
||||
} else {
|
||||
leenkx.peers[address].last = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extension protocol plumbing
|
||||
// see also https://github.com/webtorrent/ut_metadata/blob/master/index.js for another example
|
||||
|
||||
function attach(leenkx, identifier, wire, addr) {
|
||||
debug("saw wire", wire.peerId, addr);
|
||||
wire.use(extension(leenkx, identifier, wire));
|
||||
wire.on("close", partial(detach, leenkx, identifier, wire));
|
||||
}
|
||||
|
||||
function detach(leenkx, identifier, wire) {
|
||||
debug("wire left", wire.peerId, identifier);
|
||||
leenkx.emit("wireleft", leenkx.torrent.wires.length, wire);
|
||||
leenkx.connections();
|
||||
}
|
||||
|
||||
// I need to debug this pure magic -- Melvin
|
||||
function extension(leenkx, identifier, wire) {
|
||||
var ext = partial(wirefn, leenkx, identifier);
|
||||
ext.prototype.name = EXT;
|
||||
ext.prototype.onExtendedHandshake = partial(
|
||||
onExtendedHandshake,
|
||||
leenkx,
|
||||
identifier,
|
||||
wire
|
||||
);
|
||||
ext.prototype.onMessage = partial(onMessage, leenkx, identifier, wire);
|
||||
return ext;
|
||||
}
|
||||
|
||||
function wirefn(leenkx, identifier, wire) {
|
||||
// TODO: sign handshake to prove key custody
|
||||
wire.extendedHandshake.id = identifier;
|
||||
wire.extendedHandshake.pk = leenkx.pk;
|
||||
}
|
||||
|
||||
function onExtendedHandshake(leenkx, identifier, wire, handshake) {
|
||||
debug(
|
||||
"wire extended handshake",
|
||||
leenkx.address(handshake.pk.toString()),
|
||||
wire.peerId
|
||||
// handshake
|
||||
);
|
||||
leenkx.emit("wireseen", leenkx.torrent.wires.length, wire);
|
||||
leenkx.connections();
|
||||
// TODO: check sig ðfnd drop on failure - wire.peerExtendedHandshake
|
||||
sawPeer(leenkx, handshake.pk.toString(), identifier);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
_onChannelMessage (event) {
|
||||
if (this.destroyed) return;
|
||||
if(event.data.constructor.name == "String"){
|
||||
leenkx.network.Leenkx.data.set(RAWCHANNEL, event.data);
|
||||
leenkx.network.Leenkx.id.set(RAWCHANNEL, event.srcElement.label);
|
||||
leenkx.network.Leenkx.connections.h[RAWCHANNEL].onmessage();
|
||||
return;
|
||||
}
|
||||
let data = event.data;
|
||||
if (data instanceof ArrayBuffer){
|
||||
//console.log("Arrayy Buffer");
|
||||
data = Buffer.from(data);
|
||||
}
|
||||
if (data instanceof Object){
|
||||
//console.log("Objection!@");
|
||||
this.push(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_onChannelOpen () {
|
||||
if (this._connected || this.destroyed) return
|
||||
this._debug('on channel open')
|
||||
this._channelReady = true
|
||||
this._maybeReady()
|
||||
leenkx.network.Leenkx.data.set(RAWCHANNEL, leenkx.network.Leenkx.connections.h[RAWCHANNEL].client.torrent._peersLength);
|
||||
leenkx.network.Leenkx.id.set(RAWCHANNEL, this.channelName);
|
||||
leenkx.network.Leenkx.connections.h[RAWCHANNEL].onopen();
|
||||
}
|
||||
|
||||
_onChannelClose () {
|
||||
if (this.destroyed) return
|
||||
this._debug('on channel close')
|
||||
leenkx.network.Leenkx.data.set(RAWCHANNEL, leenkx.network.Leenkx.connections.h[RAWCHANNEL].client.torrent._peersLength);
|
||||
leenkx.network.Leenkx.id.set(RAWCHANNEL, this.channelName);
|
||||
leenkx.network.Leenkx.connections.h[RAWCHANNEL].onclose();
|
||||
this.destroy()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// utility fns
|
||||
|
||||
function now() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/39225475/2131094
|
||||
function toHex(x) {
|
||||
return x.reduce(function(memo, i) {
|
||||
return memo + ("0" + i.toString(16)).slice(-2);
|
||||
}, "");
|
||||
}
|
||||
|
||||
// javascript why
|
||||
function partial(fn) {
|
||||
var slice = Array.prototype.slice;
|
||||
var stored_args = slice.call(arguments, 1);
|
||||
return function() {
|
||||
var new_args = slice.call(arguments);
|
||||
var args = stored_args.concat(new_args);
|
||||
return fn.apply(null, args);
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user