P2P Export - Nodes - Patch

This commit is contained in:
2026-10-06 02:10:08 -07:00
parent 9fee67d231
commit 4ab646015d
49 changed files with 2478 additions and 1164 deletions

View File

@ -9,7 +9,8 @@ import leenkx.network.torrent.TorrentInfo;
class LeenkxClient {
static var PEERTIMEOUT:Float = 5 * 60 * 1000;
public static var PEERTIMEOUT:Float = 5 * 60 * 1000;
public static var defaultHeartbeat:Int = 30000;
public var identifier:String;
public var seed:String;
@ -53,6 +54,54 @@ class LeenkxClient {
private var deferredOpts:TransportOpts = null;
private var heartbeatInterval:Int = 30000;
public var torrents(get, never):Map<String, Torrent>;
public var onTorrentAdded:Torrent -> Void;
public var onTorrentDone:Torrent -> Void;
var swarm:TorrentClient = null;
function get_torrents():Map<String, Torrent> {
return swarm != null ? swarm.torrents : [];
}
function getSwarm():TorrentClient {
if (swarm == null) {
swarm = new TorrentClient();
swarm.onError = function(e) {
if (onError != null) onError(e);
};
swarm.onTorrentAdded = function(t) {
if (onTorrentAdded != null) onTorrentAdded(t);
};
swarm.onTorrentDone = function(t) {
if (onTorrentDone != null) onTorrentDone(t);
};
}
return swarm;
}
public function add(torrentId:Dynamic, ?opts:Dynamic, ?cb:Torrent -> Void):Torrent {
return getSwarm().add(torrentId, opts, cb);
}
public function seedMeta(meta:MetaInfo,
files:Array<{path:String, data:Bytes}>,
?opts:Dynamic, ?cb:Torrent -> Void):Torrent {
return getSwarm().seedMeta(meta, files, opts, cb);
}
public function get(id:Dynamic):Torrent {
if (Std.isOfType(id, Torrent)) return id;
return swarm != null ? swarm.get(id) : null;
}
public function remove(infoHashHex:String, ?deleteData:Bool):Void {
if (swarm != null) swarm.remove(infoHashHex, deleteData);
}
public function listen(port:Int):Void {
getSwarm().listen(port);
}
public function new(?identifier:String, ?opts:LeenkxOptions) {
if (identifier != null && !Std.isOfType(identifier, String)) {
opts = cast identifier;
@ -60,7 +109,7 @@ class LeenkxClient {
}
opts = opts != null ? opts : {};
this.announce = opts.announce != null ? opts.announce : ["wss://ws1.leenkx.com"];
this.announce = opts.announce != null ? opts.announce : TorrentClient.defaultTrackers;
this.timeout = opts.timeout != null ? opts.timeout : PEERTIMEOUT;
if (opts.seed != null) {
@ -90,7 +139,7 @@ class LeenkxClient {
createTransport(opts, opts.deferJoin == true);
this.heartbeatInterval = opts.heartbeat != null ? opts.heartbeat : 30000;
this.heartbeatInterval = opts.heartbeat != null ? opts.heartbeat : defaultHeartbeat;
}
function createTransport(opts:LeenkxOptions, defer:Bool):Void {
@ -281,6 +330,10 @@ class LeenkxClient {
}
var packet = makePacket({y: PacketType.Leave});
sendRaw(packet);
if (swarm != null) {
swarm.destroy();
swarm = null;
}
if (transport != null) {
transport.destroy(cb);
} else if (cb != null) {

View File

@ -39,12 +39,9 @@ class Torrent {
public var announcePort:Int = 0;
public var downloadDir:String = null;
public function getStats():{downloaded:Float, uploaded:Float,
left:Float, downRate:Float, upRate:Float} {
public function getStats():{downloaded:Float, uploaded:Float, left:Float, downRate:Float, upRate:Float} {
var total = meta != null ? meta.totalLength : 0;
var downloaded = pieces != null
? Math.min(pieces.numHave * (meta.pieceLength:Float), total)
: 0;
var downloaded = pieces != null ? Math.min(pieces.numHave * (meta.pieceLength:Float), total) : 0;
var now = haxe.Timer.stamp();
var dt = now - lastStatT;
var downRate:Float = 0;
@ -93,8 +90,7 @@ class Torrent {
public function new(infoHash:Bytes, peerId:Bytes, trackers:Array<String>, ?meta:MetaInfo, ?storage:IStorage, ?downloadDir:String) {
TorrentClient.initMain();
this.infoHash = infoHash;
this.infoHashHex = infoHash != null
? Crypto.toHex(infoHash) : null;
this.infoHashHex = infoHash != null ? Crypto.toHex(infoHash) : null;
this.peerId = peerId;
this.trackers = trackers;
this.storage = storage;
@ -121,12 +117,9 @@ class Torrent {
function setMeta(m:MetaInfo):Void {
meta = m;
if (storage == null) {
var sz = m.storageLength > 0 ? m.storageLength
: m.totalLength;
var sz = m.storageLength > 0 ? m.storageLength : m.totalLength;
#if (sys || kha_krom)
storage = downloadDir != null
? new FileTreeStorage(downloadDir, m.files)
: new MemoryStorage(sz);
storage = downloadDir != null ? new FileTreeStorage(downloadDir, m.files) : new MemoryStorage(sz);
#else
storage = new MemoryStorage(sz);
#end
@ -139,8 +132,7 @@ class Torrent {
if (m.fileHashes == null && m.files != null) {
m.fileHashes = [for (f in m.files) null];
}
files = [for (f in m.files)
new TorrentFile(f, storage, this)];
files = [for (f in m.files) new TorrentFile(f, storage, this)];
infoHashV2 = m.infoHashV2;
infoHashV2Hex = m.infoHashV2Hex;
if (infoHash == null && m.infoHash != null) {
@ -151,8 +143,7 @@ class Torrent {
if (m.urlList == null && pendingWebSeeds != null) {
m.urlList = pendingWebSeeds;
}
magnetURI = Magnet.build(m.infoHash, m.name, trackers,
m.urlList, m.infoHashV2);
magnetURI = Magnet.build(m.infoHash, m.name, trackers, m.urlList, m.infoHashV2);
ready = true;
initWebSeeds();
emit1("metadata", m);
@ -214,6 +205,24 @@ class Torrent {
return startTracker(url);
}
public function setTrackers(urls:Array<String>):Void {
if (stopped) {
trackers = urls != null ? urls.copy() : [];
return;
}
if (rtcPool != null) {
rtcPool.close();
rtcPool = null;
}
for (t in httpTrackers) t.stop();
for (t in udpTrackers) t.stop();
httpTrackers = [];
udpTrackers = [];
trackerObjects = [];
trackers = urls != null ? urls.copy() : [];
startTrackers();
}
function startTracker(url:String):Dynamic {
var infoHashBin = TorrentInfo.toBinaryString(infoHash);
var peerIdBin = TorrentInfo.toBinaryString(peerId);
@ -231,9 +240,7 @@ class Torrent {
}
#if (sys || kha_krom)
else if (url.indexOf("http") == 0) {
var t = new leenkx.network.torrent.tracker.HttpTracker(
url, infoHash, peerId,
announcePort > 0 ? announcePort : 6881);
var t = new leenkx.network.torrent.tracker.HttpTracker(url, infoHash, peerId, announcePort > 0 ? announcePort : 6881);
t.statsProvider = getStats;
t.onPeers = onClassicPeers;
t.onError = function(e) {
@ -246,9 +253,7 @@ class Torrent {
#end
#if (sys || kha_krom)
else if (url.indexOf("udp") == 0) {
var t = new leenkx.network.torrent.tracker.UdpTracker(
url, infoHash, peerId,
announcePort > 0 ? announcePort : 6881);
var t = new leenkx.network.torrent.tracker.UdpTracker(url, infoHash, peerId, announcePort > 0 ? announcePort : 6881);
t.statsProvider = getStats;
t.onPeers = onClassicPeers;
t.onError = function(e) {
@ -272,8 +277,7 @@ class Torrent {
for (p in list) {
var key = "tcp:" + p.host + ":" + p.port;
if (wires.exists(key)) continue;
var ch = new leenkx.network.torrent.peer.TcpPeer(
p.host, p.port);
var ch = new leenkx.network.torrent.peer.TcpPeer(p.host, p.port);
addWire(key, ch);
}
}
@ -308,8 +312,7 @@ class Torrent {
requestBlocks(key);
};
wire.onHave = function(index) {
if (meta == null || index < 0
|| index >= meta.numPieces) return;
if (meta == null || index < 0 || index >= meta.numPieces) return;
var bf = peerBitfields.get(key);
if (bf != null && index < bf.length) bf[index] = true;
requestBlocks(key);
@ -317,17 +320,13 @@ class Torrent {
wire.onPiece = function(index, begin, data) {
var n = outstanding.get(key);
if (n != null && n > 0) outstanding.set(key, n - 1);
if (pieces != null && index >= 0
&& index < meta.numPieces
&& pieces.addBlock(index, begin, data)) {
if (pieces != null && index >= 0 && index < meta.numPieces && pieces.addBlock(index, begin, data)) {
broadcastHave(index);
}
requestBlocks(key);
};
wire.onRequest = function(index, begin, length) {
if (wire.amChoking || pieces == null || index < 0
|| index >= meta.numPieces || begin < 0 || length <= 0
|| begin + length > meta.pieceSize(index)) return;
if (wire.amChoking || pieces == null || index < 0 || index >= meta.numPieces || begin < 0 || length <= 0 || begin + length > meta.pieceSize(index)) return;
if (!pieces.have[index]) return;
var data = pieces.readBlock(index, begin, length);
uploadedBytes += data.length;
@ -384,8 +383,7 @@ class Torrent {
var out:Array<Bool> = [];
for (i in 0...n) {
var byte = i >> 3;
out.push(byte < bf.length
&& (bf.get(byte) & (0x80 >> (i & 7))) != 0);
out.push(byte < bf.length && (bf.get(byte) & (0x80 >> (i & 7))) != 0);
}
return out;
}
@ -479,8 +477,7 @@ class Torrent {
if (host == "" || port == null) return false;
var key = "tcp:" + host + ":" + port;
if (wires.exists(key)) return false;
addWire(key,
new leenkx.network.torrent.peer.TcpPeer(host, port));
addWire(key, new leenkx.network.torrent.peer.TcpPeer(host, port));
return true;
#else
return false;
@ -509,8 +506,7 @@ class Torrent {
var pendingLayers:Map<Int, {arr:Array<Bytes>, got:Int}> = [];
function pieceLayerBase():Int {
return Std.int(Math.log(meta.pieceLength / 16384)
/ Math.log(2));
return Std.int(Math.log(meta.pieceLength / 16384) / Math.log(2));
}
function requestPieceLayer(wire:WireProtocol, fi:Int):Void {
@ -528,8 +524,7 @@ class Torrent {
if (chunk > 512) chunk = 512;
var len = 2;
while (len < chunk) len <<= 1;
wire.sendHashRequest(root, pieceLayerBase(), sent,
len, 0);
wire.sendHashRequest(root, pieceLayerBase(), sent, len, 0);
hashReqNext.set(fi, sent + len);
hashReqTime.set(fi, haxe.Timer.stamp());
}
@ -547,11 +542,9 @@ class Torrent {
if (count <= 0) return;
for (fi in 0...meta.files.length) {
var f = meta.files[fi];
if (f.piecesRoot == null
|| !bytesEq(root, f.piecesRoot)) continue;
if (f.piecesRoot == null || !bytesEq(root, f.piecesRoot)) continue;
if (meta.fileHashes[fi] != null) return;
var np = Std.int(Math.ceil(f.length
/ meta.pieceLength));
var np = Std.int(Math.ceil(f.length / meta.pieceLength));
var pl = pendingLayers.get(fi);
if (pl == null) {
pl = {arr: [for (i in 0...np) null], got: 0};
@ -585,8 +578,7 @@ class Torrent {
var root = payload.sub(0, 32);
for (fi in 0...meta.files.length) {
var f = meta.files[fi];
if (f.piecesRoot != null
&& bytesEq(root, f.piecesRoot)) {
if (f.piecesRoot != null && bytesEq(root, f.piecesRoot)) {
hashReqNext.remove(fi);
pendingLayers.remove(fi);
return;
@ -604,10 +596,8 @@ class Torrent {
var np = Std.int(Math.ceil(f.length / meta.pieceLength));
l = [];
for (p in 0...np) {
var size = Std.int(Math.min(meta.pieceLength,
f.length - p * meta.pieceLength));
l.push(meta.pieceRootV2(storage.read(
f.offset + p * meta.pieceLength, size)));
var size = Std.int(Math.min(meta.pieceLength, f.length - p * meta.pieceLength));
l.push(meta.pieceRootV2(storage.read(f.offset + p * meta.pieceLength, size)));
}
layerCache.set(fi, l);
return l;
@ -620,13 +610,11 @@ class Torrent {
t = [level];
var depth = 0;
while (level.length > 1) {
var zero = MetaInfo.layerZero(pieceLayerBase()
+ depth + 1);
var zero = MetaInfo.layerZero(pieceLayerBase() + depth + 1);
var next:Array<Bytes> = [];
var i = 0;
while (i < level.length) {
var b = i + 1 < level.length
? level[i + 1] : zero;
var b = i + 1 < level.length ? level[i + 1] : zero;
next.push(MetaInfo.hashPair(level[i], b));
i += 2;
}

View File

@ -5,9 +5,7 @@ import leenkx.network.torrent.storage.Storage;
import leenkx.network.torrent.storage.Storage.IStorage;
class TorrentClient {
public static var defaultTrackers:Array<String> = [
"wss://ws1.leenkx.com"
];
public static var defaultTrackers:Array<String> = [];
public static var defaultDownloadDir:String = null;
public var downloadDir:String = null;
@ -247,8 +245,7 @@ class TorrentClient {
files.push({path: name, data: Bytes.ofString(Std.string(data))});
}
return {meta: MetaInfo.create(name, files, trackers, opts),
files: files};
return {meta: MetaInfo.create(name, files, trackers, opts), files: files};
}
public static function readFolder(path:String, ?base:String):Array<{path:String, data:Bytes}> {
@ -259,8 +256,7 @@ class TorrentClient {
root = root.substr(0, root.length - 1);
}
if (base == null) {
var i = Std.int(Math.max(root.lastIndexOf("/"),
root.lastIndexOf("\\")));
var i = Std.int(Math.max(root.lastIndexOf("/"), root.lastIndexOf("\\")));
base = i >= 0 ? root.substr(i + 1) : root;
}
var files:Array<{path:String, data:Bytes}> = [];
@ -317,20 +313,79 @@ class TorrentClient {
return t;
}
public function remove(infoHashHex:String, ?deleteData:Bool):Void {
var t = torrents.get(infoHashHex);
if (t == null) {
t = torrents.get("v2:" + infoHashHex);
if (t != null) infoHashHex = "v2:" + infoHashHex;
public function get(id:Dynamic):Torrent {
if (Std.isOfType(id, Torrent)) return id;
if (id == null) return null;
var hex = Std.string(id);
if (StringTools.startsWith(hex, "v2:")) {
hex = hex.substr(3);
}
if (t != null) {
t.destroy();
if (deleteData) t.deleteData();
torrents.remove(infoHashHex);
var t = torrents.get(hex);
if (t == null) t = torrents.get("v2:" + hex);
if (t == null) {
for (f in torrents) {
if (f.infoHashHex == hex || f.infoHashV2Hex == hex || (f.infoHash != null && Crypto.toHex(f.infoHash) == hex)) {
t = f;
break;
}
}
}
return t;
}
public static function torrentIdFor(v:Dynamic):Dynamic {
if (v == null) return null;
if (Std.isOfType(v, haxe.io.Bytes)) return v;
var s = Std.string(v);
if (s.indexOf("magnet:") == 0) return s;
if (s.indexOf("v2:") == 0) {
var h = s.substr(3);
return h.length == 64 ? "magnet:?xt=urn:btmh:1220" + h : s;
}
switch (s.length) {
case 40 | 32:
return "magnet:?xt=urn:btih:" + s;
case 64:
return "magnet:?xt=urn:btmh:1220" + s;
case 68:
return "magnet:?xt=urn:btmh:" + s;
default:
return s;
}
}
static function mergeTrackers(trackers:Array<String>, urls:Array<String>):Void {
public static function trackerUrls(v:Dynamic):Array<String> {
if (v == null) return null;
var items:Array<Dynamic> = Std.isOfType(v, Array)
? (v:Array<Dynamic>) : [v];
var out:Array<String> = [];
for (item in items) {
var u:Dynamic = null;
if (!Std.isOfType(item, String)) {
try {
u = Reflect.field(item, "url");
} catch(e:Dynamic) {}
}
var s = u != null ? Std.string(u) : Std.string(item);
if (s != "" && s != "null") out.push(s);
}
return out.length > 0 ? out : null;
}
public function remove(infoHashHex:String, ?deleteData:Bool):Void {
var t = get(infoHashHex);
if (t != null) {
t.destroy();
if (deleteData) t.deleteData();
var key:String = null;
for (k in torrents.keys()) {
if (torrents.get(k) == t) { key = k; break; }
}
if (key != null) torrents.remove(key);
}
}
public static function mergeTrackers(trackers:Array<String>, urls:Array<String>):Void {
if (urls == null) return;
for (u in urls) {
if (u != null && Lambda.indexOf(trackers, u) < 0) {

View File

@ -51,10 +51,12 @@ class RtcPeerPool {
var rtcInitialized = false;
#end
public static var defaultIceServers:Array<String> =
["stun:stun.l.google.com:19302", "stun:global.stun.twilio.com:3478"];
public function new(?iceServers:Array<String>, ?channelLabel:String, unordered:Bool = false, maxRetransmits:Int = -1) {
this.iceServers = iceServers != null ? iceServers
: ["stun:stun.l.google.com:19302",
"stun:global.stun.twilio.com:3478"];
: defaultIceServers.copy();
if (channelLabel != null) this.channelLabel = channelLabel;
this.unordered = unordered;
this.maxRetransmits = maxRetransmits;
@ -72,8 +74,7 @@ class RtcPeerPool {
}
#end
var signaling = new RtcSignaling(trackerUrl, infoHashBinary,
peerIdBinary);
var signaling = new RtcSignaling(trackerUrl, infoHashBinary, peerIdBinary);
signalings.push(signaling);
signaling.statsProvider = statsProvider;
signaling.onConnected = function() {
@ -223,8 +224,7 @@ class RtcPeerPool {
}
};
var dc = pc.createDataChannel(channelLabel,
{ordered: !unordered,
maxRetransmits: unordered ? maxRetransmits : null});
{ordered: !unordered, maxRetransmits: unordered ? maxRetransmits : null});
attachOfferChannel(offerIdHex, new DataChannelPeer(dc));
var iceComplete = false;
var iceTimer:haxe.Timer = null;
@ -271,8 +271,7 @@ class RtcPeerPool {
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
if (!offerPeers.exists(offerIdHex)) return;
sig.sendAnswer(peerIdHex, offerIdHex,
pc.getLocalDescription());
sig.sendAnswer(peerIdHex, offerIdHex, pc.getLocalDescription());
};
pc.onGatheringStateChange = function(state) {
if (state == RTC_GATHERING_COMPLETE) sendAnswerSdp();
@ -312,8 +311,7 @@ class RtcPeerPool {
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
if (!offerPeers.exists(offerIdHex)) return;
sig.sendAnswer(peerIdHex, offerIdHex,
RtcNative.getLocalDescription(pcId));
sig.sendAnswer(peerIdHex, offerIdHex, RtcNative.getLocalDescription(pcId));
};
RtcNative.setGatheringStateCallback(pcId, function(state:Int) {
if (state == RtcNative.RTC_GATHERING_COMPLETE) sendAnswerSdp();

View File

@ -36,6 +36,10 @@ class DatachannelTransport implements ITransport {
if (opts.announce != null && opts.announce.length > 0) {
url = opts.announce[0];
}
if (url == null || url == "") {
if (onError != null) onError("no announce tracker configured");
return;
}
if (opts.iceServers != null) iceServers = opts.iceServers;
pk = opts.pk != null ? opts.pk : "";
ek = opts.ek != null ? opts.ek : "";

View File

@ -48,7 +48,7 @@ class WebTorrentTransport implements ITransport {
var blob:Dynamic = createBlob(identifier);
var torrentOpts:Dynamic = {
name: identifier,
announce: opts.announce != null ? opts.announce : ["wss://ws1.leenkx.com"]
announce: opts.announce != null ? opts.announce : leenkx.network.torrent.TorrentClient.defaultTrackers
};
if (opts.torrentOpts != null) {
torrentOpts = js.lib.Object.assign(

View File

@ -43,6 +43,10 @@ class WireTransport implements ITransport {
if (opts.announce != null && opts.announce.length > 0) {
url = opts.announce[0];
}
if (url == null || url == "") {
if (onError != null) onError("no announce tracker configured");
return;
}
if (opts.iceServers != null) iceServers = opts.iceServers;
pk = opts.pk != null ? opts.pk : "";
ek = opts.ek != null ? opts.ek : "";