LNXJS/Leenkx.js

484 lines
14 KiB
JavaScript
Raw Normal View History

2025-01-22 17:11:22 +00:00
var RAWCHANNEL = "";
2025-01-22 17:06:56 +00:00
module.exports = Leenkx;
2025-01-22 17:11:22 +00:00
var debug = console.log;
2025-01-22 17:06:56 +00:00
var WebTorrent = require("webtorrent");
var bencode = require("bencode");
var nacl = require("tweetnacl");
2025-01-22 17:11:22 +00:00
var EventEmitter = require("events").EventEmitter;
var inherits = require("inherits");
2025-01-22 17:06:56 +00:00
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";
2025-01-22 17:11:22 +00:00
/**
* Multi-party data channels on WebTorrent extension.
*/
2025-01-22 17:06:56 +00:00
function Leenkx(identifier, opts) {
2025-01-22 17:11:22 +00:00
if (identifier && typeof identifier == "object") {
2025-01-22 17:06:56 +00:00
opts = identifier;
identifier = null;
}
var opts = opts || {};
if (!(this instanceof Leenkx)) return new Leenkx(identifier, opts);
var trackeropts = opts.tracker || {};
2025-01-22 17:11:22 +00:00
trackeropts.getAnnounceOpts =
trackeropts.getAnnounceOpts ||
function() {
return { numwant: 4 };
};
2025-01-22 17:06:56 +00:00
if (opts.iceServers) {
2025-01-22 17:11:22 +00:00
trackeropts.rtcConfig = { iceServers: opts.iceServers };
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
this.announce = opts.announce || [
"wss://ws1.leenkx.com",
"wss://tracker.openwebtorrent.com"
];
this.wt = opts.wt || new WebTorrent({ tracker: trackeropts });
2025-01-22 17:06:56 +00:00
this.nacl = nacl;
if (opts["seed"]) {
this.seed = opts["seed"];
} else {
2025-01-22 17:11:22 +00:00
//random seed
2025-01-22 17:06:56 +00:00
this.seed = this.encodeseed(nacl.randomBytes(32));
}
2025-01-22 17:11:22 +00:00
this.timeout = opts["timeout"] || PEERTIMEOUT; //5 minutes
this.keyPair =
opts["keyPair"] ||
nacl.sign.keyPair.fromSeed(
Uint8Array.from(bs58check.decode(this.seed)).slice(2)
);
2025-01-22 17:06:56 +00:00
this.pk = bs58.encode(Buffer.from(this.keyPair.publicKey));
this.identifier = identifier || this.address();
2025-01-22 17:11:22 +00:00
this.peers = {}; // list of peers seen recently: address -> pk, timestamp
2025-01-22 17:06:56 +00:00
this.seen = {}; // messages we've seen recently: hash -> timestamp
this.lastwirecount = null;
2025-01-22 17:11:22 +00:00
// pending callback functions
2025-01-22 17:06:56 +00:00
this.callbacks = {};
this.serveraddress = null;
this.heartbeattimer = null;
debug("address", this.address());
debug("identifier", this.identifier);
debug("public key", this.pk);
2025-01-22 17:11:22 +00:00
if (typeof File == "object") {
2025-01-22 17:06:56 +00:00
var blob = new File([this.identifier], this.identifier);
} else {
var blob = new Buffer.from(this.identifier);
blob.name = this.identifier;
}
2025-01-22 17:11:22 +00:00
//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.
2025-01-22 17:06:56 +00:00
torrent.on("wire", partial(attach, this, this.identifier));
2025-01-22 17:11:22 +00:00
console.log("connected to peer with identifier " + this.identifier);
2025-01-22 17:06:56 +00:00
this.torrent = torrent;
if (opts.heartbeat) {
this.heartbeat(opts.heartbeat);
}
}
Leenkx.prototype.WebTorrent = WebTorrent;
2025-01-22 17:11:22 +00:00
//I wonder why he is encoding the seed, I don't think it was needed
2025-01-22 17:06:56 +00:00
Leenkx.encodeseed = Leenkx.prototype.encodeseed = function(material) {
2025-01-22 17:11:22 +00:00
return bs58check.encode(
Buffer.concat([Buffer.from(SEEDPREFIX, "hex"), Buffer.from(material)])
);
};
2025-01-22 17:06:56 +00:00
2025-01-22 17:11:22 +00:00
//I also don't understand why he is encoding the heartbeat
2025-01-22 17:06:56 +00:00
Leenkx.encodeaddress = Leenkx.prototype.encodeaddress = function(material) {
2025-01-22 17:11:22 +00:00
return bs58check.encode(
Buffer.concat([
Buffer.from(ADDRESSPREFIX, "hex"),
new ripemd160().update(Buffer.from(nacl.hash(material))).digest()
])
);
};
2025-01-22 17:06:56 +00:00
2025-01-22 17:11:22 +00:00
// smart way of removing old peers
2025-01-22 17:06:56 +00:00
// start a heartbeat and expire old "seen" peers who don't send us a heartbeat
Leenkx.prototype.heartbeat = function(interval) {
var interval = interval || 30000;
2025-01-22 17:11:22 +00:00
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);
}
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
}, this),
interval
);
};
2025-01-22 17:06:56 +00:00
2025-01-22 17:11:22 +00:00
// cleaning up means removing the torrent
2025-01-22 17:06:56 +00:00
// clean up this leenkx instance
Leenkx.prototype.destroy = function(cb) {
clearInterval(this.heartbeattimer);
2025-01-22 17:11:22 +00:00
var packet = makePacket(this, { y: "x" });
2025-01-22 17:06:56 +00:00
sendRaw(this, packet);
this.wt.remove(this.torrent, cb);
2025-01-22 17:11:22 +00:00
};
2025-01-22 17:06:56 +00:00
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;
2025-01-22 17:11:22 +00:00
};
2025-01-22 17:06:56 +00:00
2025-01-22 17:11:22 +00:00
// This is where this.address() goes
// So it encodes your public key, which is also your address?
2025-01-22 17:06:56 +00:00
Leenkx.prototype.address = function(pk) {
2025-01-22 17:11:22 +00:00
if (pk && typeof pk == "string") {
2025-01-22 17:06:56 +00:00
pk = bs58.decode(pk);
} else if (pk && pk.length == 32) {
pk = pk;
} else {
pk = this.keyPair.publicKey;
}
return this.encodeaddress(pk);
2025-01-22 17:11:22 +00:00
};
2025-01-22 17:06:56 +00:00
Leenkx.address = Leenkx.prototype.address;
Leenkx.prototype.ping = function() {
2025-01-22 17:11:22 +00:00
// send a ping out so they know about us too
var packet = makePacket(this, { y: "p" });
sendRaw(this, packet);
};
2025-01-22 17:06:56 +00:00
Leenkx.prototype.send = function(address, message) {
if (!message) {
var message = address;
var address = null;
}
2025-01-22 17:11:22 +00:00
var packet = makePacket(this, { y: "m", v: JSON.stringify(message) });
2025-01-22 17:06:56 +00:00
sendRaw(this, packet);
2025-01-22 17:11:22 +00:00
};
2025-01-22 17:06:56 +00:00
// outgoing
function makePacket(leenkx, params) {
var p = {
2025-01-22 17:11:22 +00:00
t: now(),
i: leenkx.identifier,
pk: leenkx.pk,
n: nacl.randomBytes(8)
2025-01-22 17:06:56 +00:00
};
for (var k in params) {
p[k] = params[k];
}
pe = bencode.encode(p);
return bencode.encode({
2025-01-22 17:11:22 +00:00
s: nacl.sign.detached(pe, leenkx.keyPair.secretKey),
p: pe
2025-01-22 17:06:56 +00:00
});
}
2025-01-22 17:11:22 +00:00
//So you need to send a message over the wires
//*And* use the extension, called lx_channel
2025-01-22 17:06:56 +00:00
function sendRaw(leenkx, message) {
var wires = leenkx.torrent.wires;
2025-01-22 17:11:22 +00:00
//for each wire
for (var w = 0; w < wires.length; w++) {
//get the key "peerExtendedHankshake"
2025-01-22 17:06:56 +00:00
var extendedhandshake = wires[w]["peerExtendedHandshake"];
if (extendedhandshake && extendedhandshake.m && extendedhandshake.m[EXT]) {
2025-01-22 17:11:22 +00:00
//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
2025-01-22 17:06:56 +00:00
wires[w].extended(EXT, message);
}
}
2025-01-22 17:11:22 +00:00
var hash = toHex(nacl.hash(message).slice(16)); //pure debug value
debug("sent", hash, "to", wires.length, "wires"); //for this log
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
// 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
2025-01-22 17:06:56 +00:00
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]) {
2025-01-22 17:11:22 +00:00
var unpacked = bencode.decode(message); //he needs to decode bencode, because that is how the bittorrent protocol communicates, I think...
2025-01-22 17:06:56 +00:00
if (unpacked && unpacked.p) {
2025-01-22 17:11:22 +00:00
debug(
"unpacked message"
// unpacked
);
2025-01-22 17:06:56 +00:00
var packet = bencode.decode(unpacked.p);
var pk = packet.pk.toString();
var id = packet.i.toString();
2025-01-22 17:11:22 +00:00
var checksig = nacl.sign.detached.verify(
unpacked.p,
unpacked.s,
bs58.decode(pk)
);
2025-01-22 17:06:56 +00:00
var checkid = id == identifier;
var checktime = packet.t + leenkx.timeout > t;
2025-01-22 17:11:22 +00:00
debug(
"packet"
// packet
);
2025-01-22 17:06:56 +00:00
if (checksig && checkid && checktime) {
2025-01-22 17:11:22 +00:00
//note that this means the sender is pinged back
sawPeer(leenkx, pk, identifier);
2025-01-22 17:06:56 +00:00
// check packet types
2025-01-22 17:11:22 +00:00
// m stands for message
2025-01-22 17:06:56 +00:00
if (packet.y == "m") {
2025-01-22 17:11:22 +00:00
debug(
"message",
identifier
// packet
);
2025-01-22 17:06:56 +00:00
var messagestring = packet.v.toString();
var messagejson = null;
try {
var messagejson = JSON.parse(messagestring);
2025-01-22 17:11:22 +00:00
} catch (e) {
2025-01-22 17:06:56 +00:00
debug("Malformed message JSON: " + messagestring);
}
if (messagejson) {
leenkx.emit("message", leenkx.address(pk), messagejson, packet);
}
2025-01-22 17:11:22 +00:00
}
// p stands for ping
else if (packet.y == "p") {
2025-01-22 17:06:56 +00:00
var address = leenkx.address(pk);
debug("ping from", address);
leenkx.emit("ping", address);
2025-01-22 17:11:22 +00:00
}
// x stands for split/leave
else if (packet.y == "x") {
2025-01-22 17:06:56 +00:00
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
2025-01-22 17:11:22 +00:00
function sawPeer(leenkx, pk, identifier) {
debug("sawPeer", leenkx.address(pk));
2025-01-22 17:06:56 +00:00
var t = now();
var address = leenkx.address(pk);
// ignore ourself
if (address != leenkx.address()) {
// if we haven't seen this peer for a while
2025-01-22 17:11:22 +00:00
if (
!leenkx.peers[address] ||
leenkx.peers[address].last + leenkx.timeout < t
) {
2025-01-22 17:06:56 +00:00
leenkx.peers[address] = {
2025-01-22 17:11:22 +00:00
pk: pk,
last: t
2025-01-22 17:06:56 +00:00
};
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
2025-01-22 17:11:22 +00:00
var packet = makePacket(leenkx, { y: "p" });
2025-01-22 17:06:56 +00:00
sendRaw(leenkx, packet);
} else {
leenkx.peers[address].last = t;
}
}
}
// extension protocol plumbing
2025-01-22 17:11:22 +00:00
// see also https://github.com/webtorrent/ut_metadata/blob/master/index.js for another example
2025-01-22 17:06:56 +00:00
function attach(leenkx, identifier, wire, addr) {
2025-01-22 17:11:22 +00:00
debug("saw wire", wire.peerId, addr);
2025-01-22 17:06:56 +00:00
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();
}
2025-01-22 17:11:22 +00:00
// I need to debug this pure magic -- Melvin
2025-01-22 17:06:56 +00:00
function extension(leenkx, identifier, wire) {
var ext = partial(wirefn, leenkx, identifier);
ext.prototype.name = EXT;
2025-01-22 17:11:22 +00:00
ext.prototype.onExtendedHandshake = partial(
onExtendedHandshake,
leenkx,
identifier,
wire
);
2025-01-22 17:06:56 +00:00
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) {
2025-01-22 17:11:22 +00:00
debug(
"wire extended handshake",
leenkx.address(handshake.pk.toString()),
wire.peerId
// handshake
);
2025-01-22 17:06:56 +00:00
leenkx.emit("wireseen", leenkx.torrent.wires.length, wire);
leenkx.connections();
2025-01-22 17:11:22 +00:00
// TODO: check sig ðfnd drop on failure - wire.peerExtendedHandshake
sawPeer(leenkx, handshake.pk.toString(), identifier);
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
_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;
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
let data = event.data;
if (data instanceof ArrayBuffer){
//console.log("Arrayy Buffer");
data = Buffer.from(data);
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
if (data instanceof Object){
//console.log("Objection!@");
this.push(data);
return;
}
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
_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();
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
_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()
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
// utility fns
2025-01-22 17:06:56 +00:00
2025-01-22 17:11:22 +00:00
function now() {
return new Date().getTime();
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
// https://stackoverflow.com/a/39225475/2131094
function toHex(x) {
return x.reduce(function(memo, i) {
return memo + ("0" + i.toString(16)).slice(-2);
}, "");
2025-01-22 17:06:56 +00:00
}
2025-01-22 17:11:22 +00:00
// 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);
};
}