Compare commits

...

14 Commits

Author SHA1 Message Date
a133c5ee94 Update leenkx.py 2026-10-06 20:48:45 +00:00
4895d7cfa5 Merge pull request 'main' (#147) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#147
2026-10-06 20:47:54 +00:00
052ce82588 Deprecate LNXJS + Cleanup 2026-10-06 13:32:39 -07:00
211cba61a6 Export Network Update 2026-10-06 11:34:17 -07:00
4ab646015d P2P Export - Nodes - Patch 2026-10-06 02:10:08 -07:00
9fee67d231 Haxe Lib Datachannel 2026-10-04 17:14:28 -07:00
860b73a8f4 hxWebsocket Upstream and Network Updates 2026-10-03 22:05:53 -07:00
da38631584 Leenkx Network Torrent 2026-10-03 17:40:22 -07:00
6839203dc5 P2P RunT + Static WebView 2026-10-03 00:02:30 -07:00
d9ac74101f Merge pull request 'Fix background color intensity for 'Filmic2' tonemap' (#146) from moisesjpelaez/LNXSDK:filmic into main
Reviewed-on: LeenkxTeam/LNXSDK#146
2026-09-23 22:45:26 +00:00
e20400654c Merge pull request 'Add missing dll to prevent error 3221225781' (#145) from moisesjpelaez/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#145
2026-09-23 22:44:13 +00:00
058cbed20f Fix background color intensity for 'Filmic2' tonemap 2026-09-21 19:44:07 -04:00
e0c10318e5 Add missing dll to prevent error 3221225781 2026-09-21 18:27:29 -04:00
7d609b126d Merge pull request 'main' (#144) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#144
2026-09-20 18:30:58 +00:00
261 changed files with 73780 additions and 58151 deletions

View File

@ -130,6 +130,11 @@ extern class Krom {
static function readStorage(name: String): haxe.io.BytesData; static function readStorage(name: String): haxe.io.BytesData;
static function fileSaveBytes(path: String, bytes: haxe.io.BytesData): Void; static function fileSaveBytes(path: String, bytes: haxe.io.BytesData): Void;
static function fileWriteBytes(path: String, bytes: haxe.io.BytesData, ?offset: Float, ?byteLength: Int): Void;
static function fileReadBytes(path: String, ?offset: Float, ?length: Int): haxe.io.BytesData;
static function fileSize(path: String): Float;
static function fileExists(path: String): Bool;
static function deleteFile(path: String): Void;
static function sysCommand(cmd: String, ?args: Array<String>): Int; static function sysCommand(cmd: String, ?args: Array<String>): Int;
static function savePath(): String; static function savePath(): String;
static function getArgCount(): Int; static function getArgCount(): Int;

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +0,0 @@
# The zlib/libpng License
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
This notice may not be removed or altered from any source distribution.

View File

@ -1,7 +0,0 @@
* 2024-12-02: Submodules have been moved in to this repository (previously contained in https://github.com/leenkx3d/lnxsdk).
* 2019-06-30: Return value of `PhysicsWorld.rayCast()` changed, see https://github.com/leenkx3d/leenkx/commit/dfb7609a28cebf3a520e6a25a8563d01c32f2b01.
* 2019-04-06: Use voxelao instead of voxelgi, gi will be reworked into raytracing.
* 2019-01-13: If you are using Leenkx Updater, get Leenkx 0.6beta from https://leenkx.itch.io/leenkx3d first (or clone the sdk from https://github.com/leenkx3d/lnxsdk).
* 2018-08-28: `LampObject` and `LampData` is now `LightObject` and `LightData`
* 2018-06-01: 'Not Equal' has been removed from the Gate logic node. Use 'Equal' and 'False' output socket instead.
* 2017-11-20: Use `leenkx.trait.physics.*` instead of `leenkx.trait.internal.*` to access physics traits.

View File

@ -6,7 +6,7 @@ bl_info = {
"location": "Properties -> Render -> Leenkx Player", "location": "Properties -> Render -> Leenkx Player",
"description": "Full Stack SDK", "description": "Full Stack SDK",
"author": "Leenkx.com", "author": "Leenkx.com",
"version": (2026, 9, 0), "version": (2026, 10, 6),
"blender": (5, 2, 0), "blender": (5, 2, 0),
"doc_url": "https://leenkx.com/", "doc_url": "https://leenkx.com/",
"tracker_url": "https://leenkx.com/support" "tracker_url": "https://leenkx.com/support"
@ -361,7 +361,7 @@ class LeenkxAddonPreferences(AddonPreferences):
layout.label(text="Welcome to Leenkx!") layout.label(text="Welcome to Leenkx!")
# Compare version Blender and Leenkx (major, minor) # Compare version Blender and Leenkx (major, minor)
if bpy.app.version[:2] not in [(4, 5), (4, 4), (4, 2), (3, 6), (3, 3)]: if bpy.app.version[:2] not in [(5, 2), (4, 5), (4, 4), (4, 2), (3, 6), (3, 3)]:
box = layout.box().column() box = layout.box().column()
box.label(text="Warning: For Leenkx to work correctly, use a Blender LTS version") box.label(text="Warning: For Leenkx to work correctly, use a Blender LTS version")

View File

@ -593,7 +593,7 @@ fragColor.rgb = min(fragColor.rgb, 65504 * 0.5);
#endif #endif
#ifdef _CToneFilmic2 #ifdef _CToneFilmic2
fragColor.rgb = acesFilm(fragColor.rgb); fragColor.rgb = acesFilm(fragColor.rgb);
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 1.0)); fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));
#endif #endif
#ifdef _CToneReinhard #ifdef _CToneReinhard
fragColor.rgb = tonemapReinhard(fragColor.rgb); fragColor.rgb = tonemapReinhard(fragColor.rgb);

View File

@ -1,79 +0,0 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if js
import leenkx.network.Leenkx;
#end
import iron.object.Object;
class AddTorrentNode extends LogicNode {
public var title: String;
public var values: Array<Dynamic>;
public var net_Url: String;
public var data: Dynamic;
public var runner: Bool = false;
public function new(tree:LogicTree) {
super(tree);
}
public function promiseResult(connection,torrentid,net_Url){
#if js
if (runner == false){
values = [];
var script = 'var torrentId = "' + torrentid + '";
lx_' + net_Url +'.wt.add(torrentId, function (torrent) {
leenkx.network.Leenkx.torrent.set("' + net_Url +'", torrent.files);
torrent.on("done", function () {
leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone();
})
});
';
js.Syntax.code('(1, eval)({0})', script.toString());
runner = true;
promiseResult(connection,torrentid,net_Url);
return;
}else{
try{
values = leenkx.network.Leenkx.torrent.get(net_Url);
if(values.length > 0){
runOutput(0);
runner = false;
return;
}
}
catch(error){
haxe.Timer.delay(function () {
promiseResult(connection,torrentid,net_Url);
return;
}, 100);
}
}
#end
}
override function run(from:Int) {
var connection = inputs[1].get();
if (connection == null) return;
var torrentid = inputs[2].get();
if (torrentid == null) return;
#if js
net_Url = connection._url;
promiseResult(connection,torrentid,net_Url);
#end
}
override function get(from: Int): Dynamic {
#if js
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: leenkx.network.Leenkx.torrent.get(net_Url);
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,17 +1,21 @@
package leenkx.logicnode; package leenkx.logicnode;
import iron.object.Object; import iron.object.Object;
#if js
import leenkx.network.Types; import leenkx.network.Types;
import leenkx.network.Util; #if lnx_torrent
import leenkx.network.Leenkx; import leenkx.network.Leenkx;
import leenkx.network.torrent.LeenkxClient;
import leenkx.network.torrent.TorrentClient;
import leenkx.network.torrent.peer.RtcPeerPool;
#end #end
import leenkx.system.Event; import leenkx.system.Event;
class CreateLeenkxNode extends LogicNode { class CreateLeenkxNode extends LogicNode {
public var property0: String;
public var property1: Bool;
public var property2: String;
public var property3: String;
public var net_Url: String; public var net_Url: String;
public var data: Dynamic;
public var client: Dynamic;
public var value: String; public var value: String;
public var listener: TEvent = null; public var listener: TEvent = null;
@ -20,112 +24,145 @@ class CreateLeenkxNode extends LogicNode {
} }
override function run(from:Int) { override function run(from:Int) {
#if js #if lnx_torrent
net_Url = inputs[1].get(); net_Url = inputs[1].get();
if (net_Url == null) return; if (net_Url == null) return;
if (Leenkx.connections[net_Url] == null) { if (Leenkx.connections[net_Url] == null) {
var object = tree.object;
value = Leenkx.onLoadEvent; value = Leenkx.onLoadEvent;
listener = Event.add(value, onEvent, tree.object.uid); listener = Event.add(value, onEvent, tree.object.uid);
client = new leenkx.network.Leenkx(net_Url, object); new leenkx.network.Leenkx(net_Url, tree.object);
} else {
return;
} }
#end #end
} }
function onEvent() { function onEvent() {
#if js if (listener != null) {
var lnxjs:Dynamic = js.Lib.global; Event.removeListener(listener);
var lnxCxNew = lnxjs.lnxNew; listener = null;
}
if (lnxCxNew == null) { #if lnx_torrent
trace("ERROR: lnxNew not available"); var sock = Leenkx.connections[net_Url];
if (sock == null) return;
if (sock.client != null) {
runOutput(0);
return; return;
} }
var opts:leenkx.network.torrent.Types.LeenkxOptions = {deferJoin: true};
var nextInput = 2;
if (property0 == "datachannel") {
opts.transport = "datachannel";
var imode = property2 != null ? property2 : "inherit";
if (imode != "inherit") {
if (inputs.length > nextInput) {
var ice = TorrentClient.trackerUrls(inputs[nextInput].get());
if (ice != null) {
opts.iceServers = imode == "override" ? ice : RtcPeerPool.defaultIceServers.concat(ice);
}
}
nextInput++;
}
}
var tmode = property3 != null ? property3 : "inherit";
var trackers:Array<String> = null;
if (tmode != "inherit" && inputs.length > nextInput) {
trackers = TorrentClient.trackerUrls(inputs[nextInput].get());
}
opts.announce = tmode == "override" && trackers != null ? trackers
: trackers != null ? TorrentClient.defaultTrackers.concat(trackers)
: TorrentClient.defaultTrackers;
var lnxCn = new LeenkxClient(net_Url, opts);
lnxCn.isRelay = property1 == true;
var lnxCn:Dynamic = lnxCxNew(net_Url); lnxCn.onOpen = function() {
Leenkx.data.set(net_Url, lnxCn.lastPeerCount);
Leenkx.id.set(net_Url, lnxCn.lastPeerId);
sock.onopen();
};
lnxCn.onConnections = function(c) {
Leenkx.data.set(net_Url, c + 1);
sock.onconnections();
};
lnxCn.onMessage = function(address, message, nonce) {
Leenkx.data.set(net_Url, message);
Leenkx.id.set(net_Url, address);
sock.onmessage();
};
lnxCn.onSeen = function(address) {
Leenkx.id.set(net_Url, address);
sock.onseen();
};
lnxCn.onLeft = function(address) {
Leenkx.id.set(net_Url, address);
sock.onleft();
};
lnxCn.onServer = function(address) {
Leenkx.id.set(net_Url, address);
sock.onserver();
};
lnxCn.onPing = function(address) {
Leenkx.id.set(net_Url, address);
sock.onping();
};
lnxCn.onTimeout = function(address) {
Leenkx.id.set(net_Url, address);
sock.ontimeout();
};
lnxCn.onWireLeft = function(wirecount, wire) {
Leenkx.data.set(net_Url, wirecount);
if (wire != null && wire.peerId != null) {
Leenkx.id.set(net_Url, wire.peerId);
}
sock.onwireleft();
};
lnxCn.onWireSeen = function(wirecount, wire) {
Leenkx.data.set(net_Url, wirecount);
if (wire != null && wire.peerId != null) {
Leenkx.id.set(net_Url, wire.peerId);
}
sock.onwireseen();
};
lnxCn.onTorrent = function(identifier, torrent) {
Leenkx.data.set(net_Url, torrent);
Leenkx.id.set(net_Url, identifier);
sock.ontorrent();
};
lnxCn.onTracker = function(identifier, tracker) {
Leenkx.id.set(net_Url, identifier);
sock.ontracker();
};
lnxCn.onAnnounce = function(identifier) {
Leenkx.id.set(net_Url, identifier);
sock.onannounce();
};
lnxCn.onError = function(msg) {
Leenkx.data.set(net_Url, msg);
sock.onerror();
};
lnxCn.onTorrentAdded = function(t) {
Leenkx.torrent.set(net_Url, t);
Leenkx.file.set(net_Url, t.files);
Leenkx.data.set(net_Url, t);
sock.ontorrentadded();
};
lnxCn.onTorrentDone = function(t) {
Leenkx.data.set(net_Url, t);
sock.ontorrentdone();
};
lnxCn.on("connections", function(c) { sock.client = lnxCn;
leenkx.network.Leenkx.data.set(net_Url, c + 1);
leenkx.network.Leenkx.connections[net_Url].onconnections();
});
lnxCn.on("message", function(address, message) {
leenkx.network.Leenkx.data.set(net_Url, message);
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onmessage();
});
lnxCn.on("seen", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onseen();
});
lnxCn.on("left", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onleft();
});
lnxCn.on("server", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onserver();
});
lnxCn.on("ping", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onping();
});
lnxCn.on("timeout", function(address) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].ontimeout();
});
lnxCn.on("rpc", function(address, call, args, nonce) {
leenkx.network.Leenkx.data.set(net_Url, call);
leenkx.network.Leenkx.id.set(net_Url, address);
call(args);
leenkx.network.Leenkx.connections[net_Url].onrpc();
});
lnxCn.on("rpc-response", function(address, nonce, response) {
leenkx.network.Leenkx.id.set(net_Url, address);
leenkx.network.Leenkx.connections[net_Url].onrpcresponse();
});
lnxCn.on("wireleft", function(wirecount, wire) {
leenkx.network.Leenkx.data.set(net_Url, wirecount);
leenkx.network.Leenkx.id.set(net_Url, wire.peerId);
leenkx.network.Leenkx.connections[net_Url].onwireleft();
});
lnxCn.on("wireseen", function(wirecount, wire) {
leenkx.network.Leenkx.data.set(net_Url, wirecount);
leenkx.network.Leenkx.id.set(net_Url, wire.peerId);
leenkx.network.Leenkx.connections[net_Url].onwireseen();
});
lnxCn.on("torrent", function(identifier, torrent) {
leenkx.network.Leenkx.data.set(net_Url, torrent);
leenkx.network.Leenkx.id.set(net_Url, identifier);
leenkx.network.Leenkx.connections[net_Url].ontorrent();
});
lnxCn.on("tracker", function(identifier) {
leenkx.network.Leenkx.id.set(net_Url, identifier);
leenkx.network.Leenkx.connections[net_Url].ontracker();
});
lnxCn.on("announce", function(identifier) {
leenkx.network.Leenkx.id.set(net_Url, identifier);
leenkx.network.Leenkx.connections[net_Url].onannounce();
});
Reflect.setField(lnxjs, "lnx_" + net_Url, lnxCn); lnxCn.connect();
Leenkx.connections[net_Url].client = lnxCn; #if (js && !kha_krom)
js.Browser.window.addEventListener("beforeunload", function(e) {
var script = 'globalThis.addEventListener("beforeunload", function (e) { lnxCn.destroy();
leenkx.network.Leenkx.connections.h["' + net_Url + '"].client.destroy(); });
delete e["returnValue"];
});';
js.Syntax.code('(1, eval)({0})', script);
runOutput(0);
#end #end
#end
runOutput(0);
} }
override function get(from: Int): Dynamic { override function get(from: Int): Dynamic {
#if js #if lnx_torrent
return switch (from) { return switch (from) {
case 1: Leenkx.connections[net_Url]; case 1: Leenkx.connections[net_Url];
case 2: Leenkx.id.get(net_Url); case 2: Leenkx.id.get(net_Url);

View File

@ -1,43 +1,100 @@
package leenkx.logicnode; package leenkx.logicnode;
import leenkx.system.Event; #if lnx_torrent
import leenkx.network.Leenkx; import leenkx.network.Leenkx;
import iron.object.Object; import leenkx.network.torrent.Magnet;
import leenkx.network.torrent.TorrentClient;
#end
class CreateTorrentNode extends LogicNode { class CreateTorrentNode extends LogicNode {
public var torrent: Dynamic; public var meta:Dynamic;
public var net_Url: String; public var metaBytes:Dynamic;
public var magnetUri:Dynamic;
public var infoHashHex:String;
public var property0:String;
public var property1:String;
public function new(tree:LogicTree) { public function new(tree:LogicTree) {
super(tree); super(tree);
} }
override function run(from:Int) { override function run(from:Int) {
#if lnx_torrent
meta = null;
metaBytes = null;
magnetUri = null;
infoHashHex = null;
var connection = inputs[1].get(); var data = inputs[1].get();
if (connection == null) return;
var data = inputs[2].get();
if (data == null) return; if (data == null) return;
net_Url = connection._url; var mode = property1 != null ? property1 : "file";
var opts:Dynamic = {};
var mv = property0 != null ? Std.parseInt(property0) : null;
if (mv != null) Reflect.setField(opts, "metaVersion", mv);
field(2, opts, "name");
field(3, opts, "comment");
field(4, opts, "createdBy");
field(5, opts, "source");
field(6, opts, "announce");
field(7, opts, "private");
field(8, opts, "pieceLength");
field(9, opts, "urlList");
var script = 'var file = ' + data + '; if (mode == "folder") {
lx_' + net_Url +'.wt.add(file, function (torrent) { #if sys
leenkx.network.Leenkx.file.set("' + net_Url +'", torrent.files); data = TorrentClient.readFolder(Std.string(data));
torrent.on("done", function () { var first:Dynamic = data[0];
leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone(); if (first != null && Reflect.field(opts, "name") == null) {
}); var p = Std.string(first.path);
}); var i = p.indexOf("/");
'; Reflect.setField(opts, "name", p.substring(0, i >= 0 ? i : p.length));
}
#else
trace("Folder mode requires a sys target";
#end
}
js.Syntax.code('(1, eval)({0})', script.toString()); var m;
try {
m = TorrentClient.buildMeta(data, opts);
} catch(e:Dynamic) {
trace(e);
return;
}
meta = m;
metaBytes = m.toTorrentBytes();
infoHashHex = m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex;
magnetUri = Magnet.build(m.infoHash, m.name, m.announce, m.urlList, m.infoHashV2);
#end
runOutput(0); runOutput(0);
} }
#if lnx_torrent
function field(i:Int, opts:Dynamic, name:String):Void {
if (i >= inputs.length) return;
var v:Dynamic = inputs[i].get();
if (v == null || Std.string(v) == "") {
return;
}
if (name == "announce") {
v = TorrentClient.trackerUrls(v);
if (v == null) return;
}
Reflect.setField(opts, name, v);
}
#end
override function get(from:Int):Dynamic { override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) { return switch (from) {
case 1: Leenkx.file.get(net_Url); case 1: meta;
case 2: metaBytes;
case 3: infoHashHex;
case 4: magnetUri;
default: throw "Unreachable"; default: throw "Unreachable";
} }
#else
return null;
#end
} }
} }

View File

@ -0,0 +1,88 @@
package leenkx.logicnode;
#if (lnx_torrent && (sys || kha_krom))
import leenkx.network.torrent.tracker.HttpTracker.HttpTrackerServer;
import leenkx.network.torrent.tracker.UdpTracker.UdpTrackerServer;
import leenkx.network.torrent.transport.WsTrackerClient.WsTrackerServer;
#end
#if (lnx_torrent && sys)
import leenkx.network.torrent.transport.WsTrackerClient.WssTrackerServer;
#end
class CreateTrackerNode extends LogicNode {
public var property0:String;
public var tracker:Dynamic;
public var url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
tracker = null;
url = null;
startServer();
runOutput(0);
}
function startServer():Void {
#if (lnx_torrent && (sys || kha_krom))
var hv = inputs[1].get();
var host = hv != null ? Std.string(hv) : "127.0.0.1";
if (host == "") host = "127.0.0.1";
var pv = inputs[2].get();
var port = pv != null ? Std.int(Std.parseFloat(Std.string(pv))) : 0;
if (port <= 0) port = 6969;
var scheme = property0 != null ? property0 : "udp";
switch (scheme) {
case "http":
var s = new HttpTrackerServer();
s.onError = function(e) trace(e);
s.start(port);
tracker = s;
url = "http://" + host + ":" + port + "/announce";
case "ws":
var s = new WsTrackerServer(host, port);
s.onError = function(e) trace(e);
s.start();
tracker = s;
url = "ws://" + host + ":" + port;
case "wss":
#if sys
var cert = inputs.length > 3 ? Std.string(inputs[3].get()) : null;
var key = inputs.length > 4 ? Std.string(inputs[4].get()) : null;
if (cert == null || cert == "" || key == null || key == "") {
trace("wss server needs Cert File and Key File");
return;
}
var s = new WssTrackerServer(host, port, cert, key);
s.onError = function(e) trace(e);
s.start();
tracker = s;
url = "wss://" + host + ":" + port;
#else
trace("wss server requires a sys target");
#end
default:
var s = new UdpTrackerServer();
s.onError = function(e) trace(e);
s.start(port);
tracker = s;
url = "udp://" + host + ":" + port;
}
#else
trace("server mode needs a sys or krom target");
#end
}
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: tracker;
case 2: url;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,101 @@
package leenkx.logicnode;
import leenkx.system.Event;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.TorrentClient;
#end
class DownloadTorrentNode extends LogicNode {
public var net_Url:String;
public var torrent:Dynamic;
public var property0:String;
public var property1:String;
var listener:TEvent = null;
public function new(tree:LogicTree) {
super(tree);
}
public function promiseResult(connection, torrentid, net_Url, ?opts:Dynamic) {
#if lnx_torrent
var socket = Leenkx.connections[net_Url];
if (socket == null) {
if (listener == null) {
var self = this;
listener = Event.add(Leenkx.onLoadEvent, function() {
if (Leenkx.connections[net_Url] == null) return;
if (self.listener != null) {
Event.removeListener(self.listener);
self.listener = null;
}
self.promiseResult(connection, torrentid, net_Url, opts);
});
}
return;
}
var client = socket.client;
if (client == null) return;
var self = this;
client.add(torrentid, opts, function(torrent) {
self.torrent = torrent;
var onReady = function() {
self.runOutput(0);
};
var onDone = function() {
self.runOutput(4);
};
if (torrent.ready) onReady();
else torrent.on("ready", onReady);
if (torrent.done) onDone();
else torrent.on("done", onDone);
});
#end
}
function onRemove() {
if (listener != null) {
Event.removeListener(listener);
listener = null;
}
}
override function run(from:Int) {
var connection = inputs[1].get();
if (connection == null) return;
var v = inputs[2].get();
if (v == null) return;
#if lnx_torrent
net_Url = connection._url;
var torrentId:Dynamic = switch (property0 != null ? property0 : "magnet") {
case "file": v;
case "id": TorrentClient.torrentIdFor(v);
default: Std.string(v);
};
var opts:Dynamic = null;
var tmode = property1 != null ? property1 : "inherit";
if (tmode != "inherit" && inputs.length > 3) {
var announce = TorrentClient.trackerUrls(inputs[3].get());
opts = {};
if (announce != null) opts.announce = announce;
if (tmode == "override") opts.announceOverride = true;
}
promiseResult(connection, torrentId, net_Url, opts);
#end
}
override function get(from:Int):Dynamic {
#if lnx_torrent
var t:leenkx.network.torrent.Torrent = torrent;
return switch (from) {
case 1: torrent;
case 2: t != null && t.meta != null ? t.meta.name : null;
case 3: t != null ? t.files : null;
case 5: t != null ? t.infoHashHex : null;
case 6: t != null ? t.magnetURI : null;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,83 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
import leenkx.network.torrent.TorrentFile;
#end
class GetTorrentFileNode extends LogicNode {
public var property0:String;
public var file:Dynamic;
var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
file = null;
var id = inputs[2].get();
var match = inputs[3].get();
if (id != null && match != null) {
var connection = inputs[1].get();
if (connection != null) net_Url = connection._url;
var socket = net_Url != null ? Leenkx.connections[net_Url] : null;
var client = socket != null ? socket.client : null;
var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null);
if (t != null) file = findFile(t, match);
}
#end
runOutput(0);
}
#if lnx_torrent
function findFile(t:Torrent, match:Dynamic):TorrentFile {
var files = t.files;
switch (property0) {
case "index":
var i = Std.parseInt(Std.string(match));
if (i != null && i >= 0 && i < files.length) {
return files[i];
}
case "name":
var s = Std.string(match);
for (f in files) {
if (f.name == s) return f;
}
case "path":
var s = Std.string(match);
for (f in files) {
if (f.path == s) return f;
}
default:
var s = Std.string(match);
if (!StringTools.startsWith(s, ".")) s = "." + s;
for (f in files) {
if (StringTools.endsWith(f.name, s)) return f;
}
}
return null;
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
var f:TorrentFile = file;
return switch (from) {
case 1: file;
case 2: f != null ? f.name : null;
case 3: f != null ? f.path : null;
case 4: f != null ? f.length : 0;
case 5: f != null ? f.type : null;
case 6: f != null ? f.getBytes() : null;
case 7: f != null;
case 8: f != null ? f.downloaded : 0.0;
case 9: f != null ? f.progress : 0.0;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,8 +1,8 @@
package leenkx.logicnode; package leenkx.logicnode;
import leenkx.system.Event; #if lnx_torrent
import leenkx.network.Connect; import leenkx.network.Leenkx;
import iron.object.Object; import leenkx.network.torrent.LeenkxClient;
#end
class LeenkxCloseConnectionNode extends LogicNode { class LeenkxCloseConnectionNode extends LogicNode {
public var property0: String; public var property0: String;
@ -12,41 +12,29 @@ class LeenkxCloseConnectionNode extends LogicNode {
} }
override function run(from:Int) { override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get(); var connection = inputs[1].get();
if (connection == null) return; if (connection == null) return;
var socket = Leenkx.connections[connection._url];
if (socket == null) return;
if(property0 == "client") { var lnxCn:LeenkxClient = socket.client;
var script = ' if (property0 == "peer") {
for (p in leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers){ var id:String = inputs.length > 2 ? inputs[2].get() : null;
try{ if (id != null && lnxCn != null) {
leenkx.network.Leenkx.id.set("' + connection._url + '",p); lnxCn.peers.remove(id);
leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn._pc.close(); lnxCn.seenFired.remove(id);
leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn.destroy();
leenkx.network.Leenkx.connections.h["' + connection._url + '"].onclose();
}catch(error){
console.log("Error: " + error);
} }
runOutput(0);
return;
} }
'; if (lnxCn != null) {
js.Syntax.code('(1, eval)({0})', script.toString()); lnxCn.destroy();
} else { socket.client = null;
var script = '
try{
var lnxConn = leenkx.network.Leenkx.connections.h["' + connection._url + '"];
if (lnxConn && lnxConn.client && lnxConn.client.torrent && lnxConn.client.torrent._peers) {
for (var p in lnxConn.client.torrent._peers) {
lnxConn.client.torrent._peers[p].conn._pc.close();
lnxConn.client.torrent._peers[p].conn.destroy();
leenkx.network.Leenkx.id.set("' + connection._url + '", p);
lnxConn.onclose();
}
}
}catch(error){
console.log("Error: " + error);
}
';
js.Syntax.code('(1, eval)({0})', script.toString());
} }
Leenkx.connections.remove(connection._url);
socket.onclose();
#end
runOutput(0); runOutput(0);
} }
} }

View File

@ -1,10 +1,10 @@
package leenkx.logicnode; package leenkx.logicnode;
import leenkx.system.Event; import leenkx.system.Event;
#if lnx_torrent
import leenkx.network.Leenkx; import leenkx.network.Leenkx;
import iron.object.Object; #end
class LeenkxEventNode extends LogicNode { class LeenkxEventNode extends LogicNode {
public var property0: String;
public var property1: String; public var property1: String;
public var value: String; public var value: String;
@ -14,90 +14,63 @@ class LeenkxEventNode extends LogicNode {
public function new(tree:LogicTree) { public function new(tree:LogicTree) {
super(tree); super(tree);
tree.notifyOnInit(init); tree.notifyOnInit(init);
tree.notifyOnRemove(onRemove);
} }
function init() { function init() {
if (property0 == "client") { #if lnx_torrent
net_Url = inputs[0].get(); var u:Dynamic = inputs[0].get();
net_Url = Std.isOfType(u, String) ? u : (u != null ? u._url : null);
switch (property1) { switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onopen": value = Leenkx.onOpenEvent;
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onmessage": value = Leenkx.onMessageEvent;
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onerror": value = Leenkx.onErrorEvent;
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onclose": value = Leenkx.onCloseEvent;
case "onseen": value = Leenkx.onSeenEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onseen": value = Leenkx.onSeenEvent;
case "onserver": value = Leenkx.onServerEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onserver": value = Leenkx.onServerEvent;
case "onconnections": value = Leenkx.onConnectionsEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onconnections": value = Leenkx.onConnectionsEvent;
case "onping": value = Leenkx.onPingEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onping": value = Leenkx.onPingEvent;
case "onleft": value = Leenkx.onLeftEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onleft": value = Leenkx.onLeftEvent;
case "ontimeout": value = Leenkx.onTimeOutEvent;listener = Event.add(value, onEvent, tree.object.uid); case "ontimeout": value = Leenkx.onTimeOutEvent;
case "onrpc": value = Leenkx.onRpcEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onwireleft": value = Leenkx.onWireLeftEvent;
case "onrpcresponse": value = Leenkx.onRpcResponseEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onwireseen": value = Leenkx.onWireSeenEvent;
case "onwireleft": value = Leenkx.onWireLeftEvent;listener = Event.add(value, onEvent, tree.object.uid); case "ontorrent": value = Leenkx.onTorrentEvent;
case "onwireseen": value = Leenkx.onWireSeenEvent;listener = Event.add(value, onEvent, tree.object.uid); case "ontorrentadded": value = Leenkx.onTorrentAddedEvent;
case "ontorrent": value = Leenkx.onTorrentEvent;listener = Event.add(value, onEvent, tree.object.uid); case "ontracker": value = Leenkx.onTrackerEvent;
case "ontracker": value = Leenkx.onTrackerEvent;listener = Event.add(value, onEvent, tree.object.uid); case "onannounce": value = Leenkx.onAnnounceEvent;
case "onannounce": value = Leenkx.onAnnounceEvent;listener = Event.add(value, onEvent, tree.object.uid); case "ontorrentdone": value = Leenkx.onTorrentDoneEvent;
default: throw "Failed to set client event type."; case "onload": value = Leenkx.onLoadEvent;
} default:
} else if (property0 == "host") { trace("Failed to set client event type");
#if (sys || kha_krom)
var net_Domain = inputs[0].get();
var net_Port = inputs[1].get();
net_Url = "ws://" + net_Domain + ":" + Std.string(net_Port);
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
default: throw "Failed to set host event type.";
} }
listener = Event.add(value, onEvent, tree.object.uid);
#end #end
} else if (property0 == "securehost"){
#if (sys || kha_krom)
var net_Domain = inputs[0].get();
var net_Port = inputs[1].get();
net_Url = "wss://" + net_Domain + ":" + Std.string(net_Port);
switch (property1) {
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
default: throw "Failed to set host event type.";
}
#end
}
} }
function onEvent() { function onEvent() {
runOutput(0); runOutput(0);
} }
function onRemove() {
if (listener != null) {
Event.removeListener(listener);
listener = null;
}
}
override function get(from: Int): Dynamic { override function get(from: Int): Dynamic {
if (property0 == "host") { #if lnx_torrent
return switch (from) { return switch (from) {
case 1: Leenkx.id.get(net_Url); case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url); case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable"; default: throw "Unreachable";
} }
} #else
else if (property0 == "securehost") { return null;
return switch (from) { #end
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
}
else {
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
}
} }
} }

View File

@ -1,8 +1,6 @@
package leenkx.logicnode; package leenkx.logicnode;
import leenkx.network.Connect;
import leenkx.network.Buffer; import leenkx.network.Buffer;
import haxe.io.Bytes; import haxe.io.Bytes;
import iron.object.Object;
import iron.math.Vec4; import iron.math.Vec4;
import iron.math.Quat; import iron.math.Quat;
import iron.math.Mat4; import iron.math.Mat4;
@ -22,98 +20,54 @@ class LeenkxMessageParserNode extends LogicNode {
if (data == null) return; if (data == null) return;
api = inputs[1].get(); api = inputs[1].get();
if (api == null) api = "";
var buffer:Buffer;
if (Std.isOfType(data, Buffer)) {
buffer = cast data;
} else {
var rawBytes:Bytes;
try {
rawBytes = haxe.crypto.Base64.decode(Std.string(data));
} catch(e) {
trace("Base64 decode failed: " + e);
return;
}
buffer = new Buffer();
buffer.writeBytes(rawBytes);
}
/**
var buffer = cast(data, Buffer);
api = inputs[1].get();
if(buffer.endsWith(api) == false){ if(buffer.endsWith(api) == false){
return; return;
} }
**/
var buffer = new Buffer();
if(!StringTools.startsWith(data,api)){
return;
}
switch (property0) {
case "string":
parsed = data.substr(api.length, data.length);
runOutput(0);
case "vector":
var vec = data.substr(api.length, data.length);
vec = StringTools.replace(vec, "(", "");
vec = StringTools.replace(vec, ")", "");
vec = StringTools.replace(vec, " ", "");
vec = StringTools.trim(vec);
var array = vec.split(",").map(Std.parseFloat);
parsed = new Vec4( array[0], array[1], array[2], array[3] );
runOutput(0);
case "float":
var flt:String = data.substr(api.length, data.length);
var float = StringTools.trim(flt);
parsed = Std.parseFloat(float);
runOutput(0);
case "integer":
var int:String = data.substr(api.length, data.length);
var integer = StringTools.trim(int);
parsed = Std.parseInt(integer);
runOutput(0);
case "boolean":
var boolean:String = data.substr(api.length, data.length);
if(boolean == "true"){
parsed = true;
} else {
parsed = false;
}
runOutput(0);
case "transform":
var tsfm = data.substr(api.length, data.length);
tsfm = StringTools.replace(tsfm, "[", "");
tsfm = StringTools.replace(tsfm, "]", "");
tsfm = StringTools.replace(tsfm, " ", "");
tsfm = StringTools.trim(tsfm);
var array = tsfm.split(",").map(Std.parseFloat);
parsed = new Mat4(
array[0], array[1], array[2], array[3],
array[4], array[5], array[6], array[7],
array[8], array[9], array[10], array[11],
array[12], array[13], array[14], array[15]
);
runOutput(0);
case "rotation":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
var rot = new Quat();
for (f in Reflect.fields(rot)) {
Reflect.setField(rot, f, bytes.readFloat());
}
parsed = rot;
runOutput(0);
default: throw "Failed to parse data.";
}
return;
switch (property0) { switch (property0) {
case "string": case "string":
var bytes = buffer.readAllAvailableBytes(); var bytes = buffer.readAllAvailableBytes();
parsed = bytes.toString().substr(0, bytes.length - api.length); parsed = bytes.toString().substr(0, bytes.length - api.length);
trace(parsed);
runOutput(0); runOutput(0);
case "vector": case "vector":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var vec = new Vec4(); var vec = new Vec4();
for (f in Reflect.fields(vec)) { vec.x = bytes.readFloat();
Reflect.setField(vec, f, bytes.readFloat()); vec.y = bytes.readFloat();
} vec.z = bytes.readFloat();
vec.w = bytes.readFloat();
parsed = vec; parsed = vec;
runOutput(0); runOutput(0);
case "float": case "float":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var float: Float = bytes.readFloat(); var float: Float = bytes.readFloat();
parsed = float; parsed = float;
runOutput(0); runOutput(0);
case "integer": case "integer":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var integer: Int = bytes.readInt32(); var integer: Int = bytes.readInt32();
parsed = integer; parsed = integer;
runOutput(0); runOutput(0);
@ -127,34 +81,45 @@ class LeenkxMessageParserNode extends LogicNode {
} }
runOutput(0); runOutput(0);
case "transform": case "transform":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var loc: Vec4 = new Vec4(); var loc: Vec4 = new Vec4();
var rot: Quat = new Quat(); var rot: Quat = new Quat();
var scl: Vec4 = new Vec4(); var scl: Vec4 = new Vec4();
for (f in Reflect.fields(loc)) { try {
Reflect.setField(loc, f, bytes.readFloat()); loc.x = bytes.readFloat();
} loc.y = bytes.readFloat();
for (f in Reflect.fields(rot)) { loc.z = bytes.readFloat();
Reflect.setField(rot, f, bytes.readFloat()); loc.w = bytes.readFloat();
} rot.x = bytes.readFloat();
for (f in Reflect.fields(scl)) { rot.y = bytes.readFloat();
Reflect.setField(scl, f, bytes.readFloat()); rot.z = bytes.readFloat();
rot.w = bytes.readFloat();
scl.x = bytes.readFloat();
scl.y = bytes.readFloat();
scl.z = bytes.readFloat();
scl.w = bytes.readFloat();
} catch(e: Dynamic) {
trace("readFloat error: " + e);
return;
} }
var transform = Mat4.identity(); var transform = Mat4.identity();
parsed = transform.compose(loc, rot, scl); parsed = transform.compose(loc, rot, scl);
runOutput(0); runOutput(0);
case "rotation": case "rotation":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api)); var readBytes = buffer.readUntil(api);
if (readBytes == null) return;
var bytes = new haxe.io.BytesInput(readBytes);
var rot = new Quat(); var rot = new Quat();
for (f in Reflect.fields(rot)) { rot.x = bytes.readFloat();
Reflect.setField(rot, f, bytes.readFloat()); rot.y = bytes.readFloat();
} rot.z = bytes.readFloat();
rot.w = bytes.readFloat();
parsed = rot; parsed = rot;
runOutput(0); runOutput(0);
default: throw "Failed to parse data."; default: throw "Failed to parse data.";
} }
} }
override function get(from: Int): Dynamic { override function get(from: Int): Dynamic {

View File

@ -1,401 +1,124 @@
package leenkx.logicnode; package leenkx.logicnode;
import leenkx.network.Connect;
import leenkx.network.Leenkx;
import leenkx.network.Buffer; import leenkx.network.Buffer;
import haxe.io.Bytes; import haxe.io.Bytes;
import iron.object.Object; #if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.LeenkxClient;
#end
import iron.math.Vec4; import iron.math.Vec4;
import iron.math.Quat; import iron.math.Quat;
import iron.math.Mat4; import iron.math.Mat4;
class LeenkxSendMessageNode extends LogicNode { class LeenkxSendMessageNode extends LogicNode {
public var property0: String;
public var property1: String; public var property1: String;
public var id: String; public var property2: Bool;
public var all: Bool;
public function new(tree:LogicTree) { public function new(tree:LogicTree) {
super(tree); super(tree);
} }
override function run(from:Int) { override function run(from:Int) {
#if js
var connection = inputs[1].get(); var connection = inputs[1].get();
if (connection == null) return; if (connection == null) return;
var api: String = inputs[2].get(); var api: String = inputs[2].get();
var message: Dynamic = inputs[3].get(); var message: Dynamic = inputs[3].get();
if (message == null) return;
try { var broadcast: Bool = inputs.length > 4 && inputs[4].get() == true;
if(inputs[5].get() == true){ var targetAddress: String = inputs.length > 5 ? inputs[5].get() : null;
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ if (!broadcast && (targetAddress == null || targetAddress == "")) {
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(`'+ api + message + '`);
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send(`' + inputs[4].get() + '`, `'+ api + message + '` );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
return; return;
}
#if lnx_torrent
var socket = Leenkx.connections[connection._url];
if (socket == null) return;
var lnxCn:LeenkxClient = socket.client;
if (lnxCn == null) return;
var buffer = new Buffer();
switch (property1) { switch (property1) {
case "string": case "string":
if(property0 == "client"){
var buffer = new Buffer();
buffer.writeBytes(Bytes.ofString(message)); buffer.writeBytes(Bytes.ofString(message));
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "vector": case "vector":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
var i = Reflect.fields(message); var f:Null<Float>;
for (b in i) { if ((f = message.x) != null) bytesOut.writeFloat(f);
bytesOut.writeFloat(Reflect.field(message, b)); if ((f = message.y) != null) bytesOut.writeFloat(f);
} if ((f = message.z) != null) bytesOut.writeFloat(f);
var buffer = new Buffer(); if ((f = message.w) != null) bytesOut.writeFloat(f);
buffer.writeBytes(bytesOut.getBytes()); buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send("' + api + message + '");
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "float": case "float":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeFloat(message); bytesOut.writeFloat(message);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes()); buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "integer": case "integer":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
bytesOut.writeInt32(message); bytesOut.writeInt32(message);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes()); buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "boolean": case "boolean":
if(property0 == "client"){
var buffer = new Buffer();
if (message == true) { if (message == true) {
buffer.writeBytes(Bytes.ofString("true")); buffer.writeBytes(Bytes.ofString("true"));
} else { } else {
buffer.writeBytes(Bytes.ofString("false")); buffer.writeBytes(Bytes.ofString("false"));
} }
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0);
return;
}
case "transform": case "transform":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
var transform:Mat4 = message; var transform:Mat4 = message;
var loc = new Vec4(); var loc = new Vec4();
var rot = new Quat(); var rot = new Quat();
var scale = new Vec4(); var scale = new Vec4();
transform.decompose(loc, rot, scale); transform.decompose(loc, rot, scale);
var l = Reflect.fields(loc); bytesOut.writeFloat(loc.x);
for (b in l) { bytesOut.writeFloat(loc.y);
bytesOut.writeFloat(Reflect.field(loc, b)); bytesOut.writeFloat(loc.z);
} bytesOut.writeFloat(loc.w);
var r = Reflect.fields(rot); bytesOut.writeFloat(rot.x);
for (b in r) { bytesOut.writeFloat(rot.y);
bytesOut.writeFloat(Reflect.field(rot, b)); bytesOut.writeFloat(rot.z);
} bytesOut.writeFloat(rot.w);
var s = Reflect.fields(scale); bytesOut.writeFloat(scale.x);
for (b in s) { bytesOut.writeFloat(scale.y);
bytesOut.writeFloat(Reflect.field(scale, b)); bytesOut.writeFloat(scale.z);
} bytesOut.writeFloat(scale.w);
var buffer = new Buffer();
buffer.writeBytes(bytesOut.getBytes()); buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){
buffer.writeBytes(Bytes.ofString(api));
}
try {
connection.buffer = buffer;
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
//console.log("Mine: " + lx_8001.torrent.discovery.peerId + " || Incomming: " + p);
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
//var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
var script = 'lnx_' + connection._url +'.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
}
case "rotation": case "rotation":
if(property0 == "client"){
var bytesOut = new haxe.io.BytesOutput(); var bytesOut = new haxe.io.BytesOutput();
var i = Reflect.fields(message); var f:Null<Float>;
for (b in i) { if ((f = message.x) != null) bytesOut.writeFloat(f);
bytesOut.writeFloat(Reflect.field(message, b)); if ((f = message.y) != null) bytesOut.writeFloat(f);
} if ((f = message.z) != null) bytesOut.writeFloat(f);
var buffer = new Buffer(); if ((f = message.w) != null) bytesOut.writeFloat(f);
buffer.writeBytes(bytesOut.getBytes()); buffer.writeBytes(bytesOut.getBytes());
if(api != "" || api != null){ default: trace("Failed to send data.");
}
if (api != null && api != "") {
buffer.writeBytes(Bytes.ofString(api)); buffer.writeBytes(Bytes.ofString(api));
} }
var rawBytes = buffer.readAllAvailableBytes();
try { try {
connection.buffer = buffer; if (property2) {
if(inputs[5].get() == true){ var payload = haxe.crypto.Base64.encode(rawBytes);
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){ if (broadcast) {
try{ lnxCn.send(null, payload);
lnx_' + connection._url +'.torrent._peers[p].conn.send(leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else { } else {
var script = 'lnx_' + connection._url +'.send("' + inputs[4].get() + '",leenkx.network.Leenkx.connections.h["'+ connection._url +'"].buffer );'; lnxCn.send(targetAddress, payload);
js.Syntax.code('(1, eval)({0})', script.toString()); }
runOutput(0); } else {
lnxCn.sendRawMessage(rawBytes, broadcast ? null : targetAddress);
} }
} catch(error) { } catch(error) {
trace("Error: " + error); trace("Error: " + error);
} }
} else {
if(inputs[5].get() == true){
var script = 'lnx_' + connection._url + '.send(`' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}else{
var script = 'lnx_' + connection._url + '.send(`' + inputs[4].get() + '`, `' + api + message + '`);';
js.Syntax.code('(1, eval)({0})', script);
}
runOutput(0); runOutput(0);
return;
} #else
} runOutput(0);
#end #end
} }
} }
/*******_________________________________________
try {
if(inputs[5].get() == true){
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lnx_' + connection._url +'.torrent._peers[p].conn.send("' + api + message + '");
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
} else {
var script = 'for (p in lnx_' + connection._url +'.torrent._peers){
try{
lx_' + connection._url +'.torrent._peers[' + inputs[4].get() + '].conn.send("' + api + message + '");
}catch(error){
console.log("Error: " + error);
}
}';
js.Syntax.code('(1, eval)({0})', script.toString());
runOutput(0);
}
} catch(error) {
trace("Error: " + error);
}
return;
_________________________________________*****/

View File

@ -23,6 +23,7 @@ class NetworkMessageParserNode extends LogicNode {
var buffer = cast(data, Buffer); var buffer = cast(data, Buffer);
api = inputs[1].get(); api = inputs[1].get();
if (api == null) api = "";
if(buffer.endsWith(api) == false){ if(buffer.endsWith(api) == false){
return; return;
} }

View File

@ -0,0 +1,65 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
#end
class OnTorrentEventNode extends LogicNode {
public var property0:String;
public var eventValue:Dynamic;
var net_Url:String;
var torrent:Dynamic;
var registeredTorrents:Map<String, Bool> = new Map();
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
var id = inputs[2].get();
if (id == null) return;
net_Url = connection._url;
var socket = Leenkx.connections[connection._url];
var client = socket != null ? socket.client : null;
var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null);
if (t == null) return;
var event = property0 != null ? property0 : "done";
var self = this;
var fire = function(v:Dynamic) {
self.eventValue = v;
self.runOutput(0);
};
var key = t.infoHashHex != null ? t.infoHashHex : "v2:" + t.infoHashV2Hex;
var alreadyRegistered = registeredTorrents.exists(key);
if (!alreadyRegistered) registeredTorrents.set(key, true);
switch (event) {
case "ready":
if (t.ready) fire(null);
else if (!alreadyRegistered) t.on("ready", function() fire(null));
case "piece" | "wire" | "metadata" | "error":
if (!alreadyRegistered) t.on(event, function(v) fire(v));
default:
if (t.done) fire(null);
else if (!alreadyRegistered) t.on("done", function() fire(null));
}
torrent = t;
#end
}
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: torrent;
case 2: eventValue;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,73 @@
package leenkx.logicnode;
#if lnx_torrent
import haxe.io.Bytes;
import leenkx.network.torrent.Magnet;
import leenkx.network.torrent.MetaInfo;
#end
class ParseTorrentNode extends LogicNode {
var meta:Dynamic;
var magnet:Dynamic;
var idHex:String;
public var property0:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
meta = null;
magnet = null;
idHex = null;
var src = inputs[1].get();
if (src == null) return;
switch (property0 != null ? property0 : "magnet") {
case "file":
if (Std.isOfType(src, MetaInfo)) {
meta = src;
} else {
var b = Std.isOfType(src, Bytes) ? src : Bytes.ofString(Std.string(src));
try {
meta = MetaInfo.fromTorrentBytes(b);
} catch(e:Dynamic) {
trace(e);
return;
}
}
case "id":
var s = Std.string(src);
idHex = s.length == 64 ? "v2:" + s : s;
default:
var m = Magnet.parse(Std.string(src));
if (m == null) {
return;
}
magnet = m;
idHex = m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex;
}
#end
runOutput(0);
}
override function get(from:Int):Dynamic {
#if lnx_torrent
var m:MetaInfo = meta;
var mg:Magnet = magnet;
return switch (from) {
case 1: m;
case 2: m != null ? (m.infoHashHex != null ? m.infoHashHex : m.infoHashV2Hex) : idHex;
case 3: m != null ? m.name : (mg != null ? mg.name : null);
case 4: m != null ? m.announce : (mg != null ? mg.trackers : null);
case 5: m != null ? m.urlList : (mg != null ? mg.webSeeds : null);
case 6: m != null ? m.files : null;
case 7: m != null ? m.totalLength : 0;
case 8: m != null ? m.toTorrentBytes() : null;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,52 +1,124 @@
package leenkx.logicnode; package leenkx.logicnode;
import leenkx.system.Event; #if lnx_torrent
import iron.object.Object; import haxe.io.Bytes;
import leenkx.network.Leenkx;
import leenkx.network.torrent.MetaInfo;
import leenkx.network.torrent.Torrent;
#end
class SeedTorrentNode extends LogicNode { class SeedTorrentNode extends LogicNode {
public var torrent:Dynamic; public var torrent:Dynamic;
public var net_Url: Dynamic; public var net_Url:String;
public function new(tree:LogicTree) { public function new(tree:LogicTree) {
super(tree); super(tree);
} }
override function run(from:Int) { override function run(from:Int) {
#if js #if lnx_torrent
var connection = inputs[1].get(); var connection = inputs[1].get();
if (connection == null) return; if (connection == null) return;
trace("got connection"); var src = inputs[2].get();
var data = inputs[3].get();
if (data == null) return;
trace("got data");
var opts = inputs[2].get();
net_Url = connection._url; net_Url = connection._url;
var cx = "lx_" + net_Url; var socket = Leenkx.connections[connection._url];
var lx:Dynamic = untyped js.Browser.window[cx]; if (socket == null || socket.client == null) return;
var args = opts.toString(); var client = socket.client;
trace(args); var self = this;
var func = Reflect.field(lx.wt, "seed");
if (func != null) { var onSeeded = function(t:Torrent) {
torrent = func(data, function (torrent) { if (t == null) return;
trace('Client is seeding ' + torrent.magnetURI); self.torrent = t;
}); var onDone = function() {
//trace(torrent); self.runOutput(2);
};
if (t.done) onDone();
else t.on("done", onDone);
};
if (Std.isOfType(src, Torrent)) {
var t:Torrent = src;
t.start();
torrent = t;
runOutput(0);
return;
} }
var meta:MetaInfo = null;
if (Std.isOfType(src, MetaInfo)) {
meta = cast src;
} else if (Std.isOfType(src, Bytes)) {
try {
meta = MetaInfo.fromTorrentBytes(cast src);
} catch(e:Dynamic) {
return;
}
}
if (meta == null) {
return;
}
var files = filesFor(meta, inputs.length > 3 ? inputs[3].get() : null);
if (files == null) {
return;
}
client.seedMeta(meta, files, null, onSeeded);
runOutput(0); runOutput(0);
#end #end
} }
#if lnx_torrent
function filesFor(meta:MetaInfo, data:Dynamic):Array<{path:String, data:Bytes}> {
if (data == null) return null;
var entries = meta.files;
if (Std.isOfType(data, Bytes)) {
var blob:Bytes = data;
if (blob.length != meta.totalLength) return null;
var files:Array<{path:String, data:Bytes}> = [];
var cursor = 0;
for (f in entries) {
files.push({path: f.path, data: blob.sub(cursor, f.length)});
cursor += f.length;
}
return files;
}
if (Std.isOfType(data, Array)) {
var items:Array<Dynamic> = data;
var files:Array<{path:String, data:Bytes}> = [];
for (it in items) {
var p = Reflect.field(it, "path");
var d:Bytes = Reflect.field(it, "data");
if (d == null) d = Reflect.field(it, "content");
if (p == null || d == null) continue;
var sp = Std.string(p);
var matched = false;
for (f in entries) {
if (f.path == sp || StringTools.endsWith(f.path, "/" + sp)) {
files.push({path: f.path, data: d});
matched = true;
break;
}
}
if (!matched) files.push({path: sp, data: d});
}
if (files.length == entries.length) return files;
return null;
}
return null;
}
#end
override function get(from:Int):Dynamic { override function get(from:Int):Dynamic {
#if lnx_torrent
var t:Torrent = torrent;
return switch (from) { return switch (from) {
case 1: torrent; case 1: torrent;
case 3: t != null ? t.infoHashHex : null;
case 4: t != null ? t.magnetURI : null;
default: throw "Unreachable"; default: throw "Unreachable";
} }
#else
return null;
#end
} }
} }

View File

@ -0,0 +1,90 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
import leenkx.network.torrent.TorrentClient;
#end
class SetTrackersNode extends LogicNode {
public var property0:String;
public var target:Dynamic;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var p = property0 != null ? property0 : "add";
switch (p) {
case "add" | "override":
setTorrentTrackers(p == "override");
default:
setTrackerProperty(p);
}
#end
runOutput(0);
}
#if lnx_torrent
function setTorrentTrackers(trackerOverride:Bool):Void {
var id = inputs[2].get();
var urls = TorrentClient.trackerUrls(inputs[3].get());
if (urls == null) return;
var t:Torrent = Std.isOfType(id, Torrent) ? id : null;
if (t == null && id != null) {
var connection = inputs[1].get();
var socket = connection != null ? Leenkx.connections[connection._url] : null;
var client = socket != null ? socket.client : null;
if (client != null) t = client.get(id);
}
if (t != null) {
target = t;
if (trackerOverride) {
t.setTrackers(urls);
} else {
for (u in urls) t.addTracker(u);
}
} else if (id == null) {
target = null;
if (trackerOverride) {
TorrentClient.defaultTrackers = urls;
} else {
TorrentClient.mergeTrackers(TorrentClient.defaultTrackers, urls);
}
}
}
function setTrackerProperty(prop:String):Void {
var tracker:Dynamic = inputs[1].get();
if (tracker == null) return;
var v = inputs[2].get();
if (v == null) return;
var field = prop == "interval" ? "intervalSec" : prop;
var val:Dynamic = field == "trackerId" ? Std.string(v)
: Std.int(Std.parseFloat(Std.string(v)));
var setter:Dynamic = field == "intervalSec"
&& Reflect.field(tracker, "intervalSec") == null
? Reflect.field(tracker, "setAnnounceInterval") : null;
if (setter != null) {
try {
Reflect.callMethod(tracker, setter, [(val:Int) * 1000]);
} catch(e:Dynamic) {}
} else {
Reflect.setField(tracker, field, val);
}
target = tracker;
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: target;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,74 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
#end
class TorrentClientInfoNode extends LogicNode {
var stats:Dynamic;
var list:Array<Dynamic>;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
list = [];
stats = {downRate: 0.0, upRate: 0.0, progress: 0.0, ratio: 0.0};
var connection = inputs[1].get();
if (connection != null) {
var socket = Leenkx.connections[connection._url];
var client = socket != null ? socket.client : null;
if (client != null) collect(client);
}
#end
runOutput(0);
}
#if lnx_torrent
function collect(client:Dynamic) {
var down = 0.0;
var up = 0.0;
var received = 0.0;
var sent = 0.0;
var total = 0.0;
var left = 0.0;
var torrents:Map<String, Torrent> = client.torrents;
for (t in torrents) {
list.push(t);
var s = t.getStats();
down += s.downRate;
up += s.upRate;
received += s.downloaded;
sent += s.uploaded;
if (t.meta != null) {
total += t.meta.totalLength;
left += s.left;
}
}
stats = {
downRate: down,
upRate: up,
progress: total > 0 ? 1.0 - left / total : 0.0,
ratio: received > 0 ? sent / received : 0.0
};
}
#end
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: list;
case 2: list.length;
case 3: stats.downRate;
case 4: stats.upRate;
case 5: stats.progress;
case 6: stats.ratio;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -0,0 +1,93 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
#end
class TorrentControlNode extends LogicNode {
public var property0:String;
var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var connection = inputs[1].get();
if (connection == null) return;
var id = inputs[2].get();
if (id == null) return;
net_Url = connection._url;
var socket = Leenkx.connections[connection._url];
if (socket == null) return;
var client = socket.client;
var t = client != null ? client.get(id) : (Std.isOfType(id, Torrent) ? id : null);
var action = property0 != null ? property0 : "remove";
var file:Dynamic = inputs.length > 3 ? inputs[3].get() : null;
var t2 = t;
var startB = 0;
var endB = -1;
if (file != null) {
startB = file.offset;
endB = file.offset + file.length - 1;
if (t2 == null) t2 = file.torrent;
} else {
var sv = inputs.length > 4 ? inputs[4].get() : null;
var ev = inputs.length > 5 ? inputs[5].get() : null;
if (sv != null && Std.string(sv) != "") {
startB = Std.int(Std.parseFloat(Std.string(sv)));
}
if (ev != null && Std.string(ev) != "") {
endB = Std.int(Std.parseFloat(Std.string(ev)));
}
}
var pv = inputs.length > 6 ? inputs[6].get() : null;
var priority = pv != null && Std.string(pv) != "" ? Std.int(Std.parseFloat(Std.string(pv))) : null;
switch (action) {
case "start":
if (t != null) t.start();
case "stop":
if (t != null) t.stop();
case "select", "critical", "deselect":
if (t2 != null) {
if (endB < 0) {
endB = t2.meta != null ? t2.meta.totalLength - 1 : 0;
}
if (action == "critical") {
t2.critical(startB, endB);
} else if (action == "select") {
t2.select(startB, endB, priority);
} else {
t2.deselect(startB, endB, priority);
}
}
case "add-peer":
var peer = inputs.length > 3 ? inputs[3].get() : null;
if (t != null && peer != null) {
t.addPeer(Std.string(peer));
}
case "add-web-seed":
var url = inputs.length > 3 ? inputs[3].get() : null;
if (t != null && url != null) {
t.addWebSeed(Std.string(url));
}
case "remove", "remove-data":
var hex = t != null ? t.infoHashHex : Std.string(id);
if (hex == null) hex = Std.string(id);
if (client != null) {
client.remove(hex, action == "remove-data");
}
if (t != null && Leenkx.torrent.get(net_Url) == t) {
Leenkx.torrent.remove(net_Url);
Leenkx.file.remove(net_Url);
}
default:
}
#end
runOutput(0);
}
}

View File

@ -0,0 +1,74 @@
package leenkx.logicnode;
#if lnx_torrent
import leenkx.network.Leenkx;
import leenkx.network.torrent.Torrent;
import leenkx.network.torrent.TorrentClient;
#end
class TorrentInfoNode extends LogicNode {
public var torrent:Dynamic;
public var net_Url:String;
public function new(tree:LogicTree) {
super(tree);
}
override function run(from:Int) {
#if lnx_torrent
var id = inputs[2].get();
if (id == null) return;
torrent = null;
if (Std.isOfType(id, Torrent)) {
torrent = id;
} else {
var connection = inputs[1].get();
if (connection == null) return;
net_Url = connection._url;
var socket = Leenkx.connections[net_Url];
if (socket == null || socket.client == null) return;
torrent = socket.client.get(id);
}
#end
runOutput(0);
}
override function get(from:Int):Dynamic {
#if lnx_torrent
var t:Torrent = torrent;
return switch (from) {
case 1: t != null ? t.infoHashHex : null;
case 2: t != null && t.meta != null ? t.meta.name : null;
case 3: t != null ? t.numPeers : 0;
case 4: progress(t);
case 5: t != null ? t.getStats().downloaded : 0.0;
case 6: t != null ? t.getStats().uploaded : 0.0;
case 7: t != null ? t.done : false;
case 8: t != null ? t.magnetURI : null;
case 9: t != null ? t.files : null;
case 10: t != null && t.meta != null ? t.meta.numPieces : 0;
case 11: t != null && t.meta != null ? t.meta.pieceLength : 0;
case 12: t != null && t.meta != null ? t.meta.totalLength : 0;
case 13: t != null && t.meta != null ? t.meta.comment : null;
case 14: t != null && t.meta != null ? t.meta.createdBy : null;
case 15: t != null && t.meta != null ? t.meta.protocolVersion : 1;
case 16: t != null ? t.infoHashV2Hex : null;
case 17: t != null ? t.getStats().downRate : 0.0;
case 18: t != null ? t.getStats().upRate : 0.0;
case 19: t != null ? t.stopped : false;
case 20: t;
default: throw "Unreachable";
}
#else
return null;
#end
}
#if lnx_torrent
function progress(t:Torrent):Float {
if (t == null || t.meta == null || t.meta.totalLength == 0) return 0;
var stats = t.getStats();
return 1.0 - stats.left / t.meta.totalLength;
}
#end
}

View File

@ -63,10 +63,10 @@ class Buffer {
public function readUntil(delimiter:String):Bytes { public function readUntil(delimiter:String):Bytes {
var dl = delimiter.length; var dl = delimiter.length;
for (i in 0 ... available - dl) { for (i in 0 ... available - dl + 1) {
var matched = true; var matched = true;
for (j in 0 ... dl) { for (j in 0 ... dl) {
if (peekByte(currentOffset + i + j + 1) == delimiter.charCodeAt(j)) { if (peekByte(i + j) == delimiter.charCodeAt(j)) {
continue; continue;
} }
matched = false; matched = false;
@ -74,7 +74,8 @@ class Buffer {
} }
if (matched) { if (matched) {
var bytes = readBytes(i + dl + 1); var bytes = readBytes(i);
readBytes(dl);
return bytes; return bytes;
} }
} }
@ -85,7 +86,19 @@ class Buffer {
public function readBytes(count:Int):Bytes { public function readBytes(count:Int):Bytes {
var count2 = Std.int(Math.min(count, available)); var count2 = Std.int(Math.min(count, available));
var out = Bytes.alloc(count2); var out = Bytes.alloc(count2);
for (n in 0 ... count2) out.set(n, readByte()); var written = 0;
while (written < count2) {
if (currentData == null || currentOffset >= currentData.length) {
currentOffset = 0;
currentData = chunks.shift();
}
var n = Std.int(Math.min(count2 - written, currentData.length - currentOffset));
out.blit(written, currentData, currentOffset, n);
written += n;
currentOffset += n;
available -= n;
}
length = available;
return out; return out;
} }
@ -115,16 +128,23 @@ class Buffer {
} }
public function peekByte(offset:Int):Int { public function peekByte(offset:Int):Int {
if (available <= 0) throw 'No bytes available'; if (available <= 0 || offset < 0 || offset >= available) throw 'No bytes available';
var tempOffset = offset; var tempData = currentData;
var tempData = chunks[0]; var chunkIndex = -1;
if (tempData == null) { var tempOffset:Int;
tempData = currentData; if (tempData != null && currentOffset < tempData.length) {
tempOffset = currentOffset + offset;
} else {
tempData = chunks[0];
chunkIndex = 0;
tempOffset = offset;
} }
var chunkIndex = 0;
while (tempOffset >= tempData.length) { while (tempOffset >= tempData.length) {
tempOffset -= tempData.length; tempOffset -= tempData.length;
chunkIndex++; chunkIndex++;
if (chunkIndex >= chunks.length){
trace('No bytes available');
}
tempData = chunks[chunkIndex]; tempData = chunks[chunkIndex];
} }
return tempData.get(tempOffset); return tempData.get(tempOffset);
@ -154,9 +174,9 @@ class Buffer {
public function endsWith(e:String):Bool { public function endsWith(e:String):Bool {
var i = available - e.length; var i = available - e.length;
var n = currentOffset; var n = 0;
if (i <= 0) { if (i < 0) {
return false; return false;
} }

View File

@ -1,6 +1,8 @@
package leenkx.network; package leenkx.network;
class Handler extends WebSocketCommon { class Handler extends WebSocketCommon {
public var validateHandshake:(HttpRequest, HttpResponse, (HttpResponse) -> Void) -> Void = null;
public function new(socket:SocketImpl) { public function new(socket:SocketImpl) {
super(socket); super(socket);
isClient = false; isClient = false;

View File

@ -4,6 +4,7 @@ class HttpHeader {
public static inline var SEC_WEBSOCKET_KEY:String = "Sec-WebSocket-Key"; public static inline var SEC_WEBSOCKET_KEY:String = "Sec-WebSocket-Key";
public static inline var SEC_WEBSOSCKET_ACCEPT:String = "Sec-WebSocket-Accept"; public static inline var SEC_WEBSOSCKET_ACCEPT:String = "Sec-WebSocket-Accept";
public static inline var SEC_WEBSOSCKET_VERSION:String = "Sec-WebSocket-Version"; public static inline var SEC_WEBSOSCKET_VERSION:String = "Sec-WebSocket-Version";
public static inline var SEC_WEBSOCKET_PROTOCOL:String = "Sec-WebSocket-Protocol";
public static inline var UPGRADE:String = "Upgrade"; public static inline var UPGRADE:String = "Upgrade";
public static inline var HOST:String = "Host"; public static inline var HOST:String = "Host";
public static inline var CONNECTION:String = "Connection"; public static inline var CONNECTION:String = "Connection";

View File

@ -1,10 +1,12 @@
package leenkx.network; package leenkx.network;
#if lnx_torrent
import leenkx.network.Types; import leenkx.network.Types;
import haxe.io.Bytes; import haxe.io.Bytes;
import iron.object.Object; import iron.object.Object;
import leenkx.system.Event; import leenkx.system.Event;
import leenkx.network.Buffer; import leenkx.network.Buffer;
import leenkx.network.torrent.LeenkxClient;
@:expose @:expose
class Leenkx { class Leenkx {
@ -20,23 +22,19 @@ class Leenkx {
public static var onPingEvent: String = "Leenkx.onPing"; public static var onPingEvent: String = "Leenkx.onPing";
public static var onLeftEvent: String = "Leenkx.onLeft"; public static var onLeftEvent: String = "Leenkx.onLeft";
public static var onTimeOutEvent: String = "Leenkx.onTimeOut"; public static var onTimeOutEvent: String = "Leenkx.onTimeOut";
public static var onRpcEvent: String = "Leenkx.onRpc";
public static var onRpcResponseEvent: String = "Leenkx.onRpcResponse";
public static var onWireLeftEvent: String = "Leenkx.onWireLeft"; public static var onWireLeftEvent: String = "Leenkx.onWireLeft";
public static var onWireSeenEvent: String = "Leenkx.onWireSeen"; public static var onWireSeenEvent: String = "Leenkx.onWireSeen";
public static var onTorrentEvent: String = "Leenkx.onTorrent"; public static var onTorrentEvent: String = "Leenkx.onTorrent";
public static var onTorrentAddedEvent: String = "Leenkx.onTorrentAdded";
public static var onTrackerEvent: String = "Leenkx.onTracker"; public static var onTrackerEvent: String = "Leenkx.onTracker";
public static var onAnnounceEvent: String = "Leenkx.onAnnounce"; public static var onAnnounceEvent: String = "Leenkx.onAnnounce";
public static var onTorrentDoneEvent: String = "Leenkx.onTorrentDone"; public static var onTorrentDoneEvent: String = "Leenkx.onTorrentDone";
public static var connections:Map<String, leenkx.network.LeenkxSocket> = []; public static var connections:Map<String, leenkx.network.LeenkxSocket> = [];
#if js public static var peers:Map<String, String> = [];
public static var peers:js.lib.Map<String, String> = new js.lib.Map<String,String>(); public static var data:Map<String, Dynamic> = [];
public static var data:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>(); public static var id:Map<String, String> = [];
public static var id:js.lib.Map<String, String> = new js.lib.Map<String,String>(); public static var torrent:Map<String, Dynamic> = [];
public static var torrent:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>(); public static var file:Map<String, Dynamic> = [];
public static var file:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>();
#end
public static var lxNew:Void->Void;
public function new(net_Url: String, net_object: Object) { public function new(net_Url: String, net_object: Object) {
if (net_Url != null && net_object != null) { if (net_Url != null && net_object != null) {
@ -54,224 +52,65 @@ class Leenkx {
} }
if (object != null) { if (object != null) {
final loadEvent = Event.get(Leenkx.onLoadEvent); var uid = object.uid;
final openEvent = Event.get(Leenkx.onOpenEvent);
final messageEvent = Event.get(Leenkx.onMessageEvent);
final errorEvent = Event.get(Leenkx.onErrorEvent);
final closeEvent = Event.get(Leenkx.onCloseEvent);
final seenEvent = Event.get(Leenkx.onSeenEvent);
final serverEvent = Event.get(Leenkx.onServerEvent);
final connectionsEvent = Event.get(Leenkx.onConnectionsEvent);
final pingEvent = Event.get(Leenkx.onPingEvent);
final leftEvent = Event.get(Leenkx.onLeftEvent);
final timeoutEvent = Event.get(Leenkx.onTimeOutEvent);
final rpcEvent = Event.get(Leenkx.onRpcEvent);
final rpcresponseEvent = Event.get(Leenkx.onRpcResponseEvent);
final wireleftEvent = Event.get(Leenkx.onWireLeftEvent);
final wireseenEvent = Event.get(Leenkx.onWireSeenEvent);
final torrentEvent = Event.get(Leenkx.onTorrentEvent);
final trackerEvent = Event.get(Leenkx.onTrackerEvent);
final announceEvent = Event.get(Leenkx.onAnnounceEvent);
final torrentDoneEvent = Event.get(Leenkx.onTorrentDoneEvent);
Leenkx.connections[net_Url].onopen = function() { Leenkx.connections[net_Url].onopen = function() {
if (openEvent != null) { Event.send(Leenkx.onOpenEvent, uid);
for (e in openEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onmessage = function() { Leenkx.connections[net_Url].onmessage = function() {
if (messageEvent != null) { Event.send(Leenkx.onMessageEvent, uid);
for (e in messageEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onerror = function() { Leenkx.connections[net_Url].onerror = function() {
if (errorEvent != null) { Event.send(Leenkx.onErrorEvent, uid);
for (e in errorEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onclose = function() { Leenkx.connections[net_Url].onclose = function() {
if (closeEvent != null) { Event.send(Leenkx.onCloseEvent, uid);
for (e in closeEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onseen = function() { Leenkx.connections[net_Url].onseen = function() {
if (seenEvent != null) { Event.send(Leenkx.onSeenEvent, uid);
for (e in seenEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onserver = function() { Leenkx.connections[net_Url].onserver = function() {
if (serverEvent != null) { Event.send(Leenkx.onServerEvent, uid);
for (e in serverEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onconnections = function() { Leenkx.connections[net_Url].onconnections = function() {
if (connectionsEvent != null) { Event.send(Leenkx.onConnectionsEvent, uid);
for (e in connectionsEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onping = function() { Leenkx.connections[net_Url].onping = function() {
if (pingEvent != null) { Event.send(Leenkx.onPingEvent, uid);
for (e in pingEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onleft = function() { Leenkx.connections[net_Url].onleft = function() {
if (leftEvent != null) { Event.send(Leenkx.onLeftEvent, uid);
for (e in leftEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].ontimeout = function() { Leenkx.connections[net_Url].ontimeout = function() {
if (timeoutEvent != null) { Event.send(Leenkx.onTimeOutEvent, uid);
for (e in timeoutEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
};
Leenkx.connections[net_Url].onrpc = function() {
if (rpcEvent != null) {
for (e in rpcEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
};
Leenkx.connections[net_Url].onrpcresponse = function() {
if (rpcresponseEvent != null) {
for (e in rpcresponseEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onwireleft = function() { Leenkx.connections[net_Url].onwireleft = function() {
if (wireleftEvent != null) { Event.send(Leenkx.onWireLeftEvent, uid);
for (e in wireleftEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onwireseen = function() { Leenkx.connections[net_Url].onwireseen = function() {
if (wireseenEvent != null) { Event.send(Leenkx.onWireSeenEvent, uid);
for (e in wireseenEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].ontorrent = function() { Leenkx.connections[net_Url].ontorrent = function() {
if (torrentEvent != null) { Event.send(Leenkx.onTorrentEvent, uid);
for (e in torrentEvent) { };
if (e.mask == object.uid) { Leenkx.connections[net_Url].ontorrentadded = function() {
e.onEvent(); Event.send(Leenkx.onTorrentAddedEvent, uid);
}
}
}
}; };
Leenkx.connections[net_Url].ontracker = function() { Leenkx.connections[net_Url].ontracker = function() {
if (trackerEvent != null) { Event.send(Leenkx.onTrackerEvent, uid);
for (e in trackerEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onannounce = function() { Leenkx.connections[net_Url].onannounce = function() {
if (announceEvent != null) { Event.send(Leenkx.onAnnounceEvent, uid);
for (e in announceEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].onload = function() { Leenkx.connections[net_Url].onload = function() {
if (loadEvent != null) { Event.send(Leenkx.onLoadEvent, uid);
for (e in loadEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
Leenkx.connections[net_Url].ontorrentdone = function() { Leenkx.connections[net_Url].ontorrentdone = function() {
if (torrentDoneEvent != null) { Event.send(Leenkx.onTorrentDoneEvent, uid);
for (e in torrentDoneEvent) {
if (e.mask == object.uid) {
e.onEvent();
}
}
}
}; };
#if js
var lnxjs:Dynamic = js.Lib.global;
var connectionUrl = net_Url; var connectionUrl = net_Url;
if (lnxjs.Leenkx != null) {
if (lnxjs.lnxNew == null) {
js.Syntax.code('globalThis.lnxNew = function(url) { return new Leenkx(url); }');
}
Leenkx.connections[connectionUrl].onload(); Leenkx.connections[connectionUrl].onload();
} else {
kha.Assets.loadBlobFromPath("Leenkx.js", function(b: kha.Blob) {
if (b != null) {
js.Syntax.code("(1,eval)({0})", b.toString());
if (lnxjs.Leenkx != null && lnxjs.lnxNew == null) {
js.Syntax.code('globalThis.lnxNew = function(url) { return new Leenkx(url); }');
}
} else {
trace("Warning: Leenkx.js blob is null - file may not be in assets");
}
Leenkx.connections[connectionUrl].onload();
}, function(err: kha.AssetError) {
trace("ERROR loading Leenkx.js: " + err.url + " - " + err.error);
Leenkx.connections[connectionUrl].onload();
});
}
#end
} }
} }
} }
@ -281,7 +120,7 @@ class Leenkx {
class LeenkxSocket { class LeenkxSocket {
public var _url:String; public var _url:String;
public var client:haxe.DynamicAccess<Dynamic>; public var client:LeenkxClient;
public var buffer:Dynamic; public var buffer:Dynamic;
public var onopen:Void->Void; public var onopen:Void->Void;
public var onclose:Void->Void; public var onclose:Void->Void;
@ -293,11 +132,10 @@ class LeenkxSocket {
public var onping:Void->Void; public var onping:Void->Void;
public var onleft:Void->Void; public var onleft:Void->Void;
public var ontimeout:Void->Void; public var ontimeout:Void->Void;
public var onrpc:Void->Void;
public var onrpcresponse:Void->Void;
public var onwireleft:Void->Void; public var onwireleft:Void->Void;
public var onwireseen:Void->Void; public var onwireseen:Void->Void;
public var ontorrent:Void->Void; public var ontorrent:Void->Void;
public var ontorrentadded:Void->Void;
public var ontracker:Void->Void; public var ontracker:Void->Void;
public var onannounce:Void->Void; public var onannounce:Void->Void;
public var onload:Void->Void; public var onload:Void->Void;
@ -308,3 +146,4 @@ class LeenkxSocket {
} }
} }
#end

View File

@ -1,51 +0,0 @@
package leenkx.network;
class Log {
public static inline var INFO:Int = 0x000001;
public static inline var DEBUG:Int = 0x000010;
public static inline var DATA:Int = 0x000100;
public static var mask:Int = 0;
#if (sys || kha_krom)
public static var logFn:Dynamic->Void = function(data:Dynamic) { trace(data); };
#elseif js
public static var logFn:Dynamic->Void = js.html.Console.log;
#end
public static function info(data:String, id:String = null) {
if (mask & INFO != INFO) {
return;
}
if (id != null) {
logFn('INFO :: ID-${id} :: ${data}');
} else {
logFn('INFO :: ${data}');
}
}
public static function debug(data:String, id:String = null) {
if (mask & DEBUG != DEBUG) {
return;
}
if (id != null) {
logFn('DEBUG :: ID-${id} :: ${data}');
} else {
logFn('DEBUG :: ${data}');
}
}
public static function data(data:String, id:String = null) {
if (mask & DATA != DATA) {
return;
}
if (id != null) {
logFn('DATA :: ID-${id}\n------------------------------\n${data}\n------------------------------');
} else {
logFn('${data}');
}
}
}

View File

@ -136,20 +136,45 @@ import haxe.io.Bytes;
class WebSocket { class WebSocket {
private var _url:String; private var _url:String;
private var _protocols:Array<String> = null;
private var _ws:js.html.WebSocket = null; private var _ws:js.html.WebSocket = null;
public function new(url:String, immediateOpen=true) { public var state(get, null): State;
function get_state() {
if (_ws == null) return Closed;
switch (_ws.readyState) {
case js.html.WebSocket.CONNECTING:
return Handshake;
case js.html.WebSocket.OPEN:
return Body;
}
return Closed;
}
public var protocol(get, null): String;
function get_protocol() {
if (_ws == null)
return null;
return _ws.protocol;
}
public function new(url:String, immediateOpen=true, protocols:Array<String> = null) {
_url = url; _url = url;
_protocols = protocols;
if (immediateOpen) { if (immediateOpen) {
open(); open();
} }
} }
private function createSocket() { private function createSocket() {
if (_protocols == null) {
return new js.html.WebSocket(_url); return new js.html.WebSocket(_url);
} }
return new js.html.WebSocket(_url, _protocols);
}
public function open() { public function open() {
if (_ws != null) { if (_ws != null) {
throw "Socket already connected"; throw "Socket already connected";
@ -292,6 +317,10 @@ class WebSocket extends WebSocketCommon {
public var _host:String; public var _host:String;
public var _port:Int = 0; public var _port:Int = 0;
public var _path:String; public var _path:String;
public var _search:String;
private var _protocols:Array<String>;
public var protocol(default, null):String = null;
private var _processThread:Thread; private var _processThread:Thread;
private var _encodedKey:String = "wskey"; private var _encodedKey:String = "wskey";
@ -300,8 +329,9 @@ class WebSocket extends WebSocketCommon {
public var additionalHeaders(get, null):Map<String, String>; public var additionalHeaders(get, null):Map<String, String>;
public function new(url:String, immediateOpen=true) { public function new(url:String, immediateOpen=true, protocols:Array<String> = null) {
parseUrl(url); parseUrl(url);
_protocols = protocols;
super(createSocket()); super(createSocket());
@ -310,40 +340,42 @@ class WebSocket extends WebSocketCommon {
} }
} }
inline private function parseUrl(url) inline private function parseUrl(url:String)
{ {
/** TO DO - FIND OUT WHAT IS BREAKING REGEX IN THE NEW PCRE2 FOR HL C var sep = url.indexOf("://");
if (sep < 0) {
var urlRegExp = ~/^(\w+?):\/\/([\w\.-]+)(:(\d+))?(\/.*)?$/;
if ( ! urlRegExp.match(url)) {
throw 'Uri not matching websocket URL "${url}"'; throw 'Uri not matching websocket URL "${url}"';
} }
_protocol = url.substr(0, sep);
var rest = url.substr(sep + 3);
_protocol = urlRegExp.matched(1); var pathStart = rest.length;
var slashIdx = rest.indexOf("/");
var qIdx = rest.indexOf("?");
if (slashIdx >= 0 && slashIdx < pathStart) pathStart = slashIdx;
if (qIdx >= 0 && qIdx < pathStart) pathStart = qIdx;
_host = urlRegExp.matched(2); var hostPort = rest.substr(0, pathStart);
var pathAndSearch = rest.substr(pathStart);
var parsedPort = Std.parseInt(urlRegExp.matched(4)); var colonIdx = hostPort.indexOf(":");
if (colonIdx >= 0) {
_host = hostPort.substr(0, colonIdx);
var parsedPort = Std.parseInt(hostPort.substr(colonIdx + 1));
if (parsedPort > 0) { if (parsedPort > 0) {
_port = parsedPort; _port = parsedPort;
} }
_path = urlRegExp.matched(5); } else {
if (_path == null || _path.length == 0) { _host = hostPort;
_path = "/";
} }
**/
var urlArr = url.split(":"); _path = pathAndSearch;
if ( urlArr.length < 3) { _search = null;
throw 'Uri not matching websocket URL "${url}"'; var searchIdx = pathAndSearch.indexOf("?");
if (searchIdx >= 0) {
_path = pathAndSearch.substr(0, searchIdx);
_search = pathAndSearch.substr(searchIdx);
} }
_protocol = urlArr[0];
_host = urlArr[1].substr(2, urlArr[1].length);
var parsedPort = Std.parseInt(urlArr[2].split("/")[0]);
if (parsedPort > 0 ) {
_port = parsedPort;
}
_path = urlArr[2].substr(urlArr[2].split("/")[0].length, urlArr[2].length);
if (_path == null || _path.length == 0) { if (_path == null || _path.length == 0) {
_path = "/"; _path = "/";
} }
@ -388,9 +420,7 @@ class WebSocket extends WebSocketCommon {
#else #else
haxe.MainLoop.addThread(function() { haxe.MainLoop.addThread(function() {
Log.debug("Thread started", this.id);
processLoop(this); processLoop(this);
Log.debug("Thread ended", this.id);
}); });
#end #end
@ -399,16 +429,13 @@ class WebSocket extends WebSocketCommon {
} }
private function processThread() { private function processThread() {
Log.debug("Thread started", this.id);
var ws:WebSocket = Thread.readMessage(true); var ws:WebSocket = Thread.readMessage(true);
processLoop(this); processLoop(this);
Log.debug("Thread ended", this.id);
} }
private function processLoop(ws:WebSocket) { private function processLoop(ws:WebSocket) {
while (ws.state != State.Closed) { // TODO: should think about mutex while (ws.state != State.Closed) { // TODO: should think about mutex
ws.process(); ws.process();
Sys.sleep(.01);
} }
} }
@ -422,8 +449,7 @@ class WebSocket extends WebSocketCommon {
public function sendHandshake() { public function sendHandshake() {
var httpRequest = new HttpRequest(); var httpRequest = new HttpRequest();
httpRequest.method = "GET"; httpRequest.method = "GET";
// TODO: should propably be hostname+port+path? httpRequest.uri = (_search != null) ? _path + _search : _path;
httpRequest.uri = _path;
httpRequest.httpVersion = "HTTP/1.1"; httpRequest.httpVersion = "HTTP/1.1";
httpRequest.headers.set(HttpHeader.HOST, _host + ":" + _port); httpRequest.headers.set(HttpHeader.HOST, _host + ":" + _port);
@ -435,6 +461,9 @@ class WebSocket extends WebSocketCommon {
httpRequest.headers.set(HttpHeader.CACHE_CONTROL, "no-cache"); httpRequest.headers.set(HttpHeader.CACHE_CONTROL, "no-cache");
httpRequest.headers.set(HttpHeader.ORIGIN, _socket.host().host.toString() + ":" + _socket.host().port); httpRequest.headers.set(HttpHeader.ORIGIN, _socket.host().host.toString() + ":" + _socket.host().port);
if (_protocols != null) {
httpRequest.headers.set(HttpHeader.SEC_WEBSOCKET_PROTOCOL, _protocols.join(', '));
}
_encodedKey = generateWSKey(); _encodedKey = generateWSKey();
httpRequest.headers.set(HttpHeader.SEC_WEBSOCKET_KEY, _encodedKey); httpRequest.headers.set(HttpHeader.SEC_WEBSOCKET_KEY, _encodedKey);
@ -487,16 +516,18 @@ class WebSocket extends WebSocketCommon {
} }
} }
var protocol = httpResponse.headers.get(HttpHeader.SEC_WEBSOCKET_PROTOCOL);
if (protocol != null) {
this.protocol = protocol;
}
_onopenCalled = false; _onopenCalled = false;
state = State.Head; state = State.Head;
} }
private function generateWSKey():String { private function generateWSKey():String {
var b = Bytes.alloc(16); return haxe.crypto.Base64.encode(
for (i in 0...16) { leenkx.network.torrent.Crypto.randomBytes(16));
b.set(i, Std.random(255));
}
return Base64.encode(b);
} }
} }

View File

@ -38,7 +38,6 @@ class WebSocketCommon {
public function send(data:Any) { public function send(data:Any) {
if (Std.isOfType(data, String)) { if (Std.isOfType(data, String)) {
Log.data(data, id);
sendFrame(Utf8Encoder.encode(data), OpCode.Text); sendFrame(Utf8Encoder.encode(data), OpCode.Text);
} else if (Std.isOfType(data, Bytes)) { } else if (Std.isOfType(data, Bytes)) {
sendFrame(data, OpCode.Binary); sendFrame(data, OpCode.Binary);
@ -118,7 +117,6 @@ class WebSocketCommon {
} }
} else { } else {
var stringPayload = Utf8Encoder.decode(unmaskedMessageData); var stringPayload = Utf8Encoder.decode(unmaskedMessageData);
Log.data(stringPayload, id);
if (this.onmessage != null) { if (this.onmessage != null) {
this.onmessage(StrMessage(stringPayload)); this.onmessage(StrMessage(stringPayload));
} }
@ -138,14 +136,12 @@ class WebSocketCommon {
case State.Closed: case State.Closed:
close(); close();
case _: case _:
trace('State not impl: ${state}');
} }
} }
public function close() { public function close() {
if (state != State.Closed) { if (state != State.Closed) {
try { try {
Log.debug("Closed", id);
sendFrame(Bytes.alloc(0), OpCode.Close); sendFrame(Bytes.alloc(0), OpCode.Close);
state = State.Closed; state = State.Closed;
_socket.close(); _socket.close();
@ -162,7 +158,6 @@ class WebSocketCommon {
_socket.output.write(data); _socket.output.write(data);
_socket.output.flush(); _socket.output.flush();
} catch (e:Dynamic) { } catch (e:Dynamic) {
Log.debug(Std.string(e), id);
if (onerror != null) { if (onerror != null) {
onerror(Std.string(e)); onerror(Std.string(e));
} }
@ -195,17 +190,24 @@ class WebSocketCommon {
} }
private static function generateMask() { private static function generateMask() {
var maskData = Bytes.alloc(4); return leenkx.network.torrent.Crypto.randomBytes(4);
maskData.set(0, Std.random(256));
maskData.set(1, Std.random(256));
maskData.set(2, Std.random(256));
maskData.set(3, Std.random(256));
return maskData;
} }
private static function applyMask(payload:Bytes, mask:Bytes) { private static function applyMask(payload:Bytes, mask:Bytes) {
var maskedPayload = Bytes.alloc(payload.length); var maskedPayload = Bytes.alloc(payload.length);
for (n in 0 ... payload.length) maskedPayload.set(n, payload.get(n) ^ mask.get(n % mask.length)); var m0 = mask.get(0), m1 = mask.get(1), m2 = mask.get(2), m3 = mask.get(3);
var i = 0;
while (i + 3 < payload.length) {
maskedPayload.set(i, payload.get(i) ^ m0);
maskedPayload.set(i + 1, payload.get(i + 1) ^ m1);
maskedPayload.set(i + 2, payload.get(i + 2) ^ m2);
maskedPayload.set(i + 3, payload.get(i + 3) ^ m3);
i += 4;
}
while (i < payload.length) {
maskedPayload.set(i, payload.get(i) ^ mask.get(i & 3));
i++;
}
return maskedPayload; return maskedPayload;
} }
@ -230,7 +232,6 @@ class WebSocketCommon {
try { try {
result = SocketImpl.select([_socket], null, null, 0.01); result = SocketImpl.select([_socket], null, null, 0.01);
} catch (e:Dynamic) { } catch (e:Dynamic) {
Log.debug("Error selecting socket: " + e);
needClose = true; needClose = true;
} }
@ -238,12 +239,11 @@ class WebSocketCommon {
if (result.read.length > 0) { if (result.read.length > 0) {
try { try {
while (true) { while (true) {
var data = Bytes.alloc(1024); var data = Bytes.alloc(65536);
var read = _socket.input.readBytes(data, 0, data.length); var read = _socket.input.readBytes(data, 0, data.length);
if (read <= 0){ if (read <= 0){
break; break;
} }
Log.debug("Bytes read: " + read, id);
_buffer.writeBytes(data.sub(0, read)); _buffer.writeBytes(data.sub(0, read));
} }
} catch (e:Dynamic) { } catch (e:Dynamic) {
@ -259,7 +259,7 @@ class WebSocketCommon {
} }
#else #else
needClose = !(e == 'Blocking' || (Std.isOfType(e, Error) && (e:Error).match(Error.Blocked))); needClose = !(e == 'Blocking' || (Std.isOfType(e, Error) && (e:Error).match(Error.Blocked)) || (state == Handshake && e == haxe.io.Eof));
#end #end
} }
@ -273,7 +273,6 @@ class WebSocketCommon {
if (needClose == true) { // dont want to send the Close frame here if (needClose == true) { // dont want to send the Close frame here
if (state != State.Closed) { if (state != State.Closed) {
try { try {
Log.debug("Closed", id);
state = State.Closed; state = State.Closed;
_socket.close(); _socket.close();
} catch (e:Dynamic) { } } catch (e:Dynamic) { }
@ -288,8 +287,6 @@ class WebSocketCommon {
public function sendHttpRequest(httpRequest:HttpRequest) { public function sendHttpRequest(httpRequest:HttpRequest) {
var data = httpRequest.build(); var data = httpRequest.build();
Log.data(data, id);
try { try {
_socket.output.write(Bytes.ofString(data)); _socket.output.write(Bytes.ofString(data));
_socket.output.flush(); _socket.output.flush();
@ -304,8 +301,6 @@ class WebSocketCommon {
public function sendHttpResponse(httpResponse:HttpResponse) { public function sendHttpResponse(httpResponse:HttpResponse) {
var data = httpResponse.build(); var data = httpResponse.build();
Log.data(data, id);
_socket.output.write(Bytes.ofString(data)); _socket.output.write(Bytes.ofString(data));
_socket.output.flush(); _socket.output.flush();
} }
@ -325,8 +320,6 @@ class WebSocketCommon {
httpRequest.addLine(line); httpRequest.addLine(line);
} }
Log.data(httpRequest.toString(), id);
return httpRequest; return httpRequest;
} }
@ -346,8 +339,6 @@ class WebSocketCommon {
} }
Log.data(httpResponse.toString(), id);
return httpResponse; return httpResponse;
} }

View File

@ -13,7 +13,6 @@ class WebSocketHandler extends Handler {
_creationTime = Sys.time(); _creationTime = Sys.time();
#end #end
_socket.setBlocking(false); _socket.setBlocking(false);
Log.debug('New socket handler', id);
} }
public override function handle() { public override function handle() {
@ -23,7 +22,6 @@ class WebSocketHandler extends Handler {
var currentTime = Sys.time(); var currentTime = Sys.time();
#end #end
if (this.state == State.Handshake && currentTime - _creationTime > (MAX_WAIT_TIME / 1000)) { if (this.state == State.Handshake && currentTime - _creationTime > (MAX_WAIT_TIME / 1000)) {
Log.info('No handshake detected in ${MAX_WAIT_TIME}ms, closing connection', id);
this.close(); this.close();
return; return;
} }
@ -70,10 +68,8 @@ class WebSocketHandler extends Handler {
httpResponse.headers.set(HttpHeader.CONNECTION, "close"); httpResponse.headers.set(HttpHeader.CONNECTION, "close");
httpResponse.headers.set(HttpHeader.X_WEBSOCKET_REJECT_REASON, 'Unsupported connection header: ${httpRequest.headers.get(HttpHeader.CONNECTION)}.'); httpResponse.headers.set(HttpHeader.X_WEBSOCKET_REJECT_REASON, 'Unsupported connection header: ${httpRequest.headers.get(HttpHeader.CONNECTION)}.');
} else { } else {
Log.debug('Handshaking', id);
var key = httpRequest.headers.get(HttpHeader.SEC_WEBSOCKET_KEY); var key = httpRequest.headers.get(HttpHeader.SEC_WEBSOCKET_KEY);
var result = makeWSKeyResponse(key); var result = makeWSKeyResponse(key);
Log.debug('Handshaking key - ${result}', id);
httpResponse.code = 101; httpResponse.code = 101;
httpResponse.text = "Switching Protocols"; httpResponse.text = "Switching Protocols";
@ -82,14 +78,21 @@ class WebSocketHandler extends Handler {
httpResponse.headers.set(HttpHeader.SEC_WEBSOSCKET_ACCEPT, result); httpResponse.headers.set(HttpHeader.SEC_WEBSOSCKET_ACCEPT, result);
} }
function callback(httpResponse: HttpResponse) {
sendHttpResponse(httpResponse); sendHttpResponse(httpResponse);
if (httpResponse.code == 101) { if (httpResponse.code == 101) {
_onopenCalled = false; _onopenCalled = false;
state = State.Head; state = State.Head;
Log.debug('Connected', id);
} else { } else {
close(); close();
} }
} }
if (validateHandshake != null) {
validateHandshake(httpRequest, httpResponse, callback);
} else {
callback(httpResponse);
}
}
} }

View File

@ -60,7 +60,6 @@ class WebSocketServer
_serverSocket.bind(new sys.net.Host(_host), _port); _serverSocket.bind(new sys.net.Host(_host), _port);
#end #end
_serverSocket.listen(_maxConnections); _serverSocket.listen(_maxConnections);
Log.info('Starting server - ${_host}:${_port} (maxConnections: ${_maxConnections})');
#if kha_krom #if kha_krom
kha.Scheduler.addTimeTask(function() { kha.Scheduler.addTimeTask(function() {
@ -114,7 +113,6 @@ class WebSocketServer
var handler = new T(socket); var handler = new T(socket);
handlers.push(handler); handlers.push(handler);
Log.debug("Adding to web server handler to list - total: " + handlers.length, handler.id);
if (onClientAdded != null) { if (onClientAdded != null) {
onClientAdded(handler); onClientAdded(handler);
} }
@ -174,7 +172,6 @@ class WebSocketServer
for (h in toRemove) { for (h in toRemove) {
handlers.remove(h); handlers.remove(h);
Log.debug("Removing web server handler from list - total: " + handlers.length, h.id);
if (onClientRemoved != null) { if (onClientRemoved != null) {
onClientRemoved(h); onClientRemoved(h);
} }
@ -186,7 +183,6 @@ class WebSocketServer
while (_handlersClosed.length > 0) { while (_handlersClosed.length > 0) {
var h = _handlersClosed.shift(); var h = _handlersClosed.shift();
handlers.remove(h); handlers.remove(h);
Log.debug("Removing web server handler from list - total: " + handlers.length, h.id);
if (onClientRemoved != null) { if (onClientRemoved != null) {
onClientRemoved(h); onClientRemoved(h);
} }

View File

@ -52,8 +52,15 @@ class KromSecureSocket extends KromSocket {
} }
override public function connect(host:KromHost, port:Int):Void { override public function connect(host:KromHost, port:Int):Void {
setBlocking(true);
try {
super.connect(host, port); super.connect(host, port);
enableSSL(); enableSSL();
setBlocking(false);
} catch (e:Dynamic) {
setBlocking(false);
trace('Failed Krom Host');
}
} }
public function isSSLEnabled():Bool { public function isSSLEnabled():Bool {

View File

@ -43,6 +43,9 @@ class KromSocket {
} }
private function setSocketId(id:Int):Void { private function setSocketId(id:Int):Void {
if (_socketId >= 0) {
krom_socket_close(_socketId);
}
_socketId = id; _socketId = id;
if (_socketId >= 0) { if (_socketId >= 0) {
input = new KromSocketInput(this); input = new KromSocketInput(this);
@ -247,3 +250,29 @@ class KromHost {
return host; return host;
} }
} }
// TODO: Replace all ticks timers and intervals for events
class KromPump {
static var ticks:Array<Void -> Void> = [];
static var timer:haxe.Timer = null;
public static function add(tick:Void -> Void):Void {
if (!ticks.contains(tick)) ticks.push(tick);
if (timer == null) {
timer = new haxe.Timer(10);
timer.run = runAll;
}
}
public static function remove(tick:Void -> Void):Void {
ticks.remove(tick);
if (ticks.length == 0 && timer != null) {
timer.stop();
timer = null;
}
}
static function runAll():Void {
var snapshot = ticks.copy();
for (tick in snapshot) {
if (ticks.contains(tick)) tick();
}
}
}

View File

@ -0,0 +1,70 @@
package leenkx.network.krom;
import haxe.io.Bytes;
@:native("krom_udp_create") extern function krom_udp_create():Int;
@:native("krom_udp_bind") extern function krom_udp_bind(id:Int,
addr:String, port:Int):Bool;
@:native("krom_udp_sendto") extern function krom_udp_sendto(id:Int,
host:String, port:Int, data:js.lib.ArrayBuffer):Int;
@:native("krom_udp_recvfrom") extern function krom_udp_recvfrom(id:Int,
maxLen:Int):Dynamic;
@:native("krom_socket_close") extern function krom_udp_close(id:Int):Void;
@:native("krom_socket_set_blocking")
extern function krom_udp_set_blocking(id:Int, blocking:Bool):Void;
class KromUdpSocket {
private var _socketId:Int = -1;
public function new() {
_socketId = krom_udp_create();
setBlocking(false);
}
public function bind(host:KromSocket.KromHost, port:Int):Void {
if (_socketId >= 0) {
if (!krom_udp_bind(_socketId, host.host, port)) {
trace("UDP bind failed to " + host.host + ":" + port);
}
}
}
public function sendTo(data:Bytes, pos:Int, len:Int,
host:KromSocket.KromHost, port:Int):Int {
if (_socketId < 0) return 0;
var arrayBuffer:js.lib.ArrayBuffer = data.getData();
var exact:js.lib.ArrayBuffer =
untyped arrayBuffer.slice(pos, pos + len);
return krom_udp_sendto(_socketId, host.host, port, exact);
}
public function recvFrom(maxLength:Int):{data:Bytes, host:String,
port:Int} {
if (_socketId < 0) return null;
var result:Dynamic = krom_udp_recvfrom(_socketId, maxLength);
if (result == -1) {
_socketId = -1;
trace("Closed");
return null;
}
if (result == null || Std.isOfType(result, Int)) return null;
return {
data: Bytes.ofData(result.data),
host: result.host,
port: result.port
};
}
public function setBlocking(blocking:Bool):Void {
if (_socketId >= 0) {
krom_udp_set_blocking(_socketId, blocking);
}
}
public function close():Void {
if (_socketId >= 0) {
krom_udp_close(_socketId);
_socketId = -1;
}
}
}

View File

@ -0,0 +1,209 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class Aegis128x4 {
static inline var D = 4;
static inline var RATE = 128;
static var C0 = Bytes.ofHex("000101020305080d1522375990e97962");
static var C1 = Bytes.ofHex("db3d18556dc22ff12011314273b528dd");
var v:Bytes;
var nv:Bytes;
var t:Bytes;
var t2:Bytes;
public function new(key:Bytes, nonce:Bytes) {
v = Bytes.alloc(512);
nv = Bytes.alloc(512);
t = Bytes.alloc(16);
t2 = Bytes.alloc(16);
init(key, nonce);
}
static inline function off(j:Int, i:Int):Int {
return (j * D + i) * 16;
}
static inline function xtime(x:Int):Int {
return ((x << 1) ^ ((x >> 7) * 0x1b)) & 0xff;
}
static function xorInto(a:Bytes, ao:Int, b:Bytes, bo:Int, out:Bytes, oo:Int):Void {
for (k in 0...16) out.set(oo + k, a.get(ao + k) ^ b.get(bo + k));
}
function aesRound(inOff:Int, rk:Bytes, rkOff:Int, out:Bytes, outOff:Int):Void {
for (c in 0...4) for (r in 0...4)
t2.set(4 * c + r, SBOX[v.get(inOff + 4 * ((c + r) & 3) + r)]);
for (c in 0...4) {
var b0 = t2.get(4 * c);
var b1 = t2.get(4 * c + 1);
var b2 = t2.get(4 * c + 2);
var b3 = t2.get(4 * c + 3);
out.set(outOff + 4 * c, (xtime(b0) ^ xtime(b1) ^ b1 ^ b2 ^ b3) ^ rk.get(rkOff + 4 * c));
out.set(outOff + 4 * c + 1, (b0 ^ xtime(b1) ^ xtime(b2) ^ b2 ^ b3) ^ rk.get(rkOff + 4 * c + 1));
out.set(outOff + 4 * c + 2, (b0 ^ b1 ^ xtime(b2) ^ xtime(b3) ^ b3) ^ rk.get(rkOff + 4 * c + 2));
out.set(outOff + 4 * c + 3, (xtime(b0) ^ b0 ^ b1 ^ b2 ^ xtime(b3)) ^ rk.get(rkOff + 4 * c + 3));
}
}
function update(m0:Bytes, m0off:Int, m1:Bytes, m1off:Int):Void {
for (i in 0...D) {
xorInto(v, off(0, i), m0, m0off + i * 16, t, 0);
aesRound(off(7, i), t, 0, nv, off(0, i));
aesRound(off(0, i), v, off(1, i), nv, off(1, i));
aesRound(off(1, i), v, off(2, i), nv, off(2, i));
aesRound(off(2, i), v, off(3, i), nv, off(3, i));
xorInto(v, off(4, i), m1, m1off + i * 16, t, 0);
aesRound(off(3, i), t, 0, nv, off(4, i));
aesRound(off(4, i), v, off(5, i), nv, off(5, i));
aesRound(off(5, i), v, off(6, i), nv, off(6, i));
aesRound(off(6, i), v, off(7, i), nv, off(7, i));
}
v.blit(0, nv, 0, 512);
}
function init(key:Bytes, nonce:Bytes):Void {
var kn = Bytes.alloc(16);
var kc0 = Bytes.alloc(16);
var kc1 = Bytes.alloc(16);
for (k in 0...16) {
kn.set(k, key.get(k) ^ nonce.get(k));
kc0.set(k, key.get(k) ^ C0.get(k));
kc1.set(k, key.get(k) ^ C1.get(k));
}
for (i in 0...D) {
v.blit(off(0, i), kn, 0, 16);
v.blit(off(1, i), C1, 0, 16);
v.blit(off(2, i), C0, 0, 16);
v.blit(off(3, i), C1, 0, 16);
v.blit(off(4, i), kn, 0, 16);
v.blit(off(5, i), kc0, 0, 16);
v.blit(off(6, i), kc1, 0, 16);
v.blit(off(7, i), kc0, 0, 16);
}
var m0 = Bytes.alloc(64);
var m1 = Bytes.alloc(64);
for (i in 0...D) {
m0.blit(i * 16, nonce, 0, 16);
m1.blit(i * 16, key, 0, 16);
}
for (r in 0...10) {
for (i in 0...D) {
v.set(off(3, i), v.get(off(3, i)) ^ i);
v.set(off(3, i) + 1, v.get(off(3, i) + 1) ^ (D - 1));
v.set(off(7, i), v.get(off(7, i)) ^ i);
v.set(off(7, i) + 1, v.get(off(7, i) + 1) ^ (D - 1));
}
update(m0, 0, m1, 0);
}
}
public function absorb(ai:Bytes, aiOff:Int):Void {
update(ai, aiOff, ai, aiOff + 64);
}
public function enc(xi:Bytes, xiOff:Int):Bytes {
var out = Bytes.alloc(RATE);
for (i in 0...D) {
for (k in 0...16) {
var z0 = v.get(off(1, i) + k) ^ v.get(off(6, i) + k)
^ (v.get(off(2, i) + k) & v.get(off(3, i) + k));
var z1 = v.get(off(2, i) + k) ^ v.get(off(5, i) + k)
^ (v.get(off(6, i) + k) & v.get(off(7, i) + k));
out.set(i * 16 + k, xi.get(xiOff + i * 16 + k) ^ z0);
out.set(64 + i * 16 + k, xi.get(xiOff + 64 + i * 16 + k) ^ z1);
}
}
update(xi, xiOff, xi, xiOff + 64);
return out;
}
public function nextBlock():Bytes {
var out = Bytes.alloc(RATE);
for (i in 0...D) {
for (k in 0...16) {
out.set(i * 16 + k, v.get(off(1, i) + k) ^ v.get(off(6, i) + k)
^ (v.get(off(2, i) + k) & v.get(off(3, i) + k)));
out.set(64 + i * 16 + k, v.get(off(2, i) + k) ^ v.get(off(5, i) + k)
^ (v.get(off(6, i) + k) & v.get(off(7, i) + k)));
}
}
for (i in 0...D) {
aesRound(off(7, i), v, off(0, i), nv, off(0, i));
aesRound(off(0, i), v, off(1, i), nv, off(1, i));
aesRound(off(1, i), v, off(2, i), nv, off(2, i));
aesRound(off(2, i), v, off(3, i), nv, off(3, i));
aesRound(off(3, i), v, off(4, i), nv, off(4, i));
aesRound(off(4, i), v, off(5, i), nv, off(5, i));
aesRound(off(5, i), v, off(6, i), nv, off(6, i));
aesRound(off(6, i), v, off(7, i), nv, off(7, i));
}
v.blit(0, nv, 0, 512);
return out;
}
public static function selfTest():Bool {
var key = Bytes.ofHex("000102030405060708090a0b0c0d0e0f");
var nonce = Bytes.ofHex("101112131415161718191a1b1c1d1e1f");
var a = new Aegis128x4(key, nonce);
var expected = Bytes.ofHex(
"924eb07635003a37e6c6575ba8ce1929c8b6a5d91475445e936d48e794be0ce2" +
"fcd37d050e24084befe3bbb219d647602e9f58cfb893a8800220242c373a8b18" +
"1a1f60c4fab64e5471dc72edfcf6fe6bc1e525ebea2d6375a9edd045dce96381" +
"97a3e25abd228a44d4a14a6d3fe9185cc2d4cf7f4287a98744645674265d4ca8" +
"7bb50c534f6ec4780530ff1cce8a16e87b08d57557da0b5ef7b5f7d98b0ba189" +
"6bfcac34ddb68404821a4d665303cb0fd95626f6dfad1aed7467622c38529932" +
"af339fd2d50ee45fc47665c647cf6586d0669b39d140f0e118a4a511efe2f95a" +
"7a94330f35c194fadda2a87e42cdeccc233b640d1f4d56e2757e72c1a9d8ecb1" +
"9f93737d699ba05c11e94f2b201bef5e61caf387cf7cfd3f8300ac7680ccfd76" +
"5825a671ecef03b7a9c98a601ae3211587a1fe4d558161a8f4c38731f3223032" +
"7a5aca78d636c05bbc702b2980196ab6915d868408495d07eb527789f282c575" +
"d0947bfbc1d3309cdffc9be1503aea628834ea57a15b9fbdc0245464a4b8cbef" +
"e46f4cf71a95ac45b6f0823e3aba1a868c4ecef682fc44a8eba911b3fc7d99f9" +
"a4fb61e2c928a2ca760b8772f2ea5f2e3d34ea89da73caa3016c280500a155a3" +
"85075f0080e9d618e7eb40f57c32d9f7d2ab2b320c6e93b155a3787cb83e5281" +
"0b3af0250ae36831a1b072e499929bcb5cce4d00329d69f1aae36aa541347512");
if (expected.compare(a.v) != 0) {
return false;
}
var ad = Bytes.alloc(128);
for (k in 0...8) ad.set(k, [1, 2, 3, 4][k & 3]);
a.absorb(ad, 0);
var msg = Bytes.alloc(128);
for (k in 0...120) msg.set(k, [4, 5, 6, 7][k & 3]);
var ct = a.enc(msg, 0);
var expectedCt = Bytes.ofHex(
"e836118562f4479c9d35c17356a833114c21f9aa39e4dda5e5c87f4152a00fce" +
"9a7c38f832eafe8b1c12f8a7cf12a81a1ad8a9c24ba9dedfbdaa586ffea67ddc" +
"801ea97d9ab4a872f42d0e352e2713dacd609f9442c17517c5a29daf3e2a3fac" +
"4ff6b1380c4e46df7b086af6ce6bc1ed594b8dd64aed2a7e");
for (k in 0...120) {
if (ct.get(k) != expectedCt.get(k)) {
return false;
}
}
return true;
}
static var SBOX = [
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
];
}

View File

@ -0,0 +1,119 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class Base58 {
static var ALPHABET:String = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
static var ALPHABET_MAP:Map<Int, Int> = {
var m = new Map<Int, Int>();
for (i in 0...ALPHABET.length) {
m.set(ALPHABET.charCodeAt(i), i);
}
m;
};
public static function encode(bytes:Bytes):String {
if (bytes.length == 0) return "";
var zeros = 0;
while (zeros < bytes.length && bytes.get(zeros) == 0) zeros++;
var size = Math.ceil(bytes.length * 138 / 100 + 1);
var b58 = [for (i in 0...size) 0];
for (i in zeros...bytes.length) {
var carry = bytes.get(i);
for (j in 0...size) {
var idx = size - 1 - j;
carry += b58[idx] << 8;
b58[idx] = carry % 58;
carry = Std.int(carry / 58);
}
}
var leadingZeros = 0;
for (i in 0...size) {
if (b58[i] != 0) break;
leadingZeros++;
}
var sb = new StringBuf();
for (i in 0...zeros) sb.addChar(ALPHABET.charCodeAt(0));
for (i in leadingZeros...size) {
sb.addChar(ALPHABET.charCodeAt(b58[i]));
}
return sb.toString();
}
public static function decode(str:String):Bytes {
if (str.length == 0) return Bytes.alloc(0);
var zeros = 0;
while (zeros < str.length && str.charCodeAt(zeros) == ALPHABET.charCodeAt(0)) zeros++;
var size = Math.ceil(str.length * 733 / 1000 + 1);
var bytes = [for (i in 0...size) 0];
for (i in zeros...str.length) {
var ch = str.charCodeAt(i);
if (!ALPHABET_MAP.exists(ch)) {
throw "Base58: invalid character " + str.charAt(i);
}
var carry = ALPHABET_MAP.get(ch);
for (j in 0...size) {
var idx = size - 1 - j;
carry += bytes[idx] * 58;
bytes[idx] = carry & 0xff;
carry = carry >> 8;
}
}
var leadingZeros = 0;
for (i in 0...size) {
if (bytes[i] != 0) break;
leadingZeros++;
}
var result = Bytes.alloc(zeros + size - leadingZeros);
for (i in 0...zeros) result.set(i, 0);
for (i in 0...(size - leadingZeros)) {
result.set(zeros + i, bytes[leadingZeros + i]);
}
return result;
}
}
class Base58Check {
static var CHECKSUM_LEN = 4;
public static function encode(bytes:Bytes):String {
var hash = sha256sha256(bytes);
var payload = Bytes.alloc(bytes.length + CHECKSUM_LEN);
payload.blit(0, bytes, 0, bytes.length);
payload.blit(bytes.length, hash, 0, CHECKSUM_LEN);
return Base58.encode(payload);
}
public static function decode(str:String):Bytes {
var payload = Base58.decode(str);
if (payload.length < CHECKSUM_LEN) {
throw "Base58Check: too short";
}
var data = payload.sub(0, payload.length - CHECKSUM_LEN);
var checksum = payload.sub(payload.length - CHECKSUM_LEN, CHECKSUM_LEN);
var hash = sha256sha256(data);
for (i in 0...CHECKSUM_LEN) {
if (hash.get(i) != checksum.get(i)) {
throw "Base58Check: checksum mismatch";
}
}
return data;
}
static function sha256sha256(bytes:Bytes):Bytes {
var h1 = haxe.crypto.Sha256.make(bytes);
return haxe.crypto.Sha256.make(h1);
}
}

View File

@ -0,0 +1,182 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class RawDict {
public var pairs:Array<{key:Bytes, val:Dynamic}> = [];
public function new() {}
}
class Bencode {
public static function encode(value:Dynamic):Bytes {
var out = new haxe.io.BytesOutput();
encodeValue(out, value);
return out.getBytes();
}
public static function encodeToString(value:Dynamic):String {
return encode(value).toString();
}
static function encodeValue(out:haxe.io.BytesOutput, value:Dynamic):Void {
if (Std.isOfType(value, Bool)) {
encodeInt(out, value ? 1 : 0);
} else if (Std.isOfType(value, Int) || Std.isOfType(value, Float)) {
encodeInt(out, value);
} else if (Std.isOfType(value, String)) {
encodeString(out, value);
} else if (Std.isOfType(value, Bytes)) {
encodeBytes(out, value);
} else if (Std.isOfType(value, RawDict)) {
encodeRawDict(out, value);
} else if (Std.isOfType(value, Array)) {
encodeList(out, value);
} else if (value != null && Reflect.isObject(value)) {
encodeDict(out, value);
} else {
throw "Bencode: cannot encode " + Type.typeof(value);
}
}
static function encodeInt(out:haxe.io.BytesOutput, value:Float):Void {
out.writeString("i");
var s = Std.string(value);
var dot = s.indexOf(".");
if (dot != -1) s = s.substring(0, dot);
if (s.indexOf("e") != -1 || s.indexOf("E") != -1) {
s = Std.string(Math.floor(value));
dot = s.indexOf(".");
if (dot != -1) s = s.substring(0, dot);
}
out.writeString(s);
out.writeString("e");
}
static function encodeString(out:haxe.io.BytesOutput, value:String):Void {
var bytes = Bytes.ofString(value);
encodeBytes(out, bytes);
}
static function encodeBytes(out:haxe.io.BytesOutput, bytes:Bytes):Void {
out.writeString(Std.string(bytes.length));
out.writeString(":");
out.writeBytes(bytes, 0, bytes.length);
}
static function encodeRawDict(out:haxe.io.BytesOutput, dict:RawDict):Void {
out.writeString("d");
for (p in dict.pairs) {
encodeBytes(out, p.key);
encodeValue(out, p.val);
}
out.writeString("e");
}
static function encodeList(out:haxe.io.BytesOutput, arr:Array<Dynamic>):Void {
out.writeString("l");
for (item in arr) {
encodeValue(out, item);
}
out.writeString("e");
}
static function encodeDict(out:haxe.io.BytesOutput, obj:Dynamic):Void {
out.writeString("d");
var keys = Reflect.fields(obj);
keys.sort(Reflect.compare);
for (key in keys) {
if (Reflect.field(obj, key) == null) continue;
encodeString(out, key);
encodeValue(out, Reflect.field(obj, key));
}
out.writeString("e");
}
public static function decode(bytes:Bytes):Dynamic {
return new Decoder(bytes).value();
}
public static function decodeString(str:String):Dynamic {
return decode(Bytes.ofString(str));
}
}
private class Decoder {
static inline var MAX_DECODE_LEN = 10 * 1024 * 1024; // 10 MB
var data:Bytes;
var pos:Int = 0;
public function new(data:Bytes) {
this.data = data;
}
public function value():Dynamic {
if (pos >= data.length) throw "Bencode: truncated input";
var b = data.get(pos++);
switch (b) {
case 'i'.code:
return decodeInt();
case 'l'.code:
return decodeList();
case 'd'.code:
return decodeDict();
default:
return decodeBytes();
}
}
function decodeInt():Float {
var start = pos;
while (pos < data.length && data.get(pos) != 'e'.code) pos++;
if (pos >= data.length) throw "Bencode: unterminated int";
var s = data.getString(start, pos - start);
pos++;
var v = Std.parseFloat(s);
if (Math.isNaN(v)) throw "Bencode: bad int " + s;
return v;
}
function decodeBytes():Bytes {
var start = pos - 1;
if (data.get(start) < '0'.code || data.get(start) > '9'.code) {
throw "Bencode: bad byte length";
}
while (pos < data.length && data.get(pos) != ':'.code) pos++;
if (pos >= data.length) throw "Bencode: unterminated length";
var len = Std.parseInt(data.getString(start, pos - start));
pos++;
if (len == null || len < 0 || len > MAX_DECODE_LEN
|| pos + len > data.length) {
throw "Bencode: invalid byte length " + len;
}
var bytes = Bytes.alloc(len);
bytes.blit(0, data, pos, len);
pos += len;
return bytes;
}
function decodeList():Array<Dynamic> {
var arr:Array<Dynamic> = [];
while (true) {
if (pos >= data.length) throw "Bencode: unterminated list";
if (data.get(pos) == 'e'.code) break;
arr.push(value());
}
pos++;
return arr;
}
function decodeDict():Dynamic {
var obj:Dynamic = {};
while (true) {
if (pos >= data.length) throw "Bencode: unterminated dict";
if (data.get(pos) == 'e'.code) break;
var key:Bytes = value();
Reflect.setField(obj, key.toString(), value());
}
pos++;
return obj;
}
}

View File

@ -0,0 +1,152 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
import leenkx.network.torrent.Base58.Base58Check;
class Crypto {
public static var SEEDPREFIX(default, null) = "490a";
public static var ADDRESSPREFIX(default, null) = "55";
static var drbg:Aegis128x4 = null;
static var drbgBuf:Bytes = null;
static var drbgPos:Int = 0;
static var drbgNative:Int = -1;
public static function randomBytes(length:Int):Bytes {
var bytes = Bytes.alloc(length);
#if (js && !kha_krom)
var webCrypto:Dynamic = js.Syntax.code("(typeof globalThis !== 'undefined' ? globalThis.crypto : null)");
if (webCrypto != null && webCrypto.getRandomValues != null) {
var view = new js.lib.Uint8Array(length);
webCrypto.getRandomValues(view);
for (i in 0...length) bytes.set(i, view[i]);
return bytes;
}
#end
#if lnx_datachannel
if (drbgNative == -1) {
var seed = osEntropy(32);
try {
drbgNative = datachannel.RTC.aegisAvailable()
&& datachannel.RTC.aegisInit(seed.sub(0, 16), seed.sub(16, 16)) ? 1 : 0;
} catch (e:Dynamic) {
drbgNative = 0;
}
}
if (drbgNative == 1) {
return datachannel.RTC.aegisRandom(length);
}
#end
if (drbg == null) {
var seed = osEntropy(32);
drbg = new Aegis128x4(seed.sub(0, 16), seed.sub(16, 16));
drbgBuf = Bytes.alloc(0);
drbgPos = 0;
}
var off = 0;
while (off < length) {
if (drbgPos >= drbgBuf.length) {
drbgBuf = drbg.nextBlock();
drbgPos = 0;
}
var n = Std.int(Math.min(drbgBuf.length - drbgPos, length - off));
bytes.blit(off, drbgBuf, drbgPos, n);
drbgPos += n;
off += n;
}
return bytes;
}
static function osEntropy(length:Int):Bytes {
var bytes = Bytes.alloc(length);
#if kha_krom
var kromBytes = leenkx.network.torrent.transport.RtcNative.randomBytes(length);
if (kromBytes != null) return kromBytes;
#end
#if sys
if (Sys.systemName() != "Windows") {
try {
var f = sys.io.File.read("/dev/urandom", true);
f.readBytes(bytes, 0, length);
f.close();
return bytes;
} catch(e:Dynamic) {}
} else {
try {
var p = new sys.io.Process("powershell", [
"-NoProfile", "-Command",
"$r = New-Object 'byte[]' " + length + "; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($r); -join ($r | ForEach-Object { $_.ToString('x2') })"
]);
var hex = StringTools.trim(p.stdout.readAll().toString());
p.close();
if (hex.length == length * 2) {
return Bytes.ofHex(hex);
}
} catch(e:Dynamic) {
trace("Windows RNG via PowerShell failed: " + e);
}
}
#end
throw "Crypto: OS entropy unavailable. Refusing to generate predictable keys.";
}
public static function hash(data:Bytes):Bytes {
return TweetNaCl.hash(data);
}
public static function signDetached(message:Bytes, secretKey:Bytes):Bytes {
return TweetNaCl.signDetached(message, secretKey);
}
public static function signDetachedVerify(message:Bytes, signature:Bytes, publicKey:Bytes):Bool {
return TweetNaCl.signDetachedVerify(message, signature, publicKey);
}
public static function box(message:Bytes, nonce:Bytes, theirPublicKey:Bytes, mySecretKey:Bytes):Bytes {
return TweetNaCl.box(message, nonce, theirPublicKey, mySecretKey);
}
public static function boxOpen(message:Bytes, nonce:Bytes, theirPublicKey:Bytes, mySecretKey:Bytes):Bytes {
return TweetNaCl.boxOpen(message, nonce, theirPublicKey, mySecretKey);
}
public static function boxKeyPair():{publicKey:Bytes, secretKey:Bytes} {
return TweetNaCl.boxKeyPair();
}
public static function signKeyPairFromSeed(seed:Bytes):{publicKey:Bytes, secretKey:Bytes} {
return TweetNaCl.signKeyPairFromSeed(seed);
}
public static var boxNonceLength(get, null):Int;
static function get_boxNonceLength():Int {
return TweetNaCl.crypto_box_NONCEBYTES;
}
public static function ripemd160(data:Bytes):Bytes {
return Ripemd160.make(data);
}
public static function encodeSeed(material:Bytes):String {
var prefix = Bytes.ofHex(SEEDPREFIX);
var payload = Bytes.alloc(prefix.length + material.length);
payload.blit(0, prefix, 0, prefix.length);
payload.blit(prefix.length, material, 0, material.length);
return Base58Check.encode(payload);
}
public static function encodeAddress(publicKey:Bytes):String {
var prefix = Bytes.ofHex(ADDRESSPREFIX);
var hash = hash(publicKey);
var ripemd = ripemd160(hash);
var payload = Bytes.alloc(prefix.length + ripemd.length);
payload.blit(0, prefix, 0, prefix.length);
payload.blit(prefix.length, ripemd, 0, ripemd.length);
return Base58Check.encode(payload);
}
public static function toHex(bytes:Bytes):String {
return bytes.toHex();
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Riccardo Mazzucco - All Rights reserved
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,576 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
import leenkx.network.torrent.Types;
import leenkx.network.torrent.Base58.Base58Check;
import leenkx.network.torrent.transport.ITransport;
import leenkx.network.torrent.transport.ITransport.TransportOpts;
import leenkx.network.torrent.TorrentInfo;
class LeenkxClient {
public static var PEERTIMEOUT:Float = 5 * 60 * 1000;
public static var defaultHeartbeat:Int = 30000;
public var identifier:String;
public var seed:String;
public var pk:String;
public var ek:String;
public var timeout:Float;
public var announce:Array<String>;
public var infoHashBinary:String;
public var peerIdBinary:String;
public var keyPair:{publicKey:Bytes, secretKey:Bytes};
public var keyPairEncrypt:{publicKey:Bytes, secretKey:Bytes};
public var peers:Map<String, PeerInfo> = [];
public var seen:Map<String, Float> = [];
public var seenFired:Map<String, Bool> = [];
public var pkToAddress:Map<String, String> = [];
public var isRelay:Bool = false;
public var lastPeerCount:Null<Int> = null;
public var lastPeerId:String = null;
public var serverAddress:String = null;
public var heartbeatTimer:Null<haxe.Timer> = null;
public var onOpen:Void -> Void;
public var onTorrent:String -> Dynamic -> Void;
public var onMessage:String -> Dynamic -> Dynamic -> Void;
public var onSeen:String -> Void;
public var onLeft:String -> Void;
public var onServer:String -> Void;
public var onPing:String -> Void;
public var onTimeout:String -> Void;
public var onConnections:Int -> Void;
public var onWireSeen:Int -> Dynamic -> Void;
public var onWireLeft:Int -> Dynamic -> Void;
public var onTracker:String -> Dynamic -> Void;
public var onAnnounce:String -> Void;
public var onError:String -> Void;
public var transport:ITransport;
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;
identifier = null;
}
opts = opts != null ? opts : {};
this.announce = opts.announce != null ? opts.announce : TorrentClient.defaultTrackers;
this.timeout = opts.timeout != null ? opts.timeout : PEERTIMEOUT;
if (opts.seed != null) {
this.seed = opts.seed;
} else {
this.seed = Crypto.encodeSeed(Crypto.randomBytes(32));
}
if (opts.keyPair != null) {
this.keyPair = opts.keyPair;
} else {
var seedBytes = Base58Check.decode(this.seed);
var material = seedBytes.sub(2, seedBytes.length - 2);
this.keyPair = Crypto.signKeyPairFromSeed(material);
}
this.keyPairEncrypt = Crypto.boxKeyPair();
this.pk = Base58.encode(this.keyPair.publicKey);
this.ek = Base58.encode(this.keyPairEncrypt.publicKey);
this.identifier = identifier != null ? identifier : this.address();
var infoHash = TorrentInfo.identifierHash(this.identifier);
this.infoHashBinary = TorrentInfo.toBinaryString(infoHash);
this.peerIdBinary = TorrentInfo.toBinaryString(TorrentInfo.peerId());
createTransport(opts, opts.deferJoin == true);
this.heartbeatInterval = opts.heartbeat != null ? opts.heartbeat : defaultHeartbeat;
}
function createTransport(opts:LeenkxOptions, defer:Bool):Void {
var transportOpts:TransportOpts = {
announce: this.announce,
iceServers: opts.iceServers,
tracker: opts.tracker,
wtOpts: opts.wtOpts,
torrentOpts: opts.torrentOpts, torrent: opts.torrent,
pk: this.pk,
ek: this.ek,
infoHashBinary: this.infoHashBinary,
peerIdBinary: this.peerIdBinary
};
var announceUrl = this.announce[0];
if (opts.transport == "datachannel") {
transport = new leenkx.network.torrent.transport.DatachannelTransport(announceUrl, opts.iceServers);
} else {
transport = new leenkx.network.torrent.transport.WireTransport(announceUrl, opts.iceServers);
}
transport.onReady = onTransportReady;
transport.onPeerConnected = onTransportPeerConnected;
transport.onPeerDisconnected = onTransportPeerDisconnected;
transport.onPeerData = onTransportPeerData;
transport.onPeerHandshake = onTransportPeerHandshake;
transport.onTrackerUpdate = onTransportTrackerUpdate;
transport.onTrackerAnnounce = onTransportTrackerAnnounce;
transport.onError = onTransportError;
if (defer) {
deferredOpts = transportOpts;
} else {
transport.join(this.identifier, transportOpts);
}
}
public function connect():Void {
if (deferredOpts != null) {
var opts = deferredOpts;
deferredOpts = null;
transport.join(this.identifier, opts);
}
this.heartbeat(this.heartbeatInterval);
}
function onTransportReady():Void {
if (onTorrent != null) onTorrent(identifier, null);
}
function onTransportPeerConnected(peerId:String, count:Int):Void {
lastPeerId = peerId;
var prevCount = lastPeerCount;
lastPeerCount = count;
if (onWireSeen != null) onWireSeen(count, {peerId: peerId});
if (count != prevCount) {
if (onConnections != null) onConnections(count);
}
}
function onTransportPeerDisconnected(count:Int):Void {
if (onWireLeft != null) onWireLeft(count, {peerId: lastPeerId});
if (count != lastPeerCount) {
lastPeerCount = count;
if (onConnections != null) onConnections(count);
}
}
function onTransportPeerData(senderPk:String, data:Bytes):Void {
onMessageData(identifier, data, senderPk);
}
function onTransportPeerHandshake(peerPk:String, peerEk:String):Void {
if (peerPk != null && peerPk != "") {
sawPeer(peerPk, peerEk, identifier);
}
}
function onTransportTrackerUpdate(update:Dynamic):Void {
if (onTracker != null) onTracker(identifier, update);
}
function onTransportTrackerAnnounce():Void {
if (onAnnounce != null) onAnnounce(identifier);
}
function onTransportError(msg:String):Void {
trace("onTransportError: " + msg);
if (onError != null) onError(msg);
}
public function address(?pk:Dynamic):String {
var pkBytes:Bytes = null;
if (pk != null && Std.isOfType(pk, String)) {
pkBytes = Base58.decode(pk);
} else if (pk != null) {
pkBytes = pk;
} else {
pkBytes = this.keyPair.publicKey;
}
return Crypto.encodeAddress(pkBytes);
}
function addressFromPk(pkStr:String):String {
var cached = pkToAddress.get(pkStr);
if (cached != null) return cached;
var addr = address(Base58.decode(pkStr));
pkToAddress.set(pkStr, addr);
return addr;
}
public function encodeSeed(material:Bytes):String {
return Crypto.encodeSeed(material);
}
public function ping():Void {
var packet = makePacket({y: PacketType.Ping});
sendRaw(packet);
}
public function send(?address:String, message:Dynamic):Void {
if (message == null) {
message = address;
address = null;
}
var packet = makePacket({
y: PacketType.Message,
v: haxe.Json.stringify(message)
});
var peerPk:String = null;
if (address != null) {
if (peers.exists(address)) {
peerPk = peers[address].pk;
packet = encryptPacket(peerPk, packet);
} else {
throw address + " not seen - no public key.";
}
}
sendRaw(packet, peerPk);
}
public function sendRawMessage(data:Bytes, ?address:String):Void {
var marked = Bytes.alloc(data.length + 1);
marked.set(0, 0x00);
marked.blit(1, data, 0, data.length);
if (transport != null) {
if (address != null) {
if (!peers.exists(address)) {
throw address + " not seen - no public key.";
}
if (!transport.sendTo(peers[address].pk, marked)) {
trace("sendRawMessage: no open channel to " + address);
}
} else {
transport.broadcast(marked);
}
}
}
public function heartbeat(?interval:Int):Void {
interval = interval != null ? interval : 30000;
if (heartbeatTimer != null) heartbeatTimer.stop();
heartbeatTimer = new haxe.Timer(interval);
heartbeatTimer.run = () -> {
ping();
var t = now();
for (p in [for (k in peers.keys()) k]) {
var peerInfo = peers[p];
if (peerInfo.last + interval < t) {
peers.remove(p);
if (onTimeout != null) onTimeout(p);
if (onLeft != null) onLeft(p);
}
}
for (h in [for (k in seen.keys()) k]) {
if (seen.get(h) + interval < t) {
seen.remove(h);
}
}
};
}
public function destroy(?cb:Void -> Void):Void {
if (heartbeatTimer != null) {
heartbeatTimer.stop();
heartbeatTimer = null;
}
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) {
cb();
}
}
public function close(?cb:Void -> Void):Void {
destroy(cb);
}
public function connections():Int {
if (transport != null) {
var count = transport.getPeerCount();
if (count != lastPeerCount) {
lastPeerCount = count;
if (onConnections != null) onConnections(count);
}
return count;
}
return 0;
}
function makePacket(params:Dynamic):Bytes {
var p:Dynamic = {
t: now(),
i: identifier,
pk: pk,
ek: ek,
n: Crypto.randomBytes(8)
};
for (k in Reflect.fields(params)) {
Reflect.setField(p, k, Reflect.field(params, k));
}
var pe = Bencode.encode(p);
var sig = Crypto.signDetached(pe, keyPair.secretKey);
return Bencode.encode({s: sig, p: pe});
}
function encryptPacket(peerPk:String, packet:Bytes):Bytes {
var addr = addressFromPk(peerPk);
if (peers.exists(addr)) {
var nonce = Crypto.randomBytes(Crypto.boxNonceLength);
var encrypted = Crypto.box(
packet,
nonce,
Base58.decode(peers[addr].ek),
keyPairEncrypt.secretKey
);
return Bencode.encode({
n: nonce,
ek: Base58.encode(keyPairEncrypt.publicKey),
e: encrypted
});
} else {
throw addressFromPk(peerPk) + " not seen - no encryption key.";
}
}
function sendRaw(message:Bytes, ?peerPk:String):Void {
var hash = Crypto.hash(message).sub(0, 16).toHex();
seen.set(hash, now());
if (transport != null) {
if (peerPk != null) {
if (!transport.sendTo(peerPk, message)) {
transport.broadcast(message);
}
} else {
transport.broadcast(message);
}
}
}
function onMessageData(identifier:String, message:Bytes, ?senderPk:String):Void {
var hash = Crypto.hash(message).sub(0, 16).toHex();
var t = now();
if (!seen.exists(hash)) {
if (message.length > 0 && (message.get(0) == 0x00 || message.get(0) == 0x01)) {
var isRelayed = message.get(0) == 0x01;
var offset = 1;
var msgSenderPk:String = senderPk;
if (isRelayed) {
if (message.length < 2) {
seen.set(hash, t);
return;
}
var pkLen = message.get(1);
if (2 + pkLen > message.length) {
seen.set(hash, t);
return;
}
var claimedPk = message.sub(2, pkLen).toString();
if (pkToAddress.exists(claimedPk)) {
msgSenderPk = claimedPk;
}
offset = 2 + pkLen;
}
var rawPayload = message.sub(offset, message.length - offset);
var payload = new leenkx.network.Buffer();
payload.writeBytes(rawPayload);
seen.set(hash, t);
var senderAddr = msgSenderPk != null ? pkToAddress.get(msgSenderPk) : null;
if (senderAddr == null && msgSenderPk != null && msgSenderPk != "") {
senderAddr = addressFromPk(msgSenderPk);
}
if (onMessage != null) {
onMessage(senderAddr, payload, null);
}
if (isRelay && transport != null && transport.getPeerCount() > 1) {
if (isRelayed) {
sendRaw(message);
} else if (msgSenderPk != null && msgSenderPk != "") {
var pkBytes = Bytes.ofString(msgSenderPk);
var relayed = Bytes.alloc(1 + 1 + pkBytes.length + rawPayload.length);
relayed.set(0, 0x01);
relayed.set(1, pkBytes.length);
relayed.blit(2, pkBytes, 0, pkBytes.length);
relayed.blit(2 + pkBytes.length, rawPayload, 0, rawPayload.length);
sendRaw(relayed);
}
}
return;
}
var unpacked:Dynamic = null;
try {
unpacked = Bencode.decode(message);
} catch(e:Dynamic) {
trace("Bencode decode error: " + e);
seen.set(hash, t);
return;
}
try {
var isEncrypted = Reflect.field(unpacked, "e") != null
&& Reflect.field(unpacked, "n") != null
&& Reflect.field(unpacked, "ek") != null;
if (isEncrypted) {
var ekStr:String = unpacked.ek.toString();
var decrypted = Crypto.boxOpen(
unpacked.e,
unpacked.n,
Base58.decode(ekStr),
keyPairEncrypt.secretKey
);
if (decrypted != null) {
unpacked = Bencode.decode(decrypted);
} else {
unpacked = null;
}
}
if (unpacked != null && unpacked.p != null) {
var packet:Dynamic = Bencode.decode(unpacked.p);
var pkStr:String = packet.pk.toString();
var idStr:String = packet.i.toString();
var checkSig = Crypto.signDetachedVerify(unpacked.p, unpacked.s, Base58.decode(pkStr));
var checkId = idStr == identifier;
var checkTime = packet.t + timeout > t;
if (checkSig && checkId && checkTime) {
var ekStr:String = packet.ek.toString();
sawPeer(pkStr, ekStr, identifier);
var yStr:String = packet.y != null ? packet.y.toString() : null;
if (yStr == PacketType.Message) {
var msgStr:String = packet.v.toString();
var msgJson:Dynamic = null;
try {
msgJson = haxe.Json.parse(msgStr);
} catch(e) {
}
if (msgJson != null) {
if (onMessage != null) {
onMessage(addressFromPk(pkStr), msgJson, packet);
}
} else {
try {
var payload = new leenkx.network.Buffer();
payload.writeBytes(
haxe.crypto.Base64.decode(msgStr));
if (onMessage != null) {
onMessage(addressFromPk(pkStr), payload, packet);
}
} catch(e:Dynamic) {}
}
} else if (yStr == PacketType.Ping) {
if (onPing != null) onPing(addressFromPk(pkStr));
} else if (yStr == PacketType.Leave) {
var addr = addressFromPk(pkStr);
peers.remove(addr);
if (onLeft != null) onLeft(addr);
}
}
}
if (transport != null && transport.getPeerCount() > 1) {
sendRaw(message);
}
} catch(e:Dynamic) {
trace("onMessageData error processing packet: " + e);
}
}
seen.set(hash, t);
}
function sawPeer(pk:String, ek:String, identifier:String):Void {
var t = now();
var addr = addressFromPk(pk);
var myAddr = address();
var isSelf = (addr == myAddr);
var alreadySeen = seenFired.exists(addr);
if (!isSelf) {
if (!peers.exists(addr) || peers[addr].last + timeout < t) {
peers.set(addr, {pk: pk, ek: ek, last: t});
var packet = makePacket({y: PacketType.Ping});
sendRaw(packet);
} else {
var p = peers.get(addr);
p.ek = ek;
p.last = t;
}
if (!alreadySeen) {
seenFired.set(addr, true);
lastPeerId = addr;
if (onOpen != null) onOpen();
if (onSeen != null) onSeen(addr);
if (addr == identifier) {
serverAddress = addr;
if (onServer != null) onServer(addr);
}
}
}
}
static function now():Float {
return Date.now().getTime();
}
}

View File

@ -0,0 +1,115 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class Magnet {
public var infoHash:Bytes;
public var infoHashHex:String;
public var infoHashV2:Bytes;
public var infoHashV2Hex:String;
public var name:String = null;
public var trackers:Array<String> = [];
public var webSeeds:Array<String> = [];
public function new() {}
public static function parse(uri:String):Magnet {
if (uri.indexOf("magnet:?") != 0) return null;
var m = new Magnet();
var query = uri.substr(8);
for (param in query.split("&")) {
var eq = param.indexOf("=");
if (eq < 0) continue;
var key = param.substr(0, eq);
var value = StringTools.urlDecode(param.substr(eq + 1));
switch (key) {
case "xt":
if (value.indexOf("urn:btih:") == 0) {
var hash = value.substr(9);
m.infoHash = decodeBtih(hash);
if (m.infoHash != null) {
m.infoHashHex = Crypto.toHex(m.infoHash);
}
} else if (value.indexOf("urn:btmh:") == 0) {
var hash = value.substr(9);
if (hash.length == 68) hash = hash.substr(4);
if (hash.length == 64) {
try {
m.infoHashV2 = Bytes.ofHex(hash);
} catch(e:Dynamic) {}
if (m.infoHashV2 != null) {
m.infoHashV2Hex =
Crypto.toHex(m.infoHashV2);
}
}
}
case "dn":
m.name = value;
case "tr":
m.trackers.push(value);
case "ws":
m.webSeeds.push(value);
default:
}
}
if (m.infoHash == null && m.infoHashV2 == null) return null;
return m;
}
public static function build(infoHash:Bytes, name:String, trackers:Array<String>, ?webSeeds:Array<String>, ?infoHashV2:Bytes):String {
var parts:Array<String> = [];
if (infoHash != null) {
parts.push("xt=urn:btih:" + Crypto.toHex(infoHash));
}
if (infoHashV2 != null) {
parts.push("xt=urn:btmh:1220"
+ Crypto.toHex(infoHashV2));
}
if (name != null) {
parts.push("dn=" + StringTools.urlEncode(name));
}
if (trackers != null) {
for (t in trackers) {
parts.push("tr=" + StringTools.urlEncode(t));
}
}
if (webSeeds != null) {
for (w in webSeeds) {
parts.push("ws=" + StringTools.urlEncode(w));
}
}
return "magnet:?" + parts.join("&");
}
static function decodeBtih(s:String):Bytes {
if (s.length == 40) {
try {
return Bytes.ofHex(s);
} catch(e:Dynamic) {
return null;
}
}
if (s.length == 32) {
return base32Decode(s.toUpperCase());
}
return null;
}
static function base32Decode(s:String):Bytes {
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var out = new haxe.io.BytesBuffer();
var buffer = 0;
var bits = 0;
for (i in 0...s.length) {
var v = alphabet.indexOf(s.charAt(i));
if (v < 0) return null;
buffer = (buffer << 5) | v;
bits += 5;
if (bits >= 8) {
bits -= 8;
out.addByte((buffer >> bits) & 0xFF);
}
}
return out.getBytes();
}
}

View File

@ -0,0 +1,711 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
typedef TorrentFileEntry = {
var path:String;
var length:Int;
var offset:Int;
@:optional var piecesRoot:Bytes;
}
class MetaInfo {
public var infoHash:Bytes;
public var infoHashHex:String;
public var infoHashV2:Bytes = null;
public var infoHashV2Hex:String = null;
public var metaVersion:Int = 1;
public var name:String;
public var pieceLength:Int;
public var pieces:Bytes;
public var files:Array<TorrentFileEntry>;
public var totalLength:Int = 0;
public var announce:Array<String>;
public var rawInfo:Bytes;
public var isPrivate:Bool = false;
public var comment:String = null;
public var createdBy:String = null;
public var creationDate:Int = 0;
public var urlList:Array<String> = null;
public var source:String = null;
public var fileHashes:Array<Array<Bytes>> = null;
public var storageLength:Int = 0;
public var pieceLayers:Array<{root:Bytes, hashes:Bytes}> = null;
public var numPieces(get, never):Int;
function get_numPieces():Int {
if (pieces != null && pieces.length > 0) {
return Std.int(pieces.length / TorrentInfo.HASH_LENGTH);
}
if (pieceLength > 0 && files != null) {
var end = 0;
for (f in files) {
var e = f.offset + f.length;
if (e > end) end = e;
}
return end > 0
? Std.int(Math.ceil(end / pieceLength)) : 0;
}
return 0;
}
public var protocolVersion(get, never):Int;
function get_protocolVersion():Int {
return metaVersion == 2 ? (pieces != null ? 3 : 2) : 1;
}
public function new() {}
public static function fromTorrentBytes(data:Bytes):MetaInfo {
var dict:Dynamic = Bencode.decode(data);
if (dict == null || Reflect.field(dict, "info") == null) {
throw "MetaInfo: no info dict";
}
var m = new MetaInfo();
m.rawInfo = extractInfoBytes(data);
var info = Reflect.field(dict, "info");
m.parseInfo(info);
if (m.metaVersion == 2) {
m.infoHashV2 = haxe.crypto.Sha256.make(m.rawInfo);
m.infoHashV2Hex = Crypto.toHex(m.infoHashV2);
}
if (m.pieces != null) {
m.infoHash = haxe.crypto.Sha1.make(m.rawInfo);
m.infoHashHex = Crypto.toHex(m.infoHash);
}
m.announce = parseAnnounce(dict);
m.initFileHashes();
m.loadPieceLayers(data);
m.comment = bytesToString(Reflect.field(dict, "comment"));
m.createdBy = bytesToString(Reflect.field(dict, "created by"));
m.creationDate = Std.int(fieldFloat(dict, "creation date"));
var ul = Reflect.field(dict, "url-list");
if (Std.isOfType(ul, Array)) {
m.urlList = [for (u in (ul:Array<Dynamic>))
bytesToString(u)];
} else if (ul != null) {
m.urlList = [bytesToString(ul)];
}
return m;
}
function parseInfo(info:Dynamic):Void {
name = bytesToString(Reflect.field(info, "name"));
if (name == null) name = "";
pieceLength = Std.int(fieldFloat(info, "piece length"));
metaVersion = Std.int(fieldFloat(info, "meta version"));
if (metaVersion == 0) metaVersion = 1;
var piecesField = Reflect.field(info, "pieces");
if (piecesField == null) {
pieces = null;
} else {
pieces = Std.isOfType(piecesField, Bytes) ? piecesField
: Bytes.ofString(Std.string(piecesField));
}
isPrivate = fieldFloat(info, "private") == 1;
source = bytesToString(Reflect.field(info, "source"));
files = [];
totalLength = 0;
var fileTree = Reflect.field(info, "file tree");
if (fileTree != null) {
walkFileTree(fileTree, [name]);
if (pieces != null) {
applyV1Layout(Reflect.field(info, "files"));
}
storageLength = pieces == null
? treeCursor * pieceLength : totalLength;
return;
}
storageLength = 0;
var filesList:Array<Dynamic> = Reflect.field(info, "files");
if (filesList != null) {
for (f in filesList) {
var len = Std.int(fieldFloat(f, "length"));
var pathList:Array<Dynamic> = Reflect.field(f, "path");
var path = name;
if (pathList != null) {
var parts = [for (p in pathList) bytesToString(p)];
path = name + "/" + parts.join("/");
}
files.push({path: sanitizePath(path), length: len,
offset: totalLength});
totalLength += len;
}
} else {
var len = Std.int(fieldFloat(info, "length"));
files.push({path: sanitizePath(name), length: len,
offset: 0});
totalLength = len;
}
storageLength = totalLength;
}
public static function sanitizeComponent(c:String):String {
if (c == null || c == "" || c == "." || c == ".."
|| c.indexOf("/") >= 0 || c.indexOf("\\") >= 0
|| c.indexOf(":") >= 0
|| c.indexOf(String.fromCharCode(0)) >= 0) {
return "_";
}
return c;
}
public static function sanitizePath(p:String):String {
if (p == null) return "_";
var parts = p.split("/");
return [for (c in parts) sanitizeComponent(c)].join("/");
}
public function fileForPiece(index:Int):Int {
for (i in 0...files.length) {
var f = files[i];
var start = Std.int(f.offset / pieceLength);
var np = Std.int(Math.ceil(f.length / pieceLength));
if (index >= start && index < start + np) return i;
}
return -1;
}
public function pieceSizeV2(index:Int):Int {
var fi = fileForPiece(index);
if (fi < 0) return 0;
var f = files[fi];
var start = Std.int(f.offset / pieceLength);
var rem = f.length - (index - start) * pieceLength;
return Std.int(Math.min(pieceLength, rem));
}
public function verifyPieceV2(index:Int, data:Bytes):Bool {
var fi = fileForPiece(index);
if (fi < 0 || fileHashes == null
|| fileHashes.length <= fi || fileHashes[fi] == null) {
return false;
}
var f = files[fi];
var start = Std.int(f.offset / pieceLength);
var expected = fileHashes[fi][index - start];
if (expected == null) return false;
var actual = f.length <= pieceLength
? merkleRoot(data) : pieceRootV2(data);
return memcmp(actual, expected);
}
public function pieceRootV2(data:Bytes):Bytes {
var base = Std.int(Math.log(pieceLength / 16384)
/ Math.log(2));
var pieceLeaves = 1 << base;
var leaves:Array<Bytes> = [];
var pos = 0;
while (pos < data.length) {
var n = Std.int(Math.min(16384, data.length - pos));
leaves.push(haxe.crypto.Sha256.make(data.sub(pos, n)));
pos += n;
}
var zero = leafZero();
while (leaves.length < pieceLeaves) leaves.push(zero);
return merkleReduce(leaves);
}
public function verifyLayer(fi:Int, hashes:Array<Bytes>):Bool {
if (fi < 0 || hashes == null) return false;
var root = files[fi].piecesRoot;
if (root == null) return false;
var base = Std.int(Math.log(pieceLength / 16384)
/ Math.log(2));
var level = hashes.copy();
var pow = 1;
while (pow < level.length) pow <<= 1;
var zero = layerZero(base);
while (level.length < pow) level.push(zero);
return memcmp(merkleReduce(level), root);
}
static var layerZeroCache:Map<Int, Bytes> = [];
public static function layerZero(level:Int):Bytes {
if (level == 0) return leafZero();
var half = layerZero(level - 1);
return hashPair(half, half);
}
public static function hashPair(a:Bytes, b:Bytes):Bytes {
var buf = new haxe.io.BytesBuffer();
buf.addBytes(a, 0, 32);
buf.addBytes(b, 0, 32);
return haxe.crypto.Sha256.make(buf.getBytes());
}
public static function merkleReduce(hashes:Array<Bytes>):Bytes {
var level = hashes;
while (level.length > 1) {
var next:Array<Bytes> = [];
for (i in 0...Std.int(level.length / 2)) {
next.push(hashPair(level[i * 2], level[i * 2 + 1]));
}
level = next;
}
return level.length > 0 ? level[0] : Bytes.alloc(32);
}
static public function memcmp(a:Bytes, b:Bytes):Bool {
if (a == null || b == null || a.length != b.length) return false;
for (i in 0...a.length) if (a.get(i) != b.get(i)) return false;
return true;
}
var treeCursor:Int = 0;
function walkFileTree(node:Dynamic, path:Array<String>):Void {
var aligned = pieces == null;
var keys = Reflect.fields(node);
keys.sort(Reflect.compare);
for (key in keys) {
var child = Reflect.field(node, key);
if (key == "") {
var len = Std.int(fieldFloat(child, "length"));
var p = path.length == 2 && path[0] == name
&& path[1] == name ? name : path.join("/");
var offset = aligned ? treeCursor * pieceLength
: totalLength;
files.push({path: sanitizePath(p), length: len,
offset: offset,
piecesRoot: toBytes(
Reflect.field(child, "pieces root"))});
if (aligned) {
treeCursor += Std.int(
Math.ceil(len / pieceLength));
}
totalLength += len;
} else {
var next = path.copy();
next.push(key);
walkFileTree(child, next);
}
}
}
function applyV1Layout(filesList:Dynamic):Void {
if (!Std.isOfType(filesList, Array)) return;
var byPath = new Map<String, TorrentFileEntry>();
for (f in files) byPath.set(f.path, f);
var ordered:Array<TorrentFileEntry> = [];
var off = 0;
for (fl in (filesList:Array<Dynamic>)) {
var len = Std.int(fieldFloat(fl, "length"));
var pathList:Array<Dynamic> = Reflect.field(fl, "path");
var path = name;
if (pathList != null) {
var parts = [for (p in pathList) bytesToString(p)];
path = name + "/" + parts.join("/");
}
var e = byPath.get(sanitizePath(path));
if (e == null) {
e = {path: sanitizePath(path), length: len,
offset: off};
} else {
e.offset = off;
e.length = len;
}
off += len;
ordered.push(e);
}
files = ordered;
totalLength = off;
}
public function initFileHashes():Void {
if (metaVersion != 2 || files == null
|| pieceLength <= 0) return;
if (fileHashes == null) {
fileHashes = [for (f in files) null];
}
for (fi in 0...files.length) {
var f = files[fi];
if (f.length > 0 && f.length <= pieceLength
&& f.piecesRoot != null) {
fileHashes[fi] = [f.piecesRoot];
}
}
}
public function loadPieceLayers(data:Bytes):Void {
if (metaVersion != 2 || fileHashes == null) return;
var pos = findTopKey(data, "piece layers");
if (pos < 0 || data.get(pos) != 'd'.code) return;
var byRoot = new Map<String, Bytes>();
var p = pos + 1;
while (p < data.length && data.get(p) != 'e'.code) {
var key = rawString(data, p);
if (key == null) return;
p = skipValue(data, p);
var val = rawString(data, p);
if (val == null) return;
p = skipValue(data, p);
byRoot.set(Crypto.toHex(key), val);
}
for (fi in 0...files.length) {
var f = files[fi];
if (f.piecesRoot == null) continue;
var np = Std.int(Math.ceil(f.length / pieceLength));
if (np <= 1) continue;
var raw = byRoot.get(Crypto.toHex(f.piecesRoot));
if (raw == null || raw.length < np * 32) continue;
var hashes = [for (i in 0...np) raw.sub(i * 32, 32)];
if (verifyLayer(fi, hashes)) fileHashes[fi] = hashes;
}
}
static function rawString(data:Bytes, pos:Int):Bytes {
var b = data.get(pos);
if (b < '0'.code || b > '9'.code) return null;
var len = 0;
while (pos < data.length && data.get(pos) != ':'.code) {
len = len * 10 + (data.get(pos) - '0'.code);
pos++;
}
if (pos + 1 + len > data.length) return null;
return data.sub(pos + 1, len);
}
static function findTopKey(data:Bytes, want:String):Int {
if (data.length == 0 || data.get(0) != 'd'.code) return -1;
var pos = 1;
while (pos < data.length && data.get(pos) != 'e'.code) {
var keyEnd = skipValue(data, pos);
var key = data.sub(pos, keyEnd - pos).toString();
var colon = key.indexOf(":");
key = key.substr(colon + 1);
var valEnd = skipValue(data, keyEnd);
if (key == want) return keyEnd;
pos = valEnd;
}
return -1;
}
static function parseAnnounce(dict:Dynamic):Array<String> {
var out:Array<String> = [];
var seen = new Map<String, Bool>();
var list:Array<Dynamic> = Reflect.field(dict, "announce-list");
if (list != null) {
for (tier in list) {
if (Std.isOfType(tier, Array)) {
for (u in (tier:Array<Dynamic>)) {
var s = bytesToString(u);
if (s != null && !seen.exists(s)) {
seen.set(s, true);
out.push(s);
}
}
}
}
}
var single = bytesToString(Reflect.field(dict, "announce"));
if (single != null && !seen.exists(single)) {
out.push(single);
}
return out;
}
public static function fromInfoDict(rawInfo:Bytes, infoHash:Bytes, ?announce:Array<String>):MetaInfo {
var m = new MetaInfo();
m.rawInfo = rawInfo;
m.infoHash = infoHash;
m.infoHashHex = infoHash != null ? Crypto.toHex(infoHash) : null;
var infoDict = Bencode.decode(rawInfo);
m.parseInfo(infoDict);
if (m.metaVersion == 2) {
m.infoHashV2 = haxe.crypto.Sha256.make(rawInfo);
m.infoHashV2Hex = Crypto.toHex(m.infoHashV2);
}
m.announce = announce != null ? announce : [];
m.initFileHashes();
return m;
}
public static function create(name:String, files:Array<{path:String, data:Bytes}>, ?announce:Array<String>, ?opts:Dynamic):MetaInfo {
var m = new MetaInfo();
m.name = name;
m.announce = announce != null ? announce : [];
m.files = [];
var pieceLengthOpt = 0;
if (opts != null) {
m.isPrivate = fieldFloat(opts, "private") == 1
|| Reflect.field(opts, "private") == true;
m.source = Reflect.field(opts, "source");
m.comment = Reflect.field(opts, "comment");
m.createdBy = Reflect.field(opts, "createdBy");
var u:Dynamic = Reflect.field(opts, "urlList");
if (u != null) {
m.urlList = Std.isOfType(u, Array)
? (u:Array<String>) : [Std.string(u)];
}
pieceLengthOpt = Std.int(fieldFloat(opts, "pieceLength"));
}
var mvOpt = opts != null
? Std.int(fieldFloat(opts, "metaVersion")) : 1;
if (mvOpt < 1) mvOpt = 1;
var isV1 = mvOpt != 2;
var isV2 = mvOpt != 1;
m.metaVersion = isV2 ? 2 : 1;
var aligned = mvOpt == 2;
var total = 0;
for (f in files) total += f.data.length;
m.pieceLength = pieceLengthOpt > 0 ? pieceLengthOpt
: TorrentInfo.pieceLength(total);
var cursor = 0;
var content = new haxe.io.BytesBuffer();
for (f in files) {
var entry:TorrentFileEntry = {path: f.path,
length: f.data.length,
offset: aligned ? cursor * m.pieceLength
: m.totalLength};
if (isV2) entry.piecesRoot = merkleRoot(f.data);
m.files.push(entry);
m.totalLength += f.data.length;
cursor += Std.int(Math.ceil(f.data.length
/ m.pieceLength));
content.addBytes(f.data, 0, f.data.length);
}
var all = content.getBytes();
m.storageLength = aligned ? cursor * m.pieceLength
: m.totalLength;
m.pieces = isV1 ? TorrentInfo.pieces(all, m.pieceLength)
: null;
var info:Dynamic = {};
Reflect.setField(info, "name", name);
Reflect.setField(info, "piece length", m.pieceLength);
if (m.isPrivate) Reflect.setField(info, "private", 1);
if (m.source != null) Reflect.setField(info, "source", m.source);
if (isV1) {
Reflect.setField(info, "pieces", m.pieces);
if (files.length == 1) {
Reflect.setField(info, "length", files[0].data.length);
} else {
var fl:Array<Dynamic> = [];
for (f in files) {
var fd:Dynamic = {};
Reflect.setField(fd, "length", f.data.length);
var rel = f.path;
if (rel.indexOf(name + "/") == 0) {
rel = rel.substr(name.length + 1);
}
Reflect.setField(fd, "path", rel.split("/"));
fl.push(fd);
}
Reflect.setField(info, "files", fl);
}
}
if (isV2) {
Reflect.setField(info, "meta version", 2);
Reflect.setField(info, "file tree",
buildFileTree(name, files, m.files));
}
if (isV2) {
m.pieceLayers = [];
for (i in 0...files.length) {
var f = files[i];
var entry = m.files[i];
if (f.data.length <= m.pieceLength
|| entry.piecesRoot == null) continue;
var np = Std.int(Math.ceil(f.data.length
/ m.pieceLength));
var buf = new haxe.io.BytesBuffer();
for (p in 0...np) {
var off = p * m.pieceLength;
var sz = Std.int(Math.min(m.pieceLength,
f.data.length - off));
buf.addBytes(m.pieceRootV2(
f.data.sub(off, sz)), 0, 32);
}
m.pieceLayers.push({root: entry.piecesRoot,
hashes: buf.getBytes()});
}
if (m.pieceLayers.length == 0) m.pieceLayers = null;
}
m.rawInfo = Bencode.encode(info);
if (isV1) {
m.infoHash = haxe.crypto.Sha1.make(m.rawInfo);
m.infoHashHex = Crypto.toHex(m.infoHash);
}
if (isV2) {
m.infoHashV2 = haxe.crypto.Sha256.make(m.rawInfo);
m.infoHashV2Hex = Crypto.toHex(m.infoHashV2);
}
return m;
}
public static function merkleRoot(data:Bytes):Bytes {
if (data.length == 0) return Bytes.alloc(32);
var leaves:Array<Bytes> = [];
var pos = 0;
while (pos < data.length) {
var n = Std.int(Math.min(16384, data.length - pos));
leaves.push(haxe.crypto.Sha256.make(data.sub(pos, n)));
pos += n;
}
var pow = 1;
while (pow < leaves.length) pow <<= 1;
var zero = leafZero();
while (leaves.length < pow) leaves.push(zero);
return merkleReduce(leaves);
}
static var leafZeroCache:Bytes = null;
static function leafZero():Bytes {
if (leafZeroCache == null) leafZeroCache = Bytes.alloc(32);
return leafZeroCache;
}
static function buildFileTree(name:String, files:Array<{path:String, data:Bytes}>, entries:Array<TorrentFileEntry>):Dynamic {
var root:Dynamic = {};
for (i in 0...files.length) {
var rel = files[i].path;
if (rel.indexOf(name + "/") == 0) {
rel = rel.substr(name.length + 1);
}
var parts = rel.split("/");
var node = root;
for (p in 0...parts.length - 1) {
var nxt = Reflect.field(node, parts[p]);
if (nxt == null) {
nxt = {};
Reflect.setField(node, parts[p], nxt);
}
node = nxt;
}
var leaf:Dynamic = {};
Reflect.setField(leaf, "length", files[i].data.length);
Reflect.setField(leaf, "pieces root",
entries[i].piecesRoot);
var leafWrap:Dynamic = {};
Reflect.setField(leafWrap, "", leaf);
Reflect.setField(node, parts[parts.length - 1], leafWrap);
}
return root;
}
public function toTorrentBytes(?extra:Dynamic):Bytes {
var dict:Dynamic = extra != null ? extra : {};
Reflect.setField(dict, "info", Bencode.decode(rawInfo));
if (announce.length > 0) {
Reflect.setField(dict, "announce", announce[0]);
var tiers:Array<Dynamic> = [for (a in announce) [a]];
Reflect.setField(dict, "announce-list", tiers);
}
if (comment != null) Reflect.setField(dict, "comment", comment);
if (createdBy != null) {
Reflect.setField(dict, "created by", createdBy);
}
if (urlList != null && urlList.length > 0) {
Reflect.setField(dict, "url-list",
urlList.length == 1 ? (urlList[0]:Dynamic)
: [for (u in urlList) u]);
}
Reflect.setField(dict, "creation date",
creationDate > 0 ? creationDate
: Std.int(Date.now().getTime() / 1000));
if (pieceLayers != null && pieceLayers.length > 0) {
var pl = new Bencode.RawDict();
var sorted = pieceLayers.copy();
sorted.sort(function(a, b) {
var n = a.root.length < b.root.length
? a.root.length : b.root.length;
for (i in 0...n) {
var d = a.root.get(i) - b.root.get(i);
if (d != 0) return d;
}
return a.root.length - b.root.length;
});
for (e in sorted) {
pl.pairs.push({key: e.root, val: e.hashes});
}
Reflect.setField(dict, "piece layers", pl);
}
return Bencode.encode(dict);
}
public function pieceHash(index:Int):Bytes {
return pieces.sub(index * TorrentInfo.HASH_LENGTH,
TorrentInfo.HASH_LENGTH);
}
public function pieceSize(index:Int):Int {
if (pieces == null) return pieceSizeV2(index); // v2 aligned
if (index == numPieces - 1) {
var rem = totalLength % pieceLength;
return rem == 0 ? pieceLength : rem;
}
return pieceLength;
}
static function fieldFloat(obj:Dynamic, name:String):Float {
var v = Reflect.field(obj, name);
if (v == null) return 0;
if (Std.isOfType(v, Float) || Std.isOfType(v, Int)) return v;
return Std.parseFloat(Std.string(v));
}
static function bytesToString(v:Dynamic):String {
if (v == null) return null;
if (Std.isOfType(v, String)) return v;
if (Std.isOfType(v, Float) || Std.isOfType(v, Int)) {
return Std.string(v);
}
if (Std.isOfType(v, Bytes)) return (v:Bytes).toString();
return Std.string(v);
}
public static function toBytes(v:Dynamic):Bytes {
if (v == null) return null;
if (Std.isOfType(v, Float) || Std.isOfType(v, Int)
|| Std.isOfType(v, String) || Std.isOfType(v, Array)) {
return null;
}
if (Std.isOfType(v, Bytes)) return (v:Bytes);
return null;
}
static function extractInfoBytes(data:Bytes):Bytes {
var pos = 0;
if (data.get(pos) != 'd'.code) throw "MetaInfo: not a dict";
pos++;
while (pos < data.length && data.get(pos) != 'e'.code) {
var keyEnd = skipValue(data, pos);
var key = data.sub(pos, keyEnd - pos).toString();
var colon = key.indexOf(":");
key = key.substr(colon + 1);
var valStart = keyEnd;
var valEnd = skipValue(data, valStart);
if (key == "info") {
return data.sub(valStart, valEnd - valStart);
}
pos = valEnd;
}
throw "MetaInfo: info key not found";
}
static public function skipValue(data:Bytes, pos:Int):Int {
var b = data.get(pos);
if (b == 'i'.code) {
pos++;
while (pos < data.length && data.get(pos) != 'e'.code) pos++;
return pos + 1;
}
if (b == 'l'.code || b == 'd'.code) {
pos++;
while (pos < data.length && data.get(pos) != 'e'.code) {
pos = skipValue(data, pos);
}
return pos + 1;
}
var len = 0;
while (pos < data.length && data.get(pos) != ':'.code) {
len = len * 10 + (data.get(pos) - '0'.code);
pos++;
}
return pos + 1 + len;
}
}

View File

@ -0,0 +1,329 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
import leenkx.network.torrent.storage.Storage.IStorage;
class PieceManager {
public static inline var BLOCK_SIZE = 16384;
var meta:MetaInfo;
var storage:IStorage;
public var have:Array<Bool>;
public var numHave:Int = 0;
public var bitfield:Bytes;
var pending:Map<Int, Map<Int, Bytes>> = [];
var pendingCount:Map<Int, Int> = [];
var requested:Map<String, Map<Int, Map<Int, Bool>>> = [];
var globalRequested:Map<Int, Map<Int, Bool>> = [];
var pendingV2:Map<Int, Bytes> = [];
public var onPieceComplete:Int -> Void;
public var onPieceFailed:Int -> Void;
public var onComplete:Void -> Void;
@:allow(leenkx.network.torrent.Torrent)
@:allow(leenkx.network.torrent.WebSeed)
var selections:Array<{start:Int, end:Int, priority:Int,
notify:Void -> Void}> = [];
public function select(startByte:Int, endByte:Int, ?priority:Int, ?notify:Void -> Void):Void {
selections.push({start: pieceIndex(startByte),
end: pieceIndex(endByte),
priority: priority != null ? priority : 0,
notify: notify});
selections.sort(function(a, b) return b.priority
- a.priority);
}
public function deselect(startByte:Int, endByte:Int, ?priority:Int):Void {
var s = pieceIndex(startByte);
var e = pieceIndex(endByte);
var p = priority != null ? priority : 0;
for (i in 0...selections.length) {
var sel = selections[i];
if (sel.start == s && sel.end == e
&& sel.priority == p) {
selections.splice(i, 1);
return;
}
}
}
function pieceIndex(byte:Int):Int {
var i = Std.int(byte / meta.pieceLength);
if (i < 0) i = 0;
if (i >= meta.numPieces) i = meta.numPieces - 1;
return i;
}
function updateSelections(index:Int):Void {
var i = 0;
while (i < selections.length) {
var sel = selections[i];
if (index < sel.start || index > sel.end
|| sel.notify == null) {
i++;
continue;
}
var done = true;
for (p in sel.start...sel.end + 1) {
if (!have[p]) {
done = false;
break;
}
}
if (done) {
selections.splice(i, 1);
sel.notify();
} else i++;
}
}
public var complete(get, never):Bool;
function get_complete():Bool return numHave == meta.numPieces;
public function new(meta:MetaInfo, storage:IStorage) {
this.meta = meta;
this.storage = storage;
have = [for (i in 0...meta.numPieces) false];
bitfield = Bytes.alloc(Std.int(Math.ceil(meta.numPieces / 8)));
}
public static function forSeeder(meta:MetaInfo, storage:IStorage):PieceManager {
var pm = new PieceManager(meta, storage);
for (i in 0...meta.numPieces) pm.markHave(i);
return pm;
}
public function markHave(index:Int):Void {
if (have[index]) return;
have[index] = true;
numHave++;
bitfield.set(index >> 3, bitfield.get(index >> 3)
| (0x80 >> (index & 7)));
}
public function addBlock(index:Int, begin:Int, data:Bytes):Bool {
if (index < 0 || index >= meta.numPieces || begin < 0
|| data == null
|| begin + data.length > meta.pieceSize(index)) {
return false;
}
if (have[index]) return true;
var blocks = pending.get(index);
if (blocks == null) {
blocks = [];
pending.set(index, blocks);
pendingCount.set(index, 0);
}
if (!blocks.exists(begin)) {
blocks.set(begin, data);
pendingCount.set(index, pendingCount.get(index) + data.length);
var gReq = globalRequested.get(index);
if (gReq != null) {
gReq.remove(begin);
if (!gReq.keys().hasNext()) globalRequested.remove(index);
}
}
if (pendingCount.get(index) < meta.pieceSize(index)) return false;
var piece = Bytes.alloc(meta.pieceSize(index));
var off = 0;
var keys = [for (k in blocks.keys()) k];
keys.sort(Reflect.compare);
for (b in keys) {
var d = blocks.get(b);
piece.blit(b, d, 0, d.length);
}
var ok = true;
if (meta.pieces == null) {
var fi = meta.fileForPiece(index);
var layer = fi >= 0 && meta.fileHashes != null
&& meta.fileHashes.length > fi
? meta.fileHashes[fi] : null;
pending.remove(index);
pendingCount.remove(index);
if (layer == null) {
pendingV2.set(index, piece);
return false;
}
ok = meta.verifyPieceV2(index, piece);
} else {
var hash = haxe.crypto.Sha1.make(piece);
ok = MetaInfo.memcmp(hash, meta.pieceHash(index));
pending.remove(index);
pendingCount.remove(index);
}
if (!ok) {
trace('[PieceManager] piece ' + index
+ ' verification FAILED');
releaseRequested(index);
if (onPieceFailed != null) onPieceFailed(index);
return false;
}
storage.write(index * meta.pieceLength, piece);
markHave(index);
updateSelections(index);
if (onPieceComplete != null) onPieceComplete(index);
if (complete && onComplete != null) onComplete();
return true;
}
public function flushPendingV2(fi:Int):Bool {
if (meta.pieces != null) return false;
var f = meta.files[fi];
var start = Std.int(f.offset / meta.pieceLength);
var np = Std.int(Math.ceil(f.length / meta.pieceLength));
var wrote = false;
for (i in start...start + np) {
var piece = pendingV2.get(i);
if (piece == null) continue;
pendingV2.remove(i);
if (!meta.verifyPieceV2(i, piece)) {
trace('[PieceManager] flush piece ' + i
+ ' verification FAILED');
releaseRequested(i);
if (onPieceFailed != null) onPieceFailed(i);
continue;
}
storage.write(i * meta.pieceLength, piece);
markHave(i);
updateSelections(i);
wrote = true;
if (onPieceComplete != null) onPieceComplete(i);
}
if (complete && onComplete != null) onComplete();
return wrote;
}
public function untouched(index:Int):Bool {
return !have[index] && !pending.exists(index)
&& !pendingV2.exists(index)
&& !globalRequested.exists(index);
}
public function readBlock(index:Int, begin:Int, length:Int):Bytes {
return storage.read(index * meta.pieceLength + begin, length);
}
public function neededBlocks(peerKey:String, index:Int):Array<{begin:Int, length:Int}> {
if (pendingV2.exists(index)) return [];
var size = meta.pieceSize(index);
var blocks = pending.get(index);
var peerReq = requested.get(peerKey);
var req = peerReq != null ? peerReq.get(index) : null;
var globalReq = globalRequested.get(index);
var out:Array<{begin:Int, length:Int}> = [];
var off = 0;
while (off < size) {
var len = size - off;
if (len > BLOCK_SIZE) len = BLOCK_SIZE;
if ((blocks == null || !blocks.exists(off))
&& (req == null || !req.exists(off))
&& (globalReq == null || !globalReq.exists(off))) {
out.push({begin: off, length: len});
}
off += len;
}
return out;
}
public function markRequested(key:String, index:Int, begin:Int):Void {
var r = requested.get(key);
if (r == null) requested.set(key,
r = new Map<Int, Map<Int, Bool>>());
var m = r.get(index);
if (m == null) r.set(index, m = new Map<Int, Bool>());
m.set(begin, true);
var g = globalRequested.get(index);
if (g == null) globalRequested.set(index,
g = new Map<Int, Bool>());
g.set(begin, true);
reqTimes.set(key + "|" + index + "|" + begin,
haxe.Timer.stamp());
}
var reqTimes:Map<String, Float> = [];
public function expireRequests(ttlSec:Float):Map<String, Int> {
var now = haxe.Timer.stamp();
var dead:Array<String> = [];
for (k in reqTimes.keys()) {
if (now - reqTimes.get(k) > ttlSec) dead.push(k);
}
var expired:Map<String, Int> = [];
for (k in dead) {
var p = k.split("|");
var idx = Std.parseInt(p[1]);
var beg = Std.parseInt(p[2]);
var peerReq = requested.get(p[0]);
if (peerReq != null) {
var r = peerReq.get(idx);
if (r != null && r.exists(beg)) {
r.remove(beg);
var n = expired.exists(p[0]) ? expired.get(p[0]) : 0;
expired.set(p[0], n + 1);
}
}
var g = globalRequested.get(idx);
if (g != null) {
g.remove(beg);
var empty = true;
for (b in g.keys()) {
empty = false;
break;
}
if (empty) globalRequested.remove(idx);
}
reqTimes.remove(k);
}
return expired;
}
public function releaseRequested(index:Int):Void {
globalRequested.remove(index);
var keys = [for (k in requested.keys()) k];
for (key in keys) {
var r = requested.get(key);
var m = r.get(index);
if (m != null) {
for (b in m.keys()) {
reqTimes.remove(key + "|" + index + "|" + b);
}
r.remove(index);
}
}
}
public function clearRequested(peerKey:String, index:Int):Void {
var peerReq = requested.get(peerKey);
if (peerReq != null) {
var req = peerReq.get(index);
if (req != null) {
var gReq = globalRequested.get(index);
if (gReq != null) {
for (off in req.keys()) gReq.remove(off);
if (!gReq.keys().hasNext()) globalRequested.remove(index);
}
}
peerReq.remove(index);
}
}
public function clearAllRequested(peerKey:String):Void {
var peerReq = requested.get(peerKey);
if (peerReq != null) {
for (index in peerReq.keys()) {
var req = peerReq.get(index);
var gReq = globalRequested.get(index);
if (gReq != null && req != null) {
for (off in req.keys()) gReq.remove(off);
if (!gReq.keys().hasNext()) globalRequested.remove(index);
}
}
requested.remove(peerKey);
}
}
}

View File

@ -0,0 +1,334 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class Ripemd160 {
static inline function ROL(x:Int, n:Int):Int {
return (x << n) | (x >>> (32 - n));
}
static inline function F(x:Int, y:Int, z:Int):Int {
return x ^ y ^ z;
}
static inline function G(x:Int, y:Int, z:Int):Int {
return (x & y) | (~x & z);
}
static inline function H(x:Int, y:Int, z:Int):Int {
return (x | ~y) ^ z;
}
static inline function I(x:Int, y:Int, z:Int):Int {
return (x & z) | (y & ~z);
}
static inline function J(x:Int, y:Int, z:Int):Int {
return x ^ (y | ~z);
}
static inline function FF(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + F(b, c, d) + x;
a = ROL(a, s) + e;
return a;
}
static inline function GG(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + G(b, c, d) + x + 0x5A827999;
a = ROL(a, s) + e;
return a;
}
static inline function HH(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + H(b, c, d) + x + 0x6ED9EBA1;
a = ROL(a, s) + e;
return a;
}
static inline function II(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + I(b, c, d) + x + 0x8F1BBCDC;
a = ROL(a, s) + e;
return a;
}
static inline function JJ(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + J(b, c, d) + x + 0xA953FD4E;
a = ROL(a, s) + e;
return a;
}
static inline function FFF(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + F(b, c, d) + x;
a = ROL(a, s) + e;
return a;
}
static inline function GGG(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + G(b, c, d) + x + 0x7A6D76E9;
a = ROL(a, s) + e;
return a;
}
static inline function HHH(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + H(b, c, d) + x + 0x6D703EF3;
a = ROL(a, s) + e;
return a;
}
static inline function III(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + I(b, c, d) + x + 0x5C4DD124;
a = ROL(a, s) + e;
return a;
}
static inline function JJJ(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
a = a + J(b, c, d) + x + 0x50A28BE6;
a = ROL(a, s) + e;
return a;
}
public static function make(data:Bytes):Bytes {
var msgLen = data.length;
var bitLen = msgLen * 8;
var padLen = 56 - ((msgLen + 1) % 64);
if (padLen < 0) padLen += 64;
var totalLen = msgLen + 1 + padLen + 8;
var padded = Bytes.alloc(totalLen);
padded.blit(0, data, 0, msgLen);
padded.set(msgLen, 0x80);
for (i in (msgLen + 1)...(totalLen - 8)) padded.set(i, 0);
padded.set(totalLen - 8, bitLen & 0xFF);
padded.set(totalLen - 7, (bitLen >> 8) & 0xFF);
padded.set(totalLen - 6, (bitLen >> 16) & 0xFF);
padded.set(totalLen - 5, (bitLen >> 24) & 0xFF);
padded.set(totalLen - 4, 0);
padded.set(totalLen - 3, 0);
padded.set(totalLen - 2, 0);
padded.set(totalLen - 1, 0);
var h0 = 0x67452301;
var h1 = 0xEFCDAB89;
var h2 = 0x98BADCFE;
var h3 = 0x10325476;
var h4 = 0xC3D2E1F0;
var numBlocks = Std.int(totalLen / 64);
for (block in 0...numBlocks) {
var base = block * 64;
var X = new Array<Int>();
for (i in 0...16) {
var off = base + i * 4;
X.push(padded.get(off) | (padded.get(off + 1) << 8) |
(padded.get(off + 2) << 16) | (padded.get(off + 3) << 24));
}
var a = h0, b = h1, c = h2, d = h3, e = h4;
var a1 = h0, b1 = h1, c1 = h2, d1 = h3, e1 = h4;
a = FF(a, b, c, d, e, X[0], 11); c = ROL(c, 10);
e = FF(e, a, b, c, d, X[1], 14); b = ROL(b, 10);
d = FF(d, e, a, b, c, X[2], 15); a = ROL(a, 10);
c = FF(c, d, e, a, b, X[3], 12); e = ROL(e, 10);
b = FF(b, c, d, e, a, X[4], 5); d = ROL(d, 10);
a = FF(a, b, c, d, e, X[5], 8); c = ROL(c, 10);
e = FF(e, a, b, c, d, X[6], 7); b = ROL(b, 10);
d = FF(d, e, a, b, c, X[7], 9); a = ROL(a, 10);
c = FF(c, d, e, a, b, X[8], 11); e = ROL(e, 10);
b = FF(b, c, d, e, a, X[9], 13); d = ROL(d, 10);
a = FF(a, b, c, d, e, X[10], 14); c = ROL(c, 10);
e = FF(e, a, b, c, d, X[11], 15); b = ROL(b, 10);
d = FF(d, e, a, b, c, X[12], 6); a = ROL(a, 10);
c = FF(c, d, e, a, b, X[13], 7); e = ROL(e, 10);
b = FF(b, c, d, e, a, X[14], 9); d = ROL(d, 10);
a = FF(a, b, c, d, e, X[15], 8); c = ROL(c, 10);
e = GG(e, a, b, c, d, X[7], 7); b = ROL(b, 10);
d = GG(d, e, a, b, c, X[4], 6); a = ROL(a, 10);
c = GG(c, d, e, a, b, X[13], 8); e = ROL(e, 10);
b = GG(b, c, d, e, a, X[1], 13); d = ROL(d, 10);
a = GG(a, b, c, d, e, X[10], 11); c = ROL(c, 10);
e = GG(e, a, b, c, d, X[6], 9); b = ROL(b, 10);
d = GG(d, e, a, b, c, X[15], 7); a = ROL(a, 10);
c = GG(c, d, e, a, b, X[3], 15); e = ROL(e, 10);
b = GG(b, c, d, e, a, X[12], 7); d = ROL(d, 10);
a = GG(a, b, c, d, e, X[0], 12); c = ROL(c, 10);
e = GG(e, a, b, c, d, X[9], 15); b = ROL(b, 10);
d = GG(d, e, a, b, c, X[5], 9); a = ROL(a, 10);
c = GG(c, d, e, a, b, X[2], 11); e = ROL(e, 10);
b = GG(b, c, d, e, a, X[14], 7); d = ROL(d, 10);
a = GG(a, b, c, d, e, X[11], 13); c = ROL(c, 10);
e = GG(e, a, b, c, d, X[8], 12); b = ROL(b, 10);
d = HH(d, e, a, b, c, X[3], 11); a = ROL(a, 10);
c = HH(c, d, e, a, b, X[10], 13); e = ROL(e, 10);
b = HH(b, c, d, e, a, X[14], 6); d = ROL(d, 10);
a = HH(a, b, c, d, e, X[4], 7); c = ROL(c, 10);
e = HH(e, a, b, c, d, X[9], 14); b = ROL(b, 10);
d = HH(d, e, a, b, c, X[15], 9); a = ROL(a, 10);
c = HH(c, d, e, a, b, X[8], 13); e = ROL(e, 10);
b = HH(b, c, d, e, a, X[1], 15); d = ROL(d, 10);
a = HH(a, b, c, d, e, X[2], 14); c = ROL(c, 10);
e = HH(e, a, b, c, d, X[7], 8); b = ROL(b, 10);
d = HH(d, e, a, b, c, X[0], 13); a = ROL(a, 10);
c = HH(c, d, e, a, b, X[6], 6); e = ROL(e, 10);
b = HH(b, c, d, e, a, X[13], 5); d = ROL(d, 10);
a = HH(a, b, c, d, e, X[11], 12); c = ROL(c, 10);
e = HH(e, a, b, c, d, X[5], 7); b = ROL(b, 10);
d = HH(d, e, a, b, c, X[12], 5); a = ROL(a, 10);
c = II(c, d, e, a, b, X[1], 11); e = ROL(e, 10);
b = II(b, c, d, e, a, X[9], 12); d = ROL(d, 10);
a = II(a, b, c, d, e, X[11], 14); c = ROL(c, 10);
e = II(e, a, b, c, d, X[10], 15); b = ROL(b, 10);
d = II(d, e, a, b, c, X[0], 14); a = ROL(a, 10);
c = II(c, d, e, a, b, X[8], 15); e = ROL(e, 10);
b = II(b, c, d, e, a, X[12], 9); d = ROL(d, 10);
a = II(a, b, c, d, e, X[4], 8); c = ROL(c, 10);
e = II(e, a, b, c, d, X[13], 9); b = ROL(b, 10);
d = II(d, e, a, b, c, X[3], 14); a = ROL(a, 10);
c = II(c, d, e, a, b, X[7], 5); e = ROL(e, 10);
b = II(b, c, d, e, a, X[15], 6); d = ROL(d, 10);
a = II(a, b, c, d, e, X[14], 8); c = ROL(c, 10);
e = II(e, a, b, c, d, X[5], 6); b = ROL(b, 10);
d = II(d, e, a, b, c, X[6], 5); a = ROL(a, 10);
c = II(c, d, e, a, b, X[2], 12); e = ROL(e, 10);
b = JJ(b, c, d, e, a, X[4], 9); d = ROL(d, 10);
a = JJ(a, b, c, d, e, X[0], 15); c = ROL(c, 10);
e = JJ(e, a, b, c, d, X[5], 5); b = ROL(b, 10);
d = JJ(d, e, a, b, c, X[9], 11); a = ROL(a, 10);
c = JJ(c, d, e, a, b, X[7], 6); e = ROL(e, 10);
b = JJ(b, c, d, e, a, X[12], 8); d = ROL(d, 10);
a = JJ(a, b, c, d, e, X[2], 13); c = ROL(c, 10);
e = JJ(e, a, b, c, d, X[10], 12); b = ROL(b, 10);
d = JJ(d, e, a, b, c, X[14], 5); a = ROL(a, 10);
c = JJ(c, d, e, a, b, X[1], 12); e = ROL(e, 10);
b = JJ(b, c, d, e, a, X[3], 13); d = ROL(d, 10);
a = JJ(a, b, c, d, e, X[8], 14); c = ROL(c, 10);
e = JJ(e, a, b, c, d, X[11], 11); b = ROL(b, 10);
d = JJ(d, e, a, b, c, X[6], 8); a = ROL(a, 10);
c = JJ(c, d, e, a, b, X[15], 5); e = ROL(e, 10);
b = JJ(b, c, d, e, a, X[13], 6); d = ROL(d, 10);
a1 = JJJ(a1, b1, c1, d1, e1, X[5], 8); c1 = ROL(c1, 10);
e1 = JJJ(e1, a1, b1, c1, d1, X[14], 9); b1 = ROL(b1, 10);
d1 = JJJ(d1, e1, a1, b1, c1, X[7], 9); a1 = ROL(a1, 10);
c1 = JJJ(c1, d1, e1, a1, b1, X[0], 11); e1 = ROL(e1, 10);
b1 = JJJ(b1, c1, d1, e1, a1, X[9], 13); d1 = ROL(d1, 10);
a1 = JJJ(a1, b1, c1, d1, e1, X[2], 15); c1 = ROL(c1, 10);
e1 = JJJ(e1, a1, b1, c1, d1, X[11], 15); b1 = ROL(b1, 10);
d1 = JJJ(d1, e1, a1, b1, c1, X[4], 5); a1 = ROL(a1, 10);
c1 = JJJ(c1, d1, e1, a1, b1, X[13], 7); e1 = ROL(e1, 10);
b1 = JJJ(b1, c1, d1, e1, a1, X[6], 7); d1 = ROL(d1, 10);
a1 = JJJ(a1, b1, c1, d1, e1, X[15], 8); c1 = ROL(c1, 10);
e1 = JJJ(e1, a1, b1, c1, d1, X[8], 11); b1 = ROL(b1, 10);
d1 = JJJ(d1, e1, a1, b1, c1, X[1], 14); a1 = ROL(a1, 10);
c1 = JJJ(c1, d1, e1, a1, b1, X[10], 14); e1 = ROL(e1, 10);
b1 = JJJ(b1, c1, d1, e1, a1, X[3], 12); d1 = ROL(d1, 10);
a1 = JJJ(a1, b1, c1, d1, e1, X[12], 6); c1 = ROL(c1, 10);
e1 = III(e1, a1, b1, c1, d1, X[6], 9); b1 = ROL(b1, 10);
d1 = III(d1, e1, a1, b1, c1, X[11], 13); a1 = ROL(a1, 10);
c1 = III(c1, d1, e1, a1, b1, X[3], 15); e1 = ROL(e1, 10);
b1 = III(b1, c1, d1, e1, a1, X[7], 7); d1 = ROL(d1, 10);
a1 = III(a1, b1, c1, d1, e1, X[0], 12); c1 = ROL(c1, 10);
e1 = III(e1, a1, b1, c1, d1, X[13], 8); b1 = ROL(b1, 10);
d1 = III(d1, e1, a1, b1, c1, X[5], 9); a1 = ROL(a1, 10);
c1 = III(c1, d1, e1, a1, b1, X[10], 11); e1 = ROL(e1, 10);
b1 = III(b1, c1, d1, e1, a1, X[14], 7); d1 = ROL(d1, 10);
a1 = III(a1, b1, c1, d1, e1, X[15], 7); c1 = ROL(c1, 10);
e1 = III(e1, a1, b1, c1, d1, X[8], 12); b1 = ROL(b1, 10);
d1 = III(d1, e1, a1, b1, c1, X[12], 7); a1 = ROL(a1, 10);
c1 = III(c1, d1, e1, a1, b1, X[4], 6); e1 = ROL(e1, 10);
b1 = III(b1, c1, d1, e1, a1, X[9], 15); d1 = ROL(d1, 10);
a1 = III(a1, b1, c1, d1, e1, X[1], 13); c1 = ROL(c1, 10);
e1 = III(e1, a1, b1, c1, d1, X[2], 11); b1 = ROL(b1, 10);
d1 = HHH(d1, e1, a1, b1, c1, X[15], 9); a1 = ROL(a1, 10);
c1 = HHH(c1, d1, e1, a1, b1, X[5], 7); e1 = ROL(e1, 10);
b1 = HHH(b1, c1, d1, e1, a1, X[1], 15); d1 = ROL(d1, 10);
a1 = HHH(a1, b1, c1, d1, e1, X[3], 11); c1 = ROL(c1, 10);
e1 = HHH(e1, a1, b1, c1, d1, X[7], 8); b1 = ROL(b1, 10);
d1 = HHH(d1, e1, a1, b1, c1, X[14], 6); a1 = ROL(a1, 10);
c1 = HHH(c1, d1, e1, a1, b1, X[6], 6); e1 = ROL(e1, 10);
b1 = HHH(b1, c1, d1, e1, a1, X[9], 14); d1 = ROL(d1, 10);
a1 = HHH(a1, b1, c1, d1, e1, X[11], 12); c1 = ROL(c1, 10);
e1 = HHH(e1, a1, b1, c1, d1, X[8], 13); b1 = ROL(b1, 10);
d1 = HHH(d1, e1, a1, b1, c1, X[12], 5); a1 = ROL(a1, 10);
c1 = HHH(c1, d1, e1, a1, b1, X[2], 14); e1 = ROL(e1, 10);
b1 = HHH(b1, c1, d1, e1, a1, X[10], 13); d1 = ROL(d1, 10);
a1 = HHH(a1, b1, c1, d1, e1, X[0], 13); c1 = ROL(c1, 10);
e1 = HHH(e1, a1, b1, c1, d1, X[4], 7); b1 = ROL(b1, 10);
d1 = HHH(d1, e1, a1, b1, c1, X[13], 5); a1 = ROL(a1, 10);
c1 = GGG(c1, d1, e1, a1, b1, X[8], 15); e1 = ROL(e1, 10);
b1 = GGG(b1, c1, d1, e1, a1, X[6], 5); d1 = ROL(d1, 10);
a1 = GGG(a1, b1, c1, d1, e1, X[4], 8); c1 = ROL(c1, 10);
e1 = GGG(e1, a1, b1, c1, d1, X[1], 11); b1 = ROL(b1, 10);
d1 = GGG(d1, e1, a1, b1, c1, X[3], 14); a1 = ROL(a1, 10);
c1 = GGG(c1, d1, e1, a1, b1, X[11], 14); e1 = ROL(e1, 10);
b1 = GGG(b1, c1, d1, e1, a1, X[15], 6); d1 = ROL(d1, 10);
a1 = GGG(a1, b1, c1, d1, e1, X[0], 14); c1 = ROL(c1, 10);
e1 = GGG(e1, a1, b1, c1, d1, X[5], 6); b1 = ROL(b1, 10);
d1 = GGG(d1, e1, a1, b1, c1, X[12], 9); a1 = ROL(a1, 10);
c1 = GGG(c1, d1, e1, a1, b1, X[2], 12); e1 = ROL(e1, 10);
b1 = GGG(b1, c1, d1, e1, a1, X[13], 9); d1 = ROL(d1, 10);
a1 = GGG(a1, b1, c1, d1, e1, X[9], 12); c1 = ROL(c1, 10);
e1 = GGG(e1, a1, b1, c1, d1, X[7], 5); b1 = ROL(b1, 10);
d1 = GGG(d1, e1, a1, b1, c1, X[10], 15); a1 = ROL(a1, 10);
c1 = GGG(c1, d1, e1, a1, b1, X[14], 8); e1 = ROL(e1, 10);
b1 = FFF(b1, c1, d1, e1, a1, X[12], 8); d1 = ROL(d1, 10);
a1 = FFF(a1, b1, c1, d1, e1, X[15], 5); c1 = ROL(c1, 10);
e1 = FFF(e1, a1, b1, c1, d1, X[10], 12); b1 = ROL(b1, 10);
d1 = FFF(d1, e1, a1, b1, c1, X[4], 9); a1 = ROL(a1, 10);
c1 = FFF(c1, d1, e1, a1, b1, X[1], 12); e1 = ROL(e1, 10);
b1 = FFF(b1, c1, d1, e1, a1, X[5], 5); d1 = ROL(d1, 10);
a1 = FFF(a1, b1, c1, d1, e1, X[8], 14); c1 = ROL(c1, 10);
e1 = FFF(e1, a1, b1, c1, d1, X[7], 6); b1 = ROL(b1, 10);
d1 = FFF(d1, e1, a1, b1, c1, X[0], 8); a1 = ROL(a1, 10);
c1 = FFF(c1, d1, e1, a1, b1, X[6], 5); e1 = ROL(e1, 10);
b1 = FFF(b1, c1, d1, e1, a1, X[11], 12); d1 = ROL(d1, 10);
a1 = FFF(a1, b1, c1, d1, e1, X[13], 6); c1 = ROL(c1, 10);
e1 = FFF(e1, a1, b1, c1, d1, X[2], 5); b1 = ROL(b1, 10);
d1 = FFF(d1, e1, a1, b1, c1, X[14], 15); a1 = ROL(a1, 10);
c1 = FFF(c1, d1, e1, a1, b1, X[9], 13); e1 = ROL(e1, 10);
b1 = FFF(b1, c1, d1, e1, a1, X[3], 11); d1 = ROL(d1, 10);
var t = h1 + c + d1;
h1 = h2 + d + e1;
h2 = h3 + e + a1;
h3 = h4 + a + b1;
h4 = h0 + b + c1;
h0 = t;
}
var result = Bytes.alloc(20);
result.set(0, h0 & 0xFF);
result.set(1, (h0 >> 8) & 0xFF);
result.set(2, (h0 >> 16) & 0xFF);
result.set(3, (h0 >> 24) & 0xFF);
result.set(4, h1 & 0xFF);
result.set(5, (h1 >> 8) & 0xFF);
result.set(6, (h1 >> 16) & 0xFF);
result.set(7, (h1 >> 24) & 0xFF);
result.set(8, h2 & 0xFF);
result.set(9, (h2 >> 8) & 0xFF);
result.set(10, (h2 >> 16) & 0xFF);
result.set(11, (h2 >> 24) & 0xFF);
result.set(12, h3 & 0xFF);
result.set(13, (h3 >> 8) & 0xFF);
result.set(14, (h3 >> 16) & 0xFF);
result.set(15, (h3 >> 24) & 0xFF);
result.set(16, h4 & 0xFF);
result.set(17, (h4 >> 8) & 0xFF);
result.set(18, (h4 >> 16) & 0xFF);
result.set(19, (h4 >> 24) & 0xFF);
return result;
}
}

View File

@ -0,0 +1,969 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
import leenkx.network.torrent.peer.IPeerChannel;
import leenkx.network.torrent.peer.WireProtocol;
import leenkx.network.torrent.peer.RtcPeerPool;
import leenkx.network.torrent.storage.Storage;
import leenkx.network.torrent.storage.Storage.IStorage;
class Torrent {
static inline var MAX_OUTSTANDING = 8;
static inline var META_BLOCK = 16384;
static inline var MAX_META_SIZE = 10000000;
static inline var KEEPALIVE_MS = 60000;
public var meta:MetaInfo = null;
public var infoHash:Bytes;
public var infoHashHex:String;
public var infoHashV2:Bytes = null;
public var infoHashV2Hex:String = null;
public var pendingWebSeeds:Array<String> = null;
public var magnetURI:String = null;
public var files:Array<TorrentFile> = [];
public var ready = false;
public var done = false;
public var numPeers(get, never):Int;
function get_numPeers():Int return Lambda.count(wires);
var listeners:Map<String, Array<Dynamic>> = [];
public var onPeerExtension:String -> String -> Bytes -> Void;
public var uploadedBytes:Float = 0;
public var stopped = false;
public var client:TorrentClient = null;
var webSeeds:Array<WebSeed> = [];
var lastStatT:Float = 0;
var lastStatDown:Float = 0;
var lastStatUp:Float = 0;
public var announcePort:Int = 0;
public var downloadDir:String = null;
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 now = haxe.Timer.stamp();
var dt = now - lastStatT;
var downRate:Float = 0;
var upRate:Float = 0;
if (lastStatT > 0 && dt > 0) {
downRate = (downloaded - lastStatDown) / dt;
upRate = (uploadedBytes - lastStatUp) / dt;
if (downRate < 0) downRate = 0;
if (upRate < 0) upRate = 0;
}
lastStatT = now;
lastStatDown = downloaded;
lastStatUp = uploadedBytes;
return {
downloaded: downloaded,
uploaded: uploadedBytes,
left: total - downloaded,
downRate: downRate,
upRate: upRate
};
}
var peerId:Bytes;
var trackers:Array<String>;
var storage:IStorage;
public var trackerObjects:Array<Dynamic> = [];
@:allow(leenkx.network.torrent.TorrentFile)
@:allow(leenkx.network.torrent.WebSeed)
var pieces:PieceManager = null;
var wires:Map<String, WireProtocol> = [];
var peerBitfields:Map<String, Array<Bool>> = [];
var outstanding:Map<String, Int> = [];
var pieceCursor:Map<String, Int> = [];
var rtcPool:RtcPeerPool = null;
var httpTrackers:Array<Dynamic> = [];
var udpTrackers:Array<Dynamic> = [];
var keepaliveTimer:haxe.Timer = null;
var destroyed = false;
var metaSize:Int = 0;
var metaReceived:Int = 0;
var metaRequested:Array<Bool> = [];
var metaRejects:Int = 0;
var metaWire:WireProtocol = null;
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.peerId = peerId;
this.trackers = trackers;
this.storage = storage;
this.downloadDir = downloadDir;
if (meta != null) setMeta(meta);
}
public function on(event:String, cb:Dynamic):Void {
var l = listeners.get(event);
if (l == null) listeners.set(event, l = []);
l.push(cb);
}
function emit(event:String):Void {
var l = listeners.get(event);
if (l != null) for (cb in l) cb();
}
function emit1(event:String, v:Dynamic):Void {
var l = listeners.get(event);
if (l != null) for (cb in l) cb(v);
}
function setMeta(m:MetaInfo):Void {
meta = m;
if (storage == null) {
var sz = m.storageLength > 0 ? m.storageLength : m.totalLength;
#if (sys || kha_krom)
storage = downloadDir != null ? new FileTreeStorage(downloadDir, m.files) : new MemoryStorage(sz);
#else
storage = new MemoryStorage(sz);
#end
}
if (pieces == null) {
pieces = new PieceManager(m, storage);
pieces.onPieceComplete = onPieceComplete;
pieces.onComplete = onTorrentComplete;
}
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)];
infoHashV2 = m.infoHashV2;
infoHashV2Hex = m.infoHashV2Hex;
if (infoHash == null && m.infoHash != null) {
infoHash = m.infoHash;
infoHashHex = m.infoHashHex;
}
if (infoHashHex == null) infoHashHex = m.infoHashV2Hex;
if (m.urlList == null && pendingWebSeeds != null) {
m.urlList = pendingWebSeeds;
}
magnetURI = Magnet.build(m.infoHash, m.name, trackers, m.urlList, m.infoHashV2);
ready = true;
initWebSeeds();
emit1("metadata", m);
emit("ready");
if (!pieces.complete) {
for (w in wires) w.sendInterested();
}
}
public function markComplete():Void {
if (pieces == null) return;
for (i in 0...meta.numPieces) pieces.markHave(i);
done = true;
}
public function start():Void {
if (destroyed || keepaliveTimer != null) return;
stopped = false;
startTrackers();
keepaliveTimer = new haxe.Timer(KEEPALIVE_MS);
keepaliveTimer.run = function() {
for (w in wires) w.sendKeepAlive();
workWebSeeds();
};
workWebSeeds();
}
public function stop():Void {
if (destroyed || stopped) return;
stopped = true;
if (keepaliveTimer != null) {
keepaliveTimer.stop();
keepaliveTimer = null;
}
for (w in [for (x in wires) x]) w.close();
wires.clear();
outstanding.clear();
if (rtcPool != null) {
rtcPool.close();
rtcPool = null;
}
for (t in httpTrackers) t.stop();
for (t in udpTrackers) t.stop();
httpTrackers = [];
udpTrackers = [];
trackerObjects = [];
}
function startTrackers():Void {
for (url in trackers) startTracker(url);
}
public function addTracker(url:String):Dynamic {
if (url == null || url == "") return null;
for (t in trackers) {
if (t == url) return null;
}
trackers.push(url);
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);
var obj:Dynamic = null;
if (url.indexOf("ws") == 0) {
if (rtcPool == null) {
rtcPool = new RtcPeerPool();
rtcPool.onPeerChannel = onRtcPeer;
rtcPool.statsProvider = getStats;
rtcPool.onError = function(e) {
emit1("error", e);
};
}
obj = rtcPool.connect(url, infoHashBin, peerIdBin);
}
#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);
t.statsProvider = getStats;
t.onPeers = onClassicPeers;
t.onError = function(e) {
emit1("error", e);
};
httpTrackers.push(t);
t.start();
obj = t;
}
#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);
t.statsProvider = getStats;
t.onPeers = onClassicPeers;
t.onError = function(e) {
emit1("error", e);
};
udpTrackers.push(t);
t.start();
obj = t;
}
#end
if (obj != null) trackerObjects.push(obj);
return obj;
}
function onRtcPeer(peerIdHex:String, ch:IPeerChannel):Void {
addWire("rtc:" + peerIdHex, ch);
}
#if (sys || kha_krom)
function onClassicPeers(list:Array<{host:String, port:Int}>):Void {
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);
addWire(key, ch);
}
}
#end
public function addIncomingPeer(key:String, ch:IPeerChannel):Void {
addWire(key, ch);
}
function addWire(key:String, ch:IPeerChannel):Void {
if (destroyed || stopped || wires.exists(key)) {
ch.close();
return;
}
var wire = new WireProtocol(ch, infoHash, peerId);
wires.set(key, wire);
outstanding.set(key, 0);
wire.metaSize = meta != null ? meta.rawInfo.length : 0;
wire.onHandshake = function(remotePeerId) {
if (ready) wire.sendBitfield(pieces.bitfield);
};
wire.onExtendedHandshake = function(hs) {
if (meta == null && wire.extIds.exists("ut_metadata")) {
requestMetadata(wire);
}
if (ready && !pieces.complete) wire.sendInterested();
};
wire.onUnchoke = function() requestBlocks(key);
wire.onBitfield = function(bf) {
peerBitfields.set(key, bitfieldToArray(bf));
if (ready && !pieces.complete) wire.sendInterested();
requestBlocks(key);
};
wire.onHave = function(index) {
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);
};
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)) {
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 (!pieces.have[index]) return;
var data = pieces.readBlock(index, begin, length);
uploadedBytes += data.length;
wire.sendPiece(index, begin, data);
};
wire.onChoke = function() {
outstanding.set(key, 0);
if (pieces != null) pieces.clearAllRequested(key);
};
wire.onInterested = function() wire.sendUnchoke();
wire.onHashRequest = function(p) onHashRequest(wire, p);
wire.onHashes = onHashesMsg;
wire.onHashReject = onHashRejectMsg;
wire.onExtended = function(name, payload) {
if (name == "ut_metadata") {
onUtMetadata(wire, payload);
} else if (onPeerExtension != null) {
onPeerExtension(key, name, payload);
}
};
wire.onClose = function() {
wires.remove(key);
peerBitfields.remove(key);
outstanding.remove(key);
if (pieces != null) pieces.clearAllRequested(key);
if (meta == null && metaSize > 0 && metaWire == wire) {
resetMetaRequests();
metaWire = null;
for (k in wires.keys()) {
var w = wires.get(k);
if (w != wire && w.extIds.exists("ut_metadata")) {
metaWire = w;
sendMetaRequest(w, nextMetaPiece());
break;
}
}
}
};
wire.onError = function(e) {
emit1("error", "wire " + key + ": " + e);
};
wire.start(["ut_metadata", "lx_channel"]);
emit1("wire", wire);
}
static function bytesEq(a:Bytes, b:Bytes):Bool {
if (a == null || b == null || a.length != b.length) return false;
for (i in 0...a.length) if (a.get(i) != b.get(i)) return false;
return true;
}
function bitfieldToArray(bf:Bytes):Array<Bool> {
var n = meta != null ? meta.numPieces : bf.length * 8;
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);
}
return out;
}
function tryRequest(wire:WireProtocol, key:String, bf:Array<Bool>, i:Int):Bool {
if (pieces.have[i] || i >= bf.length || !bf[i]) return false;
if (meta.pieces == null && meta.fileHashes != null) {
var fi = meta.fileForPiece(i);
if (fi >= 0 && meta.fileHashes[fi] == null) {
requestPieceLayer(wire, fi);
return false;
}
}
var needed = pieces.neededBlocks(key, i);
if (needed.length == 0) return false;
var b = needed[0];
wire.sendRequest(i, b.begin, b.length);
pieces.markRequested(key, i, b.begin);
return true;
}
function requestBlocks(key:String):Void {
if (pieces == null || pieces.complete) return;
var expired = pieces.expireRequests(60);
for (pk in expired.keys()) {
var c = outstanding.get(pk);
if (c != null) {
c -= expired.get(pk);
outstanding.set(pk, c < 0 ? 0 : c);
}
}
var wire = wires.get(key);
var bf = peerBitfields.get(key);
if (wire == null || bf == null || wire.peerChoking) return;
var n = outstanding.get(key);
var cursor = pieceCursor.get(key);
if (cursor == null) cursor = 0;
var numPieces = meta.numPieces;
while (n < MAX_OUTSTANDING) {
var sent = false;
for (sel in pieces.selections) {
for (i in sel.start...sel.end + 1) {
if (tryRequest(wire, key, bf, i)) {
n++;
sent = true;
break;
}
}
if (sent) break;
}
if (!sent) {
for (j in 0...numPieces) {
var i = (cursor + j) % numPieces;
if (tryRequest(wire, key, bf, i)) {
n++;
sent = true;
pieceCursor.set(key, (i + 1) % numPieces);
break;
}
}
}
if (!sent) break;
}
outstanding.set(key, n);
workWebSeeds();
}
public function select(start:Int, end:Int, ?priority:Int, ?notify:Void -> Void):Void {
if (pieces == null) return;
pieces.select(start, end, priority, notify);
for (key in wires.keys()) requestBlocks(key);
workWebSeeds();
}
public function deselect(start:Int, end:Int, ?priority:Int):Void {
if (pieces == null) return;
pieces.deselect(start, end, priority);
}
public function critical(start:Int, end:Int):Void {
select(start, end, 0x7fffffff);
}
public function addPeer(peer:String):Bool {
#if (sys || kha_krom)
if (peer == null) return false;
var i = peer.lastIndexOf(":");
if (i < 0) return false;
var host = peer.substr(0, i);
var port = Std.parseInt(peer.substr(i + 1));
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));
return true;
#else
return false;
#end
}
function broadcastHave(index:Int):Void {
for (w in wires) w.sendHave(index);
}
public function sendExtension(key:String, name:String, payload:Bytes):Void {
if (key != null) {
var w = wires.get(key);
if (w != null) w.sendExtended(name, payload);
return;
}
for (w in wires) w.sendExtended(name, payload);
}
function onPieceComplete(index:Int):Void {
emit1("piece", index);
}
var hashReqNext:Map<Int, Int> = [];
var hashReqTime:Map<Int, Float> = [];
var pendingLayers:Map<Int, {arr:Array<Bytes>, got:Int}> = [];
function pieceLayerBase():Int {
return Std.int(Math.log(meta.pieceLength / 16384) / Math.log(2));
}
function requestPieceLayer(wire:WireProtocol, fi:Int):Void {
var f = meta.files[fi];
var root = f.piecesRoot;
if (root == null || f.length <= 0) return;
var np = Std.int(Math.ceil(f.length / meta.pieceLength));
var sent = hashReqNext.exists(fi) ? hashReqNext.get(fi) : 0;
if (sent >= np) {
var t = hashReqTime.exists(fi) ? hashReqTime.get(fi) : 0;
if (haxe.Timer.stamp() - t < 60) return;
sent = 0;
}
var chunk = np - sent;
if (chunk > 512) chunk = 512;
var len = 2;
while (len < chunk) len <<= 1;
wire.sendHashRequest(root, pieceLayerBase(), sent, len, 0);
hashReqNext.set(fi, sent + len);
hashReqTime.set(fi, haxe.Timer.stamp());
}
function onHashesMsg(payload:Bytes):Void {
if (meta == null || meta.fileHashes == null) return;
if (payload.length < 80) return;
var root = payload.sub(0, 32);
var base = WireProtocol.readU32(payload, 32);
var index = WireProtocol.readU32(payload, 36);
var length = WireProtocol.readU32(payload, 40);
var proofs = WireProtocol.readU32(payload, 44);
if (base != pieceLayerBase()) return;
var count = Std.int((payload.length - 48) / 32);
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 (meta.fileHashes[fi] != null) return;
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};
pendingLayers.set(fi, pl);
}
var n = count < length ? count : length;
for (j in 0...n) {
var slot = index + j;
if (slot < np && pl.arr[slot] == null) {
pl.arr[slot] = payload.sub(48 + j * 32, 32);
pl.got++;
}
}
if (pl.got >= np) {
pendingLayers.remove(fi);
if (meta.verifyLayer(fi, pl.arr)) {
meta.fileHashes[fi] = pl.arr;
pieces.flushPendingV2(fi);
for (k in wires.keys()) requestBlocks(k);
} else {
trace('[Torrent] v2 layer REJECTED fi=' + fi);
hashReqNext.remove(fi);
}
}
return;
}
}
function onHashRejectMsg(payload:Bytes):Void {
if (meta == null) return;
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)) {
hashReqNext.remove(fi);
pendingLayers.remove(fi);
return;
}
}
}
var layerCache:Map<Int, Array<Bytes>> = [];
var treeCache:Map<Int, Array<Array<Bytes>>> = [];
function hashLayerForFile(fi:Int):Array<Bytes> {
var l = layerCache.get(fi);
if (l != null) return l;
var f = meta.files[fi];
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)));
}
layerCache.set(fi, l);
return l;
}
function treeLevelsFor(fi:Int):Array<Array<Bytes>> {
var t = treeCache.get(fi);
if (t != null) return t;
var level = hashLayerForFile(fi);
t = [level];
var depth = 0;
while (level.length > 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;
next.push(MetaInfo.hashPair(level[i], b));
i += 2;
}
t.push(next);
level = next;
depth++;
}
if (t.length > 1) t.pop();
treeCache.set(fi, t);
return t;
}
function onHashRequest(wire:WireProtocol, payload:Bytes):Void {
var root = payload.sub(0, 32);
var base = WireProtocol.readU32(payload, 32);
var index = WireProtocol.readU32(payload, 36);
var length = WireProtocol.readU32(payload, 40);
var proofs = WireProtocol.readU32(payload, 44);
var reject = function() wire.sendHashReject(root, base,
index, length, proofs);
if (meta == null || meta.pieces != null || storage == null
|| length < 2 || length > 512
|| (length & (length - 1)) != 0
|| (length > 0 && index % length != 0)) {
reject();
return;
}
var pieceBase = pieceLayerBase();
if (base != pieceBase) {
reject();
return;
}
for (fi in 0...meta.files.length) {
var f = meta.files[fi];
if (f.piecesRoot == null || f.length <= 0
|| !bytesEq(root, f.piecesRoot)) continue;
var layer = hashLayerForFile(fi);
if (index >= layer.length) {
reject();
return;
}
var n = layer.length - index;
if (n > length) n = length;
var buf = new haxe.io.BytesBuffer();
for (i in index...index + n) {
buf.addBytes(layer[i], 0, 32);
}
var lg = 0;
while ((1 << lg) < length) lg++;
var tree = treeLevelsFor(fi);
for (p in 0...(proofs - lg + 1)) {
var lvl = lg + p;
if (lvl >= tree.length) break;
var sib = (index >> lvl) ^ 1;
var h = sib < tree[lvl].length
? tree[lvl][sib]
: MetaInfo.layerZero(pieceBase + lvl);
buf.addBytes(h, 0, 32);
}
wire.sendHashes(root, base, index, n,
proofs, buf.getBytes());
return;
}
reject();
}
function onTorrentComplete():Void {
done = true;
storage.flush();
#if sys
if (downloadDir != null
&& Std.isOfType(storage, MemoryStorage)) {
writeToDisk(downloadDir);
}
#end
for (t in trackerObjects) {
var f:Dynamic = Reflect.field(t, "announceEvent");
if (f != null) {
try {
Reflect.callMethod(t, f, ["completed"]);
} catch(e:Dynamic) {}
}
}
emit("done");
if (client != null && client.onTorrentDone != null) {
client.onTorrentDone(this);
}
}
#if sys
function writeToDisk(dir:String):Void {
for (f in files) {
var p = dir + "/" + MetaInfo.sanitizePath(f.path);
try {
var i = Std.int(Math.max(p.lastIndexOf("/"),
p.lastIndexOf("\\")));
if (i > 0) {
var d = p.substr(0, i);
if (!sys.FileSystem.exists(d)) {
sys.FileSystem.createDirectory(d);
}
}
var out = sys.io.File.write(p, true);
out.write(f.getBytes());
out.close();
} catch(e:Dynamic) {
emit1("error", "write " + p + ": " + Std.string(e));
}
}
}
#end
function initWebSeeds():Void {
if (meta == null || meta.urlList == null) return;
for (u in meta.urlList) addWebSeed(u);
}
public function addWebSeed(url:String):Void {
if (url == null || url == "") return;
for (w in webSeeds) if (w.url == url) return;
var ws = new WebSeed(url, this);
var self = this;
ws.onError = function(e) {
self.emit1("error", "webseed " + url + ": " + e);
haxe.Timer.delay(self.workWebSeeds, 10000);
};
ws.onDone = workWebSeeds;
webSeeds.push(ws);
ws.work();
}
function workWebSeeds():Void {
if (destroyed || stopped || done) return;
for (w in webSeeds) w.work();
}
function requestMetadata(wire:WireProtocol):Void {
if (metaSize > 0) {
var alive = false;
for (w in wires) {
if (w == metaWire) alive = true;
}
if (!alive) {
resetMetaRequests();
metaWire = wire;
sendMetaRequest(wire, nextMetaPiece());
}
return;
}
var hs = wire.peerExtendedHandshake;
metaWire = wire;
var size:Dynamic = Reflect.field(hs, "metadata_size");
if (size == null) return;
var sizeF = Std.parseFloat(Std.string(size));
if (Math.isNaN(sizeF) || sizeF <= 0 || sizeF > MAX_META_SIZE) {
emit1("error", "invalid metadata_size " + size);
return;
}
metaSize = Std.int(sizeF);
metaRejects = 0;
var count = Std.int(Math.ceil(metaSize / META_BLOCK));
metaChunks.clear();
metaReceived = 0;
metaRequested = [for (i in 0...count) false];
sendMetaRequest(wire, 0);
}
function resetMetaRequests():Void {
for (i in 0...metaRequested.length) {
if (metaRequested[i] && !metaChunks.exists(i)) {
metaRequested[i] = false;
}
}
}
function sendMetaRequest(wire:WireProtocol, piece:Int):Void {
if (piece < 0 || piece >= metaRequested.length
|| metaRequested[piece]) return;
metaRequested[piece] = true;
var req = Bencode.encode({msg_type: 0, piece: piece});
wire.sendExtended("ut_metadata", req);
}
function onUtMetadata(wire:WireProtocol, payload:Bytes):Void {
var headerEnd = findDictEnd(payload);
if (headerEnd < 0) return;
var header:Dynamic;
try {
header = Bencode.decode(payload.sub(0, headerEnd));
} catch(e:Dynamic) {
return;
}
var msgType = Std.int(metaField(header, "msg_type"));
var piece = Std.int(metaField(header, "piece"));
if (msgType == 0) {
if (meta == null || piece < 0) {
var rej = Bencode.encode({msg_type: 2, piece: piece});
wire.sendExtended("ut_metadata", rej);
return;
}
var startF = piece * (META_BLOCK : Float);
if (startF >= meta.rawInfo.length) return;
var start = Std.int(startF);
var len = meta.rawInfo.length - start;
if (len > META_BLOCK) len = META_BLOCK;
var head = Bencode.encode({
msg_type: 1, piece: piece,
total_size: meta.rawInfo.length
});
var out = new haxe.io.BytesBuffer();
out.addBytes(head, 0, head.length);
out.addBytes(meta.rawInfo, start, len);
wire.sendExtended("ut_metadata", out.getBytes());
} else if (msgType == 1) {
var total = metaField(header, "total_size");
if (Math.isNaN(total) || total <= 0 || total > MAX_META_SIZE) {
return;
}
var totalI = Std.int(total);
if (metaSize == 0 || totalI != metaSize) {
metaSize = totalI;
metaRejects = 0;
metaChunks.clear();
metaReceived = 0;
metaRequested = [
for (i in 0...Std.int(Math.ceil(totalI / META_BLOCK)))
false
];
}
if (piece < 0 || piece >= metaRequested.length) return;
var expected = metaSize - piece * META_BLOCK;
if (expected > META_BLOCK) expected = META_BLOCK;
var chunk = payload.sub(headerEnd, payload.length - headerEnd);
if (chunk.length > expected) {
chunk = chunk.sub(0, expected);
}
appendMetaChunk(piece, chunk);
if (metaReceived >= metaSize) finishMetadata(wire);
else sendMetaRequest(wire, nextMetaPiece());
} else if (msgType == 2) {
if (piece >= 0 && piece < metaRequested.length) {
metaRequested[piece] = false;
metaRejects++;
if (metaRejects > 2 * metaRequested.length) {
metaSize = 0;
metaChunks.clear();
emit1("error", "ut_metadata rejected by peers");
} else {
for (k in wires.keys()) {
var w = wires.get(k);
if (w != wire
&& w.extIds.exists("ut_metadata")) {
sendMetaRequest(w, piece);
break;
}
}
}
}
}
}
var metaChunks:Map<Int, Bytes> = [];
function appendMetaChunk(piece:Int, chunk:Bytes):Void {
if (!metaChunks.exists(piece)) {
metaChunks.set(piece, chunk);
metaReceived += chunk.length;
}
}
function nextMetaPiece():Int {
for (i in 0...metaRequested.length) {
if (!metaRequested[i]) return i;
}
return -1;
}
function finishMetadata(wire:WireProtocol):Void {
var buf = new haxe.io.BytesBuffer();
var count = Std.int(Math.ceil(metaSize / META_BLOCK));
for (i in 0...count) {
var c = metaChunks.get(i);
if (c != null) buf.addBytes(c, 0, c.length);
}
var raw = buf.getBytes().sub(0, metaSize);
var okMeta = false;
var v1Hash:Bytes = null;
var sha1 = haxe.crypto.Sha1.make(raw);
if (infoHash != null && bytesEq(sha1, infoHash)) {
okMeta = true;
v1Hash = infoHash;
}
if (!okMeta) {
var sha2 = haxe.crypto.Sha256.make(raw);
if (infoHashV2 != null && bytesEq(sha2, infoHashV2)) {
okMeta = true;
} else if (infoHash != null
&& bytesEq(sha2.sub(0, 20), infoHash)) {
okMeta = true;
}
}
if (!okMeta) {
emit1("error", "ut_metadata hash mismatch");
metaRejects += metaRequested.length;
metaChunks.clear();
metaReceived = 0;
if (metaRejects > 2 * metaRequested.length) {
metaSize = 0;
return;
}
for (i in 0...metaRequested.length) metaRequested[i] = false;
sendMetaRequest(wire, nextMetaPiece());
return;
}
setMeta(MetaInfo.fromInfoDict(raw, v1Hash, trackers));
metaChunks.clear();
for (key in wires.keys()) requestBlocks(key);
}
static function metaField(d:Dynamic, name:String):Float {
var v = Reflect.field(d, name);
if (v == null) return 0;
return Std.parseFloat(Std.string(v));
}
static function findDictEnd(data:Bytes):Int {
if (data.length == 0 || data.get(0) != 'd'.code) return -1;
var end = MetaInfo.skipValue(data, 0);
return end <= data.length ? end : -1;
}
public function destroy():Void {
if (destroyed) return;
destroyed = true;
if (keepaliveTimer != null) {
keepaliveTimer.stop();
keepaliveTimer = null;
}
if (rtcPool != null) rtcPool.close();
for (t in httpTrackers) t.stop();
for (t in udpTrackers) t.stop();
for (w in [for (x in wires) x]) w.close();
wires.clear();
if (storage != null) storage.close();
}
public function deleteData():Void {
if (storage != null) storage.deleteData();
}
}

View File

@ -0,0 +1,407 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
import leenkx.network.torrent.storage.Storage;
import leenkx.network.torrent.storage.Storage.IStorage;
class TorrentClient {
public static var defaultTrackers:Array<String> = [];
public static var defaultDownloadDir:String = null;
public var downloadDir:String = null;
#if sys
static var mainQueue:sys.thread.Deque<Void -> Void> = null;
static var mainMutex:sys.thread.Mutex = null;
static var mainTimer:haxe.Timer = null;
#end
public static function initMain():Void {
#if sys
if (mainQueue != null) return;
mainQueue = new sys.thread.Deque();
mainMutex = new sys.thread.Mutex();
#end
}
public static function postMain(f:Void -> Void):Void {
#if sys
if (mainQueue == null) {
f();
return;
}
mainQueue.add(f);
mainMutex.acquire();
if (mainTimer == null) {
mainTimer = new haxe.Timer(5);
mainTimer.run = drainMain;
}
mainMutex.release();
#else
f();
#end
}
#if sys
static function drainMain():Void {
while (true) {
mainMutex.acquire();
var f = mainQueue.pop(false);
if (f == null) {
mainTimer.stop();
mainTimer = null;
mainMutex.release();
return;
}
mainMutex.release();
try {
f();
} catch(e:Dynamic) {}
}
}
#end
public var torrents:Map<String, Torrent> = [];
public var peerId:Bytes;
public var onError:String -> Void;
public var onTorrentAdded:Torrent -> Void;
public var onTorrentDone:Torrent -> Void;
public var listenPort:Int = 0;
var tcpListener:Dynamic = null;
public function new() {
initMain();
peerId = TorrentInfo.peerId();
}
public function listen(port:Int):Void {
#if (sys || kha_krom)
listenPort = port;
tcpListener = new leenkx.network.torrent.peer.TcpListener(port);
tcpListener.onPeer = function(ch, hex, key) {
var t = torrents.get(hex);
if (t == null) {
for (cand in torrents) {
if (cand.infoHashHex == hex) {
t = cand;
break;
}
}
}
if (t == null) {
ch.close();
return;
}
t.addIncomingPeer(key, ch);
};
tcpListener.onError = function(e) {
if (onError != null) onError(e);
};
tcpListener.start();
#end
}
public function add(torrentId:Dynamic, ?opts:Dynamic, ?cb:Torrent -> Void):Torrent {
if (opts != null && Reflect.isFunction(opts)) {
cb = opts;
opts = null;
}
var meta:MetaInfo = null;
var infoHash:Bytes = null;
var infoHashV2:Bytes = null;
var webSeeds:Array<String> = null;
var trackers = defaultTrackers.copy();
var name:String = null;
if (Std.isOfType(torrentId, Bytes)) {
meta = MetaInfo.fromTorrentBytes(torrentId);
infoHash = meta.infoHash;
infoHashV2 = meta.infoHashV2;
mergeTrackers(trackers, meta.announce);
} else {
var id = Std.string(torrentId);
var magnet = Magnet.parse(id);
if (magnet != null) {
infoHash = magnet.infoHash;
infoHashV2 = magnet.infoHashV2;
name = magnet.name;
webSeeds = magnet.webSeeds;
mergeTrackers(trackers, magnet.trackers);
} else if (id.length == 40 || id.length == 64) {
var bytes:Bytes = null;
try {
bytes = Bytes.ofHex(id);
} catch(e:Dynamic) {}
if (bytes == null) {
if (onError != null) {
onError("unrecognized torrent id: " + id);
}
return null;
}
if (bytes.length == 20) infoHash = bytes;
else infoHashV2 = bytes;
} else {
try {
meta = MetaInfo.fromTorrentBytes(Bytes.ofString(id));
infoHash = meta.infoHash;
infoHashV2 = meta.infoHashV2;
mergeTrackers(trackers, meta.announce);
} catch(e:Dynamic) {
if (onError != null) {
onError("unrecognized torrent id: " + id);
}
return null;
}
}
}
if (opts != null) {
var ann:Array<String> = Reflect.field(opts, "announce");
if (Reflect.field(opts, "announceOverride") == true) {
trackers = ann != null ? ann : [];
} else {
mergeTrackers(trackers, ann);
}
}
var key = infoHash != null ? Crypto.toHex(infoHash)
: (infoHashV2 != null ? "v2:" + Crypto.toHex(infoHashV2)
: null);
if (key == null) {
if (onError != null) onError("unrecognized torrent id");
return null;
}
var existing = torrents.get(key);
if (existing != null) {
if (cb != null) cb(existing);
return existing;
}
var wireHash = infoHash != null ? infoHash
: infoHashV2.sub(0, 20);
var t = new Torrent(wireHash, peerId, trackers, meta, null,
downloadDir != null ? downloadDir : defaultDownloadDir);
t.announcePort = listenPort > 0 ? listenPort
: 49152 + Std.random(16384);
t.infoHashV2 = infoHashV2;
if (infoHashV2 != null) {
t.infoHashV2Hex = Crypto.toHex(infoHashV2);
}
t.pendingWebSeeds = webSeeds;
if (name != null && meta == null) t.magnetURI = Magnet.build(
infoHash, name, trackers, webSeeds, infoHashV2);
t.on("error", function(e) {
if (onError != null) onError(e);
});
t.client = this;
torrents.set(key, t);
t.start();
if (onTorrentAdded != null) onTorrentAdded(t);
if (cb != null) cb(t);
return t;
}
public function seed(data:Dynamic, ?opts:Dynamic, ?cb:Torrent -> Void):Torrent {
if (opts != null && Reflect.isFunction(opts)) {
cb = opts;
opts = null;
}
var pair = buildPair(data, opts);
return seedMeta(pair.meta, pair.files, opts, cb);
}
public static function buildMeta(data:Dynamic, ?opts:Dynamic):MetaInfo {
return buildPair(data, opts).meta;
}
static function buildPair(data:Dynamic, ?opts:Dynamic):{
meta:MetaInfo, files:Array<{path:String, data:Bytes}>} {
var name = "leenkx";
var trackers = defaultTrackers.copy();
if (opts != null) {
var n = Reflect.field(opts, "name");
if (n != null) name = Std.string(n);
var ann:Array<String> = Reflect.field(opts, "announce");
mergeTrackers(trackers, ann);
}
var files:Array<{path:String, data:Bytes}> = [];
if (Std.isOfType(data, Bytes)) {
files.push({path: name, data: data});
} else if (Std.isOfType(data, Array)) {
for (f in (data:Array<Dynamic>)) {
var p = Reflect.field(f, "path");
var d:Bytes = Reflect.field(f, "data");
if (d == null && Reflect.field(f, "content") != null) {
d = Reflect.field(f, "content");
}
if (p != null && d != null) {
files.push({path: Std.string(p), data: d});
}
}
} else {
files.push({path: name, data: Bytes.ofString(Std.string(data))});
}
return {meta: MetaInfo.create(name, files, trackers, opts), files: files};
}
public static function readFolder(path:String, ?base:String):Array<{path:String, data:Bytes}> {
#if sys
var root = path;
while (root.length > 0 && (StringTools.endsWith(root, "/")
|| StringTools.endsWith(root, "\\"))) {
root = root.substr(0, root.length - 1);
}
if (base == null) {
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}> = [];
readFolderInto(root, base, files);
return files;
#else
throw "readFolder requires a sys target";
#end
}
#if sys
static function readFolderInto(dir:String, prefix:String, files:Array<{path:String, data:Bytes}>):Void {
for (entry in sys.FileSystem.readDirectory(dir)) {
var p = dir + "/" + entry;
var rel = prefix + "/" + entry;
if (sys.FileSystem.isDirectory(p)) {
readFolderInto(p, rel, files);
} else {
files.push({path: rel, data: sys.io.File.getBytes(p)});
}
}
}
#end
public function seedMeta(meta:MetaInfo, files:Array<{path:String, data:Bytes}>, ?opts:Dynamic, ?cb:Torrent -> Void):Torrent {
var trackers = meta.announce.copy();
if (opts != null) {
var ann:Array<String> = Reflect.field(opts, "announce");
mergeTrackers(trackers, ann);
}
var ms = new MemoryStorage(meta.storageLength > 0
? meta.storageLength : meta.totalLength);
for (i in 0...files.length) {
ms.write(meta.files[i].offset, files[i].data);
}
var storage:IStorage = ms;
var wireHash = meta.infoHash != null ? meta.infoHash
: meta.infoHashV2.sub(0, 20);
var t = new Torrent(wireHash, peerId, trackers, meta, storage);
t.announcePort = listenPort > 0 ? listenPort
: 49152 + Std.random(16384);
t.markComplete();
t.on("error", function(e) {
if (onError != null) onError(e);
});
t.client = this;
torrents.set(meta.infoHashHex != null ? meta.infoHashHex
: "v2:" + meta.infoHashV2Hex, t);
t.start();
if (onTorrentAdded != null) onTorrentAdded(t);
if (onTorrentDone != null) onTorrentDone(t);
if (cb != null) cb(t);
return t;
}
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);
}
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;
}
}
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) {
trackers.push(u);
}
}
}
public function destroy():Void {
for (t in torrents) t.destroy();
torrents.clear();
#if (sys || kha_krom)
if (tcpListener != null) {
tcpListener.stop();
tcpListener = null;
}
#end
}
}

View File

@ -0,0 +1,102 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
import leenkx.network.torrent.MetaInfo.TorrentFileEntry;
import leenkx.network.torrent.storage.Storage.IStorage;
class TorrentFile {
public var name:String;
public var path:String;
public var length:Int;
public var offset:Int;
var storage:IStorage;
public var torrent:Torrent;
public function new(entry:TorrentFileEntry, storage:IStorage, ?torrent:Torrent) {
this.path = entry.path;
this.name = entry.path.split("/").pop();
this.length = entry.length;
this.offset = entry.offset;
this.storage = storage;
this.torrent = torrent;
}
public var downloaded(get, never):Float;
function get_downloaded():Float {
if (torrent == null || torrent.pieces == null) return 0;
var pl = torrent.meta.pieceLength;
return Math.min(havePieces() * (pl:Float), length);
}
public var progress(get, never):Float;
function get_progress():Float {
return length > 0 ? downloaded / length : 0;
}
function havePieces():Int {
var pl = torrent.meta.pieceLength;
var first = Std.int(offset / pl);
var last = length > 0
? Std.int((offset + length - 1) / pl) : first;
var n = 0;
for (i in first...last + 1) {
if (torrent.pieces.have[i]) n++;
}
return n;
}
public function select(?priority:Int):Void {
if (torrent == null) return;
torrent.select(offset, offset + length - 1, priority);
}
public function deselect(?priority:Int):Void {
if (torrent == null) return;
torrent.deselect(offset, offset + length - 1, priority);
}
public function critical():Void {
if (torrent == null) return;
torrent.critical(offset, offset + length - 1);
}
public var type(get, never):String;
function get_type():String {
var i = name != null ? name.lastIndexOf(".") : -1;
var ext = i >= 0 ? name.substr(i + 1).toLowerCase() : "";
return switch (ext) {
case "mp4" | "m4v": "video/mp4";
case "webm": "video/webm";
case "mkv": "video/x-matroska";
case "mov": "video/quicktime";
case "avi": "video/x-msvideo";
case "mp3": "audio/mpeg";
case "ogg": "audio/ogg";
case "ogv": "video/ogg";
case "flac": "audio/flac";
case "wav": "audio/wav";
case "aac": "audio/aac";
case "jpg" | "jpeg": "image/jpeg";
case "png": "image/png";
case "gif": "image/gif";
case "webp": "image/webp";
case "svg": "image/svg+xml";
case "txt" | "log" | "md": "text/plain";
case "html" | "htm": "text/html";
case "css": "text/css";
case "json": "application/json";
case "pdf": "application/pdf";
case "zip": "application/zip";
default: "application/octet-stream";
}
}
public function getBytes():Bytes {
return storage.read(offset, length);
}
public function toString():String {
return getBytes().toString();
}
}

View File

@ -0,0 +1,72 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class TorrentInfo {
public static inline var PEERID_PREFIX = "-LX0001-";
public static inline var PEERID_LENGTH = 20;
public static inline var HASH_LENGTH = 20;
public static inline var MIN_PIECE_LENGTH = 16384;
public static function pieceLength(size:Int):Int {
var scaled = size < 1024 ? 1.0 : size / 1024;
var exp = Std.int(Math.log(scaled) / Math.log(2) + 0.5);
if (exp < 0) exp = 0;
if (exp > 30) exp = 30;
var len = 1 << exp;
return len > MIN_PIECE_LENGTH ? len : MIN_PIECE_LENGTH;
}
public static function pieces(content:Bytes, plen:Int):Bytes {
var count = content.length == 0 ? 0 : Math.ceil(content.length / plen);
var out = Bytes.alloc(count * HASH_LENGTH);
for (i in 0...count) {
var off = i * plen;
var len = content.length - off;
if (len > plen) len = plen;
var sha = haxe.crypto.Sha1.make(content.sub(off, len));
out.blit(i * HASH_LENGTH, sha, 0, HASH_LENGTH);
}
return out;
}
public static function infoDict(name:String, content:Bytes):Dynamic {
var plen = pieceLength(content.length);
var info:Dynamic = {};
Reflect.setField(info, "length", content.length);
Reflect.setField(info, "name", name);
Reflect.setField(info, "piece length", plen);
Reflect.setField(info, "pieces", pieces(content, plen));
return info;
}
public static function infoHash(name:String, content:Bytes):Bytes {
return haxe.crypto.Sha1.make(Bencode.encode(infoDict(name, content)));
}
public static function identifierHash(identifier:String):Bytes {
return infoHash(identifier, Bytes.ofString(identifier));
}
public static function peerId():Bytes {
var prefix = Bytes.ofString(PEERID_PREFIX);
var random = Bytes.ofString(Crypto.toHex(Crypto.randomBytes(6)));
var out = Bytes.alloc(PEERID_LENGTH);
out.blit(0, prefix, 0, prefix.length);
out.blit(prefix.length, random, 0, PEERID_LENGTH - prefix.length);
return out;
}
public static function toBinaryString(bytes:Bytes):String {
var sb = new StringBuf();
for (i in 0...bytes.length) sb.addChar(bytes.get(i));
return sb.toString();
}
public static function fromBinaryString(str:String):Bytes {
var bytes = Bytes.alloc(str.length);
for (i in 0...str.length) bytes.set(i, str.charCodeAt(i) & 0xFF);
return bytes;
}
}

View File

@ -0,0 +1,733 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class TweetNaCl {
static var _0:Array<Int> = [for (i in 0...16) 0];
static var _9:Array<Int> = { var a = [for (i in 0...32) 0]; a[0] = 9; a; };
static var sigma:Array<Int> = [101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107];
static var minusp:Array<Int> = [5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252];
static var L:Array<Float> = [0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10];
static inline function gf(?init:Array<Float>):Array<Float> {
var r = [for (i in 0...16) 0.0];
if (init != null) for (i in 0...init.length) r[i] = init[i];
return r;
}
static var gf0 = gf();
static var gf1 = gf([1]);
static var _121665 = gf([0xdb41, 1]);
static var D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]);
static var D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]);
static var X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]);
static var Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]);
static var I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]);
static var K:Array<Array<Int>> = [
[0x428a2f98, 0xd728ae22], [0x71374491, 0x23ef65cd], [0xb5c0fbcf, 0xec4d3b2f], [0xe9b5dba5, 0x8189dbbc],
[0x3956c25b, 0xf348b538], [0x59f111f1, 0xb605d019], [0x923f82a4, 0xaf194f9b], [0xab1c5ed5, 0xda6d8118],
[0xd807aa98, 0xa3030242], [0x12835b01, 0x45706fbe], [0x243185be, 0x4ee4b28c], [0x550c7dc3, 0xd5ffb4e2],
[0x72be5d74, 0xf27b896f], [0x80deb1fe, 0x3b1696b1], [0x9bdc06a7, 0x25c71235], [0xc19bf174, 0xcf692694],
[0xe49b69c1, 0x9ef14ad2], [0xefbe4786, 0x384f25e3], [0x0fc19dc6, 0x8b8cd5b5], [0x240ca1cc, 0x77ac9c65],
[0x2de92c6f, 0x592b0275], [0x4a7484aa, 0x6ea6e483], [0x5cb0a9dc, 0xbd41fbd4], [0x76f988da, 0x831153b5],
[0x983e5152, 0xee66dfab], [0xa831c66d, 0x2db43210], [0xb00327c8, 0x98fb213f], [0xbf597fc7, 0xbeef0ee4],
[0xc6e00bf3, 0x3da88fc2], [0xd5a79147, 0x930aa725], [0x06ca6351, 0xe003826f], [0x14292967, 0x0a0e6e70],
[0x27b70a85, 0x46d22ffc], [0x2e1b2138, 0x5c26c926], [0x4d2c6dfc, 0x5ac42aed], [0x53380d13, 0x9d95b3df],
[0x650a7354, 0x8baf63de], [0x766a0abb, 0x3c77b2a8], [0x81c2c92e, 0x47edaee6], [0x92722c85, 0x1482353b],
[0xa2bfe8a1, 0x4cf10364], [0xa81a664b, 0xbc423001], [0xc24b8b70, 0xd0f89791], [0xc76c51a3, 0x0654be30],
[0xd192e819, 0xd6ef5218], [0xd6990624, 0x5565a910], [0xf40e3585, 0x5771202a], [0x106aa070, 0x32bbd1b8],
[0x19a4c116, 0xb8d2d0c8], [0x1e376c08, 0x5141ab53], [0x2748774c, 0xdf8eeb99], [0x34b0bcb5, 0xe19b48a8],
[0x391c0cb3, 0xc5c95a63], [0x4ed8aa4a, 0xe3418acb], [0x5b9cca4f, 0x7763e373], [0x682e6ff3, 0xd6b2b8a3],
[0x748f82ee, 0x5defb2fc], [0x78a5636f, 0x43172f60], [0x84c87814, 0xa1f0ab72], [0x8cc70208, 0x1a6439ec],
[0x90befffa, 0x23631e28], [0xa4506ceb, 0xde82bde9], [0xbef9a3f7, 0xb2c67915], [0xc67178f2, 0xe372532b],
[0xca273ece, 0xea26619c], [0xd186b8c7, 0x21c0c207], [0xeada7dd6, 0xcde0eb1e], [0xf57d4f7f, 0xee6ed178],
[0x06f067aa, 0x72176fba], [0x0a637dc5, 0xa2c898a6], [0x113f9804, 0xbef90dae], [0x1b710b35, 0x131c471b],
[0x28db77f5, 0x23047d84], [0x32caab7b, 0x40c72493], [0x3c9ebe0a, 0x15c9bebc], [0x431d67c4, 0x9c100d4c],
[0x4cc5d4be, 0xcb3e42b6], [0x597f299c, 0xfc657e2a], [0x5fcb6fab, 0x3ad6faec], [0x6c44198c, 0x4a475817]
];
static var iv:Array<Int> = [
0x6a, 0x09, 0xe6, 0x67, 0xf3, 0xbc, 0xc9, 0x08,
0xbb, 0x67, 0xae, 0x85, 0x84, 0xca, 0xa7, 0x3b,
0x3c, 0x6e, 0xf3, 0x72, 0xfe, 0x94, 0xf8, 0x2b,
0xa5, 0x4f, 0xf5, 0x3a, 0x5f, 0x1d, 0x36, 0xf1,
0x51, 0x0e, 0x52, 0x7f, 0xad, 0xe6, 0x82, 0xd1,
0x9b, 0x05, 0x68, 0x8c, 0x2b, 0x3e, 0x6c, 0x1f,
0x1f, 0x83, 0xd9, 0xab, 0xfb, 0x41, 0xbd, 0x6b,
0x5b, 0xe0, 0xcd, 0x19, 0x13, 0x7e, 0x21, 0x79
];
static inline function u64(h:Int, l:Int):Array<Int> { return [h, l]; }
static inline function u64add(a:Array<Int>, b:Array<Int>):Array<Int> {
var a0 = a[1] & 65535, a1 = a[1] >>> 16, a2 = a[0] & 65535, a3 = a[0] >>> 16;
var b0 = b[1] & 65535, b1 = b[1] >>> 16, b2 = b[0] & 65535, b3 = b[0] >>> 16;
var s0 = a0 + b0; var s1 = a1 + b1 + (s0 >>> 16);
var s2 = a2 + b2 + (s1 >>> 16); var s3 = a3 + b3 + (s2 >>> 16);
return [(s2 & 65535) | ((s3 & 65535) << 16), (s0 & 65535) | ((s1 & 65535) << 16)];
}
static function u64addMulti(args:Array<Array<Int>>):Array<Int> {
var a0 = 0, a1 = 0, a2 = 0, a3 = 0;
for (v in args) {
a0 += v[1] & 65535; a1 += v[1] >>> 16;
a2 += v[0] & 65535; a3 += v[0] >>> 16;
}
a1 += a0 >>> 16; a2 += a1 >>> 16; a3 += a2 >>> 16;
return [(a2 & 65535) | ((a3 & 65535) << 16), (a0 & 65535) | ((a1 & 65535) << 16)];
}
static inline function u64xor(a:Array<Int>, b:Array<Int>):Array<Int> {
return [a[0] ^ b[0], a[1] ^ b[1]];
}
static function u64xorMulti(args:Array<Array<Int>>):Array<Int> {
var h = 0, l = 0;
for (v in args) { h ^= v[0]; l ^= v[1]; }
return [h, l];
}
static function u64R(x:Array<Int>, c:Int):Array<Int> {
var h:Int, l:Int, c1 = 32 - c;
if (c < 32) {
h = (x[0] >>> c) | (x[1] << c1);
l = (x[1] >>> c) | (x[0] << c1);
} else {
h = (x[1] >>> (c - 32)) | (x[0] << (64 - c));
l = (x[0] >>> (c - 32)) | (x[1] << (64 - c));
}
return [h, l];
}
static inline function u64shr(x:Array<Int>, c:Int):Array<Int> {
return [x[0] >>> c, (x[1] >>> c) | (x[0] << (32 - c))];
}
static inline function u64Ch(x:Array<Int>, y:Array<Int>, z:Array<Int>):Array<Int> {
return [(x[0] & y[0]) ^ (~x[0] & z[0]), (x[1] & y[1]) ^ (~x[1] & z[1])];
}
static inline function u64Maj(x:Array<Int>, y:Array<Int>, z:Array<Int>):Array<Int> {
return [(x[0] & y[0]) ^ (x[0] & z[0]) ^ (y[0] & z[0]), (x[1] & y[1]) ^ (x[1] & z[1]) ^ (y[1] & z[1])];
}
static inline function u64Sigma0(x:Array<Int>):Array<Int> { return u64xorMulti([u64R(x, 28), u64R(x, 34), u64R(x, 39)]); }
static inline function u64Sigma1(x:Array<Int>):Array<Int> { return u64xorMulti([u64R(x, 14), u64R(x, 18), u64R(x, 41)]); }
static inline function u64sigma0(x:Array<Int>):Array<Int> { return u64xorMulti([u64R(x, 1), u64R(x, 8), u64shr(x, 7)]); }
static inline function u64sigma1(x:Array<Int>):Array<Int> { return u64xorMulti([u64R(x, 19), u64R(x, 61), u64shr(x, 6)]); }
static function dl64(x:Array<Int>, i:Int):Array<Int> {
var h = (x[i] << 24) | (x[i + 1] << 16) | (x[i + 2] << 8) | x[i + 3];
var l = (x[i + 4] << 24) | (x[i + 5] << 16) | (x[i + 6] << 8) | x[i + 7];
return [h, l];
}
static function ts64u(x:Array<Int>, i:Int, v:Array<Int>):Void {
x[i] = (v[0] >> 24) & 0xff;
x[i + 1] = (v[0] >> 16) & 0xff;
x[i + 2] = (v[0] >> 8) & 0xff;
x[i + 3] = v[0] & 0xff;
x[i + 4] = (v[1] >> 24) & 0xff;
x[i + 5] = (v[1] >> 16) & 0xff;
x[i + 6] = (v[1] >> 8) & 0xff;
x[i + 7] = v[1] & 0xff;
}
static function L32(x:Int, c:Int):Int { return (x << c) | (x >>> (32 - c)); }
static function ld32(x:Array<Int>, i:Int):Int {
var u = x[i + 3] & 0xff;
u = (u << 8) | (x[i + 2] & 0xff);
u = (u << 8) | (x[i + 1] & 0xff);
return (u << 8) | (x[i + 0] & 0xff);
}
static function st32(x:Array<Int>, j:Int, u:Int):Void {
for (i in 0...4) { x[j + i] = u & 255; u = u >>> 8; }
}
static function vn(x:Array<Int>, xi:Int, y:Array<Int>, yi:Int, n:Int):Int {
var d = 0;
for (i in 0...n) d |= x[xi + i] ^ y[yi + i];
return (1 & ((d - 1) >>> 8)) - 1;
}
static function crypto_verify_16(x:Array<Int>, xi:Int, y:Array<Int>, yi:Int):Int { return vn(x, xi, y, yi, 16); }
static function crypto_verify_32(x:Array<Int>, xi:Int, y:Array<Int>, yi:Int):Int { return vn(x, xi, y, yi, 32); }
static function core(out:Array<Int>, inp:Array<Int>, k:Array<Int>, c:Array<Int>, h:Bool):Void {
var w = [for (i in 0...16) 0], x = [for (i in 0...16) 0], y = [for (i in 0...16) 0], t = [for (i in 0...4) 0];
for (i in 0...4) {
x[5 * i] = ld32(c, 4 * i); x[1 + i] = ld32(k, 4 * i);
x[6 + i] = ld32(inp, 4 * i); x[11 + i] = ld32(k, 16 + 4 * i);
}
for (i in 0...16) y[i] = x[i];
for (i in 0...20) {
for (j in 0...4) {
for (m in 0...4) t[m] = x[(5 * j + 4 * m) % 16];
t[1] ^= L32(t[0] + t[3], 7); t[2] ^= L32(t[1] + t[0], 9);
t[3] ^= L32(t[2] + t[1], 13); t[0] ^= L32(t[3] + t[2], 18);
for (m in 0...4) w[4 * j + (j + m) % 4] = t[m];
}
for (m in 0...16) x[m] = w[m];
}
if (h) {
for (i in 0...16) x[i] += y[i];
for (i in 0...4) { x[5 * i] -= ld32(c, 4 * i); x[6 + i] -= ld32(inp, 4 * i); }
for (i in 0...4) { st32(out, 4 * i, x[5 * i]); st32(out, 16 + 4 * i, x[6 + i]); }
} else {
for (i in 0...16) st32(out, 4 * i, x[i] + y[i]);
}
}
static function crypto_stream_salsa20_xor(c:Array<Int>, cpos:Int, m:Array<Int>, mpos:Int, b:Int, n:Array<Int>, k:Array<Int>):Int {
var z = [for (i in 0...16) 0], x = [for (i in 0...64) 0];
if (b == 0) return 0;
for (i in 0...8) z[i] = n[i];
while (b >= 64) {
crypto_core_salsa20(x, z, k, sigma);
for (i in 0...64) c[cpos + i] = (m != null ? m[mpos + i] : 0) ^ x[i];
var u = 1;
for (i in 8...16) { u += z[i]; z[i] = u & 0xff; u >>>= 8; }
b -= 64; cpos += 64; if (m != null) mpos += 64;
}
if (b > 0) {
crypto_core_salsa20(x, z, k, sigma);
for (i in 0...b) c[cpos + i] = (m != null ? m[mpos + i] : 0) ^ x[i];
}
return 0;
}
static function crypto_stream(c:Array<Int>, cpos:Int, d:Int, n:Array<Int>, k:Array<Int>):Int {
var np = [for (i in 0...32) 0];
for (i in 0...n.length) np[i] = n[i];
var s = [for (i in 0...32) 0];
crypto_core_hsalsa20(s, np, k, sigma);
var n2 = [for (i in 0...16) 0];
for (i in 0...16) n2[i] = np[16 + i];
return crypto_stream_salsa20(c, cpos, d, n2, s);
}
static function crypto_stream_xor(c:Array<Int>, cpos:Int, m:Array<Int>, mpos:Int, d:Int, n:Array<Int>, k:Array<Int>):Int {
var np = [for (i in 0...32) 0];
for (i in 0...n.length) np[i] = n[i];
var s = [for (i in 0...32) 0];
crypto_core_hsalsa20(s, np, k, sigma);
var n2 = [for (i in 0...16) 0];
for (i in 0...16) n2[i] = np[16 + i];
return crypto_stream_salsa20_xor(c, cpos, m, mpos, d, n2, s);
}
static inline function crypto_core_salsa20(out:Array<Int>, inp:Array<Int>, k:Array<Int>, c:Array<Int>):Int { core(out, inp, k, c, false); return 0; }
static inline function crypto_core_hsalsa20(out:Array<Int>, inp:Array<Int>, k:Array<Int>, c:Array<Int>):Int { core(out, inp, k, c, true); return 0; }
static inline function crypto_stream_salsa20(c:Array<Int>, cpos:Int, d:Int, n:Array<Int>, k:Array<Int>):Int { return crypto_stream_salsa20_xor(c, cpos, null, 0, d, n, k); }
static function add1305(h:Array<Int>, c:Array<Int>):Void {
var u = 0;
for (j in 0...17) { u += h[j] + c[j]; h[j] = u & 255; u >>>= 8; }
}
static function crypto_onetimeauth(out:Array<Int>, outpos:Int, m:Array<Int>, mpos:Int, n:Int, k:Array<Int>):Int {
var s:Int, i:Int, j:Int, u:Int;
var x = [for (i in 0...17) 0], r = [for (i in 0...17) 0], h = [for (i in 0...17) 0], c = [for (i in 0...17) 0], g = [for (i in 0...17) 0];
for (j in 0...17) r[j] = h[j] = 0;
for (j in 0...16) r[j] = k[j];
r[3] &= 15; r[4] &= 252; r[7] &= 15; r[8] &= 252; r[11] &= 15; r[12] &= 252; r[15] &= 15;
while (n > 0) {
for (j in 0...17) c[j] = 0;
j = 0; while (j < 16 && j < n) { c[j] = m[mpos + j]; j++; }
c[j] = 1; mpos += j; n -= j;
add1305(h, c);
for (i in 0...17) {
x[i] = 0;
for (j in 0...17) x[i] += Std.int(h[j] * ((j <= i) ? r[i - j] : (320 * r[i + 17 - j])));
}
for (i in 0...17) h[i] = x[i];
u = 0;
for (j in 0...16) { u += h[j]; h[j] = u & 255; u >>>= 8; }
u += h[16]; h[16] = u & 3; u = 5 * (u >>> 2);
for (j in 0...16) { u += h[j]; h[j] = u & 255; u >>>= 8; }
u += h[16]; h[16] = u;
}
for (j in 0...17) g[j] = h[j];
add1305(h, minusp);
s = -(h[16] >>> 7);
for (j in 0...17) h[j] ^= s & (g[j] ^ h[j]);
for (j in 0...16) c[j] = k[j + 16];
c[16] = 0; add1305(h, c);
for (j in 0...16) out[outpos + j] = h[j];
return 0;
}
static function crypto_secretbox(c:Array<Int>, m:Array<Int>, d:Int, n:Array<Int>, k:Array<Int>):Int {
if (d < 32) return -1;
crypto_stream_xor(c, 0, m, 0, d, n, k);
crypto_onetimeauth(c, 16, c, 32, d - 32, c);
for (i in 0...16) c[i] = 0;
return 0;
}
static function crypto_secretbox_open(m:Array<Int>, c:Array<Int>, d:Int, n:Array<Int>, k:Array<Int>):Int {
var x = [for (i in 0...32) 0];
if (d < 32) return -1;
crypto_stream(x, 0, 32, n, k);
if (crypto_onetimeauth_verify(c, 16, c, 32, d - 32, x) != 0) return -1;
crypto_stream_xor(m, 0, c, 0, d, n, k);
for (i in 0...32) m[i] = 0;
return 0;
}
static inline function crypto_onetimeauth_verify(h:Array<Int>, hpos:Int, m:Array<Int>, mpos:Int, n:Int, k:Array<Int>):Int {
var x = [for (i in 0...16) 0];
crypto_onetimeauth(x, 0, m, mpos, n, k);
return crypto_verify_16(h, hpos, x, 0);
}
static function set25519(r:Array<Float>, a:Array<Float>):Void { for (i in 0...16) r[i] = a[i]; }
static function car25519(o:Array<Float>):Void {
var c:Float;
for (i in 0...16) {
o[i] += 65536;
c = Math.floor(o[i] / 65536);
o[(i + 1) * (i < 15 ? 1 : 0)] += c - 1 + 37 * (c - 1) * (i == 15 ? 1 : 0);
o[i] -= c * 65536;
}
}
static function sel25519(p:Array<Float>, q:Array<Float>, b:Int):Void {
var c = ~((b - 1));
for (i in 0...16) {
var t:Int = c & (Std.int(p[i]) ^ Std.int(q[i]));
p[i] = Std.int(p[i]) ^ t; q[i] = Std.int(q[i]) ^ t;
}
}
static function pack25519(o:Array<Int>, n:Array<Float>):Void {
var m = gf(), t = gf();
for (i in 0...16) t[i] = n[i];
car25519(t); car25519(t); car25519(t);
for (j in 0...2) {
m[0] = t[0] - 0xffed;
for (i in 1...15) {
m[i] = t[i] - 0xffff - ((Std.int(m[i - 1]) >> 16) & 1);
m[i - 1] = Std.int(m[i - 1]) & 0xffff;
}
m[15] = t[15] - 0x7fff - ((Std.int(m[14]) >> 16) & 1);
var b = (Std.int(m[15]) >> 16) & 1;
m[14] = Std.int(m[14]) & 0xffff;
sel25519(t, m, 1 - b);
}
for (i in 0...16) {
o[2 * i] = Std.int(t[i]) & 0xff;
o[2 * i + 1] = Std.int(t[i]) >> 8;
}
}
static function neq25519(a:Array<Float>, b:Array<Float>):Int {
var c = [for (i in 0...32) 0], d = [for (i in 0...32) 0];
pack25519(c, a); pack25519(d, b);
return crypto_verify_32(c, 0, d, 0);
}
static function par25519(a:Array<Float>):Int {
var d = [for (i in 0...32) 0];
pack25519(d, a);
return d[0] & 1;
}
static function unpack25519(o:Array<Float>, n:Array<Int>):Void {
for (i in 0...16) o[i] = n[2 * i] + (n[2 * i + 1] << 8);
o[15] = Std.int(o[15]) & 0x7fff;
}
static function A(o:Array<Float>, a:Array<Float>, b:Array<Float>):Void { for (i in 0...16) o[i] = a[i] + b[i]; }
static function Z(o:Array<Float>, a:Array<Float>, b:Array<Float>):Void { for (i in 0...16) o[i] = a[i] - b[i]; }
static function M(o:Array<Float>, a:Array<Float>, b:Array<Float>):Void {
var t = [for (i in 0...31) 0.0];
for (i in 0...16) for (j in 0...16) t[i + j] += a[i] * b[j];
for (i in 0...15) t[i] += 38 * t[i + 16];
for (i in 0...16) o[i] = t[i];
car25519(o); car25519(o);
}
static inline function S(o:Array<Float>, a:Array<Float>):Void { M(o, a, a); }
static function inv25519(o:Array<Float>, i:Array<Float>):Void {
var c = gf();
for (a in 0...16) c[a] = i[a];
var a = 253; while (a >= 0) {
S(c, c);
if (a != 2 && a != 4) M(c, c, i);
a--;
}
for (a in 0...16) o[a] = c[a];
}
static function pow2523(o:Array<Float>, i:Array<Float>):Void {
var c = gf();
for (a in 0...16) c[a] = i[a];
var a = 250; while (a >= 0) {
S(c, c);
if (a != 1) M(c, c, i);
a--;
}
for (a in 0...16) o[a] = c[a];
}
static function crypto_scalarmult(q:Array<Int>, n:Array<Int>, p:Array<Int>):Int {
var z = [for (i in 0...32) 0];
var x = [for (i in 0...80) 0.0];
var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf();
for (i in 0...31) z[i] = n[i];
z[31] = (n[31] & 127) | 64;
z[0] &= 248;
unpack25519(x, p);
for (i in 0...16) { b[i] = x[i]; d[i] = a[i] = c[i] = 0; }
a[0] = d[0] = 1;
var r:Int;
var i = 254; while (i >= 0) {
r = (z[i >>> 3] >>> (i & 7)) & 1;
sel25519(a, b, r); sel25519(c, d, r);
A(e, a, c); Z(a, a, c); A(c, b, d); Z(b, b, d);
S(d, e); S(f, a); M(a, c, a); M(c, b, e);
A(e, a, c); Z(a, a, c); S(b, a); Z(c, d, f);
M(a, c, _121665); A(a, a, d); M(c, c, a); M(a, d, f); M(d, b, x);
S(b, e); sel25519(a, b, r); sel25519(c, d, r);
i--;
}
for (i in 0...16) {
x[i + 16] = a[i]; x[i + 32] = c[i]; x[i + 48] = b[i]; x[i + 64] = d[i];
}
var x32 = [for (i in 0...16) 0.0]; for (i in 0...16) x32[i] = x[i + 32];
var x16 = [for (i in 0...16) 0.0]; for (i in 0...16) x16[i] = x[i + 16];
inv25519(x32, x32);
M(x16, x16, x32);
pack25519(q, x16);
return 0;
}
static function crypto_scalarmult_base(q:Array<Int>, n:Array<Int>):Int { return crypto_scalarmult(q, n, _9); }
static function crypto_box_beforenm(k:Array<Int>, y:Array<Int>, x:Array<Int>):Int {
var s = [for (i in 0...32) 0];
crypto_scalarmult(s, x, y);
return crypto_core_hsalsa20(k, _0, s, sigma);
}
static function crypto_box(c:Array<Int>, m:Array<Int>, d:Int, n:Array<Int>, y:Array<Int>, x:Array<Int>):Int {
var k = [for (i in 0...32) 0];
crypto_box_beforenm(k, y, x);
return crypto_secretbox(c, m, d, n, k);
}
static function crypto_box_open(m:Array<Int>, c:Array<Int>, d:Int, n:Array<Int>, y:Array<Int>, x:Array<Int>):Int {
var k = [for (i in 0...32) 0];
crypto_box_beforenm(k, y, x);
return crypto_secretbox_open(m, c, d, n, k);
}
static function crypto_box_keypair(y:Array<Int>, x:Array<Int>, randomFn:Int->Array<Int>):Int {
var rb = randomFn(32);
for (i in 0...32) x[i] = rb[i];
return crypto_scalarmult_base(y, x);
}
static function crypto_hashblocks(x:Array<Int>, m:Array<Int>, mpos:Int, n:Int):Int {
var z = [for (i in 0...8) null], b = [for (i in 0...8) null], a = [for (i in 0...8) null], w = [for (i in 0...16) null];
for (i in 0...8) z[i] = a[i] = dl64(x, 8 * i);
while (n >= 128) {
for (i in 0...16) w[i] = dl64(m, 8 * i + mpos);
for (i in 0...80) {
for (j in 0...8) b[j] = a[j];
var t = u64addMulti([a[7], u64Sigma1(a[4]), u64Ch(a[4], a[5], a[6]), u64(K[i][0], K[i][1]), w[i % 16]]);
b[7] = u64addMulti([t, u64Sigma0(a[0]), u64Maj(a[0], a[1], a[2])]);
b[3] = u64add(b[3], t);
for (j in 0...8) a[(j + 1) % 8] = b[j];
if (i % 16 == 15) {
for (j in 0...16) w[j] = u64addMulti([w[j], w[(j + 9) % 16], u64sigma0(w[(j + 1) % 16]), u64sigma1(w[(j + 14) % 16])]);
}
}
for (i in 0...8) { a[i] = u64add(a[i], z[i]); z[i] = a[i]; }
mpos += 128; n -= 128;
}
for (i in 0...8) ts64u(x, 8 * i, z[i]);
return n;
}
static function crypto_hash(out:Array<Int>, m:Array<Int>, n:Int):Int {
var h = [for (i in 0...64) 0], x = [for (i in 0...256) 0];
var b = n;
for (i in 0...64) h[i] = iv[i];
crypto_hashblocks(h, m, 0, n);
n = n % 128;
for (i in 0...256) x[i] = 0;
for (i in 0...n) x[i] = m[b - n + i];
x[n] = 128;
var sz = 256 - 128 * (n < 112 ? 1 : 0);
x[sz - 9] = 0;
ts64u(x, sz - 8, u64(Std.int(b / 0x20000000), b << 3));
crypto_hashblocks(h, x, 0, sz);
for (i in 0...64) out[i] = h[i];
return 0;
}
static function edAdd(p:Array<Array<Float>>, q:Array<Array<Float>>):Void {
var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf(), g = gf(), h = gf(), t = gf();
Z(a, p[1], p[0]); Z(t, q[1], q[0]); M(a, a, t);
A(b, p[0], p[1]); A(t, q[0], q[1]); M(b, b, t);
M(c, p[3], q[3]); M(c, c, D2);
M(d, p[2], q[2]); A(d, d, d);
Z(e, b, a); Z(f, d, c); A(g, d, c); A(h, b, a);
M(p[0], e, f); M(p[1], h, g); M(p[2], g, f); M(p[3], e, h);
}
static function cswap(p:Array<Array<Float>>, q:Array<Array<Float>>, b:Int):Void {
for (i in 0...4) sel25519(p[i], q[i], b);
}
static function edPack(r:Array<Int>, p:Array<Array<Float>>):Void {
var tx = gf(), ty = gf(), zi = gf();
inv25519(zi, p[2]); M(tx, p[0], zi); M(ty, p[1], zi);
pack25519(r, ty);
r[31] ^= par25519(tx) << 7;
}
static function edScalarmult(p:Array<Array<Float>>, q:Array<Array<Float>>, s:Array<Int>):Void {
set25519(p[0], gf0); set25519(p[1], gf1); set25519(p[2], gf1); set25519(p[3], gf0);
var i = 255; while (i >= 0) {
var b = (s[Std.int(i / 8)] >> (i & 7)) & 1;
cswap(p, q, b);
edAdd(q, p);
edAdd(p, p);
cswap(p, q, b);
i--;
}
}
static function edScalarbase(p:Array<Array<Float>>, s:Array<Int>):Void {
var q = [gf(), gf(), gf(), gf()];
set25519(q[0], X); set25519(q[1], Y); set25519(q[2], gf1);
M(q[3], X, Y);
edScalarmult(p, q, s);
}
static function crypto_sign_keypair(pk:Array<Int>, sk:Array<Int>, seeded:Bool, randomFn:Int->Array<Int>):Int {
var d = [for (i in 0...64) 0];
var p = [gf(), gf(), gf(), gf()];
if (!seeded) {
var rb = randomFn(32);
for (i in 0...32) sk[i] = rb[i];
}
crypto_hash(d, sk, 32);
d[0] &= 248; d[31] &= 127; d[31] |= 64;
edScalarbase(p, d);
edPack(pk, p);
for (i in 0...32) sk[i + 32] = pk[i];
return 0;
}
static function modL(r:Array<Int>, roff:Int, x:Array<Float>):Void {
var carry:Int, i:Int, j:Int, k:Int;
i = 63; while (i >= 32) {
carry = 0;
j = i - 32; k = i - 12;
while (j < k) {
x[j] += carry - 16 * x[i] * L[j - (i - 32)];
carry = Std.int(Math.floor((x[j] + 128) / 256));
x[j] -= carry * 256;
j++;
}
x[j] += carry;
x[i] = 0;
i--;
}
carry = 0;
for (j in 0...32) {
x[j] += carry - (Std.int(x[31]) >> 4) * L[j];
carry = Std.int(x[j]) >> 8;
x[j] = Std.int(x[j]) & 255;
}
for (j in 0...32) x[j] -= carry * L[j];
for (i in 0...32) {
x[i + 1] += Std.int(x[i]) >> 8;
r[roff + i] = Std.int(x[i]) & 255;
}
}
static function reduce(r:Array<Int>):Void {
var x = [for (i in 0...64) 0.0];
for (i in 0...64) x[i] = r[i];
for (i in 0...64) r[i] = 0;
modL(r, 0, x);
}
static function crypto_sign(sm:Array<Int>, m:Array<Int>, n:Int, sk:Array<Int>):Int {
var d = [for (i in 0...64) 0], h = [for (i in 0...64) 0], r = [for (i in 0...64) 0];
var x = [for (i in 0...64) 0.0];
var p = [gf(), gf(), gf(), gf()];
crypto_hash(d, sk, 32);
d[0] &= 248; d[31] &= 127; d[31] |= 64;
for (i in 0...n) sm[64 + i] = m[i];
for (i in 0...32) sm[32 + i] = d[32 + i];
var sm32 = [for (i in 0...n + 32) 0];
for (i in 0...n + 32) sm32[i] = sm[32 + i];
crypto_hash(r, sm32, n + 32);
reduce(r);
edScalarbase(p, r);
edPack(sm, p);
for (i in 32...64) sm[i] = sk[i];
var smFull = [for (i in 0...n + 64) 0];
for (i in 0...n + 64) smFull[i] = sm[i];
crypto_hash(h, smFull, n + 64);
reduce(h);
for (i in 0...64) x[i] = 0;
for (i in 0...32) x[i] = r[i];
for (i in 0...32) for (j in 0...32) x[i + j] += h[i] * d[j];
modL(sm, 32, x);
return n + 64;
}
static function unpackneg(r:Array<Array<Float>>, p:Array<Int>):Int {
var t = gf(), chk = gf(), num = gf(), den = gf(), den2 = gf(), den4 = gf(), den6 = gf();
set25519(r[2], gf1);
unpack25519(r[1], p);
S(num, r[1]); M(den, num, D); Z(num, num, r[2]); A(den, r[2], den);
S(den2, den); S(den4, den2); M(den6, den4, den2);
M(t, den6, num); M(t, t, den);
pow2523(t, t);
M(t, t, num); M(t, t, den); M(t, t, den);
M(r[0], t, den);
S(chk, r[0]); M(chk, chk, den);
if (neq25519(chk, num) != 0) M(r[0], r[0], I);
S(chk, r[0]); M(chk, chk, den);
if (neq25519(chk, num) != 0) return -1;
if (par25519(r[0]) == (p[31] >> 7)) Z(r[0], gf0, r[0]);
M(r[3], r[0], r[1]);
return 0;
}
static function crypto_sign_open(m:Array<Int>, sm:Array<Int>, n:Int, pk:Array<Int>):Int {
var t = [for (i in 0...32) 0], h = [for (i in 0...64) 0];
var p = [gf(), gf(), gf(), gf()], q = [gf(), gf(), gf(), gf()];
if (n < 64) return -1;
if (unpackneg(q, pk) != 0) return -1;
for (i in 0...n) m[i] = sm[i];
for (i in 0...32) m[i + 32] = pk[i];
crypto_hash(h, m, n);
reduce(h);
edScalarmult(p, q, h);
var sm32 = [for (i in 0...32) 0];
for (i in 0...32) sm32[i] = sm[32 + i];
edScalarbase(q, sm32);
edAdd(p, q);
edPack(t, p);
n -= 64;
if (crypto_verify_32(sm, 0, t, 0) != 0) {
for (i in 0...n) m[i] = 0;
return -1;
}
for (i in 0...n) m[i] = sm[i + 64];
return n;
}
public static inline var crypto_box_PUBLICKEYBYTES = 32;
public static inline var crypto_box_SECRETKEYBYTES = 32;
public static inline var crypto_box_NONCEBYTES = 24;
public static inline var crypto_box_ZEROBYTES = 32;
public static inline var crypto_box_BOXZEROBYTES = 16;
public static inline var crypto_sign_BYTES = 64;
public static inline var crypto_sign_PUBLICKEYBYTES = 32;
public static inline var crypto_sign_SECRETKEYBYTES = 64;
public static inline var crypto_sign_SEEDBYTES = 32;
public static inline var crypto_hash_BYTES = 64;
public static function hash(msg:Bytes):Bytes {
var m = bytesToArray(msg);
var out = [for (i in 0...64) 0];
crypto_hash(out, m, msg.length);
return arrayToBytes(out);
}
public static function signDetached(msg:Bytes, secretKey:Bytes):Bytes {
var m = bytesToArray(msg);
var sk = bytesToArray(secretKey);
var sm = [for (i in 0...crypto_sign_BYTES + msg.length) 0];
crypto_sign(sm, m, msg.length, sk);
var sig = [for (i in 0...crypto_sign_BYTES) 0];
for (i in 0...crypto_sign_BYTES) sig[i] = sm[i];
return arrayToBytes(sig);
}
public static function signDetachedVerify(msg:Bytes, sig:Bytes, publicKey:Bytes):Bool {
var sm = [for (i in 0...crypto_sign_BYTES + msg.length) 0];
var m = [for (i in 0...crypto_sign_BYTES + msg.length) 0];
var sigArr = bytesToArray(sig);
var msgArr = bytesToArray(msg);
for (i in 0...crypto_sign_BYTES) sm[i] = sigArr[i];
for (i in 0...msg.length) sm[i + crypto_sign_BYTES] = msgArr[i];
var pk = bytesToArray(publicKey);
return crypto_sign_open(m, sm, sm.length, pk) >= 0;
}
public static function signKeyPairFromSeed(seed:Bytes):{publicKey:Bytes, secretKey:Bytes} {
var pk = [for (i in 0...32) 0];
var sk = [for (i in 0...64) 0];
var seedArr = bytesToArray(seed);
for (i in 0...32) sk[i] = seedArr[i];
crypto_sign_keypair(pk, sk, true, null);
return {publicKey: arrayToBytes(pk), secretKey: arrayToBytes(sk)};
}
public static function boxKeyPair():{publicKey:Bytes, secretKey:Bytes} {
var pk = [for (i in 0...32) 0];
var sk = [for (i in 0...32) 0];
crypto_box_keypair(pk, sk, randomBytesArray);
return {publicKey: arrayToBytes(pk), secretKey: arrayToBytes(sk)};
}
public static function box(msg:Bytes, nonce:Bytes, publicKey:Bytes, secretKey:Bytes):Bytes {
var m = [for (i in 0...crypto_box_ZEROBYTES + msg.length) 0];
var c = [for (i in 0...m.length) 0];
var msgArr = bytesToArray(msg);
for (i in 0...msg.length) m[i + crypto_box_ZEROBYTES] = msgArr[i];
var n = bytesToArray(nonce);
var pk = bytesToArray(publicKey);
var sk = bytesToArray(secretKey);
crypto_box(c, m, m.length, n, pk, sk);
var result = [for (i in 0...c.length - crypto_box_BOXZEROBYTES) 0];
for (i in 0...result.length) result[i] = c[i + crypto_box_BOXZEROBYTES];
return arrayToBytes(result);
}
public static function boxOpen(box:Bytes, nonce:Bytes, publicKey:Bytes, secretKey:Bytes):Bytes {
var c = [for (i in 0...crypto_box_BOXZEROBYTES + box.length) 0];
var m = [for (i in 0...c.length) 0];
var boxArr = bytesToArray(box);
for (i in 0...box.length) c[i + crypto_box_BOXZEROBYTES] = boxArr[i];
if (c.length < 32) return null;
var n = bytesToArray(nonce);
var pk = bytesToArray(publicKey);
var sk = bytesToArray(secretKey);
if (crypto_box_open(m, c, c.length, n, pk, sk) != 0) return null;
var result = [for (i in 0...m.length - crypto_box_ZEROBYTES) 0];
for (i in 0...result.length) result[i] = m[i + crypto_box_ZEROBYTES];
return arrayToBytes(result);
}
public static var boxNonceLength(get, null):Int;
static inline function get_boxNonceLength():Int { return crypto_box_NONCEBYTES; }
static function bytesToArray(b:Bytes):Array<Int> {
var a = [for (i in 0...b.length) 0];
for (i in 0...b.length) a[i] = b.get(i);
return a;
}
static function arrayToBytes(a:Array<Int>):Bytes {
var b = Bytes.alloc(a.length);
for (i in 0...a.length) b.set(i, a[i] & 0xff);
return b;
}
static function randomBytesArray(n:Int):Array<Int> {
var b = Crypto.randomBytes(n);
var a = [for (i in 0...n) 0];
for (i in 0...n) a[i] = b.get(i);
return a;
}
}

View File

@ -0,0 +1,56 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
typedef LeenkxOptions = {
@:optional var seed:String;
@:optional var keyPair:{publicKey:Bytes, secretKey:Bytes};
@:optional var timeout:Int;
@:optional var announce:Array<String>;
@:optional var iceServers:Array<String>;
@:optional var tracker:Dynamic;
@:optional var wtOpts:Dynamic;
@:optional var torrentOpts:Dynamic;
@:optional var torrent:Dynamic;
@:optional var wt:Dynamic;
@:optional var heartbeat:Int;
@:optional var deferJoin:Bool;
@:optional var transport:String;
}
typedef PeerInfo = {
var pk:String;
var ek:String;
var last:Float;
}
typedef Packet = {
var t:Float;
var i:String;
var pk:String;
var ek:String;
var n:Bytes;
@:optional var y:String;
@:optional var v:String;
}
enum abstract PacketType(String) from String to String {
var Message = "m";
var Ping = "p";
var Leave = "x";
}
enum abstract LeenkxEvent(String) from String to String {
var Torrent = "torrent";
var Message = "message";
var Seen = "seen";
var Left = "left";
var Server = "server";
var Ping = "ping";
var Timeout = "timeout";
var Connections = "connections";
var WireSeen = "wireseen";
var WireLeft = "wireleft";
var Tracker = "tracker";
var Announce = "announce";
}

View File

@ -0,0 +1,128 @@
package leenkx.network.torrent;
import haxe.io.Bytes;
class WebSeed {
public var url:String;
public var onError:String -> Void;
public var onDone:Void -> Void;
var t:Torrent;
var busy = false;
var key:String;
public function new(url:String, t:Torrent) {
this.url = url;
this.t = t;
this.key = "ws:" + url;
}
public function work():Void {
if (busy || !supported() || t.pieces == null
|| t.pieces.complete) return;
var i = pickPiece();
if (i < 0) return;
var range = rangeFor(i);
if (range == null) return;
busy = true;
var off = 0;
var size = t.meta.pieceSize(i);
while (off < size) {
t.pieces.markRequested(key, i, off);
off += PieceManager.BLOCK_SIZE;
}
fetch(range.url, range.start, range.end, function(body, err) {
busy = false;
if (t.pieces != null) t.pieces.clearAllRequested(key);
if (body != null && t.pieces != null) {
t.pieces.addBlock(i, 0, body);
if (onDone != null) onDone();
} else if (onError != null) {
onError(err != null ? err : "empty response");
}
});
}
function pickPiece():Int {
var pieces = t.pieces;
for (sel in pieces.selections) {
for (i in sel.start...sel.end + 1) {
if (pieces.untouched(i) && rangeFor(i) != null) {
return i;
}
}
}
for (i in 0...t.meta.numPieces) {
if (pieces.untouched(i) && rangeFor(i) != null) return i;
}
return -1;
}
function fileForByteRange(start:Int, end:Int):MetaInfo.TorrentFileEntry {
for (f in t.meta.files) {
if (start >= f.offset && end < f.offset + f.length) {
return f;
}
}
return null;
}
function rangeFor(i:Int):{url:String, start:Int, end:Int} {
var pl = t.meta.pieceLength;
var start = i * pl;
var end = start + t.meta.pieceSize(i) - 1;
var f = fileForByteRange(start, end);
if (f == null) return null;
var u = url;
if (StringTools.endsWith(u, "/") || t.meta.files.length > 1) {
if (!StringTools.endsWith(u, "/")) u += "/";
u += f.path.split("\\").join("/");
}
return {url: u, start: start - f.offset,
end: end - f.offset};
}
static function supported():Bool {
#if kha_krom
return false;
#elseif (sys || js)
return true;
#else
return false;
#end
}
function fetch(url:String, start:Int, end:Int, cb:Bytes -> String -> Void):Void {
#if kha_krom
cb(null, "webseeds unsupported on krom");
#elseif sys
var req = new haxe.Http(url);
req.setHeader("Range", "bytes=" + start + "-" + end);
req.cnxTimeout = 60;
req.onBytes = function(b) {
TorrentClient.postMain(function() cb(b, null));
};
req.onError = function(e) {
TorrentClient.postMain(function() cb(null, e));
};
sys.thread.Thread.create(function() req.request(false));
#elseif js
var xhr = new js.html.XMLHttpRequest();
xhr.open("GET", url, true);
xhr.setRequestHeader("Range", "bytes=" + start + "-" + end);
xhr.timeout = 60000;
xhr.ontimeout = function() cb(null, "request timed out");
xhr.responseType = js.html.XMLHttpRequestResponseType
.ARRAYBUFFER;
xhr.onload = function() {
var buf:js.lib.ArrayBuffer = xhr.response;
cb(buf != null ? haxe.io.Bytes.ofData(buf) : null,
buf != null ? null : "empty response");
};
xhr.onerror = function() cb(null, "http error");
xhr.send();
#else
cb(null, "webseeds unsupported on this target");
#end
}
}

View File

@ -0,0 +1,152 @@
package leenkx.network.torrent.peer;
import haxe.io.Bytes;
#if lnx_datachannel
import datachannel.RTCDataChannel.RTCDataChannelState;
#elseif kha_krom
import leenkx.network.torrent.transport.RtcNative;
#elseif js
import js.html.rtc.DataChannelState;
#end
class DataChannelPeer implements IPeerChannel {
public var onOpen:Void -> Void;
public var onData:Bytes -> Void;
public var onClose:Void -> Void;
var dc:Dynamic;
var open = false;
var pending:Array<Bytes> = [];
public var isOpen(get, never):Bool;
function get_isOpen():Bool return open;
public function new(dc:Dynamic) {
this.dc = dc;
#if lnx_datachannel
dc.onOpen = function(name) {
open = true;
if (onOpen != null) onOpen();
};
dc.onMessage = function(msg:Bytes) {
emitData(msg);
};
dc.onClosed = function() {
open = false;
if (onClose != null) onClose();
};
dc.onError = function(msg) {
open = false;
if (onClose != null) onClose();
};
if (dc.state == RTCDataChannelState.Open) {
open = true;
if (onOpen != null) onOpen();
}
#elseif kha_krom
var dcId:Int = dc;
RtcNative.setOpenCallback(dcId, function() {
open = true;
if (onOpen != null) onOpen();
});
RtcNative.setMessageCallback(dcId, function(data:Dynamic) {
var msg:Bytes = null;
if (Std.isOfType(data, js.lib.ArrayBuffer)) {
msg = Bytes.ofData(data);
} else if (js.lib.ArrayBuffer.isView(data)) {
var view:js.lib.ArrayBufferView = data;
msg = Bytes.ofData(view.buffer.slice(
view.byteOffset, view.byteOffset + view.byteLength));
} else {
msg = Bytes.ofString(data);
}
emitData(msg);
});
RtcNative.setClosedCallback(dcId, function() {
open = false;
if (onClose != null) onClose();
});
RtcNative.setErrorCallback(dcId, function(msg:String) {
open = false;
if (onClose != null) onClose();
});
if (RtcNative.isOpen(dcId)) {
open = true;
if (onOpen != null) onOpen();
}
#elseif js
dc.binaryType = "arraybuffer";
dc.onopen = function() {
open = true;
if (onOpen != null) onOpen();
};
dc.onmessage = function(e:js.html.MessageEvent) {
var msg:Bytes = null;
if (Std.isOfType(e.data, js.lib.ArrayBuffer)) {
msg = Bytes.ofData(e.data);
} else if (Std.isOfType(e.data, js.html.Blob)) {
return;
} else {
msg = Bytes.ofString(e.data);
}
emitData(msg);
};
dc.onclose = function() {
open = false;
if (onClose != null) onClose();
};
dc.onerror = function(e) {
open = false;
if (onClose != null) onClose();
};
if (dc.readyState == DataChannelState.OPEN) {
open = true;
if (onOpen != null) onOpen();
}
#end
}
function emitData(msg:Bytes):Void {
if (onData == null) {
pending.push(msg);
return;
}
flushPending();
onData(msg);
}
function flushPending():Void {
if (onData == null || pending.length == 0) return;
var q = pending;
pending = [];
for (m in q) onData(m);
}
public function send(data:Bytes):Void {
flushPending();
if (!open) return;
#if lnx_datachannel
try {
dc.sendBytes(data);
} catch (e:Dynamic) {
trace("Failed to sendBytes: " + Std.string(e));
}
#elseif kha_krom
RtcNative.sendMessage(dc, data.getData().slice(0, data.length));
#elseif js
dc.send(new js.lib.Uint8Array(data.getData(), 0, data.length));
#end
}
public function close():Void {
open = false;
#if lnx_datachannel
try dc.close() catch(e:Dynamic) {}
#elseif kha_krom
try RtcNative.close(dc) catch(e:Dynamic) {}
#elseif js
try dc.close() catch(e:Dynamic) {}
#end
}
}

View File

@ -0,0 +1,12 @@
package leenkx.network.torrent.peer;
import haxe.io.Bytes;
interface IPeerChannel {
function send(data:Bytes):Void;
function close():Void;
var isOpen(get, never):Bool;
var onOpen:Void -> Void;
var onData:Bytes -> Void;
var onClose:Void -> Void;
}

View File

@ -0,0 +1,524 @@
package leenkx.network.torrent.peer;
import haxe.io.Bytes;
import leenkx.network.torrent.transport.RtcSignaling;
#if lnx_datachannel
import datachannel.RTC;
import datachannel.RTCPeerConnection.RTCState;
import datachannel.RTCPeerConnection.RTCGatheringState;
#elseif kha_krom
import leenkx.network.torrent.transport.RtcNative;
#elseif js
import js.html.rtc.PeerConnection;
import js.html.rtc.DataChannelEvent;
import js.html.rtc.IceConnectionState;
import js.html.rtc.IceGatheringState;
import js.html.rtc.SessionDescription;
import js.html.rtc.SdpType;
#end
class RtcPeerPool {
static inline var CHANNEL_LABEL = "bt-wire";
var channelLabel:String = CHANNEL_LABEL;
var unordered:Bool = false;
var maxRetransmits:Int = -1;
public var onPeerChannel:String -> IPeerChannel -> Void;
public var onPeerLeft:String -> Void;
public var onTrackerUpdate:Dynamic -> Void;
public var onReady:Void -> Void;
public var onError:String -> Void;
public var statsProvider:Void -> {downloaded:Float, uploaded:Float,
left:Float} = null;
var signalings:Array<RtcSignaling> = [];
var iceServers:Array<String>;
#if lnx_datachannel
var eventTimer:haxe.Timer = null;
#end
var peers:Map<String, Dynamic> = [];
var channels:Map<String, IPeerChannel> = [];
var offerPeers:Map<String, Dynamic> = [];
var offerChannels:Map<String, IPeerChannel> = [];
var offerPeerIds:Map<String, {v:String}> = [];
var promotedPeerIds:Map<String, String> = [];
#if kha_krom
var dcToOffer:Map<Int, String> = [];
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
: defaultIceServers.copy();
if (channelLabel != null) this.channelLabel = channelLabel;
this.unordered = unordered;
this.maxRetransmits = maxRetransmits;
}
public function connect(trackerUrl:String, infoHashBinary:String, peerIdBinary:String):RtcSignaling {
leenkx.network.torrent.TorrentClient.initMain();
#if lnx_datachannel
RTC.init();
#end
#if kha_krom
if (!rtcInitialized) {
RtcNative.init();
rtcInitialized = true;
}
#end
var signaling = new RtcSignaling(trackerUrl, infoHashBinary, peerIdBinary);
signalings.push(signaling);
signaling.statsProvider = statsProvider;
signaling.onConnected = function() {
if (onReady != null) onReady();
};
signaling.onTrackerUpdate = function(d) {
if (onTrackerUpdate != null) onTrackerUpdate(d);
};
signaling.onCreateOffer = onCreateOffer;
signaling.onRemoteOffer = function(peerIdHex, offerIdHex, o) {
onRemoteOffer(signaling, peerIdHex, offerIdHex, o);
};
signaling.onRemoteAnswer = onRemoteAnswer;
signaling.onOfferTimeout = function(offerId) removeOfferPeer(offerId);
signaling.onPeerLeft = function(peerId) {
removePeer(peerId);
if (onPeerLeft != null) onPeerLeft(peerId);
};
signaling.onError = function(msg) {
if (onError != null) onError(msg);
};
signaling.start();
#if lnx_datachannel
if (eventTimer == null) {
eventTimer = new haxe.Timer(10);
eventTimer.run = function() RTC.processEvents();
}
#end
return signaling;
}
public function getPeerCount():Int {
return Lambda.count(channels);
}
public function close():Void {
#if lnx_datachannel
if (eventTimer != null) {
eventTimer.stop();
eventTimer = null;
}
#end
for (s in signalings) s.close();
signalings = [];
for (id in [for (k in offerPeers.keys()) k])
removeOfferPeer(id);
for (id in [for (k in peers.keys()) k]) removePeer(id);
#if kha_krom
dcToOffer.clear();
if (rtcInitialized) {
RtcNative.cleanup();
rtcInitialized = false;
}
#end
#if lnx_datachannel
RTC.finalize();
#end
}
function onCreateOffer(offerIdHex:String, cb:String -> Void):Void {
#if lnx_datachannel
var pc = new datachannel.RTCPeerConnection(iceServers, "0.0.0.0");
offerPeers.set(offerIdHex, pc);
offerPeerIds.set(offerIdHex, {v: null});
pc.onStateChange = function(state:RTCState) {
if (state == RTC_DISCONNECTED || state == RTC_FAILED
|| state == RTC_CLOSED) {
removeOfferPeer(offerIdHex);
}
};
var iceComplete = false;
var iceTimer:haxe.Timer = null;
var sendSdp = function() {
if (iceComplete) return;
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
if (!offerPeers.exists(offerIdHex)) return;
cb(pc.getLocalDescription());
};
pc.onGatheringStateChange = function(state) {
if (state == RTC_GATHERING_COMPLETE) sendSdp();
};
var dc = pc.createDatachannelWithOptions(channelLabel, unordered,
maxRetransmits < 0 ? null : maxRetransmits, null);
attachOfferChannel(offerIdHex, new DataChannelPeer(dc));
pc.onLocalDescription = function(sdp, type) {
if (type == "offer") {
iceTimer = new haxe.Timer(5000);
iceTimer.run = sendSdp;
}
};
#elseif kha_krom
var pcId = RtcNative.createPeerConnection(iceServers);
offerPeers.set(offerIdHex, pcId);
offerPeerIds.set(offerIdHex, {v: null});
RtcNative.setStateChangeCallback(pcId, function(state:Int) {
if (state == RtcNative.RTC_DISCONNECTED
|| state == RtcNative.RTC_FAILED
|| state == RtcNative.RTC_CLOSED) {
removeOfferPeer(offerIdHex);
}
});
RtcNative.setDataChannelCallback(pcId, function(dcId:Int) {
dcToOffer.set(dcId, offerIdHex);
attachOfferChannel(offerIdHex, new DataChannelPeer(dcId));
});
var dcId = RtcNative.createDataChannel(pcId, channelLabel,
unordered, maxRetransmits);
dcToOffer.set(dcId, offerIdHex);
attachOfferChannel(offerIdHex, new DataChannelPeer(dcId));
var iceComplete = false;
var iceTimer:haxe.Timer = null;
var sendSdp = function() {
if (iceComplete) return;
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
if (!offerPeers.exists(offerIdHex)) return;
cb(RtcNative.getLocalDescription(pcId));
};
RtcNative.setGatheringStateCallback(pcId, function(state:Int) {
if (state == RtcNative.RTC_GATHERING_COMPLETE) sendSdp();
});
RtcNative.setLocalDescriptionCallback(pcId,
function(sdp:String, type:String) {
if (type == "offer") {
iceTimer = new haxe.Timer(5000);
iceTimer.run = sendSdp;
}
});
#elseif js
var rtcConfig:Dynamic = {
iceServers: iceServers.map(function(s) return {urls: s}),
sdpSemantics: 'unified-plan'
};
var pc = new PeerConnection(rtcConfig);
offerPeers.set(offerIdHex, pc);
offerPeerIds.set(offerIdHex, {v: null});
pc.oniceconnectionstatechange = function(_) {
var state = pc.iceConnectionState;
if (state == IceConnectionState.FAILED
|| state == IceConnectionState.DISCONNECTED
|| state == IceConnectionState.CLOSED) {
removeOfferPeer(offerIdHex);
}
};
var dc = pc.createDataChannel(channelLabel,
{ordered: !unordered, maxRetransmits: unordered ? maxRetransmits : null});
attachOfferChannel(offerIdHex, new DataChannelPeer(dc));
var iceComplete = false;
var iceTimer:haxe.Timer = null;
var sendSdp = function() {
if (iceComplete) return;
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
var localDesc = pc.localDescription;
cb(localDesc != null ? localDesc.sdp : null);
};
pc.onicegatheringstatechange = function(_) {
if (pc.iceGatheringState == IceGatheringState.COMPLETE) sendSdp();
};
pc.createOffer({}).then(function(offer) {
pc.setLocalDescription(offer).then(function(_) {
iceTimer = new haxe.Timer(5000);
iceTimer.run = sendSdp;
});
}, function(e) {
if (onError != null) onError("onCreateOffer: " + Std.string(e));
});
#end
}
function onRemoteOffer(sig:RtcSignaling, peerIdHex:String, offerIdHex:String, offer:Dynamic):Void {
var sdp:String = offer.sdp != null ? offer.sdp : offer;
#if lnx_datachannel
var pc = new datachannel.RTCPeerConnection(iceServers, "0.0.0.0");
offerPeers.set(offerIdHex, pc);
offerPeerIds.set(offerIdHex, {v: peerIdHex});
pc.onStateChange = function(state:RTCState) {
if (state == RTC_DISCONNECTED || state == RTC_FAILED
|| state == RTC_CLOSED) {
removeOfferPeer(offerIdHex);
}
};
pc.onDataChannel = function(dc) {
attachOfferChannel(offerIdHex, new DataChannelPeer(dc));
};
var iceComplete = false;
var iceTimer:haxe.Timer = null;
var sendAnswerSdp = function() {
if (iceComplete) return;
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
if (!offerPeers.exists(offerIdHex)) return;
sig.sendAnswer(peerIdHex, offerIdHex, pc.getLocalDescription());
};
pc.onGatheringStateChange = function(state) {
if (state == RTC_GATHERING_COMPLETE) sendAnswerSdp();
};
pc.onLocalDescription = function(desc, type) {
if (type == "answer") {
iceTimer = new haxe.Timer(5000);
iceTimer.run = sendAnswerSdp;
}
};
try {
pc.setRemoteDescription(sdp, "offer");
} catch(e:Dynamic) {
removeOfferPeer(offerIdHex);
if (onError != null) onError("onRemoteOffer: " + Std.string(e));
}
#elseif kha_krom
var pcId = RtcNative.createPeerConnection(iceServers);
offerPeers.set(offerIdHex, pcId);
offerPeerIds.set(offerIdHex, {v: peerIdHex});
RtcNative.setStateChangeCallback(pcId, function(state:Int) {
if (state == RtcNative.RTC_DISCONNECTED
|| state == RtcNative.RTC_FAILED
|| state == RtcNative.RTC_CLOSED) {
removeOfferPeer(offerIdHex);
}
});
RtcNative.setDataChannelCallback(pcId, function(dcId:Int) {
dcToOffer.set(dcId, offerIdHex);
attachOfferChannel(offerIdHex, new DataChannelPeer(dcId));
});
var iceComplete = false;
var iceTimer:haxe.Timer = null;
var sendAnswerSdp = function() {
if (iceComplete) return;
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
if (!offerPeers.exists(offerIdHex)) return;
sig.sendAnswer(peerIdHex, offerIdHex, RtcNative.getLocalDescription(pcId));
};
RtcNative.setGatheringStateCallback(pcId, function(state:Int) {
if (state == RtcNative.RTC_GATHERING_COMPLETE) sendAnswerSdp();
});
RtcNative.setLocalDescriptionCallback(pcId,
function(sdp:String, type:String) {
if (type == "answer") {
iceTimer = new haxe.Timer(5000);
iceTimer.run = sendAnswerSdp;
}
});
var srd = RtcNative.setRemoteDescription(pcId, sdp, "offer");
if (srd < 0) {
removeOfferPeer(offerIdHex);
if (onError != null) {
onError("onRemoteOffer: setRemoteDescription failed");
}
return;
}
#elseif js
var rtcConfig:Dynamic = {
iceServers: iceServers.map(function(s) return {urls: s}),
sdpSemantics: 'unified-plan'
};
var pc = new PeerConnection(rtcConfig);
offerPeers.set(offerIdHex, pc);
offerPeerIds.set(offerIdHex, {v: peerIdHex});
pc.oniceconnectionstatechange = function(_) {
var state = pc.iceConnectionState;
if (state == IceConnectionState.FAILED
|| state == IceConnectionState.DISCONNECTED
|| state == IceConnectionState.CLOSED) {
removeOfferPeer(offerIdHex);
}
};
pc.ondatachannel = function(e:DataChannelEvent) {
attachOfferChannel(offerIdHex, new DataChannelPeer(e.channel));
};
var iceComplete = false;
var iceTimer:haxe.Timer = null;
var sendAnswerSdp = function() {
if (iceComplete) return;
iceComplete = true;
if (iceTimer != null) iceTimer.stop();
var localDesc = pc.localDescription;
sig.sendAnswer(peerIdHex, offerIdHex,
localDesc != null ? localDesc.sdp : null);
};
pc.onicegatheringstatechange = function(_) {
if (pc.iceGatheringState == IceGatheringState.COMPLETE) {
sendAnswerSdp();
}
};
pc.setRemoteDescription(
new SessionDescription({type: SdpType.OFFER, sdp: sdp}))
.then(function(_) {
pc.createAnswer().then(function(answer) {
pc.setLocalDescription(answer).then(function(_) {
iceTimer = new haxe.Timer(5000);
iceTimer.run = sendAnswerSdp;
});
});
}, function(e) {
if (onError != null) onError("onRemoteOffer: " + Std.string(e));
});
#end
}
function onRemoteAnswer(peerIdHex:String, offerIdHex:String, answer:Dynamic):Void {
var sdp:String = answer.sdp != null ? answer.sdp : answer;
var box = offerPeerIds.get(offerIdHex);
if (box != null) box.v = peerIdHex;
var pc = offerPeers.get(offerIdHex);
if (pc == null) return;
#if lnx_datachannel
try {
pc.setRemoteDescription(sdp, "answer");
} catch(e:Dynamic) {
removeOfferPeer(offerIdHex);
if (onError != null) onError("onRemoteAnswer: " + Std.string(e));
return;
}
#elseif kha_krom
var srd = RtcNative.setRemoteDescription(pc, sdp, "answer");
if (srd < 0) {
removeOfferPeer(offerIdHex);
if (onError != null) {
onError("onRemoteAnswer: setRemoteDescription failed");
}
return;
}
#elseif js
pc.setRemoteDescription(
new SessionDescription({type: SdpType.ANSWER, sdp: sdp}))
.then(function(_) {
promoteOfferPeer(offerIdHex);
}, function(e) {
if (onError != null) onError("onRemoteAnswer: " + Std.string(e));
});
return;
#end
promoteOfferPeer(offerIdHex);
}
function attachOfferChannel(offerIdHex:String, ch:IPeerChannel):Void {
offerChannels.set(offerIdHex, ch);
var resolvedPeerId:String = null;
var fired = false;
ch.onOpen = function() {
var box = offerPeerIds.get(offerIdHex);
resolvedPeerId = box != null ? box.v : null;
if (resolvedPeerId == null) {
resolvedPeerId = promotedPeerIds.get(offerIdHex);
}
promoteOfferPeer(offerIdHex);
if (resolvedPeerId != null && !fired) {
fired = true;
firePeerChannel(resolvedPeerId, ch);
}
};
ch.onClose = function() {
offerChannels.remove(offerIdHex);
if (resolvedPeerId != null && channels.get(resolvedPeerId) == ch) {
removePeer(resolvedPeerId);
} else {
removeOfferPeer(offerIdHex);
}
};
if (ch.isOpen) ch.onOpen();
}
function firePeerChannel(peerId:String, ch:IPeerChannel):Void {
if (onPeerChannel != null) onPeerChannel(peerId, ch);
}
function promoteOfferPeer(offerIdHex:String):Void {
if (!offerPeers.exists(offerIdHex)) return;
var box = offerPeerIds.get(offerIdHex);
if (box == null || box.v == null) return;
var peerId = box.v;
if (peers.exists(peerId)) {
removeOfferPeer(offerIdHex);
return;
}
promotedPeerIds.set(offerIdHex, peerId);
var pc = offerPeers.get(offerIdHex);
var ch = offerChannels.get(offerIdHex);
peers.set(peerId, pc);
if (ch != null) channels.set(peerId, ch);
offerPeers.remove(offerIdHex);
offerChannels.remove(offerIdHex);
offerPeerIds.remove(offerIdHex);
#if kha_krom
for (dcId in [for (k in dcToOffer.keys()) k]) {
if (dcToOffer.get(dcId) == offerIdHex) {
dcToOffer.set(dcId, peerId);
}
}
#end
}
function removeOfferPeer(offerIdHex:String):Void {
var pc = offerPeers.get(offerIdHex);
if (pc != null) closePc(pc);
var ch = offerChannels.get(offerIdHex);
if (ch != null) ch.close();
offerPeers.remove(offerIdHex);
offerChannels.remove(offerIdHex);
offerPeerIds.remove(offerIdHex);
promotedPeerIds.remove(offerIdHex);
#if kha_krom
for (dcId in [for (k in dcToOffer.keys()) k]) {
if (dcToOffer.get(dcId) == offerIdHex) dcToOffer.remove(dcId);
}
#end
}
function removePeer(peerId:String):Void {
if (!peers.exists(peerId)) return;
closePc(peers.get(peerId));
var ch = channels.get(peerId);
if (ch != null) ch.close();
peers.remove(peerId);
channels.remove(peerId);
for (offerId in [for (k in promotedPeerIds.keys()) k]) {
if (promotedPeerIds.get(offerId) == peerId) {
promotedPeerIds.remove(offerId);
}
}
#if kha_krom
for (dcId in [for (k in dcToOffer.keys()) k]) {
if (dcToOffer.get(dcId) == peerId) dcToOffer.remove(dcId);
}
#end
}
function closePc(pc:Dynamic):Void {
#if lnx_datachannel
try pc.close() catch(e:Dynamic) {}
#elseif kha_krom
try {
RtcNative.closePeerConnection(pc);
RtcNative.deletePeerConnection(pc);
} catch(e:Dynamic) {}
#elseif js
try pc.close() catch(e:Dynamic) {}
#end
}
}

View File

@ -0,0 +1,179 @@
package leenkx.network.torrent.peer;
import haxe.io.Bytes;
#if (sys || kha_krom)
class TcpListener {
public var onPeer:IPeerChannel -> String -> String -> Void;
public var onError:String -> Void;
var port:Int;
var stopped = false;
public function new(port:Int = 6881) {
this.port = port;
}
#if sys
var server:sys.net.Socket = null;
public function start():Void {
server = new sys.net.Socket();
server.bind(new sys.net.Host("0.0.0.0"), port);
server.listen(50);
var self = this;
sys.thread.Thread.create(function() {
while (!self.stopped) {
var s:sys.net.Socket = null;
try {
s = self.server.accept();
} catch(e:Dynamic) {
if (self.stopped) break;
if (self.onError != null) {
self.onError("accept: " + e);
}
Sys.sleep(0.05);
continue;
}
self.handleAccepted(s);
}
});
}
public function stop():Void {
stopped = true;
if (server != null) {
try server.close() catch(e:Dynamic) {}
server = null;
}
}
function handleAccepted(s:sys.net.Socket):Void {
var self = this;
sys.thread.Thread.create(function() {
var head = readHandshake(s);
if (head == null) {
try s.close() catch(e:Dynamic) {}
return;
}
var ih = head.sub(28, 20);
var hex = leenkx.network.torrent.Crypto.toHex(ih);
var peer = s.peer();
var key = "tcp:" + peer.host.toString() + ":" + peer.port;
leenkx.network.torrent.TorrentClient.postMain(function() {
if (self.stopped || self.onPeer == null) {
try s.close() catch(e:Dynamic) {}
return;
}
var ch = new TcpPeer("", 0, s, head);
self.onPeer(ch, hex, key);
});
});
}
function readHandshake(s:sys.net.Socket):Bytes {
s.setTimeout(10);
var buf = new haxe.io.BytesBuffer();
var deadline = haxe.Timer.stamp() + 10;
while (buf.length < 68) {
try {
var tmp = Bytes.alloc(68 - buf.length);
var n = s.input.readBytes(tmp, 0, tmp.length);
if (n <= 0) return null;
buf.addBytes(tmp, 0, n);
} catch(e:Dynamic) {
if (!TcpPeer.isBlocked(e)) return null;
if (haxe.Timer.stamp() > deadline) return null;
}
}
var b = buf.getBytes();
if (b.get(0) != 19
|| b.sub(1, 19).toString() != "BitTorrent protocol") {
return null;
}
return b;
}
#elseif kha_krom
var server:leenkx.network.krom.KromSocket = null;
var pumpRef:Void -> Void = null;
var pending:Array<{sock:leenkx.network.krom.KromSocket,
buf:haxe.io.BytesBuffer, deadline:Float}> = [];
public function start():Void {
server = new leenkx.network.krom.KromSocket();
server.bind(new leenkx.network.krom.KromSocket.KromHost(
"0.0.0.0"), port);
server.listen(50);
server.setBlocking(false);
pumpRef = pump;
leenkx.network.krom.KromSocket.KromPump.add(pumpRef);
}
public function stop():Void {
stopped = true;
if (pumpRef != null) {
leenkx.network.krom.KromSocket.KromPump.remove(pumpRef);
pumpRef = null;
}
for (p in pending) try p.sock.close() catch(e:Dynamic) {}
pending = [];
if (server != null) {
server.close();
server = null;
}
}
function pump():Void {
if (stopped || server == null) return;
while (true) {
var s:leenkx.network.krom.KromSocket = null;
try {
s = server.accept();
} catch(e:Dynamic) {
break;
}
if (s == null) break;
s.setBlocking(false);
pending.push({
sock: s,
buf: new haxe.io.BytesBuffer(),
deadline: haxe.Timer.stamp() + 10
});
}
var i = pending.length;
while (i-- > 0) {
var p = pending[i];
try {
var d = p.sock.recvRaw(68 - p.buf.length);
if (d != null) p.buf.addBytes(d, 0, d.length);
} catch(e:Dynamic) {
p.sock.close();
pending.splice(i, 1);
continue;
}
if (p.buf.length >= 68) {
pending.splice(i, 1);
routePeer(p.sock, p.buf.getBytes());
} else if (haxe.Timer.stamp() > p.deadline) {
p.sock.close();
pending.splice(i, 1);
}
}
}
function routePeer(s:leenkx.network.krom.KromSocket, head:Bytes):Void {
if (head.get(0) != 19
|| head.sub(1, 19).toString() != "BitTorrent protocol"
|| stopped || onPeer == null) {
s.close();
return;
}
var ih = head.sub(28, 20);
var hex = leenkx.network.torrent.Crypto.toHex(ih);
var key = "krom:" + s.getSocketId();
var ch = new TcpPeer("", 0, s, head);
onPeer(ch, hex, key);
}
#end
}
#end

View File

@ -0,0 +1,406 @@
package leenkx.network.torrent.peer;
import haxe.io.Bytes;
#if (sys || kha_krom)
class TcpPeer implements IPeerChannel {
public var onOpen:Void -> Void;
public var onData:Bytes -> Void;
public var onClose:Void -> Void;
static var peers:Array<TcpPeer> = [];
#if sys
static var peersMutex = new sys.thread.Mutex();
static var ioThread:sys.thread.Thread = null;
static var wakeRecv:sys.net.UdpSocket = null;
static var wakeAddr:sys.net.Address = null;
#elseif kha_krom
static var pumpTickRef:Void -> Void = null;
#end
var open = false;
var dead = false;
var pendingSend:Array<Bytes> = [];
#if sys
var sendMutex = new sys.thread.Mutex();
#end
var head:Bytes = null;
var readBuf:Bytes = Bytes.alloc(65536);
#if kha_krom
var connectDeadline:Float = 0;
#end
#if kha_krom
var socket:leenkx.network.krom.KromSocket;
#else
var socket:sys.net.Socket;
#end
public var isOpen(get, never):Bool;
function get_isOpen():Bool return open;
public function new(host:String, port:Int, ?acceptedSocket:Dynamic, ?headData:Bytes) {
if (acceptedSocket != null) {
socket = acceptedSocket;
head = headData;
socket.setBlocking(false);
open = true;
startPump();
var self = this;
leenkx.network.torrent.TorrentClient.postMain(function() {
if (self.onOpen != null) self.onOpen();
});
return;
}
#if kha_krom
socket = new leenkx.network.krom.KromSocket();
socket.setBlocking(false);
var connectFailed = false;
try {
socket.connect(
new leenkx.network.krom.KromSocket.KromHost(host), port);
} catch(e:Dynamic) {
connectFailed = true;
}
if (connectFailed) {
dead = true;
return;
}
if (socket.isConnected()) {
open = true;
startPump();
if (onOpen != null) onOpen();
} else {
connectDeadline = haxe.Timer.stamp() + 10;
startPump();
}
#else
var self = this;
sys.thread.Thread.create(function() {
var s = new sys.net.Socket();
var err:Dynamic = null;
try {
s.connect(new sys.net.Host(host), port);
s.setBlocking(false);
} catch(e:Dynamic) {
err = e;
}
leenkx.network.torrent.TorrentClient.postMain(function() {
if (self.dead) {
try s.close() catch(e:Dynamic) {}
return;
}
if (err != null) {
self.dead = true;
if (self.onClose != null) self.onClose();
return;
}
self.socket = s;
self.open = true;
self.startPump();
if (self.onOpen != null) self.onOpen();
});
});
#end
}
function fail():Void {
dead = true;
open = false;
stopPump();
if (onClose != null) onClose();
}
function deliverHead():Void {
if (head == null || onData == null) return;
var d = head;
head = null;
onData(d);
}
#if sys
function startPump():Void register(this);
function stopPump():Void unregister(this);
static function register(p:TcpPeer):Void {
peersMutex.acquire();
if (!peers.contains(p)) peers.push(p);
if (ioThread == null) {
wakeRecv = new sys.net.UdpSocket();
wakeRecv.bind(new sys.net.Host("127.0.0.1"), 0);
wakeRecv.setBlocking(false);
var h = wakeRecv.host();
wakeAddr = new sys.net.Address();
wakeAddr.host = h.host.ip;
wakeAddr.port = h.port;
ioThread = sys.thread.Thread.create(ioLoop);
}
peersMutex.release();
ioWake();
}
static function unregister(p:TcpPeer):Void {
peersMutex.acquire();
peers.remove(p);
peersMutex.release();
ioWake();
}
static function ioWake():Void {
if (wakeAddr == null) return;
try {
wakeRecv.sendTo(Bytes.alloc(1), 0, 1, wakeAddr);
} catch(e:Dynamic) {}
}
static function ioLoop():Void {
var wakeBuf = Bytes.alloc(64);
while (true) {
peersMutex.acquire();
var snapshot = peers.copy();
peersMutex.release();
for (p in snapshot) {
try {
p.deliverHead();
} catch(e:Dynamic) {
trace("[TcpPeer.ioLoop] deliverHead threw: " + e);
}
}
var reads:Array<sys.net.Socket> = [wakeRecv];
var writes:Array<sys.net.Socket> = [];
for (p in snapshot) {
if (p.dead || p.socket == null) continue;
reads.push(p.socket);
p.sendMutex.acquire();
var hasPending = p.pendingSend.length > 0;
p.sendMutex.release();
if (hasPending) writes.push(p.socket);
}
var res = try {
sys.net.Socket.select(reads, writes, null, 0.1);
} catch(e:Dynamic) {
null;
}
if (res == null) continue;
if (res.read.contains(wakeRecv)) {
try {
while (wakeRecv.readFrom(wakeBuf, 0, 64,
new sys.net.Address()) > 0) {}
} catch(e:Dynamic) {}
}
for (s in res.read) {
if (s == wakeRecv) continue;
for (p in snapshot) {
if (p.socket == s && !p.dead) {
try {
p.ioRead();
} catch(e:Dynamic) {
trace("[TcpPeer.ioLoop] ioRead threw: "
+ e);
p.fail();
}
}
}
}
for (s in res.write) {
for (p in snapshot) {
if (p.socket == s && !p.dead) {
try {
p.ioWrite();
} catch(e:Dynamic) {
trace("[TcpPeer.ioLoop] ioWrite threw: "
+ e);
p.fail();
}
}
}
}
peersMutex.acquire();
var i = peers.length;
while (i-- > 0) {
if (peers[i].dead) peers.splice(i, 1);
}
peersMutex.release();
}
}
function ioRead():Void {
try {
while (true) {
var read = socket.input.readBytes(readBuf, 0,
readBuf.length);
if (read <= 0) {
fail();
return;
}
if (onData != null) onData(readBuf.sub(0, read));
}
} catch(e:Dynamic) {
if (isBlocked(e)) return;
fail();
}
}
function ioWrite():Void {
flushPending();
}
#elseif kha_krom
function startPump():Void {
if (pumpTickRef == null) {
pumpTickRef = pumpAll;
leenkx.network.krom.KromSocket.KromPump.add(pumpTickRef);
}
peers.push(this);
}
function stopPump():Void {
peers.remove(this);
if (peers.length == 0 && pumpTickRef != null) {
leenkx.network.krom.KromSocket.KromPump.remove(pumpTickRef);
pumpTickRef = null;
}
}
static function pumpAll():Void {
var i = peers.length;
while (i-- > 0) {
if (peers[i].dead) peers.splice(i, 1);
}
var snapshot = peers.copy();
for (p in snapshot) {
if (!p.dead) p.pumpTick();
}
if (peers.length == 0 && pumpTickRef != null) {
leenkx.network.krom.KromSocket.KromPump.remove(pumpTickRef);
pumpTickRef = null;
}
}
function pumpTick():Void {
if (dead) return;
if (connectDeadline > 0) {
if (socket.isConnected()) {
connectDeadline = 0;
open = true;
var queued = pendingSend;
pendingSend = [];
for (d in queued) send(d);
if (onOpen != null) onOpen();
} else if (haxe.Timer.stamp() > connectDeadline) {
connectDeadline = 0;
fail();
}
return;
}
deliverHead();
flushPending();
if (dead) return;
try {
while (true) {
var data = socket.recvRaw(readBuf.length);
if (data == null || data.length == 0) break;
if (onData != null) onData(data);
}
} catch(e:Dynamic) {
if (isBlocked(e)) return;
fail();
}
}
#end
#if sys
public function send(data:Bytes):Void {
if (dead) return;
sendMutex.acquire();
pendingSend.push(data);
sendMutex.release();
ioWake();
}
#else
public function send(data:Bytes):Void {
if (dead) return;
if (!open || pendingSend.length > 0) {
pendingSend.push(data);
return;
}
try {
var written = socket.sendRaw(data);
if (written < data.length) {
pendingSend.push(data.sub(written, data.length - written));
}
} catch(e:Dynamic) {
if (isBlocked(e)) {
pendingSend.push(data);
return;
}
dead = true;
open = false;
stopPump();
if (onClose != null) onClose();
}
}
#end
public static function isBlocked(e:Dynamic):Bool {
var msg = Std.string(e);
if (msg == "Blocking" || msg == "Blocked") return true;
if (Std.isOfType(e, haxe.io.Error)
&& (e:haxe.io.Error).match(haxe.io.Error.Blocked)) return true;
if (msg.indexOf("Unix_error(37") == 0) return true;
return false;
}
#if sys
function flushPending():Void {
while (open && !dead) {
sendMutex.acquire();
var empty = pendingSend.length == 0;
var d = empty ? null : pendingSend[0];
sendMutex.release();
if (empty) return;
try {
var written = socket.output.writeBytes(d, 0, d.length);
socket.output.flush();
sendMutex.acquire();
if (written < d.length) {
pendingSend[0] = d.sub(written, d.length - written);
} else {
pendingSend.shift();
}
sendMutex.release();
if (written < d.length) return;
} catch(e:Dynamic) {
if (isBlocked(e)) return;
fail();
return;
}
}
}
#else
function flushPending():Void {
while (open && !dead && pendingSend.length > 0) {
var d = pendingSend[0];
try {
var written = socket.sendRaw(d);
if (written < d.length) {
pendingSend[0] = d.sub(written, d.length - written);
break;
}
pendingSend.shift();
} catch(e:Dynamic) {
if (isBlocked(e)) break;
fail();
break;
}
}
}
#end
public function close():Void {
open = false;
dead = true;
stopPump();
try socket.close() catch(e:Dynamic) {}
}
}
#end

View File

@ -0,0 +1,424 @@
package leenkx.network.torrent.peer;
import haxe.io.Bytes;
import leenkx.network.torrent.Bencode;
class WireProtocol {
static inline var PSTR = "BitTorrent protocol";
static inline var BLOCK_SIZE = 16384;
static inline var MAX_MSG_LEN = 256 * 1024;
public static inline var MSG_CHOKE = 0;
public static inline var MSG_UNCHOKE = 1;
public static inline var MSG_INTERESTED = 2;
public static inline var MSG_NOT_INTERESTED = 3;
public static inline var MSG_HAVE = 4;
public static inline var MSG_BITFIELD = 5;
public static inline var MSG_REQUEST = 6;
public static inline var MSG_PIECE = 7;
public static inline var MSG_CANCEL = 8;
public static inline var MSG_PORT = 9;
public static inline var MSG_EXTENDED = 20;
public static inline var MSG_HASH_REQUEST = 21;
public static inline var MSG_HASHES = 22;
public static inline var MSG_HASH_REJECT = 23;
var channel:IPeerChannel;
var infoHash:Bytes;
var peerId:Bytes;
var buffer:haxe.io.BytesBuffer = new haxe.io.BytesBuffer();
var handshakeDone = false;
var handshakeSent = false;
var started = false;
var closed = false;
public var extIds:Map<String, Int> = [];
public var peerExtendedHandshake:Dynamic = null;
public var remotePeerId:String = null;
public var peerChoking = true;
public var peerInterested = false;
public var amChoking = true;
public var amInterested = false;
var localExtNames:Array<String> = [];
var localExtIds:Map<Int, String> = [];
public var onHandshake:String -> Void;
public var onExtendedHandshake:Dynamic -> Void;
public var onChoke:Void -> Void;
public var onUnchoke:Void -> Void;
public var onInterested:Void -> Void;
public var onNotInterested:Void -> Void;
public var onHave:Int -> Void;
public var onBitfield:Bytes -> Void;
public var onRequest:Int -> Int -> Int -> Void;
public var onPiece:Int -> Int -> Bytes -> Void;
public var onCancel:Int -> Int -> Int -> Void;
public var onExtended:String -> Bytes -> Void;
public var onHashRequest:Bytes -> Void;
public var onHashes:Bytes -> Void;
public var onHashReject:Bytes -> Void;
public var onClose:Void -> Void;
public var onError:String -> Void;
public function new(channel:IPeerChannel, infoHash:Bytes, peerId:Bytes) {
this.channel = channel;
this.infoHash = infoHash;
this.peerId = peerId;
channel.onData = onChannelData;
var prevClose = channel.onClose;
channel.onClose = function() {
if (prevClose != null) prevClose();
closed = true;
if (onClose != null) onClose();
};
}
public function start(extNames:Array<String>):Void {
localExtNames = extNames;
started = true;
sendHandshake();
pump();
}
function sendHandshake():Void {
if (handshakeSent) return;
handshakeSent = true;
var out = new haxe.io.BytesBuffer();
out.addByte(PSTR.length);
out.addString(PSTR);
var reserved = Bytes.alloc(8);
reserved.set(5, 0x10);
out.addBytes(reserved, 0, 8);
out.addBytes(infoHash, 0, 20);
out.addBytes(peerId, 0, 20);
channel.send(out.getBytes());
}
var parsing = false;
function onChannelData(data:Bytes):Void {
if (closed) return;
buffer.addBytes(data, 0, data.length);
if (!started) return;
pump();
}
function pump():Void {
if (parsing) return;
parsing = true;
while (!closed) {
var bytes = buffer.getBytes();
buffer = new haxe.io.BytesBuffer();
if (bytes.length == 0) break;
var consumed = parse(bytes);
if (consumed < bytes.length) {
var nested = buffer.getBytes();
buffer = new haxe.io.BytesBuffer();
buffer.addBytes(bytes, consumed, bytes.length - consumed);
buffer.addBytes(nested, 0, nested.length);
if (consumed == 0 && nested.length == 0) break;
}
}
parsing = false;
}
function parse(data:Bytes):Int {
var pos = 0;
if (!handshakeDone) {
if (data.length < 68) return 0;
var pstrLen = data.get(0);
if (pstrLen != PSTR.length) {
fail("bad pstr length");
return data.length;
}
var pstr = data.sub(1, pstrLen).toString();
if (pstr != PSTR) {
fail("bad pstr");
return data.length;
}
var ihOff = 1 + pstrLen + 8;
var remoteHash = data.sub(ihOff, 20);
if (!bytesEqual(remoteHash, infoHash)) {
fail("info_hash mismatch");
return data.length;
}
remotePeerId = leenkx.network.torrent.Crypto.toHex(
data.sub(ihOff + 20, 20));
handshakeDone = true;
pos = ihOff + 40;
sendHandshake();
sendExtendedHandshake();
if (onHandshake != null) onHandshake(remotePeerId);
}
while (pos + 4 <= data.length) {
var len = (data.get(pos) << 24) | (data.get(pos + 1) << 16)
| (data.get(pos + 2) << 8) | data.get(pos + 3);
if (len < 0 || len > MAX_MSG_LEN || pos + 4 + len > data.length) break;
var msgStart = pos;
pos += 4;
if (len > 0) {
var mid = data.get(pos);
handleMessage(mid, data.sub(pos + 1, len - 1));
}
pos += len;
}
return pos;
}
function handleMessage(id:Int, payload:Bytes):Void {
switch (id) {
case MSG_CHOKE:
peerChoking = true;
if (onChoke != null) onChoke();
case MSG_UNCHOKE:
peerChoking = false;
if (onUnchoke != null) onUnchoke();
case MSG_INTERESTED:
peerInterested = true;
if (onInterested != null) onInterested();
case MSG_NOT_INTERESTED:
peerInterested = false;
if (onNotInterested != null) onNotInterested();
case MSG_HAVE:
if (payload.length >= 4 && onHave != null) {
onHave(readInt32(payload, 0));
}
case MSG_BITFIELD:
if (onBitfield != null) onBitfield(payload);
case MSG_REQUEST:
if (payload.length >= 12 && onRequest != null) {
onRequest(readInt32(payload, 0), readInt32(payload, 4),
readInt32(payload, 8));
}
case MSG_PIECE:
if (payload.length >= 8 && onPiece != null) {
onPiece(readInt32(payload, 0), readInt32(payload, 4),
payload.sub(8, payload.length - 8));
}
case MSG_CANCEL:
if (payload.length >= 12 && onCancel != null) {
onCancel(readInt32(payload, 0), readInt32(payload, 4),
readInt32(payload, 8));
}
case MSG_EXTENDED:
handleExtended(payload);
case MSG_HASH_REQUEST:
if (payload.length >= 48 && onHashRequest != null) {
onHashRequest(payload);
}
case MSG_HASHES:
if (payload.length >= 48 && onHashes != null) {
onHashes(payload);
}
case MSG_HASH_REJECT:
if (payload.length >= 48 && onHashReject != null) {
onHashReject(payload);
}
default:
}
}
function handleExtended(payload:Bytes):Void {
if (payload.length < 1) return;
var extId = payload.get(0);
var body = payload.sub(1, payload.length - 1);
if (extId == 0) {
var dict:Dynamic = null;
try {
dict = Bencode.decode(body);
} catch(e:Dynamic) {
return;
}
peerExtendedHandshake = dict;
var m:Dynamic = Reflect.field(dict, "m");
if (m != null) {
for (name in Reflect.fields(m)) {
extIds.set(name, Std.int(Reflect.field(m, name)));
}
}
if (onExtendedHandshake != null) onExtendedHandshake(dict);
} else {
var name = localExtIds.get(extId);
if (name != null) {
if (onExtended != null) onExtended(name, body);
}
}
}
function sendExtendedHandshake():Void {
var m:Dynamic = {};
var id = 1;
for (name in localExtNames) {
Reflect.setField(m, name, id);
localExtIds.set(id, name);
id++;
}
var dict:Dynamic = {m: m, v: "Leenkx 0.1"};
if (metaSize > 0) Reflect.setField(dict, "metadata_size", metaSize);
if (extHandshakeFields != null) {
for (f in Reflect.fields(extHandshakeFields)) {
Reflect.setField(dict, f, Reflect.field(extHandshakeFields, f));
}
}
sendExtendedMessage(0, Bencode.encode(dict));
}
public var metaSize:Int = 0;
public var extHandshakeFields:Dynamic = null;
public function sendExtended(name:String, payload:Bytes):Bool {
var id = extIds.get(name);
if (id == null) return false;
var out = new haxe.io.BytesBuffer();
out.addByte(id);
out.addBytes(payload, 0, payload.length);
sendExtendedMessage(-1, out.getBytes());
return true;
}
function sendExtendedMessage(id:Int, payload:Bytes):Void {
var out = new haxe.io.BytesBuffer();
if (id >= 0) out.addByte(id);
out.addBytes(payload, 0, payload.length);
sendMessage(MSG_EXTENDED, out.getBytes());
}
var keepAliveBuf:Bytes = Bytes.alloc(4);
public function sendKeepAlive():Void {
channel.send(keepAliveBuf);
}
public function sendChoke():Void {
amChoking = true;
sendMessage(MSG_CHOKE, null);
}
public function sendUnchoke():Void {
amChoking = false;
sendMessage(MSG_UNCHOKE, null);
}
public function sendInterested():Void {
amInterested = true;
sendMessage(MSG_INTERESTED, null);
}
public function sendNotInterested():Void {
amInterested = false;
sendMessage(MSG_NOT_INTERESTED, null);
}
public function sendHave(index:Int):Void {
var out = Bytes.alloc(9);
writeInt32(out, 0, 5);
out.set(4, MSG_HAVE);
writeInt32(out, 5, index);
channel.send(out);
}
public function sendBitfield(bitfield:Bytes):Void {
sendMessage(MSG_BITFIELD, bitfield);
}
public function sendRequest(index:Int, begin:Int, length:Int):Void {
var out = Bytes.alloc(17);
writeInt32(out, 0, 13);
out.set(4, MSG_REQUEST);
writeInt32(out, 5, index);
writeInt32(out, 9, begin);
writeInt32(out, 13, length);
channel.send(out);
}
public function sendPiece(index:Int, begin:Int, data:Bytes):Void {
var total = 13 + data.length;
var out = Bytes.alloc(total);
writeInt32(out, 0, total - 4);
out.set(4, MSG_PIECE);
writeInt32(out, 5, index);
writeInt32(out, 9, begin);
out.blit(13, data, 0, data.length);
channel.send(out);
}
static function hashMsg(root:Bytes, base:Int, index:Int, length:Int, proofs:Int, ?hashes:Bytes):Bytes {
var n = 48 + (hashes != null ? hashes.length : 0);
var out = Bytes.alloc(n);
out.blit(0, root, 0, 32);
writeInt32(out, 32, base);
writeInt32(out, 36, index);
writeInt32(out, 40, length);
writeInt32(out, 44, proofs);
if (hashes != null) out.blit(48, hashes, 0, hashes.length);
return out;
}
public function sendHashRequest(root:Bytes, base:Int, index:Int, length:Int, proofs:Int):Void {
sendMessage(MSG_HASH_REQUEST,
hashMsg(root, base, index, length, proofs));
}
public function sendHashes(root:Bytes, base:Int, index:Int, length:Int, proofs:Int, hashes:Bytes):Void {
sendMessage(MSG_HASHES,
hashMsg(root, base, index, length, proofs, hashes));
}
public function sendHashReject(root:Bytes, base:Int, index:Int, length:Int, proofs:Int):Void {
sendMessage(MSG_HASH_REJECT,
hashMsg(root, base, index, length, proofs));
}
public function sendCancel(index:Int, begin:Int, length:Int):Void {
var out = Bytes.alloc(17);
writeInt32(out, 0, 13);
out.set(4, MSG_CANCEL);
writeInt32(out, 5, index);
writeInt32(out, 9, begin);
writeInt32(out, 13, length);
channel.send(out);
}
function sendMessage(id:Int, payload:Bytes):Void {
if (closed) return;
var len = 1 + (payload != null ? payload.length : 0);
var total = 4 + 1 + (payload != null ? payload.length : 0);
var out = Bytes.alloc(total);
writeInt32(out, 0, len);
out.set(4, id);
if (payload != null) out.blit(5, payload, 0, payload.length);
channel.send(out);
}
public function close():Void {
closed = true;
channel.close();
}
function fail(reason:String):Void {
if (onError != null) onError(reason);
close();
}
static function bytesEqual(a:Bytes, b:Bytes):Bool {
if (a.length != b.length) return false;
for (i in 0...a.length) {
if (a.get(i) != b.get(i)) return false;
}
return true;
}
static function readInt32(b:Bytes, off:Int):Int {
return (b.get(off) << 24) | (b.get(off + 1) << 16)
| (b.get(off + 2) << 8) | b.get(off + 3);
}
static public function readU32(b:Bytes, off:Int):Int {
return readInt32(b, off);
}
static function writeInt32(b:Bytes, off:Int, v:Int):Void {
b.set(off, (v >> 24) & 0xFF);
b.set(off + 1, (v >> 16) & 0xFF);
b.set(off + 2, (v >> 8) & 0xFF);
b.set(off + 3, v & 0xFF);
}
}

View File

@ -0,0 +1,145 @@
package leenkx.network.torrent.storage;
import haxe.io.Bytes;
interface IStorage {
function write(offset:Int, data:Bytes):Void;
function read(offset:Int, length:Int):Bytes;
function flush():Void;
function close():Void;
function deleteData():Void;
}
class MemoryStorage implements IStorage {
var data:Bytes;
public function new(totalLength:Int) {
data = Bytes.alloc(totalLength);
}
public function write(offset:Int, chunk:Bytes):Void {
data.blit(offset, chunk, 0, chunk.length);
}
public function read(offset:Int, length:Int):Bytes {
return data.sub(offset, length);
}
public function flush():Void {}
public function close():Void {}
public function deleteData():Void {
data = null;
}
}
#if (sys || kha_krom)
class FileTreeStorage implements IStorage {
var dir:String;
var files:Array<{path:String, length:Int, offset:Int}>;
#if kha_krom
var madeDirs = new Map<String, Bool>();
#end
public function new(dir:String, files:Array<{path:String, length:Int, offset:Int}>) {
this.dir = dir;
this.files = files;
}
function filePath(f:{path:String, length:Int, offset:Int}):String {
return dir + "/"
+ leenkx.network.torrent.MetaInfo.sanitizePath(f.path);
}
function writeChunk(f:{path:String, length:Int, offset:Int}, foffset:Int, chunk:Bytes):Void {
var p = filePath(f);
#if kha_krom
var i = Std.int(Math.max(p.lastIndexOf("/"),
p.lastIndexOf("\\")));
if (i > 0) {
var d = p.substr(0, i);
if (!madeDirs.exists(d)) {
madeDirs.set(d, true);
var win = Krom.systemId() == "Windows";
Krom.sysCommand(win
? "mkdir \"" + StringTools.replace(d, "/", "\\") + "\""
: "mkdir -p \"" + d + "\"");
}
}
try {
Krom.fileWriteBytes(p, chunk.getData(), foffset,
chunk.length);
} catch(e:Dynamic) {
trace('[FileTreeStorage] WRITE FAILED ' + p
+ ' off=' + foffset + ' len=' + chunk.length
+ ' err=' + Std.string(e));
return;
}
#else
var i = Std.int(Math.max(p.lastIndexOf("/"),
p.lastIndexOf("\\")));
if (i > 0) {
var d = p.substr(0, i);
if (!sys.FileSystem.exists(d)) sys.FileSystem.createDirectory(d);
}
var out = sys.io.File.update(p, true);
out.seek(foffset, sys.io.FileSeek.SeekBegin);
out.writeBytes(chunk, 0, chunk.length);
out.close();
#end
}
function readChunk(f:{path:String, length:Int, offset:Int}, foffset:Int, len:Int):Bytes {
var p = filePath(f);
#if kha_krom
return Bytes.ofData(Krom.fileReadBytes(p, foffset, len));
#else
var buf = Bytes.alloc(len);
if (sys.FileSystem.exists(p)) {
var inp = sys.io.File.read(p, true);
inp.seek(foffset, sys.io.FileSeek.SeekBegin);
try inp.readBytes(buf, 0, len) catch(e:haxe.io.Eof) {}
inp.close();
}
return buf;
#end
}
public function write(offset:Int, data:Bytes):Void {
for (f in files) {
var lo = offset > f.offset ? offset : f.offset;
var hi = offset + data.length < f.offset + f.length
? offset + data.length : f.offset + f.length;
if (lo >= hi) continue;
writeChunk(f, lo - f.offset,
data.sub(lo - offset, hi - lo));
}
}
public function read(offset:Int, length:Int):Bytes {
var out = Bytes.alloc(length);
for (f in files) {
var lo = offset > f.offset ? offset : f.offset;
var hi = offset + length < f.offset + f.length
? offset + length : f.offset + f.length;
if (lo >= hi) continue;
var chunk = readChunk(f, lo - f.offset, hi - lo);
out.blit(lo - offset, chunk, 0, chunk.length);
}
return out;
}
public function flush():Void {}
public function close():Void {}
public function deleteData():Void {
for (f in files) {
var p = filePath(f);
#if sys
if (sys.FileSystem.exists(p)) sys.FileSystem.deleteFile(p);
#else
Krom.deleteFile(p);
#end
}
}
}
#end

View File

@ -0,0 +1,555 @@
package leenkx.network.torrent.tracker;
import haxe.io.Bytes;
import leenkx.network.torrent.Bencode;
import leenkx.network.torrent.Crypto;
import leenkx.network.torrent.TorrentInfo;
#if (sys || kha_krom)
class HttpTracker {
public var onPeers:Array<{host:String, port:Int}> -> Void;
public var onInterval:Int -> Void;
public var onError:String -> Void;
public var statsProvider:Void -> {downloaded:Float, uploaded:Float,
left:Float} = null;
var url:String;
var infoHash:Bytes;
var peerId:Bytes;
var port:Int;
var intervalSec:Int = 1800;
var numwant:Int = 50;
var timer:haxe.Timer = null;
var destroyed = false;
var busy = false;
var key:Int = Std.random(0x7FFFFFFF);
var trackerId:String = null;
public function new(url:String, infoHash:Bytes, peerId:Bytes, port:Int = 6881) {
this.url = url;
this.infoHash = infoHash;
this.peerId = peerId;
this.port = port;
}
public function start():Void {
announce("started");
}
public function stop():Void {
destroyed = true;
if (timer != null) {
timer.stop();
timer = null;
}
announce("stopped");
}
public function announceEvent(event:String):Void {
announce(event);
}
function announce(event:String):Void {
if (busy) return;
busy = true;
var stats = statsProvider != null ? statsProvider()
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
var sep = url.indexOf("?") >= 0 ? "&" : "?";
var req = url + sep
+ "info_hash=" + percentEncode(infoHash)
+ "&peer_id=" + percentEncode(peerId)
+ "&port=" + port
+ "&uploaded=" + Std.int(stats.uploaded)
+ "&downloaded=" + Std.int(stats.downloaded)
+ "&left=" + Std.int(stats.left)
+ "&compact=1&numwant=" + numwant + "&key="
+ StringTools.hex(key, 8);
if (trackerId != null) req += "&trackerid=" + trackerId;
if (event != null) req += "&event=" + event;
var self = this;
var finish = function(body:Bytes, err:String) {
self.busy = false;
if (self.destroyed) return;
if (err != null) {
if (self.onError != null) self.onError(err);
self.schedule();
return;
}
self.parseResponse(body);
self.schedule();
};
#if sys
var watchdog = new haxe.Timer(35000);
watchdog.run = function() {
watchdog.stop();
var s = self.currentSock;
if (s != null) try s.close() catch(e:Dynamic) {}
};
sys.thread.Thread.create(function() {
self.httpGet(req, function(body:Bytes, err:String) {
leenkx.network.torrent.TorrentClient.postMain(
function() {
watchdog.stop();
finish(body, err);
});
});
});
#else
httpGet(req, finish);
#end
}
function schedule():Void {
if (destroyed) return;
if (timer != null) timer.stop();
timer = new haxe.Timer(intervalSec * 1000);
timer.run = function() {
timer.stop();
announce(null);
};
}
function parseResponse(body:Bytes):Void {
var dict:Dynamic;
try {
dict = Bencode.decode(body);
} catch(e:Dynamic) {
if (onError != null) onError("bad tracker response");
return;
}
var failure = Reflect.field(dict, "failure reason");
if (failure != null) {
if (onError != null) onError(Std.string(failure));
return;
}
var warning = Reflect.field(dict, "warning message");
if (warning != null && onError != null) {
onError("warning: " + Std.string(warning));
}
var tid = Reflect.field(dict, "tracker id");
if (tid != null) trackerId = Std.string(tid);
var iv = Reflect.field(dict, "interval");
if (iv == null) iv = Reflect.field(dict, "min interval");
if (iv != null) {
var ivF = Std.parseFloat(Std.string(iv));
if (!Math.isNaN(ivF) && ivF > 0) intervalSec = Std.int(ivF);
}
var peersField = Reflect.field(dict, "peers");
var peers:Array<{host:String, port:Int}> = [];
if (Std.isOfType(peersField, Bytes)) {
peers = UdpTracker.parsePeers((peersField : Bytes), 0);
} else if (Std.isOfType(peersField, Array)) {
var arr:Array<Dynamic> = cast peersField;
for (pd in arr) {
var ip = Reflect.field(pd, "ip");
var prt = Reflect.field(pd, "port");
if (ip != null && prt != null) {
peers.push({
host: Std.isOfType(ip, Bytes)
? (ip:Bytes).toString() : Std.string(ip),
port: Std.int(Std.parseFloat(Std.string(prt)))
});
}
}
}
if (onInterval != null) onInterval(intervalSec);
if (peers.length > 0 && onPeers != null) onPeers(peers);
}
function httpGet(reqUrl:String, cb:Bytes -> String -> Void):Void {
var rest = reqUrl.substr(reqUrl.indexOf("://") + 3);
var secure = reqUrl.indexOf("https://") == 0;
var slash = rest.indexOf("/");
var hostPort = slash >= 0 ? rest.substr(0, slash) : rest;
var path = slash >= 0 ? rest.substr(slash) : "/";
var colon = hostPort.indexOf(":");
var host = colon >= 0 ? hostPort.substr(0, colon) : hostPort;
var port = colon >= 0
? Std.parseInt(hostPort.substr(colon + 1))
: (secure ? 443 : 80);
#if kha_krom
if (secure) {
cb(null, "https trackers not supported on krom");
return;
}
var sock = new leenkx.network.krom.KromSocket();
sock.setBlocking(false);
var connectFailed = false;
try {
sock.connect(
new leenkx.network.krom.KromSocket.KromHost(host), port);
} catch(e:Dynamic) {
connectFailed = true;
}
if (connectFailed) {
cb(null, "connect failed");
return;
}
var request = "GET " + path + " HTTP/1.0\r\nHost: " + hostPort
+ "\r\nConnection: close\r\n\r\n";
var reqBytes = Bytes.ofString(request);
var buf = new haxe.io.BytesBuffer();
var deadline = haxe.Timer.stamp() + 15;
var sentReq = false;
var tick:Void -> Void = null;
tick = function() {
if (haxe.Timer.stamp() > deadline) {
leenkx.network.krom.KromSocket.KromPump.remove(tick);
sock.close();
cb(null, "http tracker timeout");
return;
}
if (!sock.isConnected()) return;
if (!sentReq) {
sock.sendRaw(reqBytes);
sentReq = true;
return;
}
try {
var chunk = sock.recvRaw(65536);
if (chunk == null) return;
if (chunk.length == 0) {
leenkx.network.krom.KromSocket.KromPump.remove(tick);
sock.close();
finishResponse(buf.getBytes(), cb);
return;
}
buf.addBytes(chunk, 0, chunk.length);
} catch(e:Dynamic) {
leenkx.network.krom.KromSocket.KromPump.remove(tick);
sock.close();
finishResponse(buf.getBytes(), cb);
}
};
leenkx.network.krom.KromSocket.KromPump.add(tick);
#else
var sock:sys.net.Socket;
if (secure) {
#if hl
sock = new sys.ssl.Socket();
#else
cb(null, "https trackers not supported on this target");
return;
#end
} else {
sock = new sys.net.Socket();
}
sock.setTimeout(30);
try {
sock.connect(new sys.net.Host(host), port);
currentSock = sock;
var request = "GET " + path + " HTTP/1.0\r\nHost: " + hostPort
+ "\r\nConnection: close\r\n\r\n";
sock.output.write(Bytes.ofString(request));
sock.output.flush();
var buf = new haxe.io.BytesBuffer();
while (true) {
var chunk = Bytes.alloc(65536);
var read = 0;
try {
read = sock.input.readBytes(chunk, 0, chunk.length);
} catch(eof:haxe.io.Eof) {
break;
}
if (read <= 0) break;
buf.addBytes(chunk, 0, read);
}
sock.close();
currentSock = null;
finishResponse(buf.getBytes(), cb);
} catch(e:Dynamic) {
try sock.close() catch(_) {}
currentSock = null;
cb(null, Std.string(e));
}
#end
}
#if sys
var currentSock:sys.net.Socket = null;
#end
function finishResponse(raw:Bytes, cb:Bytes -> String -> Void):Void {
var body:Bytes = null;
for (i in 0...raw.length - 3) {
if (raw.get(i) == 0x0D && raw.get(i + 1) == 0x0A
&& raw.get(i + 2) == 0x0D && raw.get(i + 3) == 0x0A) {
body = raw.sub(i + 4, raw.length - i - 4);
break;
}
}
if (body == null) {
cb(null, "bad http response");
return;
}
cb(body, null);
}
static function percentEncode(b:Bytes):String {
var sb = new StringBuf();
var safe = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz-_.~";
for (i in 0...b.length) {
var c = b.get(i);
if (safe.indexOf(String.fromCharCode(c)) >= 0) {
sb.addChar(c);
} else {
sb.add("%");
sb.add(StringTools.hex(c, 2));
}
}
return sb.toString();
}
}
class HttpTrackerServer {
public var onLog:String -> Void = null;
public var onError:String -> Void = null;
var swarm = new UdpTracker.TrackerSwarm();
var running = false;
#if sys
var sock:sys.net.Socket;
#elseif kha_krom
var sock:leenkx.network.krom.KromSocket;
var timer:haxe.Timer = null;
var pending:Array<{sock:leenkx.network.krom.KromSocket,
buf:haxe.io.BytesBuffer}> = [];
#end
public function new() {}
public function start(port:Int):Void {
if (running) return;
running = true;
#if sys
sock = new sys.net.Socket();
sock.bind(new sys.net.Host("127.0.0.1"), port);
sock.listen(64);
var self = this;
sys.thread.Thread.create(function() self.loop());
#elseif kha_krom
sock = new leenkx.network.krom.KromSocket();
sock.bind(new leenkx.network.krom.KromSocket.KromHost(
"127.0.0.1"), port);
sock.listen(64);
sock.setBlocking(false);
timer = new haxe.Timer(5);
timer.run = pump;
#end
log("http tracker listening on 127.0.0.1:" + port);
}
public function stop():Void {
running = false;
#if kha_krom
if (timer != null) {
timer.stop();
timer = null;
}
for (p in pending) try p.sock.close() catch (_) {}
pending = [];
#end
if (sock != null) {
try sock.close() catch (e:Dynamic) {}
}
}
#if sys
function loop():Void {
while (running) {
try {
var c = sock.accept();
if (c != null) serve(c);
} catch (e:Dynamic) {
if (running) err("accept: " + Std.string(e));
}
}
}
#elseif kha_krom
function pump():Void {
if (!running || sock == null) return;
while (true) {
var c:leenkx.network.krom.KromSocket = null;
try {
c = sock.accept();
} catch (e:Dynamic) {
break;
}
if (c == null) break;
c.setBlocking(false);
pending.push({sock: c, buf: new haxe.io.BytesBuffer()});
}
var i = pending.length;
while (i-- > 0) {
var p = pending[i];
var drop = false;
try {
var d = p.sock.recvRaw(65536);
if (d != null) p.buf.addBytes(d, 0, d.length);
} catch (e:Dynamic) {
drop = true;
}
var done = !drop && find(p.buf.getBytes(),
"\r\n\r\n") >= 0;
if (done) {
var body = handleRequest(p.buf.getBytes(),
"127.0.0.1");
try {
var head = "HTTP/1.0 200 OK\r\n"
+ "Content-Length: " + body.length
+ "\r\n\r\n";
p.sock.sendRaw(Bytes.ofString(head));
p.sock.sendRaw(body);
} catch (e:Dynamic) {}
}
if (done || drop || p.buf.length > 65536) {
try p.sock.close() catch (e:Dynamic) {}
pending.splice(i, 1);
}
}
}
#end
#if sys
function serve(c:sys.net.Socket):Void {
try {
var raw = new haxe.io.BytesBuffer();
var tmp = Bytes.alloc(4096);
while (find(raw.getBytes(), "\r\n\r\n") < 0) {
var n = c.input.readBytes(tmp, 0, tmp.length);
if (n <= 0) break;
raw.addBytes(tmp, 0, n);
if (raw.length > 65536) break;
}
var body = handleRequest(raw.getBytes(),
c.peer().host.toString());
reply(c, 200, body);
} catch (e:Dynamic) {
try c.close() catch (_) {}
}
}
function reply(c:sys.net.Socket, code:Int, body:Bytes):Void {
var head = "HTTP/1.0 " + code + " OK\r\nContent-Length: "
+ body.length + "\r\n\r\n";
c.output.write(Bytes.ofString(head));
c.output.write(body);
c.output.flush();
c.close();
}
#end
function handleRequest(req:Bytes, peerHost:String):Bytes {
var lineEnd = find(req, "\r\n");
var line = lineEnd > 0
? req.sub(0, lineEnd).toString() : "";
var q = line.indexOf("?");
var sp2 = line.indexOf(" HTTP");
if (q < 0 || sp2 < 0) {
return Bytes.ofString("bad request");
}
var params = decodeQuery(req.sub(q + 1, sp2 - q - 1));
var infoHash = params.get("info_hash");
var peerId = params.get("peer_id");
if (infoHash == null || peerId == null) {
return Bencode.encode({
"failure reason": "missing params"});
}
var port = Std.parseInt(textParam(params, "port"));
var left = Std.parseFloat(textParam(params, "left"));
var numwant = Std.parseInt(textParam(params, "numwant"));
if (numwant == null || numwant <= 0 || numwant > 200) {
numwant = 50;
}
var ipText = textParam(params, "ip");
var host = ipText != null && ipText != ""
? ipText : peerHost;
var ev = textParam(params, "event");
var res = swarm.announce(infoHash, peerId, host,
port != null ? port : 6881,
Math.isNaN(left) ? 0.0 : left,
ev == "stopped", numwant);
log("announce " + Crypto.toHex(infoHash).substr(0, 8)
+ " peer=" + host + ":" + port + " event=" + ev
+ " swarm=" + res.complete + "s/"
+ res.incomplete + "l reply=" + res.peers.length);
var compact = new haxe.io.BytesBuffer();
UdpTracker.writePeers(compact, res.peers);
return Bencode.encode({
interval: swarm.intervalSec,
complete: res.complete,
incomplete: res.incomplete,
"tracker id": "leenkx-local",
peers: compact.getBytes()
});
}
static function find(b:Bytes, s:String):Int {
var n = s.length;
for (i in 0...b.length - n + 1) {
var ok = true;
for (j in 0...n) {
if (b.get(i + j) != s.charCodeAt(j)) {
ok = false;
break;
}
}
if (ok) return i;
}
return -1;
}
static function textParam(p:Map<String, Bytes>, k:String):String {
var b = p.get(k);
return b != null ? b.toString() : null;
}
static function decodeQuery(raw:Bytes):Map<String, Bytes> {
var out = new Map<String, Bytes>();
var s = raw.toString();
for (pair in s.split("&")) {
var eq = pair.indexOf("=");
var k = eq >= 0 ? pair.substr(0, eq) : pair;
var v = eq >= 0 ? pair.substr(eq + 1) : "";
out.set(StringTools.urlDecode(k), pctDecode(v));
}
return out;
}
static function pctDecode(s:String):Bytes {
var out = new haxe.io.BytesBuffer();
var i = 0;
while (i < s.length) {
var c = s.charCodeAt(i);
if (c == 0x25 && i + 2 < s.length) {
out.addByte(Std.parseInt("0x" + s.substr(i + 1, 2)));
i += 3;
} else if (c == 0x2B) {
out.addByte(0x20);
i++;
} else {
out.addByte(c & 0xff);
i++;
}
}
return out.getBytes();
}
function log(m:String):Void {
if (onLog != null) {
var f = onLog;
leenkx.network.torrent.TorrentClient.postMain(
function() f(m));
}
}
function err(m:String):Void {
if (onError != null) {
var f = onError;
leenkx.network.torrent.TorrentClient.postMain(
function() f(m));
}
}
}
#end

View File

@ -0,0 +1,696 @@
package leenkx.network.torrent.tracker;
import haxe.io.Bytes;
import leenkx.network.torrent.Crypto;
#if (sys || kha_krom)
typedef TrackerPeer = {
peerId:Bytes,
host:String,
port:Int,
left:Float,
stamp:Float
}
typedef SwarmResult = {
complete:Int,
incomplete:Int,
peers:Array<TrackerPeer>
}
class TrackerSwarm {
public var intervalSec:Int = 30;
var swarms:Map<String, Map<String, TrackerPeer>> = [];
public function new() {}
public function announce(infoHash:Bytes, peerId:Bytes, host:String, port:Int, left:Float, stopped:Bool, numwant:Int):SwarmResult {
var key = Crypto.toHex(infoHash);
var swarm = swarms.get(key);
if (swarm == null) {
swarm = new Map();
swarms.set(key, swarm);
}
var pid = Crypto.toHex(peerId);
if (stopped) {
swarm.remove(pid);
} else {
swarm.set(pid, {
peerId: peerId,
host: host,
port: port,
left: left,
stamp: haxe.Timer.stamp()
});
}
var peers:Array<TrackerPeer> = [];
var complete = 0;
var incomplete = 0;
for (p in swarm) {
if (p.left <= 0) complete++; else incomplete++;
if (pid == Crypto.toHex(p.peerId)) continue;
if (peers.length < numwant) peers.push(p);
}
return {complete: complete, incomplete: incomplete,
peers: peers};
}
}
class UdpTracker {
static inline var PROTOCOL_ID_HI:Int = 0x41727101;
static inline var PROTOCOL_ID_LO:Int = 0x80;
public static inline var ACTION_CONNECT:Int = 0;
public static inline var ACTION_ANNOUNCE:Int = 1;
static inline var ACTION_SCRAPE:Int = 2;
public static inline var ACTION_ERROR:Int = 3;
public static inline var EVENT_COMPLETED:Int = 1;
public static inline var EVENT_STARTED:Int = 2;
public static inline var EVENT_STOPPED:Int = 3;
public var onPeers:Array<{host:String, port:Int}> -> Void;
public var onInterval:Int -> Void;
public var onError:String -> Void;
public var statsProvider:Void -> {downloaded:Float, uploaded:Float,
left:Float} = null;
var url:String;
var infoHash:Bytes;
var peerId:Bytes;
var port:Int;
var intervalSec:Int = 1800;
var numwant:Int = 50;
var timer:haxe.Timer = null;
var destroyed = false;
var inFlight = false;
var key:Int = Std.random(0x7FFFFFFF);
var connIdHi:Int = 0;
var connIdLo:Int = 0;
var connIdTime:Float = 0;
public function new(url:String, infoHash:Bytes, peerId:Bytes, port:Int = 6881) {
this.url = url;
this.infoHash = infoHash;
this.peerId = peerId;
this.port = port;
}
public function start():Void {
announce(EVENT_STARTED);
}
public function stop():Void {
if (destroyed) return;
announce(EVENT_STOPPED);
destroyed = true;
if (timer != null) {
timer.stop();
timer = null;
}
}
public function announceEvent(event:String):Void {
var code = switch (event) {
case "completed": EVENT_COMPLETED;
case "started": EVENT_STARTED;
case "stopped": EVENT_STOPPED;
default: 0;
};
announce(code);
}
function announce(event:Int):Void {
if (inFlight) return;
inFlight = true;
#if kha_krom
kromAnnounce(event);
return;
#end
#if sys
var self = this;
var watchdog = new haxe.Timer(35000);
watchdog.run = function() {
watchdog.stop();
if (self.currentSock != null) {
try self.currentSock.close() catch(e:Dynamic) {}
}
};
sys.thread.Thread.create(function() {
var result = try {
self.doAnnounce(event);
} catch(e:Dynamic) {
{peers: [], interval: 0, error: Std.string(e)};
}
leenkx.network.torrent.TorrentClient.postMain(function() {
watchdog.stop();
self.inFlight = false;
if (self.destroyed) return;
if (result.error != null) {
if (self.onError != null) self.onError(result.error);
} else {
if (result.interval > 0) {
self.intervalSec = result.interval;
}
if (self.onInterval != null) {
self.onInterval(self.intervalSec);
}
if (result.peers.length > 0 && self.onPeers != null) {
self.onPeers(result.peers);
}
}
self.schedule();
});
});
#end
}
#if kha_krom
var ksock:leenkx.network.krom.KromUdpSocket = null;
var kHost:String = null;
var kPort:Int = 0;
var kTick:Void -> Void = null;
var kAttempt:Int = 0;
var kDeadline:Float = 0;
var kState:Int = 0;
var kTxId:Int = 0;
var kEvent:Int = 0;
var kLastError:String = "no response";
function kromAnnounce(event:Int):Void {
var rest = url.substr(url.indexOf("://") + 3);
var slash = rest.indexOf("/");
var hostPort = slash >= 0 ? rest.substr(0, slash) : rest;
var colon = hostPort.lastIndexOf(":");
kHost = colon >= 0 ? hostPort.substr(0, colon) : hostPort;
kPort = colon >= 0
? Std.parseInt(hostPort.substr(colon + 1)) : 80;
if (kPort == null) kPort = 80;
kEvent = event;
kAttempt = 0;
try {
if (ksock != null) ksock.close();
ksock = new leenkx.network.krom.KromUdpSocket();
} catch(e:Dynamic) {
finishAnnounce([], 0, "udp socket: " + Std.string(e));
return;
}
kromRound();
}
function kromRound():Void {
kAttempt++;
kDeadline = haxe.Timer.stamp() + 15;
if (connIdTime == 0
|| haxe.Timer.stamp() - connIdTime > 45) {
kTxId = Std.random(0x7FFFFFFF);
var connReq = new haxe.io.BytesBuffer();
writeInt64(connReq, PROTOCOL_ID_HI, PROTOCOL_ID_LO);
writeInt32(connReq, ACTION_CONNECT);
writeInt32(connReq, kTxId);
if (kSend(connReq.getBytes()) < 16) {
finishAnnounce([], 0, "connect send failed");
return;
}
kState = 1;
} else {
if (!kSendAnnounce()) return;
kState = 2;
}
startKTimer();
}
function kSendAnnounce():Bool {
var stats = statsProvider != null ? statsProvider()
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
kTxId = Std.random(0x7FFFFFFF);
var ann = new haxe.io.BytesBuffer();
writeInt32(ann, connIdHi);
writeInt32(ann, connIdLo);
writeInt32(ann, ACTION_ANNOUNCE);
writeInt32(ann, kTxId);
ann.addBytes(infoHash, 0, 20);
ann.addBytes(peerId, 0, 20);
writeInt64v(ann, stats.downloaded);
writeInt64v(ann, stats.left);
writeInt64v(ann, stats.uploaded);
writeInt32(ann, kEvent);
writeInt32(ann, 0);
writeInt32(ann, key);
writeInt32(ann, numwant);
var portBytes = Bytes.alloc(2);
portBytes.set(0, (this.port >> 8) & 0xFF);
portBytes.set(1, this.port & 0xFF);
ann.addBytes(portBytes, 0, 2);
if (kSend(ann.getBytes()) < 98) {
finishAnnounce([], 0, "announce send failed");
return false;
}
return true;
}
function kSend(pkt:Bytes):Int {
try {
return ksock.sendTo(pkt, 0, pkt.length,
new leenkx.network.krom.KromSocket.KromHost(kHost),
kPort);
} catch(e:Dynamic) {
return -1;
}
}
function startKTimer():Void {
if (kTick == null) {
kTick = kromPoll;
leenkx.network.krom.KromSocket.KromPump.add(kTick);
}
}
function kromPoll():Void {
if ((destroyed && kEvent != EVENT_STOPPED) || ksock == null) {
kromCleanup();
inFlight = false;
return;
}
var pkt:{data:Bytes, host:String, port:Int} = null;
try {
pkt = ksock.recvFrom(65536);
} catch(e:Dynamic) {
finishAnnounce([], 0, "recv: " + Std.string(e));
return;
}
if (pkt != null) {
var resp = pkt.data;
if (kState == 1 && resp.length >= 16
&& readInt32(resp, 0) == ACTION_CONNECT
&& readInt32(resp, 4) == kTxId) {
connIdHi = readInt32(resp, 8);
connIdLo = readInt32(resp, 12);
connIdTime = haxe.Timer.stamp();
if (!kSendAnnounce()) return;
kState = 2;
kDeadline = haxe.Timer.stamp() + 15;
} else if (kState == 2 && resp.length >= 20) {
var action = readInt32(resp, 0);
if (action == ACTION_ERROR) {
finishAnnounce([], 0, "tracker error: "
+ resp.sub(8, resp.length - 8).toString());
return;
}
if (action == ACTION_ANNOUNCE
&& readInt32(resp, 4) == kTxId) {
var interval = readInt32(resp, 8);
finishAnnounce(parsePeers(resp), interval, null);
return;
}
}
}
if (haxe.Timer.stamp() > kDeadline) {
kLastError = kState == 1 ? "connect timeout"
: "announce timeout";
if (kAttempt < 3) {
connIdTime = 0;
kromRound();
} else {
finishAnnounce([], 0, kLastError);
}
}
}
function finishAnnounce(peers:Array<{host:String, port:Int}>, interval:Int, error:String):Void {
kromCleanup();
inFlight = false;
if (destroyed) return;
if (error != null) {
if (onError != null) onError(error);
} else {
if (interval > 0) intervalSec = interval;
if (onInterval != null) onInterval(intervalSec);
if (peers.length > 0 && onPeers != null) onPeers(peers);
}
schedule();
}
function kromCleanup():Void {
if (kTick != null) {
leenkx.network.krom.KromSocket.KromPump.remove(kTick);
kTick = null;
}
if (ksock != null) {
try ksock.close() catch(e:Dynamic) {}
ksock = null;
}
}
#end
function schedule():Void {
if (destroyed) return;
if (timer != null) timer.stop();
timer = new haxe.Timer(intervalSec * 1000);
timer.run = function() {
timer.stop();
announce(0);
};
}
#if sys
function doAnnounce(event:Int):{peers:Array<{host:String, port:Int}>,
interval:Int, error:String} {
var empty = {peers: [], interval: 0, error: null};
var rest = url.substr(url.indexOf("://") + 3);
var slash = rest.indexOf("/");
var hostPort = slash >= 0 ? rest.substr(0, slash) : rest;
var colon = hostPort.lastIndexOf(":");
var host = colon >= 0 ? hostPort.substr(0, colon) : hostPort;
var port = colon >= 0
? Std.parseInt(hostPort.substr(colon + 1)) : 80;
if (port == null) port = 80;
var sock = new sys.net.UdpSocket();
var addr = new sys.net.Address();
try {
addr.host = new sys.net.Host(host).ip;
addr.port = port;
} catch(e:Dynamic) {
return {peers: [], interval: 0,
error: "resolve: " + Std.string(e)};
}
var lastError = "no response";
for (attempt in 0...3) {
var r = tryAnnounce(sock, addr, event);
if (r.error == null) {
sock.close();
return r;
}
lastError = r.error;
connIdTime = 0;
}
sock.close();
return {peers: [], interval: 0, error: lastError};
}
function tryAnnounce(sock:sys.net.UdpSocket, addr:sys.net.Address, event:Int):{peers:Array<{host:String, port:Int}>, interval:Int, error:String} {
var now = haxe.Timer.stamp();
if (connIdTime == 0 || now - connIdTime > 45) {
var txId = Std.random(0x7FFFFFFF);
var connReq = new haxe.io.BytesBuffer();
writeInt64(connReq, PROTOCOL_ID_HI, PROTOCOL_ID_LO); // protocol id
writeInt32(connReq, ACTION_CONNECT);
writeInt32(connReq, txId);
try {
sock.sendTo(connReq.getBytes(), 0, 16, addr);
} catch(e:Dynamic) {
return {peers: [], interval: 0,
error: "send: " + Std.string(e)};
}
var resp = recvWithTimeout(sock, 15000);
if (resp == null || resp.length < 16) {
return {peers: [], interval: 0, error: "connect timeout"};
}
if (readInt32(resp, 0) != ACTION_CONNECT
|| readInt32(resp, 4) != txId) {
return {peers: [], interval: 0,
error: "bad connect response"};
}
connIdHi = readInt32(resp, 8);
connIdLo = readInt32(resp, 12);
connIdTime = now;
}
var stats = statsProvider != null ? statsProvider()
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
var txId = Std.random(0x7FFFFFFF);
var ann = new haxe.io.BytesBuffer();
writeInt32(ann, connIdHi);
writeInt32(ann, connIdLo);
writeInt32(ann, ACTION_ANNOUNCE);
writeInt32(ann, txId);
ann.addBytes(infoHash, 0, 20);
ann.addBytes(peerId, 0, 20);
writeInt64v(ann, stats.downloaded);
writeInt64v(ann, stats.left);
writeInt64v(ann, stats.uploaded);
writeInt32(ann, event);
writeInt32(ann, 0);
writeInt32(ann, key);
writeInt32(ann, numwant);
var portBytes = Bytes.alloc(2);
portBytes.set(0, (this.port >> 8) & 0xFF);
portBytes.set(1, this.port & 0xFF);
ann.addBytes(portBytes, 0, 2);
try {
sock.sendTo(ann.getBytes(), 0, 98, addr);
} catch(e:Dynamic) {
return {peers: [], interval: 0,
error: "announce send: " + Std.string(e)};
}
var resp = recvWithTimeout(sock, 15000);
if (resp == null || resp.length < 20) {
return {peers: [], interval: 0, error: "announce timeout"};
}
if (readInt32(resp, 0) == ACTION_ERROR) {
return {peers: [], interval: 0,
error: "tracker error: "
+ resp.sub(8, resp.length - 8).toString()};
}
if (readInt32(resp, 0) != ACTION_ANNOUNCE
|| readInt32(resp, 4) != txId) {
return {peers: [], interval: 0, error: "bad announce response"};
}
var interval = readInt32(resp, 8);
return {peers: parsePeers(resp), interval: interval,
error: null};
}
var currentSock:sys.net.UdpSocket = null;
function recvWithTimeout(sock:sys.net.UdpSocket, ms:Int):Bytes {
currentSock = sock;
sock.setTimeout(ms / 1000);
var buf = Bytes.alloc(65536);
var addr = new sys.net.Address();
try {
var r = sock.readFrom(buf, 0, buf.length, addr);
if (r > 0) return buf.sub(0, r);
} catch(e:Dynamic) {
var m = Std.string(e);
if (m != "Blocked" && m != "Eof") trace("Error: " + e);
}
return null;
}
#end
static public function parsePeers(resp:Bytes, off:Int = 20):Array<{host:String, port:Int}> {
var peers:Array<{host:String, port:Int}> = [];
while (off + 6 <= resp.length) {
peers.push({
host: resp.get(off) + "." + resp.get(off + 1) + "."
+ resp.get(off + 2) + "." + resp.get(off + 3),
port: (resp.get(off + 4) << 8) | resp.get(off + 5)
});
off += 6;
}
return peers;
}
static public function writePeers(buf:haxe.io.BytesBuffer, peers:Array<{host:String, port:Int}>):Void {
for (p in peers) {
for (o in p.host.split(".")) {
buf.addByte(Std.parseInt(o));
}
buf.addByte((p.port >> 8) & 0xff);
buf.addByte(p.port & 0xff);
}
}
static public function readInt32(b:Bytes, pos:Int):Int {
return (b.get(pos) << 24) | (b.get(pos + 1) << 16)
| (b.get(pos + 2) << 8) | b.get(pos + 3);
}
static public function writeInt32(buf:haxe.io.BytesBuffer, v:Int):Void {
buf.addByte((v >> 24) & 0xFF);
buf.addByte((v >> 16) & 0xFF);
buf.addByte((v >> 8) & 0xFF);
buf.addByte(v & 0xFF);
}
static function writeInt64(buf:haxe.io.BytesBuffer, hi:Int, lo:Int):Void {
writeInt32(buf, hi);
writeInt32(buf, lo);
}
static function writeInt64v(buf:haxe.io.BytesBuffer, v:Float):Void {
if (v < 0) v = 0;
var hi = Std.int(v / 4294967296);
var lo = Std.int(v - hi * 4294967296);
writeInt32(buf, hi);
writeInt32(buf, lo);
}
}
class UdpTrackerServer {
public var onLog:String -> Void = null;
public var onError:String -> Void = null;
var swarm = new TrackerSwarm();
var running = false;
#if sys
var sock:sys.net.UdpSocket;
#elseif kha_krom
var sock:leenkx.network.krom.KromUdpSocket;
var timer:haxe.Timer = null;
#end
public function new() {}
public function start(port:Int):Void {
if (running) return;
running = true;
#if sys
sock = new sys.net.UdpSocket();
sock.bind(new sys.net.Host("127.0.0.1"), port);
var self = this;
sys.thread.Thread.create(function() self.loop());
#elseif kha_krom
sock = new leenkx.network.krom.KromUdpSocket();
sock.bind(new leenkx.network.krom.KromSocket.KromHost(
"127.0.0.1"), port);
timer = new haxe.Timer(5);
timer.run = pump;
#end
log("udp tracker listening on 127.0.0.1:" + port);
}
public function stop():Void {
running = false;
#if kha_krom
if (timer != null) {
timer.stop();
timer = null;
}
#end
if (sock != null) {
try sock.close() catch (e:Dynamic) {}
}
}
#if sys
function loop():Void {
var buf = Bytes.alloc(65536);
var from = new sys.net.Address();
while (running) {
try {
var n = sock.readFrom(buf, 0, buf.length, from);
if (n > 0) {
var resp = handle(buf.sub(0, n),
from.getHost().toString(), from.port);
if (resp != null) {
sock.sendTo(resp, 0, resp.length, from);
}
}
} catch (e:Dynamic) {
if (running) err("recv: " + Std.string(e));
}
}
}
#elseif kha_krom
function pump():Void {
if (!running || sock == null) return;
while (true) {
var r:{data:Bytes, host:String, port:Int} = null;
try {
r = sock.recvFrom(65536);
} catch (e:Dynamic) {
err("recv: " + Std.string(e));
break;
}
if (r == null) break;
var resp = handle(r.data, r.host, r.port);
if (resp != null) {
try {
sock.sendTo(resp, 0, resp.length,
new leenkx.network.krom.KromSocket
.KromHost(r.host), r.port);
} catch (e:Dynamic) {
err("send: " + Std.string(e));
}
}
}
}
#end
function handle(pkt:Bytes, host0:String, port0:Int):Bytes {
if (pkt.length < 16) return null;
var action = UdpTracker.readInt32(pkt, 8);
var tx = UdpTracker.readInt32(pkt, 12);
if (action == UdpTracker.ACTION_CONNECT) {
var r = new haxe.io.BytesBuffer();
UdpTracker.writeInt32(r, UdpTracker.ACTION_CONNECT);
UdpTracker.writeInt32(r, tx);
UdpTracker.writeInt32(r, Std.random(0x7FFFFFFF));
UdpTracker.writeInt32(r, Std.random(0x7FFFFFFF));
return r.getBytes();
}
if (action != UdpTracker.ACTION_ANNOUNCE
|| pkt.length < 98) {
var r = new haxe.io.BytesBuffer();
UdpTracker.writeInt32(r, 3);
UdpTracker.writeInt32(r, tx);
r.addString("bad announce");
return r.getBytes();
}
var infoHash = pkt.sub(16, 20);
var peerId = pkt.sub(36, 20);
var hi = UdpTracker.readInt32(pkt, 64);
var lo = UdpTracker.readInt32(pkt, 68);
var left = hi * 4294967296.0
+ (lo < 0 ? lo + 4294967296.0 : lo);
var event = UdpTracker.readInt32(pkt, 80);
var ipParam = UdpTracker.readInt32(pkt, 84);
var numwant = UdpTracker.readInt32(pkt, 92);
var port = (pkt.get(96) << 8) | pkt.get(97);
if (numwant <= 0 || numwant > 200) numwant = 50;
var host = host0 != null ? host0 : "127.0.0.1";
if (ipParam != 0) {
host = ((ipParam >> 24) & 0xff) + "."
+ ((ipParam >> 16) & 0xff) + "."
+ ((ipParam >> 8) & 0xff) + "."
+ (ipParam & 0xff);
}
var res = swarm.announce(infoHash, peerId, host, port,
left, event == 3, numwant);
log("announce " + Crypto.toHex(infoHash).substr(0, 8)
+ " peer=" + host + ":" + port + " event=" + event
+ " swarm=" + res.complete + "s/"
+ res.incomplete + "l reply=" + res.peers.length);
var r = new haxe.io.BytesBuffer();
UdpTracker.writeInt32(r, UdpTracker.ACTION_ANNOUNCE);
UdpTracker.writeInt32(r, tx);
UdpTracker.writeInt32(r, swarm.intervalSec);
UdpTracker.writeInt32(r, res.incomplete);
UdpTracker.writeInt32(r, res.complete);
UdpTracker.writePeers(r, res.peers);
return r.getBytes();
}
function log(m:String):Void {
if (onLog != null) {
var f = onLog;
leenkx.network.torrent.TorrentClient.postMain(
function() f(m));
}
}
function err(m:String):Void {
if (onError != null) {
var f = onError;
leenkx.network.torrent.TorrentClient.postMain(
function() f(m));
}
}
}
#end

View File

@ -0,0 +1,166 @@
package leenkx.network.torrent.transport;
import haxe.io.Bytes;
import leenkx.network.torrent.peer.IPeerChannel;
import leenkx.network.torrent.peer.RtcPeerPool;
import leenkx.network.torrent.transport.ITransport.TransportOpts;
class DatachannelTransport implements ITransport {
static inline var CHANNEL_LABEL = "lx_channel";
public var onReady:Void -> Void;
public var onPeerConnected:String -> Int -> Void;
public var onPeerDisconnected:Int -> Void;
public var onPeerData:String -> Bytes -> Void;
public var onPeerHandshake:String -> String -> Void;
public var onTrackerUpdate:Dynamic -> Void;
public var onTrackerAnnounce:Void -> Void;
public var onError:String -> Void;
var announceUrl:String;
var iceServers:Array<String>;
var pk:String = "";
var ek:String = "";
var pool:RtcPeerPool = null;
var channels:Map<String, IPeerChannel> = [];
var peerPks:Map<String, String> = [];
public function new(announceUrl:String, ?iceServers:Array<String>) {
this.announceUrl = announceUrl;
this.iceServers = iceServers;
}
public function join(identifier:String, opts:TransportOpts):Void {
var url = announceUrl;
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 : "";
#if hl
trace("WARNING: DatachannelTransport on HL requires -D lnx_datachannel");
#end
pool = new RtcPeerPool(iceServers, CHANNEL_LABEL, true, 0);
pool.onPeerChannel = onPeerChannel;
pool.onPeerLeft = onPeerLeft;
pool.onReady = function() {
if (onReady != null) onReady();
if (onTrackerAnnounce != null) onTrackerAnnounce();
};
pool.onTrackerUpdate = function(d) {
if (onTrackerUpdate != null) onTrackerUpdate(d);
};
pool.onError = function(msg) {
if (onError != null) onError(msg);
};
pool.connect(url, opts.infoHashBinary, opts.peerIdBinary);
}
public function broadcast(data:Bytes):Void {
for (peerId in channels.keys()) {
var ch = channels.get(peerId);
if (ch != null && ch.isOpen) {
try {
ch.send(data);
} catch(e:Dynamic) {
trace("DatachannelTransport broadcast error: " + e);
}
}
}
}
public function sendTo(peerPk:String, data:Bytes):Bool {
for (peerId in peerPks.keys()) {
if (peerPks.get(peerId) == peerPk) {
var ch = channels.get(peerId);
if (ch != null && ch.isOpen) {
try {
ch.send(data);
return true;
} catch(e:Dynamic) {
trace("DatachannelTransport sendTo error: " + e);
}
}
return false;
}
}
return false;
}
public function getPeerCount():Int {
return Lambda.count(channels);
}
public function destroy(?cb:Void -> Void):Void {
if (pool != null) {
pool.close();
pool = null;
}
channels.clear();
peerPks.clear();
if (cb != null) cb();
}
function onPeerChannel(peerId:String, ch:IPeerChannel):Void {
channels.set(peerId, ch);
var prevClose = ch.onClose;
ch.onClose = function() {
if (prevClose != null) prevClose();
dropChannel(peerId);
};
ch.onData = function(msg:Bytes) handleChannelMessage(peerId, msg);
sendHandshake(ch);
if (onPeerConnected != null) onPeerConnected(peerId, getPeerCount());
}
function onPeerLeft(peerId:String):Void {
dropChannel(peerId);
}
function dropChannel(peerId:String):Void {
if (!channels.exists(peerId)) return;
channels.remove(peerId);
peerPks.remove(peerId);
if (onPeerDisconnected != null) {
onPeerDisconnected(getPeerCount());
}
}
function handleChannelMessage(peerId:String, msg:Bytes):Void {
if (msg.length > 2 && msg.get(0) == 'h'.code
&& msg.get(1) == 's'.code) {
var hsStr = msg.sub(2, msg.length - 2).toString();
try {
var hs = haxe.Json.parse(hsStr);
if (hs.pk != null) {
peerPks.set(peerId, hs.pk);
}
if (onPeerHandshake != null && hs.pk != null) {
onPeerHandshake(hs.pk, hs.ek != null ? hs.ek : "");
}
} catch(e:Dynamic) {}
} else {
var peerPk = peerPks.get(peerId);
if (peerPk == null) peerPk = "";
if (onPeerData != null) onPeerData(peerPk, msg);
}
}
function sendHandshake(ch:IPeerChannel):Void {
var msg = Bytes.ofString("hs"
+ haxe.Json.stringify({pk: pk, ek: ek}));
try {
ch.send(msg);
} catch(e:Dynamic) {
trace("DatachannelTransport sendHandshake error: " + e);
}
}
}

View File

@ -0,0 +1,34 @@
package leenkx.network.torrent.transport;
import haxe.io.Bytes;
typedef TransportOpts = {
@:optional var announce:Array<String>;
@:optional var name:String;
@:optional var iceServers:Array<String>;
@:optional var tracker:Dynamic;
@:optional var wtOpts:Dynamic;
@:optional var torrentOpts:Dynamic;
@:optional var torrent:Dynamic;
@:optional var pk:String;
@:optional var ek:String;
@:optional var infoHashBinary:String;
@:optional var peerIdBinary:String;
}
interface ITransport {
function join(identifier:String, opts:TransportOpts):Void;
function broadcast(data:Bytes):Void;
function sendTo(peerPk:String, data:Bytes):Bool;
function getPeerCount():Int;
function destroy(?cb:Void -> Void):Void;
var onReady:Void -> Void;
var onPeerConnected:String -> Int -> Void;
var onPeerDisconnected:Int -> Void;
var onPeerData:String -> Bytes -> Void;
var onPeerHandshake:String -> String -> Void;
var onTrackerUpdate:Dynamic -> Void;
var onTrackerAnnounce:Void -> Void;
var onError:String -> Void;
}

View File

@ -0,0 +1,159 @@
package leenkx.network.torrent.transport;
#if kha_krom
@:native("krom_rtc_init") extern function krom_rtc_init():Int;
@:native("krom_rtc_cleanup") extern function krom_rtc_cleanup():Void;
@:native("krom_rtc_create_peer_connection") extern function krom_rtc_create_peer_connection(iceServers:Array<String>):Int;
@:native("krom_rtc_close_peer_connection") extern function krom_rtc_close_peer_connection(pc:Int):Void;
@:native("krom_rtc_delete_peer_connection") extern function krom_rtc_delete_peer_connection(pc:Int):Void;
@:native("krom_rtc_set_local_description") extern function krom_rtc_set_local_description(pc:Int, ?type:String):Void;
@:native("krom_rtc_set_remote_description") extern function krom_rtc_set_remote_description(pc:Int, sdp:String, type:String):Int;
@:native("krom_rtc_add_remote_candidate") extern function krom_rtc_add_remote_candidate(pc:Int, cand:String, mid:String):Int;
@:native("krom_rtc_get_local_description") extern function krom_rtc_get_local_description(pc:Int):String;
@:native("krom_rtc_get_local_description_type") extern function krom_rtc_get_local_description_type(pc:Int):String;
@:native("krom_rtc_create_data_channel") extern function krom_rtc_create_data_channel(pc:Int, label:String, ?unordered:Bool, ?maxRetransmits:Int):Int;
@:native("krom_rtc_delete_data_channel") extern function krom_rtc_delete_data_channel(dc:Int):Void;
@:native("krom_rtc_send_message") extern function krom_rtc_send_message(id:Int, data:Dynamic):Int;
@:native("krom_rtc_close") extern function krom_rtc_close(id:Int):Void;
@:native("krom_rtc_delete") extern function krom_rtc_delete(id:Int):Void;
@:native("krom_rtc_is_open") extern function krom_rtc_is_open(id:Int):Bool;
@:native("krom_rtc_get_buffered_amount") extern function krom_rtc_get_buffered_amount(id:Int):Int;
@:native("krom_rtc_set_local_description_callback") extern function krom_rtc_set_local_description_callback(pc:Int, fn:String -> String -> Void):Void;
@:native("krom_rtc_set_local_candidate_callback") extern function krom_rtc_set_local_candidate_callback(pc:Int, fn:String -> String -> Void):Void;
@:native("krom_rtc_set_state_change_callback") extern function krom_rtc_set_state_change_callback(pc:Int, fn:Int -> Void):Void;
@:native("krom_rtc_set_gathering_state_callback") extern function krom_rtc_set_gathering_state_callback(pc:Int, fn:Int -> Void):Void;
@:native("krom_rtc_set_data_channel_callback") extern function krom_rtc_set_data_channel_callback(pc:Int, fn:Int -> Void):Void;
@:native("krom_rtc_set_open_callback") extern function krom_rtc_set_open_callback(id:Int, fn:Void -> Void):Void;
@:native("krom_rtc_set_closed_callback") extern function krom_rtc_set_closed_callback(id:Int, fn:Void -> Void):Void;
@:native("krom_rtc_set_error_callback") extern function krom_rtc_set_error_callback(id:Int, fn:String -> Void):Void;
@:native("krom_rtc_set_message_callback") extern function krom_rtc_set_message_callback(id:Int, fn:Dynamic -> Void):Void;
@:native("krom_rtc_random_bytes") extern function krom_rtc_random_bytes(len:Int):js.lib.ArrayBuffer;
class RtcNative {
public static inline var RTC_NEW:Int = 0;
public static inline var RTC_CONNECTING:Int = 1;
public static inline var RTC_CONNECTED:Int = 2;
public static inline var RTC_DISCONNECTED:Int = 3;
public static inline var RTC_FAILED:Int = 4;
public static inline var RTC_CLOSED:Int = 5;
public static inline var RTC_GATHERING_NEW:Int = 0;
public static inline var RTC_GATHERING_INPROGRESS:Int = 1;
public static inline var RTC_GATHERING_COMPLETE:Int = 2;
public static function init():Int {
return krom_rtc_init();
}
public static function cleanup():Void {
krom_rtc_cleanup();
}
public static function createPeerConnection(iceServers:Array<String>):Int {
return krom_rtc_create_peer_connection(iceServers);
}
public static function closePeerConnection(pc:Int):Void {
krom_rtc_close_peer_connection(pc);
}
public static function deletePeerConnection(pc:Int):Void {
krom_rtc_delete_peer_connection(pc);
}
public static function setLocalDescription(pc:Int, ?type:String):Void {
krom_rtc_set_local_description(pc, type);
}
public static function setRemoteDescription(pc:Int, sdp:String, type:String):Int {
return krom_rtc_set_remote_description(pc, sdp, type);
}
public static function addRemoteCandidate(pc:Int, cand:String, mid:String):Int {
return krom_rtc_add_remote_candidate(pc, cand, mid);
}
public static function getLocalDescription(pc:Int):String {
return krom_rtc_get_local_description(pc);
}
public static function getLocalDescriptionType(pc:Int):String {
return krom_rtc_get_local_description_type(pc);
}
public static function createDataChannel(pc:Int, label:String, unordered:Bool = true, maxRetransmits:Int = 0):Int {
return krom_rtc_create_data_channel(pc, label, unordered, maxRetransmits);
}
public static function deleteDataChannel(dc:Int):Void {
krom_rtc_delete_data_channel(dc);
}
public static function sendMessage(id:Int, data:Dynamic):Int {
return krom_rtc_send_message(id, data);
}
public static function close(id:Int):Void {
krom_rtc_close(id);
}
public static function delete(id:Int):Void {
krom_rtc_delete(id);
}
public static function isOpen(id:Int):Bool {
return krom_rtc_is_open(id);
}
public static function getBufferedAmount(id:Int):Int {
return krom_rtc_get_buffered_amount(id);
}
public static function setLocalDescriptionCallback(pc:Int, fn:String -> String -> Void):Void {
krom_rtc_set_local_description_callback(pc, fn);
}
public static function setLocalCandidateCallback(pc:Int, fn:String -> String -> Void):Void {
krom_rtc_set_local_candidate_callback(pc, fn);
}
public static function setStateChangeCallback(pc:Int, fn:Int -> Void):Void {
krom_rtc_set_state_change_callback(pc, fn);
}
public static function setGatheringStateCallback(pc:Int, fn:Int -> Void):Void {
krom_rtc_set_gathering_state_callback(pc, fn);
}
public static function setDataChannelCallback(pc:Int, fn:Int -> Void):Void {
krom_rtc_set_data_channel_callback(pc, fn);
}
public static function setOpenCallback(id:Int, fn:Void -> Void):Void {
krom_rtc_set_open_callback(id, fn);
}
public static function setClosedCallback(id:Int, fn:Void -> Void):Void {
krom_rtc_set_closed_callback(id, fn);
}
public static function setErrorCallback(id:Int, fn:String -> Void):Void {
krom_rtc_set_error_callback(id, fn);
}
public static function setMessageCallback(id:Int, fn:Dynamic -> Void):Void {
krom_rtc_set_message_callback(id, fn);
}
public static function randomBytes(length:Int):haxe.io.Bytes {
try {
var buf = krom_rtc_random_bytes(length);
return buf != null ? haxe.io.Bytes.ofData(buf) : null;
} catch (e:Dynamic) {
return null;
}
}
}
#end

View File

@ -0,0 +1,268 @@
package leenkx.network.torrent.transport;
import haxe.io.Bytes;
import leenkx.network.torrent.Crypto;
import leenkx.network.torrent.TorrentInfo;
class RtcSignaling {
static inline var OFFER_TIMEOUT = 50 * 1000;
static inline var NUM_WANT = 5;
public var numwant:Int = NUM_WANT;
var client:WsTrackerClient;
var infoHashBinary:String;
var peerIdBinary:String;
var destroyed:Bool = false;
var trackerId:String = null;
var intervalTimer:haxe.Timer = null;
var pendingOffers:Map<String, haxe.Timer> = [];
public var onConnected:Void -> Void;
public var onTrackerUpdate:Dynamic -> Void;
public var onCreateOffer:String -> (String -> Void) -> Void;
public var onRemoteOffer:String -> String -> Dynamic -> Void;
public var onRemoteAnswer:String -> String -> Dynamic -> Void;
public var onOfferTimeout:String -> Void;
public var onPeerLeft:String -> Void;
public var onError:String -> Void;
public var statsProvider:Void -> {downloaded:Float, uploaded:Float,
left:Float} = null;
public function new(url:String, infoHashBinary:String, peerIdBinary:String) {
this.infoHashBinary = infoHashBinary;
this.peerIdBinary = peerIdBinary;
client = new WsTrackerClient(url);
client.onOpen = function() {
if (destroyed) return;
announce(null);
if (onConnected != null) onConnected();
};
client.onMessage = onMessage;
client.onClose = cleanupTimers;
client.onError = function(err) {
if (destroyed) return;
cleanupTimers();
if (onError != null) onError(err);
};
}
public function start():Void {
client.start();
}
function onMessage(data:String):Void {
if (destroyed) return;
var msg:Dynamic;
try {
msg = haxe.Json.parse(data);
} catch(e) {
return;
}
if (msg.action == "announce") {
onAnnounceResponse(msg);
}
}
function cleanupTimers():Void {
if (intervalTimer != null) {
intervalTimer.stop();
intervalTimer = null;
}
for (offerId => timer in pendingOffers) {
timer.stop();
}
pendingOffers.clear();
}
public function setAnnounceInterval(intervalMs:Int):Void {
if (intervalTimer != null) {
intervalTimer.stop();
intervalTimer = null;
}
if (intervalMs > 0) {
intervalTimer = new haxe.Timer(intervalMs);
intervalTimer.run = function() {
announce(null);
};
}
}
public function announceEvent(event:String):Void {
announce(event);
}
function announce(event:String):Void {
if (destroyed || !client.isConnected) return;
var stats = statsProvider != null ? statsProvider()
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
var params:Dynamic = {
action: "announce",
info_hash: infoHashBinary,
peer_id: peerIdBinary,
numwant: numwant,
uploaded: Std.int(stats.uploaded),
downloaded: Std.int(stats.downloaded),
left: Std.int(stats.left)
};
if (event != null) {
Reflect.setField(params, "event", event);
}
if (trackerId != null) {
Reflect.setField(params, "trackerid", trackerId);
}
if (event == "stopped" || event == "completed") {
client.send(params);
} else if (onCreateOffer != null) {
var nw = numwant;
var offers:Array<Dynamic> = [];
var completed = 0;
var sent = false;
var announceTimer = new haxe.Timer(Std.int(OFFER_TIMEOUT));
announceTimer.run = function() {
announceTimer.stop();
if (!sent) {
sent = true;
if (offers.length > 0) {
Reflect.setField(params, "numwant", offers.length);
Reflect.setField(params, "offers", offers);
}
client.send(params);
}
};
for (i in 0...nw) {
var offerIdHex = Crypto.toHex(Crypto.randomBytes(20));
createSingleOffer(offerIdHex, function(sdp:String) {
offers.push({
offer: {type: "offer", sdp: sdp},
offer_id: hexToBinaryString(offerIdHex)
});
completed++;
if (completed == nw && !sent) {
announceTimer.stop();
sent = true;
Reflect.setField(params, "numwant", nw);
Reflect.setField(params, "offers", offers);
client.send(params);
}
});
}
} else {
client.send(params);
}
}
function createSingleOffer(offerIdHex:String, cb:String -> Void):Void {
onCreateOffer(offerIdHex, cb);
var timer = new haxe.Timer(Std.int(OFFER_TIMEOUT));
timer.run = function() {
timer.stop();
pendingOffers.remove(offerIdHex);
if (onOfferTimeout != null) onOfferTimeout(offerIdHex);
};
pendingOffers.set(offerIdHex, timer);
}
function onAnnounceResponse(data:Dynamic):Void {
var dataIh = Reflect.field(data, "info_hash");
if (Std.string(dataIh) != infoHashBinary) {
return;
}
var dataPeerId = Reflect.field(data, "peer_id");
if (dataPeerId != null && Std.string(dataPeerId) == peerIdBinary) {
return;
}
var failure = Reflect.field(data, "failure reason");
if (failure != null) {
trace('[RtcSignaling] FAILURE: ' + Std.string(failure));
return;
}
var warning = Reflect.field(data, "warning message");
if (warning != null) {
trace('[RtcSignaling] WARNING: ' + Std.string(warning));
return;
}
var interval = Reflect.field(data, "interval");
if (interval == null) {
interval = Reflect.field(data, "min interval");
}
if (interval != null) {
setAnnounceInterval(Std.int(Std.parseFloat(Std.string(interval)) * 1000));
}
var tid = Reflect.field(data, "tracker id");
if (tid != null) {
trackerId = Std.string(tid);
}
if (Reflect.field(data, "complete") != null) {
if (onTrackerUpdate != null) onTrackerUpdate(data);
}
var offer = Reflect.field(data, "offer");
var peerId = Reflect.field(data, "peer_id");
if (offer != null && peerId != null) {
var peerIdHex = binaryStringToHex(Std.string(peerId));
var offerIdHex = binaryStringToHex(
Std.string(Reflect.field(data, "offer_id")));
if (onRemoteOffer != null) {
onRemoteOffer(peerIdHex, offerIdHex, offer);
}
}
var answer = Reflect.field(data, "answer");
if (answer != null && peerId != null) {
var offerIdHex = binaryStringToHex(
Std.string(Reflect.field(data, "offer_id")));
var peerIdHex = binaryStringToHex(Std.string(peerId));
var timer = pendingOffers.get(offerIdHex);
if (timer != null) {
timer.stop();
pendingOffers.remove(offerIdHex);
}
if (onRemoteAnswer != null) {
onRemoteAnswer(peerIdHex, offerIdHex, answer);
}
}
var event = Reflect.field(data, "event");
if (peerId != null && Std.string(event) == "stopped") {
var peerIdHex = binaryStringToHex(Std.string(peerId));
if (onPeerLeft != null) onPeerLeft(peerIdHex);
}
}
public function sendAnswer(toPeerIdHex:String, offerIdHex:String, sdp:String):Void {
if (destroyed || !client.isConnected) return;
var stats = statsProvider != null ? statsProvider()
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
var params:Dynamic = {
action: "announce",
info_hash: infoHashBinary,
peer_id: peerIdBinary,
left: Std.int(stats.left),
to_peer_id: hexToBinaryString(toPeerIdHex),
answer: {type: "answer", sdp: sdp},
offer_id: hexToBinaryString(offerIdHex)
};
if (trackerId != null) {
Reflect.setField(params, "trackerid", trackerId);
}
client.send(params);
}
public function close():Void {
if (destroyed) return;
if (client.isConnected) {
announce("stopped");
}
destroyed = true;
cleanupTimers();
client.close();
}
function hexToBinaryString(hex:String):String {
return TorrentInfo.toBinaryString(Bytes.ofHex(hex));
}
function binaryStringToHex(bin:String):String {
return Crypto.toHex(TorrentInfo.fromBinaryString(bin));
}
}

View File

@ -0,0 +1,50 @@
package leenkx.network.torrent.transport;
#if (js && !kha_krom)
@:native("WebTorrent")
extern class WebTorrent {
function new(?opts:Dynamic);
function seed(input:Dynamic, ?opts:Dynamic, ?cb:Dynamic -> Void):Torrent;
function add(torrentId:Dynamic, ?opts:Dynamic, ?cb:Torrent -> Void):Torrent;
function remove(torrent:Dynamic, ?cb:Void -> Void):Void;
function destroy(?cb:Void -> Void):Void;
function on(event:String, cb:Dynamic -> Void):Void;
var torrents:Array<Torrent>;
}
@:native("Torrent")
extern class Torrent {
var ready:Bool;
var wires:Array<Wire>;
var files:Array<TorrentFile>;
var magnetURI:String;
var infoHash:String;
var length:Int;
var downloaded:Int;
var uploaded:Int;
var numPeers:Int;
var discovery:Dynamic;
function on(event:String, cb:Dynamic -> Void):Void;
function destroy(?cb:Void -> Void):Void;
}
@:native("Wire")
extern class Wire {
var peerId:String;
var addr:String;
var peerExtendedHandshake:Dynamic;
var extendedHandshake:Dynamic;
function use(ext:Dynamic):Void;
function extended(name:String, message:Dynamic):Void;
function on(event:String, cb:Dynamic -> Void):Void;
}
extern class TorrentFile {
var name:String;
var path:String;
var length:Int;
function getBuffer(cb:Dynamic -> Void):Void;
function appendTo(elem:Dynamic):Void;
function streamTo(elem:Dynamic):Void;
}
#end

View File

@ -0,0 +1,221 @@
package leenkx.network.torrent.transport;
import haxe.io.Bytes;
import leenkx.network.torrent.transport.ITransport.TransportOpts;
#if (js && !kha_krom)
import leenkx.network.torrent.transport.WebTorrent;
import js.lib.Uint8Array;
class WebTorrentTransport implements ITransport {
public var onReady:Void -> Void;
public var onPeerConnected:String -> Int -> Void;
public var onPeerDisconnected:Int -> Void;
public var onPeerData:String -> Bytes -> Void;
public var onPeerHandshake:String -> String -> Void;
public var onTrackerUpdate:Dynamic -> Void;
public var onTrackerAnnounce:Void -> Void;
public var onError:String -> Void;
public var wt:WebTorrent;
public var torrent:Torrent;
var torrentCreated:Bool = false;
var extName:String = "lx_channel";
var lastWireCount:Int = 0;
var pk:String;
var ek:String;
var identifier:String;
public function new(?existingWt:WebTorrent) {
#if js
this.wt = existingWt;
#end
}
public function join(identifier:String, opts:TransportOpts):Void {
#if js
var trackerOpts = opts.tracker != null ? opts.tracker : {};
if (opts.iceServers != null) {
Reflect.setField(trackerOpts, "rtcConfig", {iceServers: opts.iceServers});
}
if (this.wt == null) {
var wtOpts:Dynamic = {tracker: trackerOpts};
if (opts.wtOpts != null) {
wtOpts = js.lib.Object.assign(js.lib.Object.assign({}, wtOpts), opts.wtOpts);
}
this.wt = new WebTorrent(wtOpts);
}
var blob:Dynamic = createBlob(identifier);
var torrentOpts:Dynamic = {
name: identifier,
announce: opts.announce != null ? opts.announce : leenkx.network.torrent.TorrentClient.defaultTrackers
};
if (opts.torrentOpts != null) {
torrentOpts = js.lib.Object.assign(
js.lib.Object.assign({}, torrentOpts), opts.torrentOpts);
}
this.pk = opts.pk;
this.ek = opts.ek;
this.identifier = identifier;
if (opts.torrent != null) {
this.torrent = opts.torrent;
this.torrentCreated = false;
if (this.torrent.ready) {
onTorrentReady();
} else {
this.torrent.on("ready", (_) -> onTorrentReady());
}
for (wire in (this.torrent.wires:Array<Dynamic>)) {
attachWire(wire);
}
} else {
this.torrent = this.wt.seed(blob, torrentOpts, (t) -> onTorrentReady());
this.torrentCreated = true;
}
this.torrent.on("wire", (wire) -> attachWire(wire));
this.wt.on("error", (err:Dynamic) -> {
if (onError != null) onError(Std.string(err));
});
#end
}
public function broadcast(data:Bytes):Void {
#if js
if (torrent == null) return;
var wires = torrent.wires;
for (w in 0...wires.length) {
var extHs = wires[w].peerExtendedHandshake;
if (extHs != null && extHs.m != null) {
var m:Dynamic = extHs.m;
if (Reflect.field(m, extName) != null) {
wires[w].extended(extName, haxeBytesToJs(data));
}
}
}
#end
}
public function sendTo(peerPk:String, data:Bytes):Bool {
#if js
if (torrent == null) return false;
var wires = torrent.wires;
for (w in 0...wires.length) {
var extHs = wires[w].peerExtendedHandshake;
if (extHs != null && extHs.m != null && extHs.pk != null
&& extHs.pk.toString() == peerPk
&& Reflect.field(extHs.m, extName) != null) {
wires[w].extended(extName, haxeBytesToJs(data));
return true;
}
}
#end
return false;
}
public function getPeerCount():Int {
#if js
if (torrent == null) return 0;
return torrent.wires.length;
#else
return 0;
#end
}
public function destroy(?cb:Void -> Void):Void {
#if js
if (wt != null && torrentCreated) {
wt.remove(torrent, cb);
} else if (cb != null) {
cb();
}
#else
if (cb != null) cb();
#end
}
#if js
function onTorrentReady():Void {
if (onReady != null) onReady();
if (torrent.discovery != null && torrent.discovery.tracker != null) {
torrent.discovery.tracker.on("update", (update:Dynamic) -> {
if (onTrackerUpdate != null) onTrackerUpdate(update);
});
}
if (torrent.discovery != null) {
torrent.discovery.on("trackerAnnounce", () -> {
if (onTrackerAnnounce != null) onTrackerAnnounce();
checkWireCount();
});
}
}
function attachWire(wire:Wire):Void {
wire.use(wireExtension(wire));
wire.on("close", (_) -> {
if (onPeerDisconnected != null)
onPeerDisconnected(torrent.wires.length);
});
}
function wireExtension(wire:Wire):Dynamic {
var ext:Dynamic = {
name: extName,
onExtendedHandshake: (handshake:Dynamic) -> {
checkWireCount();
if (onPeerHandshake != null) {
var hsPk:String = handshake.pk != null ? handshake.pk.toString() : "";
var hsEk:String = handshake.ek != null ? handshake.ek.toString() : "";
onPeerHandshake(hsPk, hsEk);
}
},
onMessage: (msg:Dynamic) -> {
var peerPk:String = "";
var extHs = wire.peerExtendedHandshake;
if (extHs != null && extHs.pk != null) {
peerPk = extHs.pk.toString();
}
if (onPeerData != null)
onPeerData(peerPk, jsBytesToHaxe(msg));
}
};
wire.extendedHandshake.id = identifier;
wire.extendedHandshake.pk = pk;
wire.extendedHandshake.ek = ek;
return ext;
}
function checkWireCount():Void {
var count = torrent.wires.length;
if (count != lastWireCount) {
if (count > lastWireCount) {
if (onPeerConnected != null) onPeerConnected(null, count);
} else {
if (onPeerDisconnected != null) onPeerDisconnected(count);
}
lastWireCount = count;
}
}
function createBlob(identifier:String):Dynamic {
if (js.Syntax.typeof(js.Syntax.code("File")) != "undefined") {
return new js.html.File([identifier], identifier);
} else {
var buf:Dynamic = js.Syntax.code('Buffer.from({0})', identifier);
Reflect.setField(buf, "name", identifier);
return buf;
}
}
static function haxeBytesToJs(bytes:Bytes):Uint8Array {
return new Uint8Array(@:privateAccess bytes.b);
}
static function jsBytesToHaxe(arr:Dynamic):Bytes {
var len:Int = arr.length;
var bytes = Bytes.alloc(len);
for (i in 0...len) bytes.set(i, arr[i]);
return bytes;
}
#end
}
#end

View File

@ -0,0 +1,149 @@
package leenkx.network.torrent.transport;
import haxe.io.Bytes;
import leenkx.network.torrent.TorrentInfo;
import leenkx.network.torrent.peer.IPeerChannel;
import leenkx.network.torrent.peer.RtcPeerPool;
import leenkx.network.torrent.peer.WireProtocol;
import leenkx.network.torrent.transport.ITransport.TransportOpts;
class WireTransport implements ITransport {
static inline var EXT = "lx_channel";
public var onReady:Void -> Void;
public var onPeerConnected:String -> Int -> Void;
public var onPeerDisconnected:Int -> Void;
public var onPeerData:String -> Bytes -> Void;
public var onPeerHandshake:String -> String -> Void;
public var onTrackerUpdate:Dynamic -> Void;
public var onTrackerAnnounce:Void -> Void;
public var onError:String -> Void;
var announceUrl:String;
var iceServers:Array<String>;
var identifier:String;
var pk:String = "";
var ek:String = "";
var infoHash:Bytes;
var btPeerId:Bytes;
var pool:RtcPeerPool = null;
var wires:Map<String, WireProtocol> = [];
var peerPks:Map<String, String> = [];
var ready:Bool = false;
public function new(announceUrl:String, ?iceServers:Array<String>) {
this.announceUrl = announceUrl;
this.iceServers = iceServers;
}
public function join(identifier:String, opts:TransportOpts):Void {
this.identifier = identifier;
var url = announceUrl;
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 : "";
infoHash = TorrentInfo.fromBinaryString(opts.infoHashBinary);
btPeerId = TorrentInfo.fromBinaryString(opts.peerIdBinary);
pool = new RtcPeerPool(iceServers);
pool.onPeerChannel = onPeerChannel;
pool.onPeerLeft = onPeerLeft;
pool.onReady = function() {
ready = true;
if (onTrackerAnnounce != null) onTrackerAnnounce();
if (onReady != null) onReady();
};
pool.onTrackerUpdate = function(d) {
if (onTrackerUpdate != null) onTrackerUpdate(d);
};
pool.onError = function(msg) {
if (onError != null) onError(msg);
};
pool.connect(url, opts.infoHashBinary, opts.peerIdBinary);
}
public function broadcast(data:Bytes):Void {
for (peerId in wires.keys()) {
var wire = wires.get(peerId);
if (wire != null) wire.sendExtended(EXT, data);
}
}
public function sendTo(peerPk:String, data:Bytes):Bool {
for (peerId in peerPks.keys()) {
if (peerPks.get(peerId) == peerPk) {
var wire = wires.get(peerId);
if (wire != null) return wire.sendExtended(EXT, data);
return false;
}
}
return false;
}
public function getPeerCount():Int {
return Lambda.count(wires);
}
public function destroy(?cb:Void -> Void):Void {
if (pool != null) {
pool.close();
pool = null;
}
wires.clear();
peerPks.clear();
ready = false;
if (cb != null) cb();
}
function onPeerChannel(peerId:String, ch:IPeerChannel):Void {
var wire = new WireProtocol(ch, infoHash, btPeerId);
wires.set(peerId, wire);
wire.extHandshakeFields = {id: identifier, pk: pk, ek: ek};
wire.onExtendedHandshake = function(hs:Dynamic) {
var pkBytes:Bytes = Reflect.field(hs, "pk");
var ekBytes:Bytes = Reflect.field(hs, "ek");
var remotePk = pkBytes != null ? pkBytes.toString() : "";
var remoteEk = ekBytes != null ? ekBytes.toString() : "";
if (remotePk != "") peerPks.set(peerId, remotePk);
if (onPeerHandshake != null && remotePk != "") {
onPeerHandshake(remotePk, remoteEk);
}
if (onPeerConnected != null) {
onPeerConnected(peerId, getPeerCount());
}
};
wire.onExtended = function(name:String, data:Bytes) {
if (name != EXT) return;
var senderPk = peerPks.get(peerId);
if (onPeerData != null) {
onPeerData(senderPk != null ? senderPk : peerId, data);
}
};
wire.onClose = function() dropWire(peerId);
wire.onError = function(msg:String) {
if (onError != null) onError(msg);
};
wire.start([EXT]);
}
function onPeerLeft(peerId:String):Void {
dropWire(peerId);
}
function dropWire(peerId:String):Void {
if (!wires.exists(peerId)) return;
wires.remove(peerId);
peerPks.remove(peerId);
if (onPeerDisconnected != null) {
onPeerDisconnected(getPeerCount());
}
}
}

View File

@ -0,0 +1,414 @@
package leenkx.network.torrent.transport;
import leenkx.network.Types.MessageType;
import leenkx.network.WebSocket;
import leenkx.network.torrent.Crypto;
import leenkx.network.torrent.TorrentClient;
import leenkx.network.torrent.TorrentInfo;
class WsTrackerClient {
static inline var RECONNECT_MINIMUM = 10 * 1000;
static inline var RECONNECT_MAXIMUM = 60 * 60 * 1000;
static inline var RECONNECT_VARIANCE = 5 * 60 * 1000;
public var onOpen:Void -> Void;
public var onMessage:String -> Void;
public var onClose:Void -> Void;
public var onError:String -> Void;
public var isConnected(get, never):Bool;
function get_isConnected():Bool return connected;
var url:String;
var ws:WebSocket = null;
var connected:Bool = false;
var destroyed:Bool = false;
var reconnecting:Bool = false;
var retries:Int = 0;
var reconnectTimer:haxe.Timer = null;
public function new(url:String) {
this.url = url;
}
public function start():Void {
openSocket();
}
function openSocket():Void {
destroyed = false;
var sock:WebSocket = null;
try {
sock = new WebSocket(url);
} catch(e:Dynamic) {
TorrentClient.postMain(function() onSocketError(e));
return;
}
ws = sock;
sock.onopen = function() {
TorrentClient.postMain(onSocketConnect);
};
sock.onmessage = function(m:MessageType) {
switch (m) {
case StrMessage(s):
var msg = s;
TorrentClient.postMain(function() onSocketData(msg));
case BytesMessage(b):
var msg = b.readAllAvailableBytes().toString();
TorrentClient.postMain(function() onSocketData(msg));
}
};
sock.onclose = function() {
TorrentClient.postMain(onSocketClose);
};
sock.onerror = function(e:Dynamic) {
var err = Std.string(e);
TorrentClient.postMain(function() onSocketError(err));
};
}
function onSocketConnect():Void {
if (destroyed) return;
connected = true;
if (reconnecting) {
reconnecting = false;
retries = 0;
}
if (onOpen != null) onOpen();
}
function onSocketData(msg:String):Void {
if (destroyed) return;
if (onMessage != null) onMessage(msg);
}
function onSocketClose():Void {
if (destroyed) return;
connected = false;
if (onClose != null) onClose();
startReconnectTimer();
}
function onSocketError(err:String):Void {
if (destroyed) return;
connected = false;
if (onError != null) onError(err);
startReconnectTimer();
}
function startReconnectTimer():Void {
if (reconnecting || destroyed) return;
reconnecting = true;
var ms = Math.floor(Math.random() * RECONNECT_VARIANCE)
+ Math.min(Math.pow(2, retries) * RECONNECT_MINIMUM,
RECONNECT_MAXIMUM);
if (reconnectTimer != null) reconnectTimer.stop();
reconnectTimer = new haxe.Timer(Std.int(ms));
reconnectTimer.run = function() {
retries++;
reconnectTimer.stop();
reconnectTimer = null;
reconnecting = false;
openSocket();
};
}
public function send(msg:Dynamic):Bool {
if (ws == null || !connected) return false;
var json = haxe.Json.stringify(msg);
var out = new StringBuf();
for (i in 0...json.length) {
var c = StringTools.fastCodeAt(json, i);
if (c > 0x7F || c < 0x20) {
out.add("\\u");
out.add(StringTools.hex(c, 4));
} else {
out.addChar(c);
}
}
json = out.toString();
ws.send(json);
return true;
}
public function close():Void {
destroyed = true;
connected = false;
if (reconnectTimer != null) {
reconnectTimer.stop();
reconnectTimer = null;
}
if (ws != null) {
ws.close();
ws = null;
}
}
}
#if (sys || kha_krom)
class WsTrackerHandler extends leenkx.network.WebSocketHandler {
public function new(socket:leenkx.network.SocketImpl) {
super(socket);
}
}
typedef WsSwarmPeer = {
h:WsTrackerHandler,
peerId:String,
infoHash:String,
left:Float
}
class WsTrackerServer {
public var intervalSec:Int = 30;
public var onLog:String -> Void = null;
public var onError:String -> Void = null;
var host:String;
var port:Int;
var maxConns:Int;
var server:leenkx.network.WebSocketServer<WsTrackerHandler>
= null;
var peers:Map<String, WsSwarmPeer> = [];
var connKeys:Map<WsTrackerHandler, Array<String>> = [];
var rrCursor:Map<String, Int> = [];
public function new(host:String, port:Int, maxConns:Int = 128) {
this.host = host;
this.port = port;
this.maxConns = maxConns;
}
function createServer():leenkx.network.WebSocketServer<WsTrackerHandler> {
return new leenkx.network.WebSocketServer
<WsTrackerHandler>(host, port, maxConns);
}
public function start():Void {
if (server != null) return;
server = createServer();
var self = this;
server.onClientAdded = function(h:WsTrackerHandler) {
h.onmessage = function(m:MessageType) {
self.onMsg(h, m);
};
h.onclose = function() self.onConnClose(h);
h.onerror = function(e:Dynamic) {
self.err("client " + h.id.substr(0, 8)
+ ": " + Std.string(e));
};
self.log("client " + h.id.substr(0, 8)
+ " connected");
};
server.onClientRemoved = function(h:WsTrackerHandler) {
self.onConnClose(h);
};
server.start();
log("ws tracker listening on " + url()
+ " maxConns=" + maxConns);
}
public function url():String {
return scheme() + "://" + host + ":" + port;
}
function scheme():String return "ws";
public function stop():Void {
if (server != null) server.stop();
server = null;
peers.clear();
connKeys.clear();
rrCursor.clear();
}
function onMsg(h:WsTrackerHandler, m:MessageType):Void {
var s:String = switch (m) {
case StrMessage(t): t;
case BytesMessage(b):
b.readAllAvailableBytes().toString();
};
var msg:Dynamic = null;
try {
msg = haxe.Json.parse(s);
} catch (e:Dynamic) {
return;
}
if (msg == null || msg.action != "announce") return;
var ih = Std.string(Reflect.field(msg, "info_hash"));
var pid = Std.string(Reflect.field(msg, "peer_id"));
if (ih == null || ih == "null" || pid == null
|| pid == "null") {
return;
}
var key = pid + "|" + ih;
var left = Std.parseFloat(
Std.string(Reflect.field(msg, "left")));
if (Math.isNaN(left)) left = 0;
if (!peers.exists(key)) {
var arr = connKeys.get(h);
if (arr == null) connKeys.set(h, arr = []);
arr.push(key);
}
peers.set(key, {h: h, peerId: pid, infoHash: ih,
left: left});
var ev:Dynamic = Reflect.field(msg, "event");
var offers:Array<Dynamic> = Reflect.field(msg, "offers");
if (offers != null) {
for (o in offers) {
var t = pickPeer(ih, key);
if (t == null) break;
send(t.h, {
action: "announce",
info_hash: ih,
offer: Reflect.field(o, "offer"),
offer_id: Reflect.field(o, "offer_id"),
peer_id: pid
});
log("offer " + shortId(pid) + " -> "
+ shortId(t.peerId) + " ih="
+ Crypto.toHex(TorrentInfo.fromBinaryString(ih))
.substr(0, 8));
}
}
var answer:Dynamic = Reflect.field(msg, "answer");
var toPid:Dynamic = Reflect.field(msg, "to_peer_id");
if (answer != null && toPid != null) {
var t = peers.get(Std.string(toPid) + "|" + ih);
if (t != null) {
send(t.h, {
action: "announce",
info_hash: ih,
peer_id: pid,
answer: answer,
offer_id: Reflect.field(msg, "offer_id")
});
log("answer " + shortId(pid) + " -> "
+ shortId(t.peerId));
}
}
var counts = swarmCounts(ih);
send(h, {
action: "announce",
info_hash: ih,
interval: intervalSec,
"tracker id": "leenkx-local-ws",
complete: counts.complete,
incomplete: counts.incomplete
});
log("announce peer=" + shortId(pid) + " ih="
+ Crypto.toHex(TorrentInfo.fromBinaryString(ih))
.substr(0, 8)
+ (ev != null ? " event=" + Std.string(ev) : "")
+ " swarm=" + counts.complete + "s/"
+ counts.incomplete + "l");
if (Std.string(ev) == "stopped") {
removePeer(key);
}
}
function pickPeer(ih:String, selfKey:String):WsSwarmPeer {
var list = [for (p in peers)
if (p.infoHash == ih
&& (p.peerId + "|" + ih) != selfKey) p];
if (list.length == 0) return null;
var cur = rrCursor.exists(ih) ? rrCursor.get(ih) : 0;
var p = list[cur % list.length];
rrCursor.set(ih, cur + 1);
return p;
}
function swarmCounts(ih:String):{complete:Int, incomplete:Int} {
var c = 0;
var i = 0;
for (p in peers) {
if (p.infoHash != ih) continue;
if (p.left <= 0) c++; else i++;
}
return {complete: c, incomplete: i};
}
function removePeer(key:String):Void {
var p = peers.get(key);
if (p == null) return;
peers.remove(key);
for (q in peers) {
if (q.infoHash != p.infoHash) continue;
send(q.h, {
action: "announce",
info_hash: p.infoHash,
peer_id: p.peerId,
event: "stopped"
});
}
}
function onConnClose(h:WsTrackerHandler):Void {
var keys = connKeys.get(h);
if (keys != null) {
connKeys.remove(h);
for (k in keys) {
var p = peers.get(k);
if (p != null && p.h == h) removePeer(k);
}
}
}
function send(h:WsTrackerHandler, msg:Dynamic):Void {
try {
h.send(haxe.Json.stringify(msg));
} catch (e:Dynamic) {
err("send: " + Std.string(e));
}
}
static function shortId(bin:String):String {
return Crypto.toHex(TorrentInfo.fromBinaryString(bin))
.substr(-8);
}
function log(m:String):Void {
if (onLog != null) {
var f = onLog;
TorrentClient.postMain(function() f(m));
}
}
function err(m:String):Void {
if (onError != null) {
var f = onError;
TorrentClient.postMain(function() f(m));
}
}
}
#end
#if sys
class WssTrackerServer extends WsTrackerServer {
var certPem:String;
var keyPem:String;
public function new(host:String, port:Int, certPem:String, keyPem:String, maxConns:Int = 128) {
super(host, port, maxConns);
this.certPem = certPem;
this.keyPem = keyPem;
}
override function scheme():String return "wss";
override function createServer():leenkx.network.WebSocketServer<WsTrackerHandler> {
var cert = sys.ssl.Certificate.loadFile(certPem);
var key = sys.ssl.Key.loadFile(keyPem);
return new leenkx.network.WebSocketSecureServer
<WsTrackerHandler>(host, port, cert, key, cert,
maxConns);
}
}
#end

View File

@ -325,7 +325,7 @@ class PhysicsWorld extends Trait {
world.stepSimulation(t, currMaxSteps, Time.fixedStep); world.stepSimulation(t, currMaxSteps, Time.fixedStep);
updateContacts(); updateContacts();
for (rb in rbMap) { @:privateAccess try { rb.physicsUpdate(); } catch(e:haxe.Exception) { trace(e.message); } } // HACK: see this recommendation: https://github.com/armory3d/armory/issues/3044#issuecomment-2558199944. for (rb in rbMap) { @:privateAccess try { rb.physicsUpdate(); } catch(e:haxe.Exception) { trace(e.message); } }
#if lnx_debug #if lnx_debug
physTime = kha.Scheduler.realTime() - startTime; physTime = kha.Scheduler.realTime() - startTime;

View File

@ -223,6 +223,9 @@ class LeenkxExporter:
cls.export_network = False cls.export_network = False
if wrd.lnx_network == 'Enabled': if wrd.lnx_network == 'Enabled':
cls.export_network = True cls.export_network = True
cls.export_torrent = False
if wrd.lnx_torrent == 'Enabled' and wrd.lnx_network != 'Disabled':
cls.export_torrent = True
@staticmethod @staticmethod
def write_matrix(matrix): def write_matrix(matrix):

View File

@ -1,30 +0,0 @@
from lnx.logicnode.lnx_nodes import *
class AddTorrentNode(LnxLogicTreeNode):
"""Add Torrent"""
bl_idname = 'LNAddTorrentNode'
bl_label = 'Add Torrent'
bl_description = 'Add a torrent file from the network'
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(AddTorrentNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Title')
self.add_output('LnxDynamicSocket', 'Files')
self.add_output('LnxNodeSocketAction', 'Done')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
AddTorrentNode.on_register()

View File

@ -4,19 +4,71 @@ class CreateLeenkxNode(LnxLogicTreeNode):
"""Create Leenkx""" """Create Leenkx"""
bl_idname = 'LNCreateLeenkxNode' bl_idname = 'LNCreateLeenkxNode'
bl_label = 'Create Leenkx' bl_label = 'Create Leenkx'
bl_description = 'Create Leenkx' bl_description = ('Open a Leenkx peer connection on the given swarm identifier')
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
lnx_version = 1 lnx_version = 1
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[2:]:
self.inputs.remove(socket)
if (self.property0 == 'datachannel'
and self.property2 != 'inherit'):
self.add_input('LnxNodeSocketArray', 'ICE Servers')
if self.property3 != 'inherit':
self.add_input('LnxNodeSocketArray', 'Trackers')
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
property0: HaxeEnumProperty('property0',
items=[('wire', 'Wire', 'BitTorrent wire protocol transport (default)'),
('datachannel', 'DataChannel', 'WebRTC data channels for browser/krom peers - shows the ICE Servers input')],
name='Transport', default='wire', update=update_sockets)
property1: HaxeBoolProperty('property1',
name='Gossip', description='Relay mode - forward messages to all connected peers', default=False)
property2: HaxeEnumProperty('property2',
items=[('inherit', 'Inherit', 'Use the built-in STUN server list; no ICE Servers input'),
('add', 'Add', 'Merge the ICE Servers input on top of the built-in STUN servers'),
('override', 'Override', 'Replace the ICE server list with the ICE Servers input')],
name='ICE Servers', default='inherit', update=update_sockets)
property3: HaxeEnumProperty('property3',
items=[('inherit', 'Inherit', 'Use the project torrent tracker list; no Trackers input'),
('add', 'Add', 'Merge the Trackers input on top of the project tracker list'),
('override', 'Override', 'Replace the announce tracker list with the Trackers input')],
name='Trackers', default='inherit', update=update_sockets)
def lnx_init(self, context): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxStringSocket', 'Url', default_value="8001") self.add_input('LnxStringSocket', 'Channel', default_value='8001')
if (self.property0 == 'datachannel'
and self.property2 != 'inherit'):
self.add_input('LnxNodeSocketArray', 'ICE Servers')
if self.property3 != 'inherit':
self.add_input('LnxNodeSocketArray', 'Trackers')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Connection') self.add_output('LnxDynamicSocket', 'Connection')
self.add_output('LnxDynamicSocket', 'ID') self.add_output('LnxDynamicSocket', 'ID')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1')
if self.property0 == 'datachannel':
layout.prop(self, 'property2')
layout.prop(self, 'property3')
def register(): def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR') add_category('Leenkx', icon='ORIENTATION_CURSOR')

View File

@ -5,7 +5,7 @@ class CreateTorrentNode(LnxLogicTreeNode):
"""Create Torrent""" """Create Torrent"""
bl_idname = 'LNCreateTorrentNode' bl_idname = 'LNCreateTorrentNode'
bl_label = 'Create Torrent' bl_label = 'Create Torrent'
bl_description = 'Create a torrent file from the network' bl_description = ('Build a .torrent from content (file, files or folder)')
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
lnx_version = 1 lnx_version = 1
@ -13,15 +13,65 @@ class CreateTorrentNode(LnxLogicTreeNode):
super(CreateTorrentNode, self).__init__(*args, **kwargs) super(CreateTorrentNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[1:]:
self.inputs.remove(socket)
self._add_data_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_data_inputs(self):
mode_sockets = {
'file': ('LnxDynamicSocket', 'File (Bytes)', 'Content of the single file as Bytes'),
'files': ('LnxNodeSocketArray', 'Files ({path, data})', 'Array of {path, data} objects'),
'folder': ('LnxStringSocket', 'Folder Path', 'Folder read recursively for content (native targets only)'),
}
socket_type, label, desc = mode_sockets.get(self.property1, mode_sockets['file'])
self.add_input(socket_type, label)
self.add_input('LnxStringSocket', 'Name')
self.add_input('LnxStringSocket', 'Comment')
self.add_input('LnxStringSocket', 'Created By')
self.add_input('LnxStringSocket', 'Source')
self.add_input('LnxNodeSocketArray', 'Trackers')
self.add_input('LnxBoolSocket', 'Private')
self.add_input('LnxIntSocket', 'Piece Length')
self.add_input('LnxNodeSocketArray', 'Web Seeds')
property0: HaxeEnumProperty('property0',
items=[('1', 'V1', 'BitTorrent v1 - SHA-1 piece list over one packed stream; works with every client'),
('2', 'V2', 'BitTorrent v2 (BEP 52) - SHA-256 merkle tree per file, pieces aligned to file boundaries; needs modern peers'),
('3', 'Hybrid', 'One .torrent with both v1 and v2 metadata - two infohashes (btih + btmh) over the same info dict')],
name='Version', default='1')
property1: HaxeEnumProperty('property1',
items=[('file', 'File', 'Single file: Data is a Bytes payload'),
('files', 'Files', 'Multiple files: Data is an array of {path, data}'),
('folder', 'Folder', 'Read a folder recursively (native targets only): Data is a folder path string')],
name='Data', default='file', update=update_sockets)
def lnx_init(self, context): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection') self._add_data_inputs()
self.add_input('LnxDynamicSocket', 'Data')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent') self.add_output('LnxDynamicSocket', 'Meta')
self.add_output('LnxNodeSocketAction', 'Done') self.add_output('LnxDynamicSocket', 'Torrent Bytes')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Magnet')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1')
def register(): def register():

View File

@ -0,0 +1,61 @@
from lnx.logicnode.lnx_nodes import *
class CreateTrackerNode(LnxLogicTreeNode):
"""Create Tracker"""
bl_idname = 'LNCreateTrackerNode'
bl_label = 'Create Tracker Server'
bl_description = ('Hosts a local tracker')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(CreateTrackerNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[1:]:
self.inputs.remove(socket)
self._add_mode_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_mode_inputs(self):
self.add_input('LnxStringSocket', 'Host', default_value='127.0.0.1')
self.add_input('LnxIntSocket', 'Port', default_value=6969)
if self.property0 == 'wss':
self.add_input('LnxStringSocket', 'Cert File')
self.add_input('LnxStringSocket', 'Key File')
property0: HaxeEnumProperty('property0',
items=[('udp', 'UDP', 'UDP tracker (BEP 15, sys targets only)'),
('http', 'HTTP', 'HTTP tracker (BEP 3)'),
('ws', 'WS', 'Plain WebSocket tracker (local relay, WebTorrent/web peers)'),
('wss', 'WSS', 'Secure WebSocket tracker (WebTorrent/web peers)')],
name='Type', default='udp', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self._add_mode_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Tracker')
self.add_output('LnxStringSocket', 'URL')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
CreateTrackerNode.on_register()

View File

@ -0,0 +1,76 @@
from lnx.logicnode.lnx_nodes import *
class DownloadTorrentNode(LnxLogicTreeNode):
"""Download Torrent"""
bl_idname = 'LNDownloadTorrentNode'
bl_label = 'Download Torrent'
bl_description = ('Add a torrent to a Leenkx connection and start downloading')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(DownloadTorrentNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[2:]:
self.inputs.remove(socket)
self._add_id_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_id_inputs(self):
mode_sockets = {
'magnet': ('LnxStringSocket', 'Magnet URI', 'Magnet URI (magnet:?xt=...)'),
'id': ('LnxStringSocket', 'Infohash', '40-char v1 or 64-char v2 infohash hex'),
'file': ('LnxDynamicSocket', 'Torrent File (Bytes)', '.torrent file bytes'),
}
socket_type, label, desc = mode_sockets.get(self.property0, mode_sockets['magnet'])
self.add_input(socket_type, label)
if self.property1 != 'inherit':
self.add_input('LnxNodeSocketArray', 'Trackers')
property0: HaxeEnumProperty('property0',
items=[('magnet', 'Magnet', 'Magnet URI (magnet:?xt=...)'),
('id', 'Infohash', '40-char v1 or 64-char v2 infohash hex'),
('file', 'Torrent File', '.torrent file bytes')],
name='Input', default='magnet', update=update_sockets)
property1: HaxeEnumProperty('property1',
items=[('inherit', 'Inherit', 'Use the torrent\'s own trackers plus the connection\'s; no Trackers input'),
('add', 'Add', 'Merge the Trackers input on top of the torrent\'s own trackers'),
('override', 'Override', 'Replace all trackers with the Trackers input (torrent\'s own and connection defaults are dropped)')],
name='Trackers', default='inherit', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self._add_id_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent')
self.add_output('LnxStringSocket', 'Name')
self.add_output('LnxNodeSocketArray', 'Files')
self.add_output('LnxNodeSocketAction', 'Done')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Magnet')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
DownloadTorrentNode.on_register()

View File

@ -0,0 +1,77 @@
from lnx.logicnode.lnx_nodes import *
class GetTorrentFileNode(LnxLogicTreeNode):
"""Get Torrent File"""
bl_idname = 'LNGetTorrentFileNode'
bl_label = 'Get Torrent File'
bl_description = ('Find one file inside a torrent and read its '
'stored bytes (webtorrent torrent.files.find + '
'file.arrayBuffer). Match By selects how the '
'Match input is interpreted: index, exact file '
'name, exact path, or extension')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(GetTorrentFileNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[3:]:
self.inputs.remove(socket)
self._add_match_input()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_match_input(self):
match_sockets = {
'index': ('LnxIntSocket', 'Index', 'Position in the torrent file list'),
'name': ('LnxStringSocket', 'Name', 'Exact file name, e.g. movie.mp4'),
'path': ('LnxStringSocket', 'Path', 'Exact path inside the torrent, e.g. docs/readme.txt'),
'ext': ('LnxStringSocket', 'Extension', 'File extension like mp4 or .mp4; first matching file wins'),
}
socket_type, label, desc = match_sockets.get(self.property0, match_sockets['name'])
self.add_input(socket_type, label)
property0: HaxeEnumProperty('property0',
items=[('index', 'Index', 'Position in the torrent file list; Match is an int'),
('name', 'Name', 'Exact file name; Match is a string like movie.mp4'),
('path', 'Path', 'Exact path inside the torrent; Match is a string like docs/readme.txt'),
('ext', 'Extension', 'First file whose name ends with the extension; Match is mp4 or .mp4')],
name='Match By', default='name', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self._add_match_input()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'File')
self.add_output('LnxStringSocket', 'Name')
self.add_output('LnxStringSocket', 'Path')
self.add_output('LnxFloatSocket', 'Size')
self.add_output('LnxStringSocket', 'Type')
self.add_output('LnxDynamicSocket', 'Data')
self.add_output('LnxBoolSocket', 'Found')
self.add_output('LnxFloatSocket', 'Downloaded')
self.add_output('LnxFloatSocket', 'Progress')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
GetTorrentFileNode.on_register()

View File

@ -5,63 +5,40 @@ class LeenkxCloseConnectionNode(LnxLogicTreeNode):
"""Close connection on the network""" """Close connection on the network"""
bl_idname = 'LNLeenkxCloseConnectionNode' bl_idname = 'LNLeenkxCloseConnectionNode'
bl_label = 'Close Leenkx Connection' bl_label = 'Close Leenkx Connection'
bl_description = ('Tear down a Leenkx connection, or drop a single peer from it')
lnx_version = 1 lnx_version = 1
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
ind = 2
@staticmethod def update_sockets(self, context):
def get_enum_id_value(obj, prop_name, value): if not self.inputs:
return obj.bl_rna.properties[prop_name].enum_items[value].identifier return
links = {}
@staticmethod for socket in self.inputs:
def get_count_in(type_name): for link in socket.links:
return { links.setdefault(socket.name, []).append(
'client': 0, (link.from_node, link.from_socket))
'peer': 1 for socket in self.inputs[2:]:
}.get(type_name, 0) self.inputs.remove(socket)
if self.property0 == 'peer':
def get_enum(self):
return self.get('property0', 0)
def set_enum(self, value):
# Checking the selection of each type
select_current = self.get_enum_id_value(self, 'property0', value)
select_prev = self.property0
#Check if type changed
if select_prev != select_current:
for i in self.inputs:
self.inputs.remove(i)
# Arguements for type Host
if (self.get_count_in(select_current) == 0):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'Url')
# Arguements for type Client
if (self.get_count_in(select_current) == 1):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'ID') self.add_input('LnxStringSocket', 'ID')
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
self['property0'] = value property0: HaxeEnumProperty('property0',
property0: HaxeEnumProperty(
'property1',
items = [('client', 'Leenkx Client', 'Close the Leenkx Client connection on the network'), items = [('client', 'Leenkx Client', 'Close the Leenkx Client connection on the network'),
('peer', 'Single Peer', 'Close a Peer connection on network')], ('peer', 'Single Peer', 'Drop a peer from the connection (forgets its address; it can rejoin on a new handshake)')],
name='', default='client', name='', default='client',
set=set_enum, update=update_sockets)
get=get_enum)
def lnx_init(self, context): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection') self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'Url') if self.property0 == 'peer':
self.add_input('LnxStringSocket', 'ID')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')

View File

@ -9,84 +9,26 @@ class LeenkxEventNode(LnxLogicTreeNode):
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
lnx_version = 1 lnx_version = 1
@staticmethod property1: HaxeEnumProperty('property1',
def get_enum_id_value(obj, prop_name, value): items = [('onopen', 'OnOpen', 'The connection to the server is open'),
return obj.bl_rna.properties[prop_name].enum_items[value].identifier ('onmessage', 'OnMessage', 'A message arrived; ID is the sender peer id, Data the payload'),
('onerror', 'OnError', 'A connection error occurred'),
@staticmethod ('onclose', 'OnClose', 'The connection was closed'),
def get_count_in(type_name): ('onseen', 'OnSeen', 'A peer was discovered; ID is the peer id'),
return { ('onserver', 'OnServer', 'The signaling server responded'),
'client': 0, ('onconnections', 'OnConnections', 'The server sent the peer list; Data is the connection list'),
'host': 1, ('onping', 'OnPing', 'Heartbeat ping roundtrip completed'),
'securehost': 2 ('onleft', 'OnLeft', 'A peer left; ID is the peer id'),
}.get(type_name, 0) ('ontimeout', 'OnTimeout', 'A handshake timed out'),
('onwireleft', 'OnWireLeft', 'A BitTorrent wire peer disconnected; ID is the peer id'),
def get_enum(self): ('onwireseen', 'OnWireSeen', 'A BitTorrent wire peer connected; ID is the peer id'),
return self.get('property0', 0) ('ontorrent', 'OnTorrent', 'Torrent activity on the connection; Data is the torrent'),
('ontorrentadded', 'OnTorrentAdded', 'A torrent registered on the connection; Data is the Torrent handle'),
def set_enum(self, value): ('ontracker', 'OnTracker', 'A tracker event on the connection'),
# Checking the selection of each type ('onannounce', 'OnAnnounce', 'A tracker announce completed'),
select_current = self.get_enum_id_value(self, 'property0', value) ('ontorrentdone', 'OnTorrentDone', 'A torrent finished downloading; Data is the torrent'),
select_prev = self.property0 ('onload', 'OnLoad', 'The connection was registered; fires once at startup')],
name='', default='onopen')
#Check if type changed
if select_prev != select_current:
for i in self.inputs:
self.inputs.remove(i)
# Arguements for type Client
if (self.get_count_in(select_current) == 0):
self.add_input('LnxStringSocket', 'Url', default_value="8001")
# Arguements for type Host
if (self.get_count_in(select_current) == 1):
self.add_input('LnxStringSocket', 'Domain', default_value="127.0.0.1")
self.add_input('LnxIntSocket', 'Port', default_value=8001)
# Arguements for type Secure Host
if (self.get_count_in(select_current) == 2):
self.add_input('LnxStringSocket', 'Domain', default_value="127.0.0.1")
self.add_input('LnxIntSocket', 'Port', default_value=8001)
self['property0'] = value
property0: HaxeEnumProperty(
'property0',
items = [('client', 'Client', 'Network client Event listener'),
('host', 'Host', 'Network host Event listener'),
('securehost', 'Secure Host', 'Network secure host Event listener')],
name='',
default='client',
set=set_enum,
get=get_enum)
property1: HaxeEnumProperty(
'property1',
items = [('onopen', 'OnOpen', 'Listens to onOpen event'),
('onmessage', 'OnMessage', 'Listens to onMessage event'),
('onerror', 'OnError', 'Listens to onError event'),
('onclose', 'OnClose', 'Listens to onClose event'),
('onseen', 'OnSeen', 'Listens to onClose event'),
('onserver', 'OnServer', 'Listens to onClose event'),
('onconnections', 'OnConnections', 'Listens to onClose event'),
('onping', 'OnPing', 'Listens to onClose event'),
('onleft', 'OnLeft', 'Listens to onClose event'),
('ontimeout', 'OnTimeout', 'Listens to onClose event'),
('onrpc', 'OnRpc', 'Listens to onClose event'),
('onrpcresponse', 'OnRpcResponse', 'Listens to onClose event'),
('onwireleft', 'OnWireLeft', 'Listens to onClose event'),
('onwireseen', 'OnWireSeen', 'Listens to onClose event'),
('ontorrent', 'OnTorrent', 'Listens to onClose event'),
('ontracker', 'OnTracker', 'Listens to onClose event'),
('onannounce', 'OnAnnounce', 'Listens to onClose event')],
name='',
default='onopen')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LeenkxEventNode, self).__init__(*args, **kwargs) super(LeenkxEventNode, self).__init__(*args, **kwargs)
@ -95,7 +37,7 @@ class LeenkxEventNode(LnxLogicTreeNode):
def lnx_init(self, context): def lnx_init(self, context):
#self.add_input('LnxNodeSocketAction', 'In') #self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxStringSocket', 'Url', default_value="ws://127.0.0.1:8001") self.add_input('LnxStringSocket', 'Channel', default_value='8001')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'ID') self.add_output('LnxDynamicSocket', 'ID')
@ -103,7 +45,6 @@ class LeenkxEventNode(LnxLogicTreeNode):
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1') layout.prop(self, 'property1')
def register(): def register():

View File

@ -9,16 +9,15 @@ class LeenkxMessageParserNode(LnxLogicTreeNode):
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
lnx_version = 1 lnx_version = 1
property0: HaxeEnumProperty( property0: HaxeEnumProperty('property0', items=[
'property0', ('string', 'String', 'Parse the message as a string'),
items = [('string', 'String', 'Event for a string over the network'), ('vector', 'Vector', 'Parse the message as a vector (Vec4)'),
('vector', 'Vector', 'Event for a vector over the network'), ('float', 'Float', 'Parse the message as a float'),
('float', 'Float', 'Event for a float over the network'), ('integer', 'Integer', 'Parse the message as an integer'),
('integer', 'Integer', 'Event for an integer over the network'), ('boolean', 'Boolean', 'Parse the message as a boolean'),
('boolean', 'Boolean', 'Event for a boolean over the network'), ('transform', 'Transform', 'Parse the message as a transform (Mat4)'),
('transform', 'Transform', 'Event for a transform over the network'), ('rotation', 'Rotation', 'Parse the message as a rotation (Quat)')
('rotation', 'Rotation', 'Event for a rotation over the network')], ], name='', default='string')
name='', default='string')
def lnx_init(self, context): def lnx_init(self, context):

View File

@ -9,77 +9,55 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
lnx_category = 'Leenkx' lnx_category = 'Leenkx'
lnx_version = 1 lnx_version = 1
@staticmethod def update_sockets(self, context):
def get_enum_id_value(obj, prop_name, value): if not self.inputs:
return obj.bl_rna.properties[prop_name].enum_items[value].identifier return
links = {}
send_all = True
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
if socket.name == 'Send All':
send_all = socket.default_value_raw
for socket in self.inputs[3:]:
self.inputs.remove(socket)
self._add_data_inputs(send_all)
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
@staticmethod def _add_data_inputs(self, send_all=True):
def get_count_in(type_name): data_sockets = {
return { 'string': ('LnxStringSocket', 'String payload to send'),
'client': 0, 'vector': ('LnxVectorSocket', 'Vector payload to send'),
'host': 1, 'float': ('LnxFloatSocket', 'Float payload to send'),
'securehost': 2 'integer': ('LnxIntSocket', 'Integer payload to send'),
}.get(type_name, 0) 'boolean': ('LnxBoolSocket', 'Boolean payload to send'),
'transform': ('LnxDynamicSocket', 'Transform (Mat4) payload to send'),
'rotation': ('LnxRotationSocket', 'Rotation (Quat) payload to send'),
}
socket_type, desc = data_sockets.get(self.property1, data_sockets['string'])
self.add_input(socket_type, 'Data')
self.add_input('LnxBoolSocket', 'Send All', default_value=send_all)
if not send_all:
self._add_id_input()
def get_enum(self): def _add_id_input(self):
return self.get('property0', 0)
def set_enum(self, value):
# Checking the selection of each type
select_current = self.get_enum_id_value(self, 'property0', value)
select_prev = self.property0
#Check if type changed
if select_prev != select_current:
for i in self.inputs:
self.inputs.remove(i)
# Arguements for type Client
if (self.get_count_in(select_current) == 0):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'API')
self.add_input('LnxDynamicSocket', 'Data')
self.add_input('LnxStringSocket', 'ID') self.add_input('LnxStringSocket', 'ID')
self.add_input('LnxBoolSocket', 'Send All')
# Arguements for type Host def on_socket_val_update(self, context, socket):
if (self.get_count_in(select_current) == 1): if socket.name == 'Send All':
self.add_input('LnxNodeSocketAction', 'In') if (len(self.inputs) > 5
self.add_input('LnxDynamicSocket', 'Connection') and self.inputs[5].name == 'ID'):
self.add_input('LnxStringSocket', 'API') self.inputs.remove(self.inputs[5])
self.add_input('LnxDynamicSocket', 'Data') if not socket.default_value_raw:
self.add_input('LnxStringSocket', 'ID') self._add_id_input()
self.add_input('LnxBoolSocket', 'Send All') super().on_socket_val_update(context, socket)
property1: HaxeEnumProperty('property1',
# Arguements for type Secure Host
if (self.get_count_in(select_current) == 2):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'API')
self.add_input('LnxDynamicSocket', 'Data')
self.add_input('LnxStringSocket', 'ID')
self.add_input('LnxBoolSocket', 'Send All')
self['property0'] = value
property0: HaxeEnumProperty(
'property0',
items = [('client', 'Client', 'Network client Event listener'),
('host', 'Host', 'Network host Event listener'),
('securehost', 'Secure Host', 'Network secure host Event listener')],
name='',
default='client',
set=set_enum,
get=get_enum)
property1: HaxeEnumProperty(
'property1',
items = [('string', 'String', 'Send a string over the network to one or all of the clients'), items = [('string', 'String', 'Send a string over the network to one or all of the clients'),
('vector', 'Vector', 'Send a vector over the network to one or all of the clients'), ('vector', 'Vector', 'Send a vector over the network to one or all of the clients'),
('float', 'Float', 'Send a float over the network to one or all of the clients'), ('float', 'Float', 'Send a float over the network to one or all of the clients'),
@ -87,8 +65,12 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
('boolean', 'Boolean', 'Send a boolean over the network to one or all of the clients'), ('boolean', 'Boolean', 'Send a boolean over the network to one or all of the clients'),
('transform', 'Transform', 'Send a transform over the network to one or all of the clients'), ('transform', 'Transform', 'Send a transform over the network to one or all of the clients'),
('rotation', 'Rotation', 'Send a rotation over the network to one or all of the clients')], ('rotation', 'Rotation', 'Send a rotation over the network to one or all of the clients')],
name='', name='', default='string', update=update_sockets)
default='string')
property2: HaxeBoolProperty('property2',
name='Encrypted',
description='Use full encryption protocol (signing, bencode, box encryption). Disable for fast raw message mode',
default=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -100,17 +82,15 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection') self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxStringSocket', 'API') self.add_input('LnxStringSocket', 'API')
self.add_input('LnxDynamicSocket', 'Data') self._add_data_inputs()
self.add_input('LnxStringSocket', 'ID')
self.add_input('LnxBoolSocket', 'Send All')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout): def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
layout.prop(self, 'property1') layout.prop(self, 'property1')
layout.prop(self, 'property2')
def register(): def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR') add_category('Leenkx', icon='ORIENTATION_CURSOR')

View File

@ -0,0 +1,68 @@
from lnx.logicnode.lnx_nodes import *
class ParseTorrentNode(LnxLogicTreeNode):
"""Parse Torrent"""
bl_idname = 'LNParseTorrentNode'
bl_label = 'Parse Torrent'
bl_description = ('Parse a torrent id without joining a swarm')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(ParseTorrentNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[1:]:
self.inputs.remove(socket)
self._add_id_input()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_id_input(self):
mode_sockets = {
'magnet': ('LnxStringSocket', 'Magnet URI', 'Magnet URI (magnet:?xt=...)'),
'id': ('LnxStringSocket', 'Infohash', '40-char v1 or 64-char v2 infohash hex'),
'file': ('LnxDynamicSocket', 'Torrent File (Bytes)', '.torrent file bytes'),
}
socket_type, label, desc = mode_sockets.get(self.property0, mode_sockets['magnet'])
self.add_input(socket_type, label)
property0: HaxeEnumProperty('property0',
items=[('magnet', 'Magnet', 'Magnet URI (magnet:?xt=...)'),
('id', 'Infohash', '40-char v1 or 64-char v2 infohash hex'),
('file', 'Torrent File', '.torrent file bytes')],
name='Input', default='magnet', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self._add_id_input()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Meta')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Name')
self.add_output('LnxNodeSocketArray', 'Trackers')
self.add_output('LnxNodeSocketArray', 'Web Seeds')
self.add_output('LnxNodeSocketArray', 'Files')
self.add_output('LnxFloatSocket', 'Total Size')
self.add_output('LnxDynamicSocket', 'Torrent Bytes')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
ParseTorrentNode.on_register()

View File

@ -17,12 +17,14 @@ class SeedTorrentNode(LnxLogicTreeNode):
def lnx_init(self, context): def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In') self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection') self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Opts [Map]') self.add_input('LnxDynamicSocket', 'Torrent')
self.add_input('LnxDynamicSocket', 'Data') self.add_input('LnxDynamicSocket', 'Data')
self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent') self.add_output('LnxDynamicSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Done') self.add_output('LnxNodeSocketAction', 'Done')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Magnet')
def register(): def register():

View File

@ -0,0 +1,75 @@
from lnx.logicnode.lnx_nodes import *
class SetTrackersNode(LnxLogicTreeNode):
"""Set Trackers"""
bl_idname = 'LNSetTrackersNode'
bl_label = 'Set Trackers'
bl_description = ('Add/Override edit the tracker announce list of a live torrent')
lnx_category = 'Leenkx'
lnx_version = 1
tracker_props = {
'interval': ('LnxIntSocket', 'Interval'),
'port': ('LnxIntSocket', 'Port'),
'key': ('LnxIntSocket', 'Key'),
'numwant': ('LnxIntSocket', 'Num Want'),
'trackerId': ('LnxStringSocket', 'Tracker ID'),
}
def __init__(self, *args, **kwargs):
super(SetTrackersNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def update_sockets(self, context):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[1:]:
self.inputs.remove(socket)
self._add_target_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_target_inputs(self):
if self.property0 in ('add', 'override'):
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self.add_input('LnxNodeSocketArray', 'Trackers')
else:
self.add_input('LnxDynamicSocket', 'Tracker')
socket_type, label = self.tracker_props.get(
self.property0, self.tracker_props['interval'])
self.add_input(socket_type, label)
property0: HaxeEnumProperty('property0',
items=[('add', 'Add', 'Merge the Trackers input on top of the existing announce list'),
('override', 'Override', 'Replace the announce list entirely; running tracker connections are restarted'),
('interval', 'Interval', 'Announce interval in seconds'),
('port', 'Port', 'Listening port reported to the tracker'),
('key', 'Key', 'Session key sent with announces'),
('numwant', 'Num Want', 'Number of peers requested per announce'),
('trackerId', 'Tracker ID', 'Tracker id echoed back by the tracker')],
name='Property', default='add', update=update_sockets)
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self._add_target_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Target')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
SetTrackersNode.on_register()

View File

@ -0,0 +1,31 @@
from lnx.logicnode.lnx_nodes import *
class TorrentClientInfoNode(LnxLogicTreeNode):
"""Torrent Client Info"""
bl_idname = 'LNTorrentClientInfoNode'
bl_label = 'Connection Torrents'
bl_description = ('Aggregate stats for the torrent client attached')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(TorrentClientInfoNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxNodeSocketArray', 'Torrents')
self.add_output('LnxIntSocket', 'Count')
self.add_output('LnxFloatSocket', 'Down Rate')
self.add_output('LnxFloatSocket', 'Up Rate')
self.add_output('LnxFloatSocket', 'Progress')
self.add_output('LnxFloatSocket', 'Ratio')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
TorrentClientInfoNode.on_register()

View File

@ -0,0 +1,71 @@
from lnx.logicnode.lnx_nodes import *
class TorrentControlNode(LnxLogicTreeNode):
"""Torrent Control"""
bl_idname = 'LNTorrentControlNode'
bl_label = 'Torrent Control'
bl_description = ('Start, stop, remove or prioritize a torrent.')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(TorrentControlNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
property0: HaxeEnumProperty('property0',
items=[('start', 'Start', 'Start or resume the torrent session'),
('stop', 'Stop', 'Pause the torrent (announces stopped to trackers); it stays registered and can be started again'),
('remove', 'Remove', 'Destroy the torrent and remove it from the client'),
('remove-data', 'Remove + Data', 'Destroy the torrent and delete the stored content bytes'),
('select', 'Select', 'Prioritize a piece range: File handle or Start/End bytes, Priority optional (webtorrent torrent.select)'),
('deselect', 'Deselect', 'Drop a priority selection, back to normal order'),
('critical', 'Critical', 'Top-priority selection for streaming seeks'),
('add-peer', 'Add Peer', 'Connect to a peer directly; Address is host:port (sys/Krom targets only)'),
('add-web-seed', 'Add Web Seed', 'Register a BEP 19 webseed; Address is an http(s) URL, fills pieces when peers can\'t serve them')],
name='Action', default='start',
update=lambda self, context: self._rebuild_inputs())
def _rebuild_inputs(self):
if not self.inputs:
return
links = {}
for socket in self.inputs:
for link in socket.links:
links.setdefault(socket.name, []).append(
(link.from_node, link.from_socket))
for socket in self.inputs[3:]:
self.inputs.remove(socket)
self._add_action_inputs()
tree = self.id_data
for socket in self.inputs:
if socket.name in links:
for from_node, from_socket in links[socket.name]:
tree.links.new(from_socket, socket)
def _add_action_inputs(self):
if self.property0 in ('select', 'deselect', 'critical'):
self.add_input('LnxDynamicSocket', 'File')
self.add_input('LnxIntSocket', 'Start')
self.add_input('LnxIntSocket', 'End')
self.add_input('LnxIntSocket', 'Priority')
elif self.property0 == 'add-peer':
self.add_input('LnxStringSocket', 'Address')
elif self.property0 == 'add-web-seed':
self.add_input('LnxStringSocket', 'Address')
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self._add_action_inputs()
self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
TorrentControlNode.on_register()

View File

@ -0,0 +1,40 @@
from lnx.logicnode.lnx_nodes import *
class OnTorrentEventNode(LnxLogicTreeNode):
"""On Torrent Event"""
bl_idname = 'LNOnTorrentEventNode'
bl_label = 'On Torrent Event'
bl_description = ('Fires when the selected torrent event happens ')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(OnTorrentEventNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
property0: HaxeEnumProperty('property0',
items=[('ready', 'Ready', 'Metainfo is known; fires immediately if already ready'),
('piece', 'Piece', 'A piece finished downloading; Value is the piece index'),
('wire', 'Wire', 'A peer connected; Value is the WireProtocol'),
('metadata', 'Metadata', 'ut_metadata fetch completed; Value is the MetaInfo'),
('done', 'Done', 'All pieces are downloaded'),
('error', 'Error', 'Torrent failed; Value is the error string')],
name='Event', default='done')
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxDynamicSocket', 'Torrent')
self.add_output('LnxDynamicSocket', 'Value')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
OnTorrentEventNode.on_register()

View File

@ -0,0 +1,46 @@
from lnx.logicnode.lnx_nodes import *
class TorrentInfoNode(LnxLogicTreeNode):
"""Torrent Info"""
bl_idname = 'LNTorrentInfoNode'
bl_label = 'Torrent Info'
bl_description = ('Read the state of a torrent: infohash, name, peer count, progress, transfer totals, magnet URI')
lnx_category = 'Leenkx'
lnx_version = 1
def __init__(self, *args, **kwargs):
super(TorrentInfoNode, self).__init__(*args, **kwargs)
array_nodes[str(id(self))] = self
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxDynamicSocket', 'Connection')
self.add_input('LnxDynamicSocket', 'Torrent')
self.add_output('LnxNodeSocketAction', 'Out')
self.add_output('LnxStringSocket', 'InfoHash')
self.add_output('LnxStringSocket', 'Name')
self.add_output('LnxIntSocket', 'Peers')
self.add_output('LnxFloatSocket', 'Progress')
self.add_output('LnxFloatSocket', 'Downloaded')
self.add_output('LnxFloatSocket', 'Uploaded')
self.add_output('LnxBoolSocket', 'Done')
self.add_output('LnxStringSocket', 'Magnet')
self.add_output('LnxNodeSocketArray', 'Files')
self.add_output('LnxIntSocket', 'Num Pieces')
self.add_output('LnxIntSocket', 'Piece Length')
self.add_output('LnxFloatSocket', 'Total Size')
self.add_output('LnxStringSocket', 'Comment')
self.add_output('LnxStringSocket', 'Created By')
self.add_output('LnxIntSocket', 'Version')
self.add_output('LnxStringSocket', 'InfoHash2')
self.add_output('LnxFloatSocket', 'Down Rate')
self.add_output('LnxFloatSocket', 'Up Rate')
self.add_output('LnxBoolSocket', 'Stopped')
self.add_output('LnxDynamicSocket', 'Torrent')
def register():
add_category('Leenkx', icon='ORIENTATION_CURSOR')
TorrentInfoNode.on_register()

View File

@ -248,6 +248,7 @@ def export_data_impl(fp, sdk_path):
export_navigation = bpy.data.worlds['Lnx'].lnx_navigation != 'Disabled' export_navigation = bpy.data.worlds['Lnx'].lnx_navigation != 'Disabled'
export_ui = bpy.data.worlds['Lnx'].lnx_ui != 'Disabled' export_ui = bpy.data.worlds['Lnx'].lnx_ui != 'Disabled'
export_network = bpy.data.worlds['Lnx'].lnx_network != 'Disabled' export_network = bpy.data.worlds['Lnx'].lnx_network != 'Disabled'
export_torrent = bpy.data.worlds['Lnx'].lnx_torrent != 'Disabled' and bpy.data.worlds['Lnx'].lnx_network != 'Disabled'
assets.reset() assets.reset()
mat_state.material_cache.clear() mat_state.material_cache.clear()
@ -264,6 +265,7 @@ def export_data_impl(fp, sdk_path):
navigation_found = False navigation_found = False
ui_found = False ui_found = False
network_found = False network_found = False
torrent_found = False
LeenkxExporter.compress_enabled = wrd.lnx_asset_compression LeenkxExporter.compress_enabled = wrd.lnx_asset_compression
LeenkxExporter.optimize_enabled = state.is_publish and wrd.lnx_optimize_data LeenkxExporter.optimize_enabled = state.is_publish and wrd.lnx_optimize_data
if not os.path.exists(build_dir + '/compiled/Assets'): if not os.path.exists(build_dir + '/compiled/Assets'):
@ -326,6 +328,8 @@ def export_data_impl(fp, sdk_path):
ui_found = True ui_found = True
if LeenkxExporter.export_network: if LeenkxExporter.export_network:
network_found = True network_found = True
if LeenkxExporter.export_torrent:
torrent_found = True
assets.add(asset_path) assets.add(asset_path)
if physics_found is False: # Disable physics if no rigid body is exported if physics_found is False: # Disable physics if no rigid body is exported
@ -340,6 +344,9 @@ def export_data_impl(fp, sdk_path):
if network_found == False: if network_found == False:
export_network = False export_network = False
if torrent_found == False:
export_torrent = False
# Ugly workaround: some logic nodes require Zui code even if no UI is used, # Ugly workaround: some logic nodes require Zui code even if no UI is used,
# for now enable UI export unless explicitly disabled. # for now enable UI export unless explicitly disabled.
export_ui = True export_ui = True
@ -349,6 +356,9 @@ def export_data_impl(fp, sdk_path):
if wrd.lnx_network == 'Enabled': if wrd.lnx_network == 'Enabled':
export_network = True export_network = True
if wrd.lnx_torrent == 'Enabled' and wrd.lnx_network != 'Disabled':
export_torrent = True
modules = [] modules = []
if wrd.lnx_audio == 'Enabled': if wrd.lnx_audio == 'Enabled':
modules.append('audio') modules.append('audio')
@ -360,6 +370,8 @@ def export_data_impl(fp, sdk_path):
modules.append('ui') modules.append('ui')
if export_network: if export_network:
modules.append('network') modules.append('network')
if export_torrent:
modules.append('torrent')
defs = lnx.utils.def_strings_to_array(wrd.world_defs) defs = lnx.utils.def_strings_to_array(wrd.world_defs)
cdefs = lnx.utils.def_strings_to_array(wrd.compo_defs) cdefs = lnx.utils.def_strings_to_array(wrd.compo_defs)
@ -451,7 +463,7 @@ def export_data_impl(fp, sdk_path):
wrd.lnx_project_version = lnx.utils.change_version_project(wrd.lnx_project_version) wrd.lnx_project_version = lnx.utils.change_version_project(wrd.lnx_project_version)
# Write khafile.js # Write khafile.js
write_data.write_khafilejs(state.is_play, export_physics, export_navigation, export_ui, export_network, state.is_publish, LeenkxExporter.import_traits) write_data.write_khafilejs(state.is_play, export_physics, export_navigation, export_ui, export_network, export_torrent, state.is_publish, LeenkxExporter.import_traits)
# Write Main.hx - depends on write_khafilejs for writing number of assets # Write Main.hx - depends on write_khafilejs for writing number of assets
scene_name = lnx.utils.get_project_scene_name() scene_name = lnx.utils.get_project_scene_name()

View File

@ -34,6 +34,16 @@ else:
lnx_version = '2026.9' lnx_version = '2026.9'
lnx_commit = '$Id: 6b2644d47db169cedd95593497cc283207d23a74 $' lnx_commit = '$Id: 6b2644d47db169cedd95593497cc283207d23a74 $'
LNX_TORRENT_DEFAULT_TRACKERS = [
'wss://ws1.leenkx.com',
'wss://video.blender.org/tracker/socket',
]
LNX_TORRENT_DEFAULT_ICE_SERVERS = [
'stun:stun.l.google.com:19302',
'stun:global.stun.twilio.com:3478',
]
def get_project_html5_copy(self): def get_project_html5_copy(self):
return self.get('lnx_project_html5_copy', False) return self.get('lnx_project_html5_copy', False)
@ -282,6 +292,17 @@ def init_properties():
('Enabled', 'Enabled', 'Enabled'), ('Enabled', 'Enabled', 'Enabled'),
('Auto', 'Auto', 'Auto')], ('Auto', 'Auto', 'Auto')],
name="Networking", default='Auto', description="Include Network library", update=assets.invalidate_compiler_cache) name="Networking", default='Auto', description="Include Network library", update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_torrent = EnumProperty(
items=[('Disabled', 'Disabled', 'Disabled'),
('Enabled', 'Enabled', 'Enabled')],
name="P2P", default='Disabled', description="Include P2P (Leenkx) library", update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_torrent_download_dir = StringProperty(name="Download Path", description="Directory torrent downloads are written to. Relative paths resolve against the project directory at runtime; absolute paths are used as-is", default="Downloads", subtype='DIR_PATH', update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_torrent_trackers = CollectionProperty(name="Torrent Trackers", description="Default torrent tracker announce URLs", type=bpy.types.PropertyGroup)
bpy.types.World.lnx_torrent_trackers_index = IntProperty(name="Torrent Tracker Index", default=0)
bpy.types.World.lnx_torrent_ice_servers = CollectionProperty(name="ICE Servers", description="Default ICE/STUN servers used for WebRTC peer connections", type=bpy.types.PropertyGroup)
bpy.types.World.lnx_torrent_ice_servers_index = IntProperty(name="ICE Server Index", default=0)
bpy.types.World.lnx_torrent_peer_timeout = IntProperty(name="Peer Timeout", description="Milliseconds after which an unresponsive peer is dropped", default=5 * 60 * 1000, min=0, update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_torrent_heartbeat = IntProperty(name="Heartbeat", description="Milliseconds between heartbeat packets sent to peers", default=30000, min=0, update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_audio = EnumProperty( bpy.types.World.lnx_audio = EnumProperty(
items=[('Disabled', 'Disabled', 'Disabled'), items=[('Disabled', 'Disabled', 'Disabled'),
('Enabled', 'Enabled', 'Enabled')], ('Enabled', 'Enabled', 'Enabled')],
@ -701,13 +722,23 @@ def create_wrd():
wrd.use_fake_user = True # Store data world object, add fake user to keep it alive wrd.use_fake_user = True # Store data world object, add fake user to keep it alive
wrd.lnx_version = lnx_version wrd.lnx_version = lnx_version
wrd.lnx_commit = lnx_commit wrd.lnx_commit = lnx_commit
seed_torrent_trackers(wrd)
def seed_torrent_trackers(wrd):
if len(wrd.lnx_torrent_trackers) == 0:
for url in LNX_TORRENT_DEFAULT_TRACKERS:
wrd.lnx_torrent_trackers.add().name = url
if len(wrd.lnx_torrent_ice_servers) == 0:
for url in LNX_TORRENT_DEFAULT_ICE_SERVERS:
wrd.lnx_torrent_ice_servers.add().name = url
def init_properties_on_load(): def init_properties_on_load():
if 'Lnx' not in bpy.data.worlds: if 'Lnx' not in bpy.data.worlds:
init_properties() init_properties()
wrd = bpy.data.worlds['Lnx']
seed_torrent_trackers(wrd)
# New project? # New project?
if bpy.data.filepath == '': if bpy.data.filepath == '':
wrd = bpy.data.worlds['Lnx']
wrd.lnx_debug_console = lnx.utils.get_debug_console_auto() wrd.lnx_debug_console = lnx.utils.get_debug_console_auto()
lnx.utils.fetch_script_names() lnx.utils.fetch_script_names()

View File

@ -1358,11 +1358,151 @@ class LNX_PT_ProjectModulesPanel(bpy.types.Panel):
layout.prop(wrd, 'lnx_navigation_engine') layout.prop(wrd, 'lnx_navigation_engine')
layout.prop(wrd, 'lnx_ui') layout.prop(wrd, 'lnx_ui')
layout.prop(wrd, 'lnx_network') layout.prop(wrd, 'lnx_network')
if wrd.lnx_network != 'Disabled':
layout.prop(wrd, 'lnx_torrent')
layout.prop_search(wrd, 'lnx_khafile', bpy.data, 'texts') layout.prop_search(wrd, 'lnx_khafile', bpy.data, 'texts')
layout.prop(wrd, 'lnx_project_root') layout.prop(wrd, 'lnx_project_root')
layout.prop(wrd, 'lnx_external_blends_path') layout.prop(wrd, 'lnx_external_blends_path')
class LNX_UL_TorrentTrackerList(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.prop(item, "name", text="", emboss=False, icon='NETWORK_DRIVE')
class LnxTorrentTrackerListNewItem(bpy.types.Operator):
bl_idname = "lnx_torrent_tracker_list.new_item"
bl_label = "Add a new tracker"
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
wrd.lnx_torrent_trackers.add()
wrd.lnx_torrent_trackers_index = len(wrd.lnx_torrent_trackers) - 1
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LnxTorrentTrackerListDeleteItem(bpy.types.Operator):
bl_idname = "lnx_torrent_tracker_list.delete_item"
bl_label = "Deletes a tracker"
@classmethod
def poll(cls, context):
wrd = bpy.data.worlds['Lnx']
return len(wrd.lnx_torrent_trackers) > 0
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
index = wrd.lnx_torrent_trackers_index
wrd.lnx_torrent_trackers.remove(index)
wrd.lnx_torrent_trackers_index = min(max(0, index - 1), len(wrd.lnx_torrent_trackers) - 1)
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LnxTorrentTrackerListReset(bpy.types.Operator):
bl_idname = "lnx_torrent_tracker_list.reset_items"
bl_label = "Reset To Default Trackers"
bl_description = "Remove all trackers and add back the default ones"
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
wrd.lnx_torrent_trackers.clear()
for url in props.LNX_TORRENT_DEFAULT_TRACKERS:
wrd.lnx_torrent_trackers.add().name = url
wrd.lnx_torrent_trackers_index = 0
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LNX_UL_TorrentIceServerList(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.prop(item, "name", text="", emboss=False, icon='URL')
class LnxTorrentIceServerListNewItem(bpy.types.Operator):
bl_idname = "lnx_torrent_ice_server_list.new_item"
bl_label = "Add a new ICE server"
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
wrd.lnx_torrent_ice_servers.add()
wrd.lnx_torrent_ice_servers_index = len(wrd.lnx_torrent_ice_servers) - 1
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LnxTorrentIceServerListDeleteItem(bpy.types.Operator):
bl_idname = "lnx_torrent_ice_server_list.delete_item"
bl_label = "Deletes an ICE server"
@classmethod
def poll(cls, context):
wrd = bpy.data.worlds['Lnx']
return len(wrd.lnx_torrent_ice_servers) > 0
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
index = wrd.lnx_torrent_ice_servers_index
wrd.lnx_torrent_ice_servers.remove(index)
wrd.lnx_torrent_ice_servers_index = min(max(0, index - 1), len(wrd.lnx_torrent_ice_servers) - 1)
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LnxTorrentIceServerListReset(bpy.types.Operator):
bl_idname = "lnx_torrent_ice_server_list.reset_items"
bl_label = "Reset To Default ICE Servers"
bl_description = "Remove all ICE servers and add back the default ones"
def execute(self, context):
wrd = bpy.data.worlds['Lnx']
wrd.lnx_torrent_ice_servers.clear()
for url in props.LNX_TORRENT_DEFAULT_ICE_SERVERS:
wrd.lnx_torrent_ice_servers.add().name = url
wrd.lnx_torrent_ice_servers_index = 0
assets.invalidate_compiler_cache(wrd, context)
return{'FINISHED'}
class LNX_PT_LeenkxNetworkPanel(bpy.types.Panel):
bl_label = "Leenkx Network"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "render"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
wrd = bpy.data.worlds['Lnx']
col = layout.column()
col.enabled = wrd.lnx_torrent != 'Disabled' and wrd.lnx_network != 'Disabled'
col.prop(wrd, "lnx_torrent_download_dir")
col.prop(wrd, "lnx_torrent_peer_timeout")
col.prop(wrd, "lnx_torrent_heartbeat")
col.label(text="Trackers")
rows = 2
if len(wrd.lnx_torrent_trackers) > 1:
rows = 4
row = col.row()
row.template_list("LNX_UL_TorrentTrackerList", "The_List", wrd, "lnx_torrent_trackers", wrd, "lnx_torrent_trackers_index", rows=rows)
btns = row.column(align=True)
btns.operator("lnx_torrent_tracker_list.new_item", icon='ADD', text="")
btns.operator("lnx_torrent_tracker_list.delete_item", icon='REMOVE', text="")
col.operator("lnx_torrent_tracker_list.reset_items", icon='FILE_REFRESH')
col.label(text="ICE Servers")
rows = 2
if len(wrd.lnx_torrent_ice_servers) > 1:
rows = 4
row = col.row()
row.template_list("LNX_UL_TorrentIceServerList", "The_List", wrd, "lnx_torrent_ice_servers", wrd, "lnx_torrent_ice_servers_index", rows=rows)
btns = row.column(align=True)
btns.operator("lnx_torrent_ice_server_list.new_item", icon='ADD', text="")
btns.operator("lnx_torrent_ice_server_list.delete_item", icon='REMOVE', text="")
col.operator("lnx_torrent_ice_server_list.reset_items", icon='FILE_REFRESH')
class LnxVirtualInputPanel(bpy.types.Panel): class LnxVirtualInputPanel(bpy.types.Panel):
bl_label = "Leenkx Virtual Input" bl_label = "Leenkx Virtual Input"
bl_space_type = "PROPERTIES" bl_space_type = "PROPERTIES"
@ -3107,6 +3247,15 @@ __REG_CLASSES = (
LNX_PT_ProjectFlagsDebugConsolePanel, LNX_PT_ProjectFlagsDebugConsolePanel,
LNX_PT_ProjectWindowPanel, LNX_PT_ProjectWindowPanel,
LNX_PT_ProjectModulesPanel, LNX_PT_ProjectModulesPanel,
LNX_UL_TorrentTrackerList,
LnxTorrentTrackerListNewItem,
LnxTorrentTrackerListDeleteItem,
LnxTorrentTrackerListReset,
LNX_UL_TorrentIceServerList,
LnxTorrentIceServerListNewItem,
LnxTorrentIceServerListDeleteItem,
LnxTorrentIceServerListReset,
LNX_PT_LeenkxNetworkPanel,
LNX_PT_RenderPathPanel, LNX_PT_RenderPathPanel,
LNX_PT_RenderPathRendererPanel, LNX_PT_RenderPathRendererPanel,
LNX_PT_RenderPathShadowsPanel, LNX_PT_RenderPathShadowsPanel,

View File

@ -158,7 +158,7 @@ def remove_readonly(func, path, excinfo):
func(path) func(path)
def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, export_ui: bool, export_network: bool, is_publish: bool, def write_khafilejs(is_play, export_physics: bool, export_navigation: bool, export_ui: bool, export_network: bool, export_torrent: bool, is_publish: bool,
import_traits: List[str]) -> None: import_traits: List[str]) -> None:
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
rpdat = lnx.utils.get_rp() rpdat = lnx.utils.get_rp()
@ -482,9 +482,16 @@ let project = new Project('""" + lnx.utils.safesrc(wrd.lnx_project_name + '-' +
if not os.path.exists('Libraries/network'): if not os.path.exists('Libraries/network'):
khafile.write(add_leenkx_library(sdk_path, 'lib/network', rel_path=do_relpath_sdk)) khafile.write(add_leenkx_library(sdk_path, 'lib/network', rel_path=do_relpath_sdk))
assets.add_khafile_def('lnx_network') assets.add_khafile_def('lnx_network')
lnxjs_path = sdk_path + '/lib/leenkx_tools/lnxjs/Leenkx.js' #lnxjs_path = sdk_path + '/lib/leenkx_tools/lnxjs/Leenkx.js'
lnxjs_path = lnxjs_path.replace('\\', '/').replace('//', '/') #lnxjs_path = lnxjs_path.replace('\\', '/').replace('//', '/')
khafile.write(add_assets(lnxjs_path, rel_path=do_relpath_sdk)) #khafile.write(add_assets(lnxjs_path, rel_path=do_relpath_sdk))
if export_torrent:
assets.add_khafile_def('lnx_torrent')
if state.target in ('windows-hl', 'linux-hl', 'macos-hl'):
if not os.path.exists('Libraries/datachannel'):
khafile.write(add_leenkx_library(sdk_path + '/lib/', 'datachannel', rel_path=do_relpath_sdk))
assets.add_khafile_def('lnx_datachannel')
if not wrd.lnx_minimize: if not wrd.lnx_minimize:
assets.add_khafile_def('lnx_json') assets.add_khafile_def('lnx_json')
@ -736,6 +743,19 @@ class Main {
elif wrd.lnx_canvas_img_scaling_quality == 'high': elif wrd.lnx_canvas_img_scaling_quality == 'high':
f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;") f.write("\n" + content_indent + "leenkx.ui.Canvas.imageScaleQuality = kha.graphics2.ImageScaleQuality.High;")
if wrd.lnx_torrent != 'Disabled':
dl_dir = wrd.lnx_torrent_download_dir
if dl_dir.startswith('//'):
dl_dir = bpy.path.abspath(dl_dir)
if dl_dir != '':
f.write("\n" + content_indent + "leenkx.network.torrent.TorrentClient.defaultDownloadDir = " + json.dumps(dl_dir) + ";")
trackers_list = [json.dumps(t.name) for t in wrd.lnx_torrent_trackers if t.name != '']
f.write("\n" + content_indent + "leenkx.network.torrent.TorrentClient.defaultTrackers = [" + ", ".join(trackers_list) + "];")
ice_list = [json.dumps(t.name) for t in wrd.lnx_torrent_ice_servers if t.name != '']
f.write("\n" + content_indent + "leenkx.network.torrent.peer.RtcPeerPool.defaultIceServers = [" + ", ".join(ice_list) + "];")
f.write("\n" + content_indent + "leenkx.network.torrent.LeenkxClient.PEERTIMEOUT = " + str(wrd.lnx_torrent_peer_timeout) + ";")
f.write("\n" + content_indent + "leenkx.network.torrent.LeenkxClient.defaultHeartbeat = " + str(wrd.lnx_torrent_heartbeat) + ";")
# Write Starter.main call # Write Starter.main call
starter_indent = content_indent + " " starter_indent = content_indent + " "
f.write("\n" + content_indent + "leenkx.system.Starter.main(") f.write("\n" + content_indent + "leenkx.system.Starter.main(")

8
lib/datachannel/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.vs
cpp/out
cpp/CMakeSettings.json
bin
index2.html
examples/copy-paste/bin
examples/heaps/bin
node_modules

21
lib/datachannel/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 RandomityGuy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@ -0,0 +1,553 @@
Mbed TLS files are provided under a dual [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html)
OR [GPL-2.0-or-later](https://spdx.org/licenses/GPL-2.0-or-later.html) license.
This means that users may choose which of these licenses they take the code
under.
The full text of each of these licenses is given below.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
===============================================================================
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@ -0,0 +1,4 @@
Makefile
*.sln
*.vcxproj
mbedtls/check_config

View File

@ -0,0 +1,22 @@
option(INSTALL_MBEDTLS_HEADERS "Install Mbed TLS headers." ON)
if(INSTALL_MBEDTLS_HEADERS)
file(GLOB headers "mbedtls/*.h")
file(GLOB psa_headers "psa/*.h")
install(FILES ${headers}
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/mbedtls
PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ)
install(FILES ${psa_headers}
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/psa
PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ)
endif(INSTALL_MBEDTLS_HEADERS)
# Make mbedtls_config.h available in an out-of-source build. ssl-opt.sh requires it.
if (ENABLE_TESTING AND NOT ${CMAKE_CURRENT_BINARY_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR})
link_to_source(mbedtls)
link_to_source(psa)
endif()

Some files were not shown because too many files have changed in this diff Show More