Leenkx Network Torrent

This commit is contained in:
2026-10-03 17:40:22 -07:00
parent 6839203dc5
commit da38631584
34 changed files with 9667 additions and 0 deletions

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,523 @@
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 {
static var PEERTIMEOUT:Float = 5 * 60 * 1000;
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 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 : ["wss://ws1.leenkx.com"];
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 : 30000;
}
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 (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,981 @@
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);
}
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,352 @@
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> = [
"wss://ws1.leenkx.com"
];
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 remove(infoHashHex:String, ?deleteData:Bool):Void {
var t = torrents.get(infoHashHex);
if (t == null) {
t = torrents.get("v2:" + infoHashHex);
if (t != null) infoHashHex = "v2:" + infoHashHex;
}
if (t != null) {
t.destroy();
if (deleteData) t.deleteData();
torrents.remove(infoHashHex);
}
}
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,526 @@
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 function new(?iceServers:Array<String>, ?channelLabel:String, unordered:Bool = false, maxRetransmits:Int = -1) {
this.iceServers = iceServers != null ? iceServers
: ["stun:stun.l.google.com:19302",
"stun:global.stun.twilio.com:3478"];
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,162 @@
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 (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 : ["wss://ws1.leenkx.com"]
};
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,145 @@
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 (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