Compare commits

...

31 Commits

Author SHA1 Message Date
211cba61a6 Export Network Update 2026-10-06 11:34:17 -07:00
4ab646015d P2P Export - Nodes - Patch 2026-10-06 02:10:08 -07:00
9fee67d231 Haxe Lib Datachannel 2026-10-04 17:14:28 -07:00
860b73a8f4 hxWebsocket Upstream and Network Updates 2026-10-03 22:05:53 -07:00
da38631584 Leenkx Network Torrent 2026-10-03 17:40:22 -07:00
6839203dc5 P2P RunT + Static WebView 2026-10-03 00:02:30 -07:00
d9ac74101f Merge pull request 'Fix background color intensity for 'Filmic2' tonemap' (#146) from moisesjpelaez/LNXSDK:filmic into main
Reviewed-on: LeenkxTeam/LNXSDK#146
2026-09-23 22:45:26 +00:00
e20400654c Merge pull request 'Add missing dll to prevent error 3221225781' (#145) from moisesjpelaez/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#145
2026-09-23 22:44:13 +00:00
058cbed20f Fix background color intensity for 'Filmic2' tonemap 2026-09-21 19:44:07 -04:00
e0c10318e5 Add missing dll to prevent error 3221225781 2026-09-21 18:27:29 -04:00
7d609b126d Merge pull request 'main' (#144) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#144
2026-09-20 18:30:58 +00:00
36ecaea919 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-20 11:15:57 -07:00
fac01a2849 Group Nodes, Shaders, Textures, Displacement 2026-09-20 11:15:14 -07:00
c71c1a98e4 Merge pull request 'MRT Index' (#143) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#143
2026-09-17 23:43:07 +00:00
f6d5f142e9 MRT Index 2026-09-17 16:38:36 -07:00
70961b0336 Merge pull request 'Fix thinWall and copy_pass in Forward rendering' (#141) from Moises/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#141
2026-09-17 21:16:33 +00:00
9c50274f29 Merge remote-tracking branch 'upstream/main' 2026-09-17 17:15:55 -04:00
a94b95afc8 Merge pull request 'main' (#142) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#142
2026-09-17 21:09:32 +00:00
cee36635e0 Cleanup 2026-09-17 16:07:10 -04:00
d0337f5dae Fix thinWall and passes 2026-09-17 15:59:45 -04:00
fca51c7e61 Water Level/Reflectopm 2026-09-16 22:44:16 -07:00
0bb4bceb57 Water Foam Fix 2026-09-16 18:12:12 -07:00
af9a259fbb Water Sun Direction 2026-09-16 17:36:31 -07:00
f1d8522f51 Bloom Isnan Isinf 2026-09-16 16:04:32 -07:00
56d1b0e4a7 Forward / Mobile / Solid 2026-09-16 12:40:39 -07:00
73e4a0ad30 Scene Gamma / Exposure 2026-09-13 23:34:36 -07:00
67d5e6b2c7 SSRS Fix 2026-09-13 21:52:13 -07:00
635f8f6645 SSRS WebGL Fix 2026-09-13 20:18:00 -07:00
b2444be7e2 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-13 20:16:57 -07:00
b4de377433 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-13 15:23:25 -07:00
d787f9a7fc Update Theme 2026-09-13 12:10:51 -07:00
286 changed files with 74784 additions and 1886 deletions

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -48,6 +48,10 @@ void main() {
} }
if (currentMipLevel == 0) { if (currentMipLevel == 0) {
if (any(isnan(fragColor.rgb)) || any(isinf(fragColor.rgb))) {
fragColor.rgb = vec3(0.0);
}
// https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.2 // https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.2
// https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4 // https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4

View File

@ -69,6 +69,7 @@ uniform vec4 PPComp15;
uniform vec4 PPComp16; uniform vec4 PPComp16;
uniform vec4 PPComp18; uniform vec4 PPComp18;
uniform vec4 PPComp19; uniform vec4 PPComp19;
uniform vec4 PPComp20;
#endif #endif
// #ifdef _CPos // #ifdef _CPos
@ -499,14 +500,15 @@ void main() {
#ifdef _CExposure #ifdef _CExposure
#ifdef _CPostprocess #ifdef _CPostprocess
fragColor.rgb+=fragColor.rgb*PPComp8.x; fragColor.rgb *= pow(2.0, PPComp8.x);
#else #else
fragColor.rgb+= fragColor.rgb*compoExposureStrength; fragColor.rgb *= pow(2.0, compoExposureStrength);
#endif #endif
#endif #endif
#ifdef _CPostprocess #ifdef _CPostprocess
fragColor.rgb *= ComputeEV(0.0); fragColor.rgb *= ComputeEV(0.0);
fragColor.rgb *= pow(2.0, PPComp20.x); // exposure and gamma
#endif #endif
#ifdef _AutoExposure #ifdef _AutoExposure
@ -582,14 +584,16 @@ fragColor.rgb = min(fragColor.rgb, 65504 * 0.5);
fragColor.rgb = tonemapAgXFull(fragColor.rgb); fragColor.rgb = tonemapAgXFull(fragColor.rgb);
} //else { fragColor.rgb = vec3(0,1,0); //ERROR} } //else { fragColor.rgb = vec3(0,1,0); //ERROR}
#endif #endif
#ifdef _CGamma
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / PPComp20.y));
#endif
#else #else
#ifdef _CToneFilmic #ifdef _CToneFilmic
fragColor.rgb = tonemapFilmic(fragColor.rgb); // With gamma fragColor.rgb = tonemapFilmic(fragColor.rgb); // With gamma
#endif #endif
#ifdef _CToneFilmic2 #ifdef _CToneFilmic2
fragColor.rgb = acesFilm(fragColor.rgb); fragColor.rgb = acesFilm(fragColor.rgb);
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 1.0)); fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / 2.2));
#endif #endif
#ifdef _CToneReinhard #ifdef _CToneReinhard
fragColor.rgb = tonemapReinhard(fragColor.rgb); fragColor.rgb = tonemapReinhard(fragColor.rgb);
@ -615,6 +619,10 @@ fragColor.rgb = min(fragColor.rgb, 65504 * 0.5);
#endif #endif
#endif #endif
#ifdef _CGamma
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / compoGammaStrength));
#endif
#ifdef _CBW #ifdef _CBW
// fragColor.rgb = vec3(clamp(dot(fragColor.rgb, fragColor.rgb), 0.0, 1.0)); // fragColor.rgb = vec3(clamp(dot(fragColor.rgb, fragColor.rgb), 0.0, 1.0));
fragColor.rgb = vec3((fragColor.r * 0.3 + fragColor.g * 0.59 + fragColor.b * 0.11) / 3.0) * 2.5; fragColor.rgb = vec3((fragColor.r * 0.3 + fragColor.g * 0.59 + fragColor.b * 0.11) / 3.0) * 2.5;

View File

@ -907,26 +907,10 @@ tileBounds = vec4(0.0, 0.0, 1.0, 1.0);
fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[li], p, n, cLightDir, lightPlane.y, shadowMapAtlas, sssColorCL, sssRadiusCL, tileBoundsSpotArray[li]); fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[li], p, n, cLightDir, lightPlane.y, shadowMapAtlas, sssColorCL, sssRadiusCL, tileBoundsSpotArray[li]);
#endif #endif
#else #else
if (li == 0) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[0], p, n, cLightDir, lightPlane.y, shadowMapSpot[0], sssColorCL, sssRadiusCL if (li == 0) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[0], p, n, cLightDir, lightPlane.y, shadowMapSpot[0], sssColorCL, sssRadiusCL);
#ifdef _ShadowMapAtlas else if (li == 1) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[1], p, n, cLightDir, lightPlane.y, shadowMapSpot[1], sssColorCL, sssRadiusCL);
, vec4(0.0, 0.0, 1.0, 1.0) else if (li == 2) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[2], p, n, cLightDir, lightPlane.y, shadowMapSpot[2], sssColorCL, sssRadiusCL);
#endif else if (li == 3) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[3], p, n, cLightDir, lightPlane.y, shadowMapSpot[3], sssColorCL, sssRadiusCL);
);
else if (li == 1) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[1], p, n, cLightDir, lightPlane.y, shadowMapSpot[1], sssColorCL, sssRadiusCL
#ifdef _ShadowMapAtlas
, vec4(0.0, 0.0, 1.0, 1.0)
#endif
);
else if (li == 2) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[2], p, n, cLightDir, lightPlane.y, shadowMapSpot[2], sssColorCL, sssRadiusCL
#ifdef _ShadowMapAtlas
, vec4(0.0, 0.0, 1.0, 1.0)
#endif
);
else if (li == 3) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[3], p, n, cLightDir, lightPlane.y, shadowMapSpot[3], sssColorCL, sssRadiusCL
#ifdef _ShadowMapAtlas
, vec4(0.0, 0.0, 1.0, 1.0)
#endif
);
#endif #endif
} else { } else {
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas

View File

@ -56,9 +56,15 @@ uniform vec2 cameraPlane;
#ifdef _SinglePoint #ifdef _SinglePoint
#ifdef _Spot #ifdef _Spot
//!uniform sampler2DShadow shadowMapSpot[1]; //!uniform sampler2DShadow shadowMapSpot[1];
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapSpotTransparent[1];
#endif
//!uniform mat4 LWVPSpotArray[1]; //!uniform mat4 LWVPSpotArray[1];
#else #else
//!uniform samplerCubeShadow shadowMapPoint[1]; //!uniform samplerCubeShadow shadowMapPoint[1];
#ifdef _ShadowMapTransparent
//!uniform samplerCube shadowMapPointTransparent[1];
#endif
//!uniform vec2 lightProj; //!uniform vec2 lightProj;
#endif #endif
#endif #endif
@ -66,26 +72,41 @@ uniform vec2 cameraPlane;
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
#ifdef _SingleAtlas #ifdef _SingleAtlas
uniform sampler2DShadow shadowMapAtlas; uniform sampler2DShadow shadowMapAtlas;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapAtlasTransparent;
#endif
#endif #endif
#endif #endif
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
#ifndef _SingleAtlas #ifndef _SingleAtlas
//!uniform sampler2DShadow shadowMapAtlasPoint; //!uniform sampler2DShadow shadowMapAtlasPoint;
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapAtlasPointTransparent;
#endif #endif
//!uniform vec4 pointLightDataArray[4]; #endif
//!uniform vec4 pointLightDataArray[maxLightsCluster * 6];
#else #else
//!uniform samplerCubeShadow shadowMapPoint[4]; //!uniform samplerCubeShadow shadowMapPoint[4];
#ifdef _ShadowMapTransparent
//!uniform samplerCube shadowMapPointTransparent[4];
#endif
#endif #endif
//!uniform vec2 lightProj; //!uniform vec2 lightProj;
#ifdef _Spot #ifdef _Spot
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
#ifndef _SingleAtlas #ifndef _SingleAtlas
//!uniform sampler2DShadow shadowMapAtlasSpot; //!uniform sampler2DShadow shadowMapAtlasSpot;
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapAtlasSpotTransparent;
#endif
#endif #endif
#else #else
//!uniform sampler2DShadow shadowMapSpot[4]; //!uniform sampler2DShadow shadowMapSpot[maxLightsCluster];
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapSpotTransparent[maxLightsCluster];
#endif #endif
//!uniform mat4 LWVPSpotArray[4]; #endif
//!uniform mat4 LWVPSpotArray[maxLightsCluster];
#endif #endif
#endif #endif
#endif #endif
@ -97,10 +118,16 @@ uniform vec3 sunCol;
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
#ifndef _SingleAtlas #ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasSun; uniform sampler2DShadow shadowMapAtlasSun;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapAtlasSunTransparent;
#endif
#endif #endif
//!uniform vec4 tileBoundsSunArray[maxLights * shadowmapCascades]; //!uniform vec4 tileBoundsSunArray[maxLights * shadowmapCascades];
#else #else
uniform sampler2DShadow shadowMap; uniform sampler2DShadow shadowMap;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapTransparent;
#endif
#endif #endif
uniform float shadowsBias; uniform float shadowsBias;
#ifdef _CSM #ifdef _CSM
@ -167,10 +194,11 @@ void main() {
vec4 g1 = textureLod(gbuffer1, texCoord, 0.0); // Basecolor.rgb, spec/occ vec4 g1 = textureLod(gbuffer1, texCoord, 0.0); // Basecolor.rgb, spec/occ
vec2 occspec = unpackFloat2(g1.a); vec2 occspec = unpackFloat2(g1.a);
vec3 albedo = surfaceAlbedo(g1.rgb, metallic); // g1.rgb - basecolor vec3 basecolor = min(g1.rgb, vec3(2.0));
vec3 f0 = surfaceF0(g1.rgb, metallic); vec3 albedo = surfaceAlbedo(basecolor, metallic);
vec3 f0 = surfaceF0(basecolor, metallic);
#ifdef _ExtBRDF #ifdef _ExtBRDF
f0 = mix(f0, min(g1.rgb, vec3(2.0)), vec3(matp6.y, matp6.z, matp6.w)); f0 = mix(f0, basecolor, vec3(matp6.y, matp6.z, matp6.w));
#endif #endif
float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0; float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0;
@ -206,7 +234,7 @@ void main() {
envl /= PI; envl /= PI;
#endif #endif
#else #else
vec3 envl = vec3(1.0); vec3 envl = vec3(0.0);
#endif #endif
#ifdef _Rad #ifdef _Rad
@ -227,13 +255,23 @@ void main() {
#endif #endif
#endif #endif
#ifdef _Brdf
vec3 F = f0 * envBRDF.x + envBRDF.y;
#else
vec3 F = f0;
#endif
envl.rgb *= diffuseIBL(albedo, roughness, f0, dotNV); envl.rgb *= diffuseIBL(albedo, roughness, f0, dotNV);
#ifdef _Brdf
envl.rgb *= 1.0 - F;
#endif
#ifdef _Rad // Indirect specular #ifdef _Rad // Indirect specular
envl.rgb += prefilteredColor * (f0 * envBRDF.x + envBRDF.y) * 1.5 * occspec.y; envl.rgb += prefilteredColor * F;
#else #else
#ifdef _EnvCol #ifdef _EnvCol
envl.rgb += backgroundCol * surfaceF0(g1.rgb, metallic); // f0 envl.rgb += backgroundCol * F;
#endif #endif
#endif #endif
@ -281,7 +319,12 @@ void main() {
float transmittance = 1.0 - transF; float transmittance = 1.0 - transF;
#ifdef _Rad #ifdef _Rad
if (matp2.z > 0.0 && transmittance > 0.0) { if (matp2.z > 0.0 && transmittance > 0.0) {
vec3 refrDir = transmissionIBLDirection(n, v, matp3.x); vec3 refrDir;
if (matp3.y > 0.5) {
refrDir = reflect(-v, n);
} else {
refrDir = transmissionIBLDirection(n, v, matp3.x);
}
float transLod = getMipFromRoughness(matp2.w, envmapNumMipmaps); float transLod = getMipFromRoughness(matp2.w, envmapNumMipmaps);
vec3 transColor = textureLod(senvmapRadiance, vec3 transColor = textureLod(senvmapRadiance,
envMapEquirect(refrDir), transLod).rgb; envMapEquirect(refrDir), transLod).rgb;
@ -337,7 +380,7 @@ void main() {
float sdotNH = max(0.0, dot(n, sh)); float sdotNH = max(0.0, dot(n, sh));
float sdotVH = max(0.0, dot(v, sh)); float sdotVH = max(0.0, dot(v, sh));
float sdotNL = max(0.0, dot(n, sunDir)); float sdotNL = max(0.0, dot(n, sunDir));
float svisibility = 1.0; vec3 svisibility = vec3(1.0);
#ifdef _Anisotropy #ifdef _Anisotropy
vec3 sdirect; vec3 sdirect;
if (abs(matp0.x) > 0.001 && dot(wTangent, wTangent) > 0.001) { if (abs(matp0.x) > 0.001 && dot(wTangent, wTangent) > 0.001) {
@ -385,29 +428,59 @@ void main() {
#ifdef _CSM #ifdef _CSM
svisibility = shadowTestCascade( svisibility = shadowTestCascade(
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
#ifdef _ShadowMapTransparent
#ifndef _SingleAtlas
shadowMapAtlasSun, shadowMapAtlasSunTransparent
#else
shadowMapAtlas, shadowMapAtlasTransparent
#endif
#else
#ifndef _SingleAtlas #ifndef _SingleAtlas
shadowMapAtlasSun shadowMapAtlasSun
#else #else
shadowMapAtlas shadowMapAtlas
#endif #endif
#endif
#else #else
shadowMap #ifdef _ShadowMapTransparent
shadowMap, shadowMapTransparent
#else
shadowMap
#endif
#endif
, eye, p + n * shadowsBias * 2, shadowsBias
#ifdef _ShadowMapTransparent
, false
#endif #endif
, eye, p + n * shadowsBias * 10, shadowsBias
); );
#else #else
vec4 lPos = LWVP * vec4(p + n * shadowsBias * 100, 1.0); vec4 lPos = LWVP * vec4(p + n * shadowsBias * 2, 1.0);
if (lPos.w > 0.0) svisibility = shadowTest( if (lPos.w > 0.0) svisibility = shadowTest(
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
#ifdef _ShadowMapTransparent
#ifndef _SingleAtlas
shadowMapAtlasSun, shadowMapAtlasSunTransparent
#else
shadowMapAtlas, shadowMapAtlasTransparent
#endif
#else
#ifndef _SingleAtlas #ifndef _SingleAtlas
shadowMapAtlasSun shadowMapAtlasSun
#else #else
shadowMapAtlas shadowMapAtlas
#endif #endif
#endif
#else #else
shadowMap #ifdef _ShadowMapTransparent
shadowMap, shadowMapTransparent
#else
shadowMap
#endif
#endif #endif
, lPos.xyz / lPos.w, shadowsBias , lPos.xyz / lPos.w, shadowsBias
#ifdef _ShadowMapTransparent
, false
#endif
); );
#endif #endif
#endif #endif
@ -415,11 +488,18 @@ void main() {
fragColor.rgb += sdirect * svisibility * sunCol; fragColor.rgb += sdirect * svisibility * sunCol;
#endif #endif
#ifdef _ShadowMapAtlas
tileBounds = vec4(0.0, 0.0, 1.0, 1.0);
#endif
#ifdef _SinglePoint #ifdef _SinglePoint
fragColor.rgb += sampleLight( fragColor.rgb += sampleLight(
p, n, v, dotNV, pointPos, pointCol, albedo, roughness, occspec.y, f0 p, n, v, dotNV, pointPos, pointCol, albedo, roughness, occspec.y, f0
#ifdef _ShadowMap #ifdef _ShadowMap
, 0, pointBias, true , 0, pointBias, true
#ifdef _ShadowMapTransparent
, false
#endif
#endif #endif
#ifdef _Spot #ifdef _Spot
, true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight // TODO: Test! , true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight // TODO: Test!
@ -472,12 +552,15 @@ void main() {
#ifdef _ShadowMap #ifdef _ShadowMap
// light index, shadow bias, cast_shadows // light index, shadow bias, cast_shadows
, li, lightsArray[li * 3 + 2].x, lightsArray[li * 3 + 2].z != 0.0 , li, lightsArray[li * 3 + 2].x, lightsArray[li * 3 + 2].z != 0.0
#ifdef _ShadowMapTransparent
, false
#endif
#endif #endif
#ifdef _Spot #ifdef _Spot
, lightsArray[li * 3 + 2].y != 0.0 , lightsArray[li * 3 + 2].y != 0.0
, lightsArray[li * 3 + 2].y // spot size (cutoff) , lightsArray[li * 3 + 2].y // spot size (cutoff)
, lightsArraySpot[li].w // spot blend (exponent) , lightsArraySpot[li * 2].w // spot blend (exponent)
, lightsArraySpot[li].xyz // spotDir , lightsArraySpot[li * 2].xyz // spotDir
, vec2(lightsArray[li * 3].w, lightsArray[li * 3 + 1].w) // scale , vec2(lightsArray[li * 3].w, lightsArray[li * 3 + 1].w) // scale
, lightsArraySpot[li * 2 + 1].xyz // right , lightsArraySpot[li * 2 + 1].xyz // right
#endif #endif

View File

@ -122,11 +122,6 @@
"link": "_inverseViewProjectionMatrixRight", "link": "_inverseViewProjectionMatrixRight",
"ifdef": ["_VRStereo"] "ifdef": ["_VRStereo"]
}, },
{
"name": "invVP",
"link": "_viewProjectionMatrix",
"ifdef": ["_SSRS"]
},
{ {
"name": "smSizeUniform", "name": "smSizeUniform",
"link": "_shadowMapSize", "link": "_shadowMapSize",
@ -137,14 +132,6 @@
"link": "_lightPlane", "link": "_lightPlane",
"ifdef": ["_SSS"] "ifdef": ["_SSS"]
}, },
{
"name": "VP",
"link": "_viewProjectionMatrix",
"ifdef": ["_SSRS"]
},
{
"ifdef": ["_SMSizeUniform"]
},
{ {
"name": "lightProj", "name": "lightProj",
"link": "_lightPlaneProj", "link": "_lightPlaneProj",

View File

@ -14,15 +14,24 @@
#ifdef _SinglePoint #ifdef _SinglePoint
#ifdef _Spot #ifdef _Spot
uniform sampler2DShadow shadowMapSpot[1]; uniform sampler2DShadow shadowMapSpot[1];
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapSpotTransparent[1];
#endif
uniform mat4 LWVPSpotArray[1]; uniform mat4 LWVPSpotArray[1];
#else #else
uniform samplerCubeShadow shadowMapPoint[1]; uniform samplerCubeShadow shadowMapPoint[1];
#ifdef _ShadowMapTransparent
uniform samplerCube shadowMapPointTransparent[1];
#endif
uniform vec2 lightProj; uniform vec2 lightProj;
#endif #endif
#endif #endif
#ifdef _Clusters #ifdef _Clusters
#ifdef _SingleAtlas #ifdef _SingleAtlas
//!uniform sampler2DShadow shadowMapAtlas; //!uniform sampler2DShadow shadowMapAtlas;
#ifdef _ShadowMapTransparent
//!uniform sampler2D shadowMapAtlasTransparent;
#endif
#endif #endif
#ifndef _SinglePoint #ifndef _SinglePoint
uniform vec2 lightProj; uniform vec2 lightProj;
@ -30,17 +39,29 @@
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
#ifndef _SingleAtlas #ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasPoint; uniform sampler2DShadow shadowMapAtlasPoint;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapAtlasPointTransparent;
#endif
#endif #endif
#else #else
uniform samplerCubeShadow shadowMapPoint[4]; uniform samplerCubeShadow shadowMapPoint[4];
#ifdef _ShadowMapTransparent
uniform samplerCube shadowMapPointTransparent[4];
#endif
#endif #endif
#ifdef _Spot #ifdef _Spot
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
#ifndef _SingleAtlas #ifndef _SingleAtlas
uniform sampler2DShadow shadowMapAtlasSpot; uniform sampler2DShadow shadowMapAtlasSpot;
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapAtlasSpotTransparent;
#endif
#endif #endif
#else #else
uniform sampler2DShadow shadowMapSpot[maxLightsCluster]; uniform sampler2DShadow shadowMapSpot[maxLightsCluster];
#ifdef _ShadowMapTransparent
uniform sampler2D shadowMapSpotTransparent[maxLightsCluster];
#endif
#endif #endif
uniform mat4 LWVPSpotArray[maxLightsCluster]; uniform mat4 LWVPSpotArray[maxLightsCluster];
#endif #endif
@ -51,6 +72,9 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
const vec3 albedo, const float rough, const float spec, const vec3 f0 const vec3 albedo, const float rough, const float spec, const vec3 f0
#ifdef _ShadowMap #ifdef _ShadowMap
, int index, float bias, bool receiveShadow , int index, float bias, bool receiveShadow
#ifdef _ShadowMapTransparent
, bool transparent
#endif
#endif #endif
#ifdef _Spot #ifdef _Spot
, bool isSpot, float spotSize, float spotBlend, vec3 spotDir, vec2 scale, vec3 right , bool isSpot, float spotSize, float spotBlend, vec3 spotDir, vec2 scale, vec3 right
@ -121,26 +145,77 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
#ifdef _ShadowMap #ifdef _ShadowMap
if (receiveShadow) { if (receiveShadow) {
#ifdef _SinglePoint #ifdef _SinglePoint
vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 10, 1.0); vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 10, 1.0);
direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0],
#endif
lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#endif #endif
#ifdef _Clusters #ifdef _Clusters
vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0); vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0);
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
tileBounds = tileBoundsSpotArray[index]; tileBounds = tileBoundsSpotArray[index];
direct *= shadowTest( direct *= shadowTest(
#ifndef _SingleAtlas #ifdef _ShadowMapTransparent
shadowMapAtlasSpot #ifndef _SingleAtlas
#else shadowMapAtlasSpot, shadowMapAtlasSpotTransparent
shadowMapAtlas #else
shadowMapAtlas, shadowMapAtlasTransparent
#endif
#else
#ifndef _SingleAtlas
shadowMapAtlasSpot
#else
shadowMapAtlas
#endif
#endif
, lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#else
if (index == 0) direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0],
#endif #endif
, lPos.xyz / lPos.w, bias lPos.xyz / lPos.w, bias
); #ifdef _ShadowMapTransparent
#else , transparent
if (index == 0) direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias); #endif
else if (index == 1) direct *= shadowTest(shadowMapSpot[1], lPos.xyz / lPos.w, bias); );
else if (index == 2) direct *= shadowTest(shadowMapSpot[2], lPos.xyz / lPos.w, bias); else if (index == 1) direct *= shadowTest(shadowMapSpot[1],
else if (index == 3) direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias); #ifdef _ShadowMapTransparent
shadowMapSpotTransparent[1],
#endif
lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 2) direct *= shadowTest(shadowMapSpot[2],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[2],
#endif
lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 3) direct *= shadowTest(shadowMapSpot[3],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[3],
#endif
lPos.xyz / lPos.w, bias
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#endif #endif
#endif #endif
} }
@ -153,25 +228,76 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
if (receiveShadow) { if (receiveShadow) {
#ifdef _SinglePoint #ifdef _SinglePoint
#ifndef _Spot #ifndef _Spot
direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); direct *= PCFCube(shadowMapPoint[0],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[0],
#endif #endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif #endif
);
#endif
#endif
#ifdef _Clusters #ifdef _Clusters
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
direct *= PCFFakeCube( direct *= PCFFakeCube(
#ifndef _SingleAtlas #ifdef _ShadowMapTransparent
shadowMapAtlasPoint #ifndef _SingleAtlas
#else shadowMapAtlasPoint, shadowMapAtlasPointTransparent
shadowMapAtlas
#endif
, ld, -l, bias, lightProj, n, index
);
#else #else
if (index == 0) direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n); shadowMapAtlas, shadowMapAtlasTransparent
else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n); #endif
else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n); #else
else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n); #ifndef _SingleAtlas
shadowMapAtlasPoint
#else
shadowMapAtlas
#endif
#endif
, ld, -l, bias, lightProj, n, index
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#else
if (index == 0) direct *= PCFCube(shadowMapPoint[0],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[0],
#endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 1) direct *= PCFCube(shadowMapPoint[1],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[1],
#endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 2) direct *= PCFCube(shadowMapPoint[2],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[2],
#endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif
);
else if (index == 3) direct *= PCFCube(shadowMapPoint[3],
#ifdef _ShadowMapTransparent
shadowMapPointTransparent[3],
#endif
ld, -l, bias, lightProj, n
#ifdef _ShadowMapTransparent
, transparent
#endif
);
#endif #endif
#endif #endif
} }
@ -186,6 +312,9 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
const vec3 albedo, const float rough, const float spec, const vec3 f0 const vec3 albedo, const float rough, const float spec, const vec3 f0
#ifdef _ShadowMap #ifdef _ShadowMap
, int index, float bias, bool receiveShadow , int index, float bias, bool receiveShadow
#ifdef _ShadowMapTransparent
, bool transparent
#endif
#endif #endif
#ifdef _Spot #ifdef _Spot
, bool isSpot, float spotSize, float spotBlend, vec3 spotDir, vec2 scale, vec3 right , bool isSpot, float spotSize, float spotBlend, vec3 spotDir, vec2 scale, vec3 right
@ -194,6 +323,9 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
return sampleLight(p, n, v, dotNV, lp, lightCol, albedo, rough, spec, f0 return sampleLight(p, n, v, dotNV, lp, lightCol, albedo, rough, spec, f0
#ifdef _ShadowMap #ifdef _ShadowMap
, index, bias, receiveShadow , index, bias, receiveShadow
#ifdef _ShadowMapTransparent
, transparent
#endif
#endif #endif
#ifdef _Spot #ifdef _Spot
, isSpot, spotSize, spotBlend, spotDir, scale, right , isSpot, spotSize, spotBlend, spotDir, scale, right

View File

@ -151,6 +151,7 @@ void main() {
} }
// Displace surface // Displace surface
float geomZ = p.z; // undisplaced depth for foam/fog tests
p.z += (sin(p.x * 10.0 / waterDisplace + speed) * cos(p.y * 10.0 / waterDisplace + speed) p.z += (sin(p.x * 10.0 / waterDisplace + speed) * cos(p.y * 10.0 / waterDisplace + speed)
+ sin(p.x * 20.0 / waterDisplace + speed * 1.3) * cos(p.y * 20.0 / waterDisplace + speed * 1.3) * 0.5) + sin(p.x * 20.0 / waterDisplace + speed * 1.3) * cos(p.y * 20.0 / waterDisplace + speed * 1.3) * 0.5)
/ 50.0 * waterDisplace; / 50.0 * waterDisplace;
@ -227,15 +228,19 @@ void main() {
// Blinn-Phong specular using half-vector, faded at horizon // Blinn-Phong specular using half-vector, faded at horizon
vec3 h = normalize(v + ld); vec3 h = normalize(v + ld);
float specAmount = pow(max(dot(n2, h), 0.0), 200.0) * (200.0 + 8.0) / (PI * 8.0); float specAmount = pow(max(dot(n2, h), 0.0), 200.0) * (200.0 + 8.0) / (PI * 8.0);
fragColor.rgb += specAmount * (isSky ? 0.3 : 1.0) * horizonFactor; fragColor.rgb += specAmount * (isSky ? 0.3 : 1.0);
// Depth fog - blend toward waterColor with depth, faded at horizon // Depth fog - blend toward waterColor with depth, faded at horizon
float depthFog = clamp(-(p.z - waterLevel) * waterDensity, 0.0, 0.9); float depthFog = clamp(-(geomZ - waterLevel) * waterDensity, 0.0, 0.9);
fragColor.rgb = mix(fragColor.rgb, waterColor, depthFog * horizonFactor); fragColor.rgb = mix(fragColor.rgb, waterColor, depthFog * horizonFactor);
// Alpha fades smoothly at horizon instead of hard cut // Alpha fades smoothly at horizon instead of hard cut
fragColor.a = isSky ? horizonFactor : clamp(abs(p.z - waterLevel) * 5.0, 0.0, 1.0); // Skydome sits 3.5 below the camera (_skydomeMatrix), so its horizon
// appears where the ray z reaches -3.5 / domeRadius
float farPlane = cameraProj.y / (1.0 - cameraProj.x);
float horizonFade = clamp((-3.5 / (farPlane * 0.95) - vray.z) * 20.0, 0.0, 1.0);
fragColor.a = isSky ? horizonFade : clamp(abs(geomZ - waterLevel) * 5.0, 0.0, 1.0);
// Foam - based on actual geometry depth below water surface // Foam - based on actual geometry depth below water surface
float fd = isSky ? 1.0 : abs(p.z - waterLevel); float fd = isSky ? 1.0 : abs(geomZ - waterLevel);
if (fd < 0.1) { if (fd < 0.1) {
// Based on foam by Owen Deery // Based on foam by Owen Deery
// http://fire-face.com/personal/water // http://fire-face.com/personal/water

View File

@ -34,7 +34,7 @@
}, },
{ {
"name": "ld", "name": "ld",
"link": "_lightDirection" "link": "_sunDirection"
}, },
{ {
"name": "invVP", "name": "invVP",

View File

@ -908,7 +908,9 @@ class RenderPath {
#end #end
Data.getShader(shaderPath[0], shaderPath[1], function(res: ShaderData) { Data.getShader(shaderPath[0], shaderPath[1], function(res: ShaderData) {
cc.context = res.getContext(shaderPath[2]); if (res != null) {
cc.context = res.getContext(shaderPath[2]);
}
loading--; loading--;
}); });
} }

View File

@ -55,6 +55,7 @@ class ShaderData {
if (raw == null) { if (raw == null) {
trace('Shader data "$name" not found!'); trace('Shader data "$name" not found!');
done(null); done(null);
return;
} }
new ShaderData(raw, done, overrideContext); new ShaderData(raw, done, overrideContext);
}); });

View File

@ -90,8 +90,16 @@ class LightObject extends Object {
#if lnx_spot #if lnx_spot
if (data.raw.type == "spot"){ if (data.raw.type == "spot"){
this.size = data.raw.spot_size; this.size = 0.0;
this.blend = data.raw.spot_blend; this.blend = 0.0;
if (data.raw.spot_size != null) {
var s: kha.FastFloat = data.raw.spot_size;
this.size = s;
}
if (data.raw.spot_blend != null) {
var b: kha.FastFloat = data.raw.spot_blend;
this.blend = b;
}
} }
#end #end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -669,8 +669,10 @@ class Inc {
#end #end
#if rp_ssrs #if rp_ssrs
#if kha_opengl
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");
#end #end
#end
path.drawMeshes("translucent"); path.drawMeshes("translucent");
@ -1314,7 +1316,11 @@ class ShadowMapAtlas {
function new(light: LightObject, transparent: Bool) { function new(light: LightObject, transparent: Bool) {
#if lnx_shadowmap_atlas_single_map
var maxTileSize = Std.int(Math.max(Inc.getCubeSize(), Inc.getCascadeSize()));
#else
var maxTileSize = shadowMapAtlasSize(light); var maxTileSize = shadowMapAtlasSize(light);
#end
this.target = shadowMapAtlasName(light.data.raw.type, transparent); this.target = shadowMapAtlasName(light.data.raw.type, transparent);
this.sizew = this.sizeh = this.baseTileSizeConst = maxTileSize; this.sizew = this.sizeh = this.baseTileSizeConst = maxTileSize;
this.depth = getSubdivisions(); this.depth = getSubdivisions();
@ -1418,8 +1424,8 @@ class ShadowMapAtlas {
public static inline function shadowMapAtlasName(type: String, transparent: Bool): String { public static inline function shadowMapAtlasName(type: String, transparent: Bool): String {
#if lnx_shadowmap_atlas_single_map #if lnx_shadowmap_atlas_single_map
return "shadowMapAtlas"; return transparent ? "shadowMapAtlasTransparent" : "shadowMapAtlas";
#else #else
switch (type) { switch (type) {
case "point": case "point":
return transparent ? "shadowMapAtlasPointTransparent" : "shadowMapAtlasPoint"; return transparent ? "shadowMapAtlasPointTransparent" : "shadowMapAtlasPoint";

View File

@ -116,6 +116,11 @@ class Postprocess {
1 //0: Exposure 1 //0: Exposure
]; ];
public static var scene_color_uniforms = [
0.0, //0: Scene Exposure
1.0 //1: Scene Gamma
];
public static var auto_exposure_uniforms = [ public static var auto_exposure_uniforms = [
1, //0: Auto Exposure Strength 1, //0: Auto Exposure Strength
1 //1: Auto Exposure Speed 1 //1: Auto Exposure Speed
@ -406,6 +411,10 @@ class Postprocess {
v.x = auto_focus[0]; v.x = auto_focus[0];
v.y = auto_focus[1]; v.y = auto_focus[1];
v.z = auto_focus[2]; v.z = auto_focus[2];
case "_PPComp20":
v = iron.object.Uniforms.helpVec;
v.x = scene_color_uniforms[0]; // Scene Exposure
v.y = scene_color_uniforms[1]; // Scene Gamma
} }
return v; return v;

View File

@ -560,6 +560,7 @@ class RenderPathDeferred {
@:access(iron.RenderPath) @:access(iron.RenderPath)
public static function commands() { public static function commands() {
path.setTarget("gbuffer0"); // Only clear gbuffer0 path.setTarget("gbuffer0"); // Only clear gbuffer0
//path.setTarget("gbuffer0", ["gbuffer1"]);
#if (rp_background == "Clear") #if (rp_background == "Clear")
{ {
path.clearTarget(-1, 1.0); path.clearTarget(-1, 1.0);
@ -649,7 +650,11 @@ class RenderPathDeferred {
{ {
if (leenkx.data.Config.raw.rp_ssao != false) { if (leenkx.data.Config.raw.rp_ssao != false) {
path.setTarget("singlea"); path.setTarget("singlea");
#if rp_ssao_half
path.bindTarget("half", "gbufferD");
#else
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");
#end
path.bindTarget("gbuffer0", "gbuffer0"); path.bindTarget("gbuffer0", "gbuffer0");
path.drawShader("shader_datas/ssao_pass/ssao_pass"); path.drawShader("shader_datas/ssao_pass/ssao_pass");
@ -670,7 +675,11 @@ class RenderPathDeferred {
{ {
if (leenkx.data.Config.raw.rp_ssgi != false) { if (leenkx.data.Config.raw.rp_ssgi != false) {
path.setTarget("ssgi_a"); path.setTarget("ssgi_a");
#if rp_ssgi_half
path.bindTarget("half", "gbufferD");
#else
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");
#end
path.bindTarget("gbuffer0", "gbuffer0"); path.bindTarget("gbuffer0", "gbuffer0");
path.bindTarget("gbuffer1", "gbuffer1"); path.bindTarget("gbuffer1", "gbuffer1");
#if rp_gbuffer_emission #if rp_gbuffer_emission
@ -684,13 +693,21 @@ class RenderPathDeferred {
path.setTarget("ssgi_b"); path.setTarget("ssgi_b");
path.bindTarget("ssgi_a", "tex"); path.bindTarget("ssgi_a", "tex");
path.bindTarget("gbuffer0", "gbuffer0"); path.bindTarget("gbuffer0", "gbuffer0");
#if rp_ssgi_half
path.bindTarget("half", "gbufferD");
#else
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");
#end
path.drawShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_x"); path.drawShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_x");
path.setTarget("ssgi_a"); path.setTarget("ssgi_a");
path.bindTarget("ssgi_b", "tex"); path.bindTarget("ssgi_b", "tex");
path.bindTarget("gbuffer0", "gbuffer0"); path.bindTarget("gbuffer0", "gbuffer0");
#if rp_ssgi_half
path.bindTarget("half", "gbufferD");
#else
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");
#end
path.drawShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_y"); path.drawShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_y");
} }
} }
@ -960,7 +977,7 @@ class RenderPathDeferred {
path.bindTarget("tex", "tex"); path.bindTarget("tex", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass"); path.drawShader("shader_datas/copy_pass/copy_pass");
path.setTarget("gbuffer0", ["gbuffer1", "gbuffer_refraction"]); setTargetMeshes();
#if (rp_voxels != "Off") #if (rp_voxels != "Off")
path.bindTarget("voxelsOut", "voxels"); path.bindTarget("voxelsOut", "voxels");
@ -980,7 +997,7 @@ class RenderPathDeferred {
#end #end
#if rp_ssrs #if rp_ssrs
path.bindTarget("_main", "gbufferD"); path.bindTarget("gbufferD1", "gbufferD");
#end #end
path.drawMeshes("refraction"); path.drawMeshes("refraction");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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