forked from LeenkxTeam/LNXSDK
Leenkx Network Torrent
This commit is contained in:
209
leenkx/Sources/leenkx/network/torrent/Aegis128x4.hx
Normal file
209
leenkx/Sources/leenkx/network/torrent/Aegis128x4.hx
Normal 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
|
||||
];
|
||||
}
|
||||
119
leenkx/Sources/leenkx/network/torrent/Base58.hx
Normal file
119
leenkx/Sources/leenkx/network/torrent/Base58.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
182
leenkx/Sources/leenkx/network/torrent/Bencode.hx
Normal file
182
leenkx/Sources/leenkx/network/torrent/Bencode.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
152
leenkx/Sources/leenkx/network/torrent/Crypto.hx
Normal file
152
leenkx/Sources/leenkx/network/torrent/Crypto.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
21
leenkx/Sources/leenkx/network/torrent/LICENSE.md
Normal file
21
leenkx/Sources/leenkx/network/torrent/LICENSE.md
Normal 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.
|
||||
523
leenkx/Sources/leenkx/network/torrent/LeenkxClient.hx
Normal file
523
leenkx/Sources/leenkx/network/torrent/LeenkxClient.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
115
leenkx/Sources/leenkx/network/torrent/Magnet.hx
Normal file
115
leenkx/Sources/leenkx/network/torrent/Magnet.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
711
leenkx/Sources/leenkx/network/torrent/MetaInfo.hx
Normal file
711
leenkx/Sources/leenkx/network/torrent/MetaInfo.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
329
leenkx/Sources/leenkx/network/torrent/PieceManager.hx
Normal file
329
leenkx/Sources/leenkx/network/torrent/PieceManager.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
334
leenkx/Sources/leenkx/network/torrent/Ripemd160.hx
Normal file
334
leenkx/Sources/leenkx/network/torrent/Ripemd160.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
981
leenkx/Sources/leenkx/network/torrent/Torrent.hx
Normal file
981
leenkx/Sources/leenkx/network/torrent/Torrent.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
352
leenkx/Sources/leenkx/network/torrent/TorrentClient.hx
Normal file
352
leenkx/Sources/leenkx/network/torrent/TorrentClient.hx
Normal 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
|
||||
}
|
||||
}
|
||||
102
leenkx/Sources/leenkx/network/torrent/TorrentFile.hx
Normal file
102
leenkx/Sources/leenkx/network/torrent/TorrentFile.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
72
leenkx/Sources/leenkx/network/torrent/TorrentInfo.hx
Normal file
72
leenkx/Sources/leenkx/network/torrent/TorrentInfo.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
733
leenkx/Sources/leenkx/network/torrent/TweetNaCl.hx
Normal file
733
leenkx/Sources/leenkx/network/torrent/TweetNaCl.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
56
leenkx/Sources/leenkx/network/torrent/Types.hx
Normal file
56
leenkx/Sources/leenkx/network/torrent/Types.hx
Normal 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";
|
||||
}
|
||||
128
leenkx/Sources/leenkx/network/torrent/WebSeed.hx
Normal file
128
leenkx/Sources/leenkx/network/torrent/WebSeed.hx
Normal 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
|
||||
}
|
||||
}
|
||||
152
leenkx/Sources/leenkx/network/torrent/peer/DataChannelPeer.hx
Normal file
152
leenkx/Sources/leenkx/network/torrent/peer/DataChannelPeer.hx
Normal 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
|
||||
}
|
||||
}
|
||||
12
leenkx/Sources/leenkx/network/torrent/peer/IPeerChannel.hx
Normal file
12
leenkx/Sources/leenkx/network/torrent/peer/IPeerChannel.hx
Normal 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;
|
||||
}
|
||||
526
leenkx/Sources/leenkx/network/torrent/peer/RtcPeerPool.hx
Normal file
526
leenkx/Sources/leenkx/network/torrent/peer/RtcPeerPool.hx
Normal 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
|
||||
}
|
||||
}
|
||||
179
leenkx/Sources/leenkx/network/torrent/peer/TcpListener.hx
Normal file
179
leenkx/Sources/leenkx/network/torrent/peer/TcpListener.hx
Normal 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
|
||||
406
leenkx/Sources/leenkx/network/torrent/peer/TcpPeer.hx
Normal file
406
leenkx/Sources/leenkx/network/torrent/peer/TcpPeer.hx
Normal 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
|
||||
424
leenkx/Sources/leenkx/network/torrent/peer/WireProtocol.hx
Normal file
424
leenkx/Sources/leenkx/network/torrent/peer/WireProtocol.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
145
leenkx/Sources/leenkx/network/torrent/storage/Storage.hx
Normal file
145
leenkx/Sources/leenkx/network/torrent/storage/Storage.hx
Normal 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
|
||||
555
leenkx/Sources/leenkx/network/torrent/tracker/HttpTracker.hx
Normal file
555
leenkx/Sources/leenkx/network/torrent/tracker/HttpTracker.hx
Normal 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
|
||||
696
leenkx/Sources/leenkx/network/torrent/tracker/UdpTracker.hx
Normal file
696
leenkx/Sources/leenkx/network/torrent/tracker/UdpTracker.hx
Normal 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
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
159
leenkx/Sources/leenkx/network/torrent/transport/RtcNative.hx
Normal file
159
leenkx/Sources/leenkx/network/torrent/transport/RtcNative.hx
Normal 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
|
||||
268
leenkx/Sources/leenkx/network/torrent/transport/RtcSignaling.hx
Normal file
268
leenkx/Sources/leenkx/network/torrent/transport/RtcSignaling.hx
Normal 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));
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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
|
||||
145
leenkx/Sources/leenkx/network/torrent/transport/WireTransport.hx
Normal file
145
leenkx/Sources/leenkx/network/torrent/transport/WireTransport.hx
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
Reference in New Issue
Block a user