forked from LeenkxTeam/LNXSDK
Compare commits
31 Commits
7d00036973
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 211cba61a6 | |||
| 4ab646015d | |||
| 9fee67d231 | |||
| 860b73a8f4 | |||
| da38631584 | |||
| 6839203dc5 | |||
| d9ac74101f | |||
| e20400654c | |||
| 058cbed20f | |||
| e0c10318e5 | |||
| 7d609b126d | |||
| 36ecaea919 | |||
| fac01a2849 | |||
| c71c1a98e4 | |||
| f6d5f142e9 | |||
| 70961b0336 | |||
| 9c50274f29 | |||
| a94b95afc8 | |||
| cee36635e0 | |||
| d0337f5dae | |||
| fca51c7e61 | |||
| 0bb4bceb57 | |||
| af9a259fbb | |||
| f1d8522f51 | |||
| 56d1b0e4a7 | |||
| 73e4a0ad30 | |||
| 67d5e6b2c7 | |||
| 635f8f6645 | |||
| b2444be7e2 | |||
| b4de377433 | |||
| d787f9a7fc |
@ -130,6 +130,11 @@ extern class Krom {
|
||||
static function readStorage(name: String): haxe.io.BytesData;
|
||||
|
||||
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 savePath(): String;
|
||||
static function getArgCount(): Int;
|
||||
|
||||
BIN
Krom/Krom.exe
BIN
Krom/Krom.exe
Binary file not shown.
Binary file not shown.
@ -48,6 +48,10 @@ void main() {
|
||||
}
|
||||
|
||||
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.4
|
||||
|
||||
|
||||
@ -69,6 +69,7 @@ uniform vec4 PPComp15;
|
||||
uniform vec4 PPComp16;
|
||||
uniform vec4 PPComp18;
|
||||
uniform vec4 PPComp19;
|
||||
uniform vec4 PPComp20;
|
||||
#endif
|
||||
|
||||
// #ifdef _CPos
|
||||
@ -499,14 +500,15 @@ void main() {
|
||||
|
||||
#ifdef _CExposure
|
||||
#ifdef _CPostprocess
|
||||
fragColor.rgb+=fragColor.rgb*PPComp8.x;
|
||||
fragColor.rgb *= pow(2.0, PPComp8.x);
|
||||
#else
|
||||
fragColor.rgb+= fragColor.rgb*compoExposureStrength;
|
||||
fragColor.rgb *= pow(2.0, compoExposureStrength);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef _CPostprocess
|
||||
fragColor.rgb *= ComputeEV(0.0);
|
||||
fragColor.rgb *= pow(2.0, PPComp20.x); // exposure and gamma
|
||||
#endif
|
||||
|
||||
#ifdef _AutoExposure
|
||||
@ -582,14 +584,16 @@ fragColor.rgb = min(fragColor.rgb, 65504 * 0.5);
|
||||
fragColor.rgb = tonemapAgXFull(fragColor.rgb);
|
||||
} //else { fragColor.rgb = vec3(0,1,0); //ERROR}
|
||||
#endif
|
||||
|
||||
#ifdef _CGamma
|
||||
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / PPComp20.y));
|
||||
#endif
|
||||
#else
|
||||
#ifdef _CToneFilmic
|
||||
fragColor.rgb = tonemapFilmic(fragColor.rgb); // With gamma
|
||||
#endif
|
||||
#ifdef _CToneFilmic2
|
||||
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
|
||||
#ifdef _CToneReinhard
|
||||
fragColor.rgb = tonemapReinhard(fragColor.rgb);
|
||||
@ -615,6 +619,10 @@ fragColor.rgb = min(fragColor.rgb, 65504 * 0.5);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef _CGamma
|
||||
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / compoGammaStrength));
|
||||
#endif
|
||||
|
||||
#ifdef _CBW
|
||||
// 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;
|
||||
|
||||
@ -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]);
|
||||
#endif
|
||||
#else
|
||||
if (li == 0) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[0], p, n, cLightDir, lightPlane.y, shadowMapSpot[0], sssColorCL, sssRadiusCL
|
||||
#ifdef _ShadowMapAtlas
|
||||
, vec4(0.0, 0.0, 1.0, 1.0)
|
||||
#endif
|
||||
);
|
||||
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
|
||||
);
|
||||
if (li == 0) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[0], p, n, cLightDir, lightPlane.y, shadowMapSpot[0], sssColorCL, sssRadiusCL);
|
||||
else if (li == 1) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[1], p, n, cLightDir, lightPlane.y, shadowMapSpot[1], sssColorCL, sssRadiusCL);
|
||||
else if (li == 2) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[2], p, n, cLightDir, lightPlane.y, shadowMapSpot[2], sssColorCL, sssRadiusCL);
|
||||
else if (li == 3) fragColor.rgb += cLightCol * sssStrengthCL * SSSSTransmittance(LWVPSpotArray[3], p, n, cLightDir, lightPlane.y, shadowMapSpot[3], sssColorCL, sssRadiusCL);
|
||||
#endif
|
||||
} else {
|
||||
#ifdef _ShadowMapAtlas
|
||||
|
||||
@ -56,9 +56,15 @@ uniform vec2 cameraPlane;
|
||||
#ifdef _SinglePoint
|
||||
#ifdef _Spot
|
||||
//!uniform sampler2DShadow shadowMapSpot[1];
|
||||
#ifdef _ShadowMapTransparent
|
||||
//!uniform sampler2D shadowMapSpotTransparent[1];
|
||||
#endif
|
||||
//!uniform mat4 LWVPSpotArray[1];
|
||||
#else
|
||||
//!uniform samplerCubeShadow shadowMapPoint[1];
|
||||
#ifdef _ShadowMapTransparent
|
||||
//!uniform samplerCube shadowMapPointTransparent[1];
|
||||
#endif
|
||||
//!uniform vec2 lightProj;
|
||||
#endif
|
||||
#endif
|
||||
@ -66,26 +72,41 @@ uniform vec2 cameraPlane;
|
||||
#ifdef _ShadowMapAtlas
|
||||
#ifdef _SingleAtlas
|
||||
uniform sampler2DShadow shadowMapAtlas;
|
||||
#ifdef _ShadowMapTransparent
|
||||
uniform sampler2D shadowMapAtlasTransparent;
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#ifdef _ShadowMapAtlas
|
||||
#ifndef _SingleAtlas
|
||||
//!uniform sampler2DShadow shadowMapAtlasPoint;
|
||||
#ifdef _ShadowMapTransparent
|
||||
//!uniform sampler2D shadowMapAtlasPointTransparent;
|
||||
#endif
|
||||
//!uniform vec4 pointLightDataArray[4];
|
||||
#endif
|
||||
//!uniform vec4 pointLightDataArray[maxLightsCluster * 6];
|
||||
#else
|
||||
//!uniform samplerCubeShadow shadowMapPoint[4];
|
||||
#ifdef _ShadowMapTransparent
|
||||
//!uniform samplerCube shadowMapPointTransparent[4];
|
||||
#endif
|
||||
#endif
|
||||
//!uniform vec2 lightProj;
|
||||
#ifdef _Spot
|
||||
#ifdef _ShadowMapAtlas
|
||||
#ifndef _SingleAtlas
|
||||
//!uniform sampler2DShadow shadowMapAtlasSpot;
|
||||
#ifdef _ShadowMapTransparent
|
||||
//!uniform sampler2D shadowMapAtlasSpotTransparent;
|
||||
#endif
|
||||
#endif
|
||||
#else
|
||||
//!uniform sampler2DShadow shadowMapSpot[4];
|
||||
//!uniform sampler2DShadow shadowMapSpot[maxLightsCluster];
|
||||
#ifdef _ShadowMapTransparent
|
||||
//!uniform sampler2D shadowMapSpotTransparent[maxLightsCluster];
|
||||
#endif
|
||||
//!uniform mat4 LWVPSpotArray[4];
|
||||
#endif
|
||||
//!uniform mat4 LWVPSpotArray[maxLightsCluster];
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
@ -97,10 +118,16 @@ uniform vec3 sunCol;
|
||||
#ifdef _ShadowMapAtlas
|
||||
#ifndef _SingleAtlas
|
||||
uniform sampler2DShadow shadowMapAtlasSun;
|
||||
#ifdef _ShadowMapTransparent
|
||||
uniform sampler2D shadowMapAtlasSunTransparent;
|
||||
#endif
|
||||
#endif
|
||||
//!uniform vec4 tileBoundsSunArray[maxLights * shadowmapCascades];
|
||||
#else
|
||||
uniform sampler2DShadow shadowMap;
|
||||
#ifdef _ShadowMapTransparent
|
||||
uniform sampler2D shadowMapTransparent;
|
||||
#endif
|
||||
#endif
|
||||
uniform float shadowsBias;
|
||||
#ifdef _CSM
|
||||
@ -167,10 +194,11 @@ void main() {
|
||||
|
||||
vec4 g1 = textureLod(gbuffer1, texCoord, 0.0); // Basecolor.rgb, spec/occ
|
||||
vec2 occspec = unpackFloat2(g1.a);
|
||||
vec3 albedo = surfaceAlbedo(g1.rgb, metallic); // g1.rgb - basecolor
|
||||
vec3 f0 = surfaceF0(g1.rgb, metallic);
|
||||
vec3 basecolor = min(g1.rgb, vec3(2.0));
|
||||
vec3 albedo = surfaceAlbedo(basecolor, metallic);
|
||||
vec3 f0 = surfaceF0(basecolor, metallic);
|
||||
#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
|
||||
|
||||
float depth = textureLod(gbufferD, texCoord, 0.0).r * 2.0 - 1.0;
|
||||
@ -206,7 +234,7 @@ void main() {
|
||||
envl /= PI;
|
||||
#endif
|
||||
#else
|
||||
vec3 envl = vec3(1.0);
|
||||
vec3 envl = vec3(0.0);
|
||||
#endif
|
||||
|
||||
#ifdef _Rad
|
||||
@ -227,13 +255,23 @@ void main() {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef _Brdf
|
||||
vec3 F = f0 * envBRDF.x + envBRDF.y;
|
||||
#else
|
||||
vec3 F = f0;
|
||||
#endif
|
||||
|
||||
envl.rgb *= diffuseIBL(albedo, roughness, f0, dotNV);
|
||||
|
||||
#ifdef _Brdf
|
||||
envl.rgb *= 1.0 - F;
|
||||
#endif
|
||||
|
||||
#ifdef _Rad // Indirect specular
|
||||
envl.rgb += prefilteredColor * (f0 * envBRDF.x + envBRDF.y) * 1.5 * occspec.y;
|
||||
envl.rgb += prefilteredColor * F;
|
||||
#else
|
||||
#ifdef _EnvCol
|
||||
envl.rgb += backgroundCol * surfaceF0(g1.rgb, metallic); // f0
|
||||
envl.rgb += backgroundCol * F;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@ -281,7 +319,12 @@ void main() {
|
||||
float transmittance = 1.0 - transF;
|
||||
#ifdef _Rad
|
||||
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);
|
||||
vec3 transColor = textureLod(senvmapRadiance,
|
||||
envMapEquirect(refrDir), transLod).rgb;
|
||||
@ -337,7 +380,7 @@ void main() {
|
||||
float sdotNH = max(0.0, dot(n, sh));
|
||||
float sdotVH = max(0.0, dot(v, sh));
|
||||
float sdotNL = max(0.0, dot(n, sunDir));
|
||||
float svisibility = 1.0;
|
||||
vec3 svisibility = vec3(1.0);
|
||||
#ifdef _Anisotropy
|
||||
vec3 sdirect;
|
||||
if (abs(matp0.x) > 0.001 && dot(wTangent, wTangent) > 0.001) {
|
||||
@ -385,29 +428,59 @@ void main() {
|
||||
#ifdef _CSM
|
||||
svisibility = shadowTestCascade(
|
||||
#ifdef _ShadowMapAtlas
|
||||
#ifdef _ShadowMapTransparent
|
||||
#ifndef _SingleAtlas
|
||||
shadowMapAtlasSun, shadowMapAtlasSunTransparent
|
||||
#else
|
||||
shadowMapAtlas, shadowMapAtlasTransparent
|
||||
#endif
|
||||
#else
|
||||
#ifndef _SingleAtlas
|
||||
shadowMapAtlasSun
|
||||
#else
|
||||
shadowMapAtlas
|
||||
#endif
|
||||
#endif
|
||||
#else
|
||||
#ifdef _ShadowMapTransparent
|
||||
shadowMap, shadowMapTransparent
|
||||
#else
|
||||
shadowMap
|
||||
#endif
|
||||
, eye, p + n * shadowsBias * 10, shadowsBias
|
||||
#endif
|
||||
, eye, p + n * shadowsBias * 2, shadowsBias
|
||||
#ifdef _ShadowMapTransparent
|
||||
, false
|
||||
#endif
|
||||
);
|
||||
#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(
|
||||
#ifdef _ShadowMapAtlas
|
||||
#ifdef _ShadowMapTransparent
|
||||
#ifndef _SingleAtlas
|
||||
shadowMapAtlasSun, shadowMapAtlasSunTransparent
|
||||
#else
|
||||
shadowMapAtlas, shadowMapAtlasTransparent
|
||||
#endif
|
||||
#else
|
||||
#ifndef _SingleAtlas
|
||||
shadowMapAtlasSun
|
||||
#else
|
||||
shadowMapAtlas
|
||||
#endif
|
||||
#endif
|
||||
#else
|
||||
#ifdef _ShadowMapTransparent
|
||||
shadowMap, shadowMapTransparent
|
||||
#else
|
||||
shadowMap
|
||||
#endif
|
||||
#endif
|
||||
, lPos.xyz / lPos.w, shadowsBias
|
||||
#ifdef _ShadowMapTransparent
|
||||
, false
|
||||
#endif
|
||||
);
|
||||
#endif
|
||||
#endif
|
||||
@ -415,11 +488,18 @@ void main() {
|
||||
fragColor.rgb += sdirect * svisibility * sunCol;
|
||||
#endif
|
||||
|
||||
#ifdef _ShadowMapAtlas
|
||||
tileBounds = vec4(0.0, 0.0, 1.0, 1.0);
|
||||
#endif
|
||||
|
||||
#ifdef _SinglePoint
|
||||
fragColor.rgb += sampleLight(
|
||||
p, n, v, dotNV, pointPos, pointCol, albedo, roughness, occspec.y, f0
|
||||
#ifdef _ShadowMap
|
||||
, 0, pointBias, true
|
||||
#ifdef _ShadowMapTransparent
|
||||
, false
|
||||
#endif
|
||||
#endif
|
||||
#ifdef _Spot
|
||||
, true, spotData.x, spotData.y, spotDir, spotData.zw, spotRight // TODO: Test!
|
||||
@ -472,12 +552,15 @@ void main() {
|
||||
#ifdef _ShadowMap
|
||||
// light index, shadow bias, cast_shadows
|
||||
, li, lightsArray[li * 3 + 2].x, lightsArray[li * 3 + 2].z != 0.0
|
||||
#ifdef _ShadowMapTransparent
|
||||
, false
|
||||
#endif
|
||||
#endif
|
||||
#ifdef _Spot
|
||||
, lightsArray[li * 3 + 2].y != 0.0
|
||||
, lightsArray[li * 3 + 2].y // spot size (cutoff)
|
||||
, lightsArraySpot[li].w // spot blend (exponent)
|
||||
, lightsArraySpot[li].xyz // spotDir
|
||||
, lightsArraySpot[li * 2].w // spot blend (exponent)
|
||||
, lightsArraySpot[li * 2].xyz // spotDir
|
||||
, vec2(lightsArray[li * 3].w, lightsArray[li * 3 + 1].w) // scale
|
||||
, lightsArraySpot[li * 2 + 1].xyz // right
|
||||
#endif
|
||||
|
||||
@ -122,11 +122,6 @@
|
||||
"link": "_inverseViewProjectionMatrixRight",
|
||||
"ifdef": ["_VRStereo"]
|
||||
},
|
||||
{
|
||||
"name": "invVP",
|
||||
"link": "_viewProjectionMatrix",
|
||||
"ifdef": ["_SSRS"]
|
||||
},
|
||||
{
|
||||
"name": "smSizeUniform",
|
||||
"link": "_shadowMapSize",
|
||||
@ -137,14 +132,6 @@
|
||||
"link": "_lightPlane",
|
||||
"ifdef": ["_SSS"]
|
||||
},
|
||||
{
|
||||
"name": "VP",
|
||||
"link": "_viewProjectionMatrix",
|
||||
"ifdef": ["_SSRS"]
|
||||
},
|
||||
{
|
||||
"ifdef": ["_SMSizeUniform"]
|
||||
},
|
||||
{
|
||||
"name": "lightProj",
|
||||
"link": "_lightPlaneProj",
|
||||
|
||||
@ -14,15 +14,24 @@
|
||||
#ifdef _SinglePoint
|
||||
#ifdef _Spot
|
||||
uniform sampler2DShadow shadowMapSpot[1];
|
||||
#ifdef _ShadowMapTransparent
|
||||
uniform sampler2D shadowMapSpotTransparent[1];
|
||||
#endif
|
||||
uniform mat4 LWVPSpotArray[1];
|
||||
#else
|
||||
uniform samplerCubeShadow shadowMapPoint[1];
|
||||
#ifdef _ShadowMapTransparent
|
||||
uniform samplerCube shadowMapPointTransparent[1];
|
||||
#endif
|
||||
uniform vec2 lightProj;
|
||||
#endif
|
||||
#endif
|
||||
#ifdef _Clusters
|
||||
#ifdef _SingleAtlas
|
||||
//!uniform sampler2DShadow shadowMapAtlas;
|
||||
#ifdef _ShadowMapTransparent
|
||||
//!uniform sampler2D shadowMapAtlasTransparent;
|
||||
#endif
|
||||
#endif
|
||||
#ifndef _SinglePoint
|
||||
uniform vec2 lightProj;
|
||||
@ -30,17 +39,29 @@
|
||||
#ifdef _ShadowMapAtlas
|
||||
#ifndef _SingleAtlas
|
||||
uniform sampler2DShadow shadowMapAtlasPoint;
|
||||
#ifdef _ShadowMapTransparent
|
||||
uniform sampler2D shadowMapAtlasPointTransparent;
|
||||
#endif
|
||||
#endif
|
||||
#else
|
||||
uniform samplerCubeShadow shadowMapPoint[4];
|
||||
#ifdef _ShadowMapTransparent
|
||||
uniform samplerCube shadowMapPointTransparent[4];
|
||||
#endif
|
||||
#endif
|
||||
#ifdef _Spot
|
||||
#ifdef _ShadowMapAtlas
|
||||
#ifndef _SingleAtlas
|
||||
uniform sampler2DShadow shadowMapAtlasSpot;
|
||||
#ifdef _ShadowMapTransparent
|
||||
uniform sampler2D shadowMapAtlasSpotTransparent;
|
||||
#endif
|
||||
#endif
|
||||
#else
|
||||
uniform sampler2DShadow shadowMapSpot[maxLightsCluster];
|
||||
#ifdef _ShadowMapTransparent
|
||||
uniform sampler2D shadowMapSpotTransparent[maxLightsCluster];
|
||||
#endif
|
||||
#endif
|
||||
uniform mat4 LWVPSpotArray[maxLightsCluster];
|
||||
#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
|
||||
#ifdef _ShadowMap
|
||||
, int index, float bias, bool receiveShadow
|
||||
#ifdef _ShadowMapTransparent
|
||||
, bool transparent
|
||||
#endif
|
||||
#endif
|
||||
#ifdef _Spot
|
||||
, bool isSpot, float spotSize, float spotBlend, vec3 spotDir, vec2 scale, vec3 right
|
||||
@ -122,25 +146,76 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
|
||||
if (receiveShadow) {
|
||||
#ifdef _SinglePoint
|
||||
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
|
||||
#ifdef _Clusters
|
||||
vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0);
|
||||
#ifdef _ShadowMapAtlas
|
||||
tileBounds = tileBoundsSpotArray[index];
|
||||
direct *= shadowTest(
|
||||
#ifdef _ShadowMapTransparent
|
||||
#ifndef _SingleAtlas
|
||||
shadowMapAtlasSpot, shadowMapAtlasSpotTransparent
|
||||
#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], lPos.xyz / lPos.w, bias);
|
||||
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 == 3) direct *= shadowTest(shadowMapSpot[3], lPos.xyz / lPos.w, bias);
|
||||
if (index == 0) direct *= shadowTest(shadowMapSpot[0],
|
||||
#ifdef _ShadowMapTransparent
|
||||
shadowMapSpotTransparent[0],
|
||||
#endif
|
||||
lPos.xyz / lPos.w, bias
|
||||
#ifdef _ShadowMapTransparent
|
||||
, transparent
|
||||
#endif
|
||||
);
|
||||
else if (index == 1) direct *= shadowTest(shadowMapSpot[1],
|
||||
#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
|
||||
}
|
||||
@ -154,24 +229,75 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
|
||||
if (receiveShadow) {
|
||||
#ifdef _SinglePoint
|
||||
#ifndef _Spot
|
||||
direct *= PCFCube(shadowMapPoint[0], ld, -l, bias, lightProj, n);
|
||||
direct *= PCFCube(shadowMapPoint[0],
|
||||
#ifdef _ShadowMapTransparent
|
||||
shadowMapPointTransparent[0],
|
||||
#endif
|
||||
ld, -l, bias, lightProj, n
|
||||
#ifdef _ShadowMapTransparent
|
||||
, transparent
|
||||
#endif
|
||||
);
|
||||
#endif
|
||||
#endif
|
||||
#ifdef _Clusters
|
||||
#ifdef _ShadowMapAtlas
|
||||
direct *= PCFFakeCube(
|
||||
#ifdef _ShadowMapTransparent
|
||||
#ifndef _SingleAtlas
|
||||
shadowMapAtlasPoint, shadowMapAtlasPointTransparent
|
||||
#else
|
||||
shadowMapAtlas, shadowMapAtlasTransparent
|
||||
#endif
|
||||
#else
|
||||
#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], ld, -l, bias, lightProj, n);
|
||||
else if (index == 1) direct *= PCFCube(shadowMapPoint[1], ld, -l, bias, lightProj, n);
|
||||
else if (index == 2) direct *= PCFCube(shadowMapPoint[2], ld, -l, bias, lightProj, n);
|
||||
else if (index == 3) direct *= PCFCube(shadowMapPoint[3], ld, -l, bias, lightProj, n);
|
||||
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
|
||||
}
|
||||
@ -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
|
||||
#ifdef _ShadowMap
|
||||
, int index, float bias, bool receiveShadow
|
||||
#ifdef _ShadowMapTransparent
|
||||
, bool transparent
|
||||
#endif
|
||||
#endif
|
||||
#ifdef _Spot
|
||||
, 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
|
||||
#ifdef _ShadowMap
|
||||
, index, bias, receiveShadow
|
||||
#ifdef _ShadowMapTransparent
|
||||
, transparent
|
||||
#endif
|
||||
#endif
|
||||
#ifdef _Spot
|
||||
, isSpot, spotSize, spotBlend, spotDir, scale, right
|
||||
|
||||
@ -151,6 +151,7 @@ void main() {
|
||||
}
|
||||
|
||||
// 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)
|
||||
+ sin(p.x * 20.0 / waterDisplace + speed * 1.3) * cos(p.y * 20.0 / waterDisplace + speed * 1.3) * 0.5)
|
||||
/ 50.0 * waterDisplace;
|
||||
@ -227,15 +228,19 @@ void main() {
|
||||
// Blinn-Phong specular using half-vector, faded at horizon
|
||||
vec3 h = normalize(v + ld);
|
||||
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
|
||||
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);
|
||||
// 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
|
||||
float fd = isSky ? 1.0 : abs(p.z - waterLevel);
|
||||
float fd = isSky ? 1.0 : abs(geomZ - waterLevel);
|
||||
if (fd < 0.1) {
|
||||
// Based on foam by Owen Deery
|
||||
// http://fire-face.com/personal/water
|
||||
|
||||
@ -34,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
"name": "ld",
|
||||
"link": "_lightDirection"
|
||||
"link": "_sunDirection"
|
||||
},
|
||||
{
|
||||
"name": "invVP",
|
||||
|
||||
@ -908,7 +908,9 @@ class RenderPath {
|
||||
#end
|
||||
|
||||
Data.getShader(shaderPath[0], shaderPath[1], function(res: ShaderData) {
|
||||
if (res != null) {
|
||||
cc.context = res.getContext(shaderPath[2]);
|
||||
}
|
||||
loading--;
|
||||
});
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@ class ShaderData {
|
||||
if (raw == null) {
|
||||
trace('Shader data "$name" not found!');
|
||||
done(null);
|
||||
return;
|
||||
}
|
||||
new ShaderData(raw, done, overrideContext);
|
||||
});
|
||||
|
||||
@ -90,8 +90,16 @@ class LightObject extends Object {
|
||||
|
||||
#if lnx_spot
|
||||
if (data.raw.type == "spot"){
|
||||
this.size = data.raw.spot_size;
|
||||
this.blend = data.raw.spot_blend;
|
||||
this.size = 0.0;
|
||||
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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,17 +1,21 @@
|
||||
package leenkx.logicnode;
|
||||
import iron.object.Object;
|
||||
#if js
|
||||
import leenkx.network.Types;
|
||||
import leenkx.network.Util;
|
||||
#if lnx_torrent
|
||||
import leenkx.network.Leenkx;
|
||||
import leenkx.network.torrent.LeenkxClient;
|
||||
import leenkx.network.torrent.TorrentClient;
|
||||
import leenkx.network.torrent.peer.RtcPeerPool;
|
||||
#end
|
||||
import leenkx.system.Event;
|
||||
|
||||
|
||||
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 data: Dynamic;
|
||||
public var client: Dynamic;
|
||||
public var value: String;
|
||||
public var listener: TEvent = null;
|
||||
|
||||
@ -20,112 +24,145 @@ class CreateLeenkxNode extends LogicNode {
|
||||
}
|
||||
|
||||
override function run(from:Int) {
|
||||
#if js
|
||||
#if lnx_torrent
|
||||
net_Url = inputs[1].get();
|
||||
if (net_Url == null) return;
|
||||
if (Leenkx.connections[net_Url] == null) {
|
||||
var object = tree.object;
|
||||
value = Leenkx.onLoadEvent;
|
||||
listener = Event.add(value, onEvent, tree.object.uid);
|
||||
client = new leenkx.network.Leenkx(net_Url, object);
|
||||
|
||||
} else {
|
||||
return;
|
||||
new leenkx.network.Leenkx(net_Url, tree.object);
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
function onEvent() {
|
||||
#if js
|
||||
var lnxjs:Dynamic = js.Lib.global;
|
||||
var lnxCxNew = lnxjs.lnxNew;
|
||||
|
||||
if (lnxCxNew == null) {
|
||||
trace("ERROR: lnxNew not available");
|
||||
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;
|
||||
|
||||
var lnxCn:Dynamic = lnxCxNew(net_Url);
|
||||
lnxCn.onOpen = function() {
|
||||
Leenkx.data.set(net_Url, lnxCn.lastPeerCount);
|
||||
Leenkx.id.set(net_Url, lnxCn.lastPeerId);
|
||||
sock.onopen();
|
||||
};
|
||||
lnxCn.onConnections = function(c) {
|
||||
Leenkx.data.set(net_Url, c + 1);
|
||||
sock.onconnections();
|
||||
};
|
||||
lnxCn.onMessage = function(address, message, nonce) {
|
||||
Leenkx.data.set(net_Url, message);
|
||||
Leenkx.id.set(net_Url, address);
|
||||
sock.onmessage();
|
||||
};
|
||||
lnxCn.onSeen = function(address) {
|
||||
Leenkx.id.set(net_Url, address);
|
||||
sock.onseen();
|
||||
};
|
||||
lnxCn.onLeft = function(address) {
|
||||
Leenkx.id.set(net_Url, address);
|
||||
sock.onleft();
|
||||
};
|
||||
lnxCn.onServer = function(address) {
|
||||
Leenkx.id.set(net_Url, address);
|
||||
sock.onserver();
|
||||
};
|
||||
lnxCn.onPing = function(address) {
|
||||
Leenkx.id.set(net_Url, address);
|
||||
sock.onping();
|
||||
};
|
||||
lnxCn.onTimeout = function(address) {
|
||||
Leenkx.id.set(net_Url, address);
|
||||
sock.ontimeout();
|
||||
};
|
||||
lnxCn.onWireLeft = function(wirecount, wire) {
|
||||
Leenkx.data.set(net_Url, wirecount);
|
||||
if (wire != null && wire.peerId != null) {
|
||||
Leenkx.id.set(net_Url, wire.peerId);
|
||||
}
|
||||
sock.onwireleft();
|
||||
};
|
||||
lnxCn.onWireSeen = function(wirecount, wire) {
|
||||
Leenkx.data.set(net_Url, wirecount);
|
||||
if (wire != null && wire.peerId != null) {
|
||||
Leenkx.id.set(net_Url, wire.peerId);
|
||||
}
|
||||
sock.onwireseen();
|
||||
};
|
||||
lnxCn.onTorrent = function(identifier, torrent) {
|
||||
Leenkx.data.set(net_Url, torrent);
|
||||
Leenkx.id.set(net_Url, identifier);
|
||||
sock.ontorrent();
|
||||
};
|
||||
lnxCn.onTracker = function(identifier, tracker) {
|
||||
Leenkx.id.set(net_Url, identifier);
|
||||
sock.ontracker();
|
||||
};
|
||||
lnxCn.onAnnounce = function(identifier) {
|
||||
Leenkx.id.set(net_Url, identifier);
|
||||
sock.onannounce();
|
||||
};
|
||||
lnxCn.onError = function(msg) {
|
||||
Leenkx.data.set(net_Url, msg);
|
||||
sock.onerror();
|
||||
};
|
||||
lnxCn.onTorrentAdded = function(t) {
|
||||
Leenkx.torrent.set(net_Url, t);
|
||||
Leenkx.file.set(net_Url, t.files);
|
||||
Leenkx.data.set(net_Url, t);
|
||||
sock.ontorrentadded();
|
||||
};
|
||||
lnxCn.onTorrentDone = function(t) {
|
||||
Leenkx.data.set(net_Url, t);
|
||||
sock.ontorrentdone();
|
||||
};
|
||||
|
||||
lnxCn.on("connections", function(c) {
|
||||
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();
|
||||
});
|
||||
sock.client = lnxCn;
|
||||
|
||||
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);
|
||||
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 {
|
||||
#if js
|
||||
#if lnx_torrent
|
||||
return switch (from) {
|
||||
case 1: Leenkx.connections[net_Url];
|
||||
case 2: Leenkx.id.get(net_Url);
|
||||
|
||||
@ -1,43 +1,100 @@
|
||||
package leenkx.logicnode;
|
||||
import leenkx.system.Event;
|
||||
#if lnx_torrent
|
||||
import leenkx.network.Leenkx;
|
||||
import iron.object.Object;
|
||||
import leenkx.network.torrent.Magnet;
|
||||
import leenkx.network.torrent.TorrentClient;
|
||||
#end
|
||||
|
||||
|
||||
class CreateTorrentNode extends LogicNode {
|
||||
public var torrent: Dynamic;
|
||||
public var net_Url: String;
|
||||
public var meta:Dynamic;
|
||||
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) {
|
||||
super(tree);
|
||||
}
|
||||
|
||||
override function run(from:Int) {
|
||||
#if lnx_torrent
|
||||
meta = null;
|
||||
metaBytes = null;
|
||||
magnetUri = null;
|
||||
infoHashHex = null;
|
||||
|
||||
var connection = inputs[1].get();
|
||||
if (connection == null) return;
|
||||
var data = inputs[2].get();
|
||||
var data = inputs[1].get();
|
||||
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 + ';
|
||||
lx_' + net_Url +'.wt.add(file, function (torrent) {
|
||||
leenkx.network.Leenkx.file.set("' + net_Url +'", torrent.files);
|
||||
torrent.on("done", function () {
|
||||
leenkx.network.Leenkx.connections.h["' + net_Url +'"].ontorrentdone();
|
||||
});
|
||||
});
|
||||
';
|
||||
if (mode == "folder") {
|
||||
#if sys
|
||||
data = TorrentClient.readFolder(Std.string(data));
|
||||
var first:Dynamic = data[0];
|
||||
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);
|
||||
}
|
||||
|
||||
#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 {
|
||||
#if lnx_torrent
|
||||
return switch (from) {
|
||||
case 1: Leenkx.file.get(net_Url);
|
||||
case 1: meta;
|
||||
case 2: metaBytes;
|
||||
case 3: infoHashHex;
|
||||
case 4: magnetUri;
|
||||
default: throw "Unreachable";
|
||||
}
|
||||
#else
|
||||
return null;
|
||||
#end
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
88
leenkx/Sources/leenkx/logicnode/CreateTrackerNode.hx
Normal file
88
leenkx/Sources/leenkx/logicnode/CreateTrackerNode.hx
Normal 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
|
||||
}
|
||||
}
|
||||
101
leenkx/Sources/leenkx/logicnode/DownloadTorrentNode.hx
Normal file
101
leenkx/Sources/leenkx/logicnode/DownloadTorrentNode.hx
Normal 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
|
||||
}
|
||||
}
|
||||
83
leenkx/Sources/leenkx/logicnode/GetTorrentFileNode.hx
Normal file
83
leenkx/Sources/leenkx/logicnode/GetTorrentFileNode.hx
Normal 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
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
package leenkx.logicnode;
|
||||
import leenkx.system.Event;
|
||||
import leenkx.network.Connect;
|
||||
import iron.object.Object;
|
||||
|
||||
#if lnx_torrent
|
||||
import leenkx.network.Leenkx;
|
||||
import leenkx.network.torrent.LeenkxClient;
|
||||
#end
|
||||
|
||||
class LeenkxCloseConnectionNode extends LogicNode {
|
||||
public var property0: String;
|
||||
@ -12,41 +12,29 @@ class LeenkxCloseConnectionNode extends LogicNode {
|
||||
}
|
||||
|
||||
override function run(from:Int) {
|
||||
#if lnx_torrent
|
||||
var connection = inputs[1].get();
|
||||
if (connection == null) return;
|
||||
var socket = Leenkx.connections[connection._url];
|
||||
if (socket == null) return;
|
||||
|
||||
if(property0 == "client") {
|
||||
var script = '
|
||||
for (p in leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers){
|
||||
try{
|
||||
leenkx.network.Leenkx.id.set("' + connection._url + '",p);
|
||||
leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn._pc.close();
|
||||
leenkx.network.Leenkx.connections.h["' + connection._url + '"].client.torrent._peers[p].conn.destroy();
|
||||
leenkx.network.Leenkx.connections.h["' + connection._url + '"].onclose();
|
||||
}catch(error){
|
||||
console.log("Error: " + error);
|
||||
var lnxCn:LeenkxClient = socket.client;
|
||||
if (property0 == "peer") {
|
||||
var id:String = inputs.length > 2 ? inputs[2].get() : null;
|
||||
if (id != null && lnxCn != null) {
|
||||
lnxCn.peers.remove(id);
|
||||
lnxCn.seenFired.remove(id);
|
||||
}
|
||||
runOutput(0);
|
||||
return;
|
||||
}
|
||||
';
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package leenkx.logicnode;
|
||||
import leenkx.system.Event;
|
||||
#if lnx_torrent
|
||||
import leenkx.network.Leenkx;
|
||||
import iron.object.Object;
|
||||
#end
|
||||
|
||||
class LeenkxEventNode extends LogicNode {
|
||||
public var property0: String;
|
||||
public var property1: String;
|
||||
|
||||
public var value: String;
|
||||
@ -14,90 +14,63 @@ class LeenkxEventNode extends LogicNode {
|
||||
public function new(tree:LogicTree) {
|
||||
super(tree);
|
||||
tree.notifyOnInit(init);
|
||||
tree.notifyOnRemove(onRemove);
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (property0 == "client") {
|
||||
net_Url = inputs[0].get();
|
||||
#if lnx_torrent
|
||||
var u:Dynamic = inputs[0].get();
|
||||
net_Url = Std.isOfType(u, String) ? u : (u != null ? u._url : null);
|
||||
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);
|
||||
case "onseen": value = Leenkx.onSeenEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onserver": value = Leenkx.onServerEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onconnections": value = Leenkx.onConnectionsEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onping": value = Leenkx.onPingEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onleft": value = Leenkx.onLeftEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "ontimeout": value = Leenkx.onTimeOutEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onrpc": value = Leenkx.onRpcEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onrpcresponse": value = Leenkx.onRpcResponseEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onwireleft": value = Leenkx.onWireLeftEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onwireseen": value = Leenkx.onWireSeenEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "ontorrent": value = Leenkx.onTorrentEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "ontracker": value = Leenkx.onTrackerEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onannounce": value = Leenkx.onAnnounceEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
default: throw "Failed to set client event type.";
|
||||
}
|
||||
} else if (property0 == "host") {
|
||||
#if (sys || kha_krom)
|
||||
var net_Domain = inputs[0].get();
|
||||
var net_Port = inputs[1].get();
|
||||
net_Url = "ws://" + net_Domain + ":" + Std.string(net_Port);
|
||||
switch (property1) {
|
||||
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
default: throw "Failed to set host event type.";
|
||||
case "onopen": value = Leenkx.onOpenEvent;
|
||||
case "onmessage": value = Leenkx.onMessageEvent;
|
||||
case "onerror": value = Leenkx.onErrorEvent;
|
||||
case "onclose": value = Leenkx.onCloseEvent;
|
||||
case "onseen": value = Leenkx.onSeenEvent;
|
||||
case "onserver": value = Leenkx.onServerEvent;
|
||||
case "onconnections": value = Leenkx.onConnectionsEvent;
|
||||
case "onping": value = Leenkx.onPingEvent;
|
||||
case "onleft": value = Leenkx.onLeftEvent;
|
||||
case "ontimeout": value = Leenkx.onTimeOutEvent;
|
||||
case "onwireleft": value = Leenkx.onWireLeftEvent;
|
||||
case "onwireseen": value = Leenkx.onWireSeenEvent;
|
||||
case "ontorrent": value = Leenkx.onTorrentEvent;
|
||||
case "ontorrentadded": value = Leenkx.onTorrentAddedEvent;
|
||||
case "ontracker": value = Leenkx.onTrackerEvent;
|
||||
case "onannounce": value = Leenkx.onAnnounceEvent;
|
||||
case "ontorrentdone": value = Leenkx.onTorrentDoneEvent;
|
||||
case "onload": value = Leenkx.onLoadEvent;
|
||||
default:
|
||||
trace("Failed to set client event type");
|
||||
}
|
||||
listener = Event.add(value, onEvent, tree.object.uid);
|
||||
#end
|
||||
} else if (property0 == "securehost"){
|
||||
#if (sys || kha_krom)
|
||||
var net_Domain = inputs[0].get();
|
||||
var net_Port = inputs[1].get();
|
||||
net_Url = "wss://" + net_Domain + ":" + Std.string(net_Port);
|
||||
switch (property1) {
|
||||
case "onopen": value = Leenkx.onOpenEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onmessage": value = Leenkx.onMessageEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onerror": value = Leenkx.onErrorEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
case "onclose": value = Leenkx.onCloseEvent;listener = Event.add(value, onEvent, tree.object.uid);
|
||||
default: throw "Failed to set host event type.";
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function onEvent() {
|
||||
runOutput(0);
|
||||
}
|
||||
|
||||
function onRemove() {
|
||||
if (listener != null) {
|
||||
Event.removeListener(listener);
|
||||
listener = null;
|
||||
}
|
||||
}
|
||||
|
||||
override function get(from: Int): Dynamic {
|
||||
if (property0 == "host") {
|
||||
#if lnx_torrent
|
||||
return switch (from) {
|
||||
case 1: Leenkx.id.get(net_Url);
|
||||
case 2: Leenkx.data.get(net_Url);
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
package leenkx.logicnode;
|
||||
import leenkx.network.Connect;
|
||||
import leenkx.network.Buffer;
|
||||
import haxe.io.Bytes;
|
||||
import iron.object.Object;
|
||||
import iron.math.Vec4;
|
||||
import iron.math.Quat;
|
||||
import iron.math.Mat4;
|
||||
@ -22,98 +20,54 @@ class LeenkxMessageParserNode extends LogicNode {
|
||||
if (data == null) return;
|
||||
|
||||
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){
|
||||
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) {
|
||||
case "string":
|
||||
var bytes = buffer.readAllAvailableBytes();
|
||||
parsed = bytes.toString().substr(0, bytes.length - api.length);
|
||||
trace(parsed);
|
||||
runOutput(0);
|
||||
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();
|
||||
for (f in Reflect.fields(vec)) {
|
||||
Reflect.setField(vec, f, bytes.readFloat());
|
||||
}
|
||||
vec.x = bytes.readFloat();
|
||||
vec.y = bytes.readFloat();
|
||||
vec.z = bytes.readFloat();
|
||||
vec.w = bytes.readFloat();
|
||||
parsed = vec;
|
||||
runOutput(0);
|
||||
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();
|
||||
parsed = float;
|
||||
runOutput(0);
|
||||
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();
|
||||
parsed = integer;
|
||||
runOutput(0);
|
||||
@ -127,34 +81,45 @@ class LeenkxMessageParserNode extends LogicNode {
|
||||
}
|
||||
runOutput(0);
|
||||
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 rot: Quat = new Quat();
|
||||
var scl: Vec4 = new Vec4();
|
||||
for (f in Reflect.fields(loc)) {
|
||||
Reflect.setField(loc, f, bytes.readFloat());
|
||||
}
|
||||
for (f in Reflect.fields(rot)) {
|
||||
Reflect.setField(rot, f, bytes.readFloat());
|
||||
}
|
||||
for (f in Reflect.fields(scl)) {
|
||||
Reflect.setField(scl, f, bytes.readFloat());
|
||||
try {
|
||||
loc.x = bytes.readFloat();
|
||||
loc.y = bytes.readFloat();
|
||||
loc.z = bytes.readFloat();
|
||||
loc.w = bytes.readFloat();
|
||||
rot.x = bytes.readFloat();
|
||||
rot.y = 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();
|
||||
parsed = transform.compose(loc, rot, scl);
|
||||
runOutput(0);
|
||||
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();
|
||||
for (f in Reflect.fields(rot)) {
|
||||
Reflect.setField(rot, f, bytes.readFloat());
|
||||
}
|
||||
rot.x = bytes.readFloat();
|
||||
rot.y = bytes.readFloat();
|
||||
rot.z = bytes.readFloat();
|
||||
rot.w = bytes.readFloat();
|
||||
parsed = rot;
|
||||
runOutput(0);
|
||||
default: throw "Failed to parse data.";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
override function get(from: Int): Dynamic {
|
||||
|
||||
@ -1,401 +1,124 @@
|
||||
package leenkx.logicnode;
|
||||
import leenkx.network.Connect;
|
||||
import leenkx.network.Leenkx;
|
||||
import leenkx.network.Buffer;
|
||||
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.Quat;
|
||||
import iron.math.Mat4;
|
||||
|
||||
|
||||
class LeenkxSendMessageNode extends LogicNode {
|
||||
public var property0: String;
|
||||
public var property1: String;
|
||||
public var id: String;
|
||||
public var all: Bool;
|
||||
public var property2: Bool;
|
||||
|
||||
public function new(tree:LogicTree) {
|
||||
super(tree);
|
||||
}
|
||||
|
||||
override function run(from:Int) {
|
||||
#if js
|
||||
var connection = inputs[1].get();
|
||||
if (connection == null) return;
|
||||
var api: String = inputs[2].get();
|
||||
var message: Dynamic = inputs[3].get();
|
||||
|
||||
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 = 'lnx_' + connection._url +'.send(`' + inputs[4].get() + '`, `'+ api + message + '` );';
|
||||
js.Syntax.code('(1, eval)({0})', script.toString());
|
||||
runOutput(0);
|
||||
}
|
||||
|
||||
} catch(error) {
|
||||
trace("Error: " + error);
|
||||
}
|
||||
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":
|
||||
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();
|
||||
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());
|
||||
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();
|
||||
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());
|
||||
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();
|
||||
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());
|
||||
if(api != "" || api != null){
|
||||
default: trace("Failed to send data.");
|
||||
}
|
||||
|
||||
if (api != null && api != "") {
|
||||
buffer.writeBytes(Bytes.ofString(api));
|
||||
}
|
||||
|
||||
var rawBytes = buffer.readAllAvailableBytes();
|
||||
|
||||
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);
|
||||
if (property2) {
|
||||
var payload = haxe.crypto.Base64.encode(rawBytes);
|
||||
if (broadcast) {
|
||||
lnxCn.send(null, payload);
|
||||
} 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);
|
||||
lnxCn.send(targetAddress, payload);
|
||||
}
|
||||
} else {
|
||||
lnxCn.sendRawMessage(rawBytes, broadcast ? null : targetAddress);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
runOutput(0);
|
||||
|
||||
#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;
|
||||
|
||||
_________________________________________*****/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ class NetworkMessageParserNode extends LogicNode {
|
||||
|
||||
var buffer = cast(data, Buffer);
|
||||
api = inputs[1].get();
|
||||
if (api == null) api = "";
|
||||
if(buffer.endsWith(api) == false){
|
||||
return;
|
||||
}
|
||||
|
||||
65
leenkx/Sources/leenkx/logicnode/OnTorrentEventNode.hx
Normal file
65
leenkx/Sources/leenkx/logicnode/OnTorrentEventNode.hx
Normal 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
|
||||
}
|
||||
}
|
||||
73
leenkx/Sources/leenkx/logicnode/ParseTorrentNode.hx
Normal file
73
leenkx/Sources/leenkx/logicnode/ParseTorrentNode.hx
Normal 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
|
||||
}
|
||||
}
|
||||
@ -1,52 +1,124 @@
|
||||
package leenkx.logicnode;
|
||||
import leenkx.system.Event;
|
||||
import iron.object.Object;
|
||||
|
||||
#if lnx_torrent
|
||||
import haxe.io.Bytes;
|
||||
import leenkx.network.Leenkx;
|
||||
import leenkx.network.torrent.MetaInfo;
|
||||
import leenkx.network.torrent.Torrent;
|
||||
#end
|
||||
|
||||
|
||||
class SeedTorrentNode extends LogicNode {
|
||||
public var torrent:Dynamic;
|
||||
public var net_Url: Dynamic;
|
||||
|
||||
public var net_Url:String;
|
||||
|
||||
public function new(tree:LogicTree) {
|
||||
super(tree);
|
||||
}
|
||||
|
||||
|
||||
override function run(from:Int) {
|
||||
#if js
|
||||
#if lnx_torrent
|
||||
var connection = inputs[1].get();
|
||||
if (connection == null) return;
|
||||
trace("got connection");
|
||||
var data = inputs[3].get();
|
||||
if (data == null) return;
|
||||
trace("got data");
|
||||
var opts = inputs[2].get();
|
||||
var src = inputs[2].get();
|
||||
net_Url = connection._url;
|
||||
|
||||
var cx = "lx_" + net_Url;
|
||||
var lx:Dynamic = untyped js.Browser.window[cx];
|
||||
var args = opts.toString();
|
||||
trace(args);
|
||||
var func = Reflect.field(lx.wt, "seed");
|
||||
if (func != null) {
|
||||
torrent = func(data, function (torrent) {
|
||||
trace('Client is seeding ' + torrent.magnetURI);
|
||||
});
|
||||
//trace(torrent);
|
||||
var socket = Leenkx.connections[connection._url];
|
||||
if (socket == null || socket.client == null) return;
|
||||
var client = socket.client;
|
||||
var self = this;
|
||||
|
||||
var onSeeded = function(t:Torrent) {
|
||||
if (t == null) return;
|
||||
self.torrent = t;
|
||||
var onDone = function() {
|
||||
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);
|
||||
#end
|
||||
}
|
||||
|
||||
#if lnx_torrent
|
||||
function filesFor(meta:MetaInfo, data:Dynamic):Array<{path:String, data:Bytes}> {
|
||||
if (data == null) return null;
|
||||
var entries = meta.files;
|
||||
if (Std.isOfType(data, Bytes)) {
|
||||
var blob:Bytes = data;
|
||||
if (blob.length != meta.totalLength) return null;
|
||||
var files:Array<{path:String, data:Bytes}> = [];
|
||||
var cursor = 0;
|
||||
for (f in entries) {
|
||||
files.push({path: f.path, data: blob.sub(cursor, f.length)});
|
||||
cursor += f.length;
|
||||
}
|
||||
return files;
|
||||
}
|
||||
if (Std.isOfType(data, Array)) {
|
||||
var items:Array<Dynamic> = data;
|
||||
var files:Array<{path:String, data:Bytes}> = [];
|
||||
for (it in items) {
|
||||
var p = Reflect.field(it, "path");
|
||||
var d:Bytes = Reflect.field(it, "data");
|
||||
if (d == null) d = Reflect.field(it, "content");
|
||||
if (p == null || d == null) continue;
|
||||
var sp = Std.string(p);
|
||||
var matched = false;
|
||||
for (f in entries) {
|
||||
if (f.path == sp || StringTools.endsWith(f.path, "/" + sp)) {
|
||||
files.push({path: f.path, data: d});
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) files.push({path: sp, data: d});
|
||||
}
|
||||
if (files.length == entries.length) return files;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
#end
|
||||
|
||||
override function get(from:Int):Dynamic {
|
||||
#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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
90
leenkx/Sources/leenkx/logicnode/SetTrackersNode.hx
Normal file
90
leenkx/Sources/leenkx/logicnode/SetTrackersNode.hx
Normal 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
|
||||
}
|
||||
}
|
||||
74
leenkx/Sources/leenkx/logicnode/TorrentClientInfoNode.hx
Normal file
74
leenkx/Sources/leenkx/logicnode/TorrentClientInfoNode.hx
Normal 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
|
||||
}
|
||||
}
|
||||
93
leenkx/Sources/leenkx/logicnode/TorrentControlNode.hx
Normal file
93
leenkx/Sources/leenkx/logicnode/TorrentControlNode.hx
Normal 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);
|
||||
}
|
||||
}
|
||||
74
leenkx/Sources/leenkx/logicnode/TorrentInfoNode.hx
Normal file
74
leenkx/Sources/leenkx/logicnode/TorrentInfoNode.hx
Normal 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
|
||||
}
|
||||
@ -63,10 +63,10 @@ class Buffer {
|
||||
public function readUntil(delimiter:String):Bytes {
|
||||
var dl = delimiter.length;
|
||||
|
||||
for (i in 0 ... available - dl) {
|
||||
for (i in 0 ... available - dl + 1) {
|
||||
var matched = true;
|
||||
for (j in 0 ... dl) {
|
||||
if (peekByte(currentOffset + i + j + 1) == delimiter.charCodeAt(j)) {
|
||||
if (peekByte(i + j) == delimiter.charCodeAt(j)) {
|
||||
continue;
|
||||
}
|
||||
matched = false;
|
||||
@ -74,7 +74,8 @@ class Buffer {
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
var bytes = readBytes(i + dl + 1);
|
||||
var bytes = readBytes(i);
|
||||
readBytes(dl);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@ -85,7 +86,19 @@ class Buffer {
|
||||
public function readBytes(count:Int):Bytes {
|
||||
var count2 = Std.int(Math.min(count, available));
|
||||
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;
|
||||
}
|
||||
|
||||
@ -115,16 +128,23 @@ class Buffer {
|
||||
}
|
||||
|
||||
public function peekByte(offset:Int):Int {
|
||||
if (available <= 0) throw 'No bytes available';
|
||||
var tempOffset = offset;
|
||||
var tempData = chunks[0];
|
||||
if (tempData == null) {
|
||||
tempData = currentData;
|
||||
if (available <= 0 || offset < 0 || offset >= available) throw 'No bytes available';
|
||||
var tempData = currentData;
|
||||
var chunkIndex = -1;
|
||||
var tempOffset:Int;
|
||||
if (tempData != null && currentOffset < tempData.length) {
|
||||
tempOffset = currentOffset + offset;
|
||||
} else {
|
||||
tempData = chunks[0];
|
||||
chunkIndex = 0;
|
||||
tempOffset = offset;
|
||||
}
|
||||
var chunkIndex = 0;
|
||||
while (tempOffset >= tempData.length) {
|
||||
tempOffset -= tempData.length;
|
||||
chunkIndex++;
|
||||
if (chunkIndex >= chunks.length){
|
||||
trace('No bytes available');
|
||||
}
|
||||
tempData = chunks[chunkIndex];
|
||||
}
|
||||
return tempData.get(tempOffset);
|
||||
@ -154,9 +174,9 @@ class Buffer {
|
||||
|
||||
public function endsWith(e:String):Bool {
|
||||
var i = available - e.length;
|
||||
var n = currentOffset;
|
||||
var n = 0;
|
||||
|
||||
if (i <= 0) {
|
||||
if (i < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package leenkx.network;
|
||||
|
||||
class Handler extends WebSocketCommon {
|
||||
public var validateHandshake:(HttpRequest, HttpResponse, (HttpResponse) -> Void) -> Void = null;
|
||||
|
||||
public function new(socket:SocketImpl) {
|
||||
super(socket);
|
||||
isClient = false;
|
||||
|
||||
@ -4,6 +4,7 @@ class HttpHeader {
|
||||
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_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 HOST:String = "Host";
|
||||
public static inline var CONNECTION:String = "Connection";
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package leenkx.network;
|
||||
|
||||
#if lnx_torrent
|
||||
import leenkx.network.Types;
|
||||
import haxe.io.Bytes;
|
||||
import iron.object.Object;
|
||||
import leenkx.system.Event;
|
||||
import leenkx.network.Buffer;
|
||||
import leenkx.network.torrent.LeenkxClient;
|
||||
|
||||
@:expose
|
||||
class Leenkx {
|
||||
@ -20,23 +22,19 @@ class Leenkx {
|
||||
public static var onPingEvent: String = "Leenkx.onPing";
|
||||
public static var onLeftEvent: String = "Leenkx.onLeft";
|
||||
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 onWireSeenEvent: String = "Leenkx.onWireSeen";
|
||||
public static var onTorrentEvent: String = "Leenkx.onTorrent";
|
||||
public static var onTorrentAddedEvent: String = "Leenkx.onTorrentAdded";
|
||||
public static var onTrackerEvent: String = "Leenkx.onTracker";
|
||||
public static var onAnnounceEvent: String = "Leenkx.onAnnounce";
|
||||
public static var onTorrentDoneEvent: String = "Leenkx.onTorrentDone";
|
||||
public static var connections:Map<String, leenkx.network.LeenkxSocket> = [];
|
||||
#if js
|
||||
public static var peers:js.lib.Map<String, String> = new js.lib.Map<String,String>();
|
||||
public static var data:js.lib.Map<String, Dynamic> = new js.lib.Map<String,Dynamic>();
|
||||
public static var id:js.lib.Map<String, String> = new js.lib.Map<String,String>();
|
||||
public static var torrent:js.lib.Map<String, Dynamic> = new js.lib.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 static var peers:Map<String, String> = [];
|
||||
public static var data:Map<String, Dynamic> = [];
|
||||
public static var id:Map<String, String> = [];
|
||||
public static var torrent:Map<String, Dynamic> = [];
|
||||
public static var file:Map<String, Dynamic> = [];
|
||||
|
||||
public function new(net_Url: String, net_object: Object) {
|
||||
if (net_Url != null && net_object != null) {
|
||||
@ -54,224 +52,65 @@ class Leenkx {
|
||||
}
|
||||
|
||||
if (object != null) {
|
||||
final loadEvent = Event.get(Leenkx.onLoadEvent);
|
||||
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);
|
||||
var uid = object.uid;
|
||||
|
||||
Leenkx.connections[net_Url].onopen = function() {
|
||||
if (openEvent != null) {
|
||||
for (e in openEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onOpenEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onmessage = function() {
|
||||
if (messageEvent != null) {
|
||||
for (e in messageEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onMessageEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onerror = function() {
|
||||
if (errorEvent != null) {
|
||||
for (e in errorEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onErrorEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onclose = function() {
|
||||
if (closeEvent != null) {
|
||||
for (e in closeEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onCloseEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onseen = function() {
|
||||
if (seenEvent != null) {
|
||||
for (e in seenEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onSeenEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onserver = function() {
|
||||
if (serverEvent != null) {
|
||||
for (e in serverEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onServerEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onconnections = function() {
|
||||
if (connectionsEvent != null) {
|
||||
for (e in connectionsEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onConnectionsEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onping = function() {
|
||||
if (pingEvent != null) {
|
||||
for (e in pingEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onPingEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onleft = function() {
|
||||
if (leftEvent != null) {
|
||||
for (e in leftEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onLeftEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].ontimeout = function() {
|
||||
if (timeoutEvent != null) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onTimeOutEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onwireleft = function() {
|
||||
if (wireleftEvent != null) {
|
||||
for (e in wireleftEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onWireLeftEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onwireseen = function() {
|
||||
if (wireseenEvent != null) {
|
||||
for (e in wireseenEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onWireSeenEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].ontorrent = function() {
|
||||
if (torrentEvent != null) {
|
||||
for (e in torrentEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onTorrentEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].ontorrentadded = function() {
|
||||
Event.send(Leenkx.onTorrentAddedEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].ontracker = function() {
|
||||
if (trackerEvent != null) {
|
||||
for (e in trackerEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onTrackerEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onannounce = function() {
|
||||
if (announceEvent != null) {
|
||||
for (e in announceEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onAnnounceEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].onload = function() {
|
||||
if (loadEvent != null) {
|
||||
for (e in loadEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onLoadEvent, uid);
|
||||
};
|
||||
Leenkx.connections[net_Url].ontorrentdone = function() {
|
||||
if (torrentDoneEvent != null) {
|
||||
for (e in torrentDoneEvent) {
|
||||
if (e.mask == object.uid) {
|
||||
e.onEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
Event.send(Leenkx.onTorrentDoneEvent, uid);
|
||||
};
|
||||
|
||||
#if js
|
||||
var lnxjs:Dynamic = js.Lib.global;
|
||||
var connectionUrl = net_Url;
|
||||
|
||||
if (lnxjs.Leenkx != null) {
|
||||
if (lnxjs.lnxNew == null) {
|
||||
js.Syntax.code('globalThis.lnxNew = function(url) { return new Leenkx(url); }');
|
||||
}
|
||||
Leenkx.connections[connectionUrl].onload();
|
||||
} 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 {
|
||||
public var _url:String;
|
||||
public var client:haxe.DynamicAccess<Dynamic>;
|
||||
public var client:LeenkxClient;
|
||||
public var buffer:Dynamic;
|
||||
public var onopen:Void->Void;
|
||||
public var onclose:Void->Void;
|
||||
@ -293,11 +132,10 @@ class LeenkxSocket {
|
||||
public var onping:Void->Void;
|
||||
public var onleft:Void->Void;
|
||||
public var ontimeout:Void->Void;
|
||||
public var onrpc:Void->Void;
|
||||
public var onrpcresponse:Void->Void;
|
||||
public var onwireleft:Void->Void;
|
||||
public var onwireseen:Void->Void;
|
||||
public var ontorrent:Void->Void;
|
||||
public var ontorrentadded:Void->Void;
|
||||
public var ontracker:Void->Void;
|
||||
public var onannounce:Void->Void;
|
||||
public var onload:Void->Void;
|
||||
@ -308,3 +146,4 @@ class LeenkxSocket {
|
||||
}
|
||||
|
||||
}
|
||||
#end
|
||||
|
||||
@ -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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,20 +136,45 @@ import haxe.io.Bytes;
|
||||
|
||||
class WebSocket {
|
||||
private var _url:String;
|
||||
|
||||
private var _protocols:Array<String> = 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;
|
||||
_protocols = protocols;
|
||||
if (immediateOpen) {
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
private function createSocket() {
|
||||
if (_protocols == null) {
|
||||
return new js.html.WebSocket(_url);
|
||||
}
|
||||
|
||||
return new js.html.WebSocket(_url, _protocols);
|
||||
}
|
||||
|
||||
public function open() {
|
||||
if (_ws != null) {
|
||||
throw "Socket already connected";
|
||||
@ -292,6 +317,10 @@ class WebSocket extends WebSocketCommon {
|
||||
public var _host:String;
|
||||
public var _port:Int = 0;
|
||||
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 _encodedKey:String = "wskey";
|
||||
@ -300,8 +329,9 @@ class WebSocket extends WebSocketCommon {
|
||||
|
||||
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);
|
||||
_protocols = protocols;
|
||||
|
||||
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 urlRegExp = ~/^(\w+?):\/\/([\w\.-]+)(:(\d+))?(\/.*)?$/;
|
||||
|
||||
if ( ! urlRegExp.match(url)) {
|
||||
var sep = url.indexOf("://");
|
||||
if (sep < 0) {
|
||||
throw 'Uri not matching websocket URL "${url}"';
|
||||
}
|
||||
_protocol = url.substr(0, sep);
|
||||
var rest = url.substr(sep + 3);
|
||||
|
||||
_protocol = urlRegExp.matched(1);
|
||||
var pathStart = rest.length;
|
||||
var slashIdx = rest.indexOf("/");
|
||||
var qIdx = rest.indexOf("?");
|
||||
if (slashIdx >= 0 && slashIdx < pathStart) pathStart = slashIdx;
|
||||
if (qIdx >= 0 && qIdx < pathStart) pathStart = qIdx;
|
||||
|
||||
_host = urlRegExp.matched(2);
|
||||
var hostPort = rest.substr(0, pathStart);
|
||||
var pathAndSearch = rest.substr(pathStart);
|
||||
|
||||
var parsedPort = Std.parseInt(urlRegExp.matched(4));
|
||||
var colonIdx = hostPort.indexOf(":");
|
||||
if (colonIdx >= 0) {
|
||||
_host = hostPort.substr(0, colonIdx);
|
||||
var parsedPort = Std.parseInt(hostPort.substr(colonIdx + 1));
|
||||
if (parsedPort > 0) {
|
||||
_port = parsedPort;
|
||||
}
|
||||
_path = urlRegExp.matched(5);
|
||||
if (_path == null || _path.length == 0) {
|
||||
_path = "/";
|
||||
} else {
|
||||
_host = hostPort;
|
||||
}
|
||||
**/
|
||||
var urlArr = url.split(":");
|
||||
if ( urlArr.length < 3) {
|
||||
throw 'Uri not matching websocket URL "${url}"';
|
||||
|
||||
_path = pathAndSearch;
|
||||
_search = null;
|
||||
var searchIdx = pathAndSearch.indexOf("?");
|
||||
if (searchIdx >= 0) {
|
||||
_path = pathAndSearch.substr(0, searchIdx);
|
||||
_search = pathAndSearch.substr(searchIdx);
|
||||
}
|
||||
_protocol = urlArr[0];
|
||||
_host = urlArr[1].substr(2, urlArr[1].length);
|
||||
var parsedPort = Std.parseInt(urlArr[2].split("/")[0]);
|
||||
if (parsedPort > 0 ) {
|
||||
_port = parsedPort;
|
||||
}
|
||||
_path = urlArr[2].substr(urlArr[2].split("/")[0].length, urlArr[2].length);
|
||||
if (_path == null || _path.length == 0) {
|
||||
_path = "/";
|
||||
}
|
||||
@ -388,9 +420,7 @@ class WebSocket extends WebSocketCommon {
|
||||
#else
|
||||
|
||||
haxe.MainLoop.addThread(function() {
|
||||
Log.debug("Thread started", this.id);
|
||||
processLoop(this);
|
||||
Log.debug("Thread ended", this.id);
|
||||
});
|
||||
|
||||
#end
|
||||
@ -399,16 +429,13 @@ class WebSocket extends WebSocketCommon {
|
||||
}
|
||||
|
||||
private function processThread() {
|
||||
Log.debug("Thread started", this.id);
|
||||
var ws:WebSocket = Thread.readMessage(true);
|
||||
processLoop(this);
|
||||
Log.debug("Thread ended", this.id);
|
||||
}
|
||||
|
||||
private function processLoop(ws:WebSocket) {
|
||||
while (ws.state != State.Closed) { // TODO: should think about mutex
|
||||
ws.process();
|
||||
Sys.sleep(.01);
|
||||
}
|
||||
}
|
||||
|
||||
@ -422,8 +449,7 @@ class WebSocket extends WebSocketCommon {
|
||||
public function sendHandshake() {
|
||||
var httpRequest = new HttpRequest();
|
||||
httpRequest.method = "GET";
|
||||
// TODO: should propably be hostname+port+path?
|
||||
httpRequest.uri = _path;
|
||||
httpRequest.uri = (_search != null) ? _path + _search : _path;
|
||||
httpRequest.httpVersion = "HTTP/1.1";
|
||||
|
||||
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.ORIGIN, _socket.host().host.toString() + ":" + _socket.host().port);
|
||||
|
||||
if (_protocols != null) {
|
||||
httpRequest.headers.set(HttpHeader.SEC_WEBSOCKET_PROTOCOL, _protocols.join(', '));
|
||||
}
|
||||
|
||||
_encodedKey = generateWSKey();
|
||||
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;
|
||||
state = State.Head;
|
||||
}
|
||||
|
||||
private function generateWSKey():String {
|
||||
var b = Bytes.alloc(16);
|
||||
for (i in 0...16) {
|
||||
b.set(i, Std.random(255));
|
||||
}
|
||||
return Base64.encode(b);
|
||||
return haxe.crypto.Base64.encode(
|
||||
leenkx.network.torrent.Crypto.randomBytes(16));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,6 @@ class WebSocketCommon {
|
||||
|
||||
public function send(data:Any) {
|
||||
if (Std.isOfType(data, String)) {
|
||||
Log.data(data, id);
|
||||
sendFrame(Utf8Encoder.encode(data), OpCode.Text);
|
||||
} else if (Std.isOfType(data, Bytes)) {
|
||||
sendFrame(data, OpCode.Binary);
|
||||
@ -118,7 +117,6 @@ class WebSocketCommon {
|
||||
}
|
||||
} else {
|
||||
var stringPayload = Utf8Encoder.decode(unmaskedMessageData);
|
||||
Log.data(stringPayload, id);
|
||||
if (this.onmessage != null) {
|
||||
this.onmessage(StrMessage(stringPayload));
|
||||
}
|
||||
@ -138,14 +136,12 @@ class WebSocketCommon {
|
||||
case State.Closed:
|
||||
close();
|
||||
case _:
|
||||
trace('State not impl: ${state}');
|
||||
}
|
||||
}
|
||||
|
||||
public function close() {
|
||||
if (state != State.Closed) {
|
||||
try {
|
||||
Log.debug("Closed", id);
|
||||
sendFrame(Bytes.alloc(0), OpCode.Close);
|
||||
state = State.Closed;
|
||||
_socket.close();
|
||||
@ -162,7 +158,6 @@ class WebSocketCommon {
|
||||
_socket.output.write(data);
|
||||
_socket.output.flush();
|
||||
} catch (e:Dynamic) {
|
||||
Log.debug(Std.string(e), id);
|
||||
if (onerror != null) {
|
||||
onerror(Std.string(e));
|
||||
}
|
||||
@ -195,17 +190,24 @@ class WebSocketCommon {
|
||||
}
|
||||
|
||||
private static function generateMask() {
|
||||
var maskData = Bytes.alloc(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;
|
||||
return leenkx.network.torrent.Crypto.randomBytes(4);
|
||||
}
|
||||
|
||||
private static function applyMask(payload:Bytes, mask:Bytes) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -230,7 +232,6 @@ class WebSocketCommon {
|
||||
try {
|
||||
result = SocketImpl.select([_socket], null, null, 0.01);
|
||||
} catch (e:Dynamic) {
|
||||
Log.debug("Error selecting socket: " + e);
|
||||
needClose = true;
|
||||
}
|
||||
|
||||
@ -238,12 +239,11 @@ class WebSocketCommon {
|
||||
if (result.read.length > 0) {
|
||||
try {
|
||||
while (true) {
|
||||
var data = Bytes.alloc(1024);
|
||||
var data = Bytes.alloc(65536);
|
||||
var read = _socket.input.readBytes(data, 0, data.length);
|
||||
if (read <= 0){
|
||||
break;
|
||||
}
|
||||
Log.debug("Bytes read: " + read, id);
|
||||
_buffer.writeBytes(data.sub(0, read));
|
||||
}
|
||||
} catch (e:Dynamic) {
|
||||
@ -259,7 +259,7 @@ class WebSocketCommon {
|
||||
}
|
||||
#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
|
||||
}
|
||||
@ -273,7 +273,6 @@ class WebSocketCommon {
|
||||
if (needClose == true) { // dont want to send the Close frame here
|
||||
if (state != State.Closed) {
|
||||
try {
|
||||
Log.debug("Closed", id);
|
||||
state = State.Closed;
|
||||
_socket.close();
|
||||
} catch (e:Dynamic) { }
|
||||
@ -288,8 +287,6 @@ class WebSocketCommon {
|
||||
public function sendHttpRequest(httpRequest:HttpRequest) {
|
||||
var data = httpRequest.build();
|
||||
|
||||
Log.data(data, id);
|
||||
|
||||
try {
|
||||
_socket.output.write(Bytes.ofString(data));
|
||||
_socket.output.flush();
|
||||
@ -304,8 +301,6 @@ class WebSocketCommon {
|
||||
public function sendHttpResponse(httpResponse:HttpResponse) {
|
||||
var data = httpResponse.build();
|
||||
|
||||
Log.data(data, id);
|
||||
|
||||
_socket.output.write(Bytes.ofString(data));
|
||||
_socket.output.flush();
|
||||
}
|
||||
@ -325,8 +320,6 @@ class WebSocketCommon {
|
||||
httpRequest.addLine(line);
|
||||
}
|
||||
|
||||
Log.data(httpRequest.toString(), id);
|
||||
|
||||
return httpRequest;
|
||||
}
|
||||
|
||||
@ -346,8 +339,6 @@ class WebSocketCommon {
|
||||
|
||||
}
|
||||
|
||||
Log.data(httpResponse.toString(), id);
|
||||
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ class WebSocketHandler extends Handler {
|
||||
_creationTime = Sys.time();
|
||||
#end
|
||||
_socket.setBlocking(false);
|
||||
Log.debug('New socket handler', id);
|
||||
}
|
||||
|
||||
public override function handle() {
|
||||
@ -23,7 +22,6 @@ class WebSocketHandler extends Handler {
|
||||
var currentTime = Sys.time();
|
||||
#end
|
||||
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();
|
||||
return;
|
||||
}
|
||||
@ -70,10 +68,8 @@ class WebSocketHandler extends Handler {
|
||||
httpResponse.headers.set(HttpHeader.CONNECTION, "close");
|
||||
httpResponse.headers.set(HttpHeader.X_WEBSOCKET_REJECT_REASON, 'Unsupported connection header: ${httpRequest.headers.get(HttpHeader.CONNECTION)}.');
|
||||
} else {
|
||||
Log.debug('Handshaking', id);
|
||||
var key = httpRequest.headers.get(HttpHeader.SEC_WEBSOCKET_KEY);
|
||||
var result = makeWSKeyResponse(key);
|
||||
Log.debug('Handshaking key - ${result}', id);
|
||||
|
||||
httpResponse.code = 101;
|
||||
httpResponse.text = "Switching Protocols";
|
||||
@ -82,14 +78,21 @@ class WebSocketHandler extends Handler {
|
||||
httpResponse.headers.set(HttpHeader.SEC_WEBSOSCKET_ACCEPT, result);
|
||||
}
|
||||
|
||||
function callback(httpResponse: HttpResponse) {
|
||||
sendHttpResponse(httpResponse);
|
||||
|
||||
if (httpResponse.code == 101) {
|
||||
_onopenCalled = false;
|
||||
state = State.Head;
|
||||
Log.debug('Connected', id);
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
if (validateHandshake != null) {
|
||||
validateHandshake(httpRequest, httpResponse, callback);
|
||||
} else {
|
||||
callback(httpResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,6 @@ class WebSocketServer
|
||||
_serverSocket.bind(new sys.net.Host(_host), _port);
|
||||
#end
|
||||
_serverSocket.listen(_maxConnections);
|
||||
Log.info('Starting server - ${_host}:${_port} (maxConnections: ${_maxConnections})');
|
||||
|
||||
#if kha_krom
|
||||
kha.Scheduler.addTimeTask(function() {
|
||||
@ -114,7 +113,6 @@ class WebSocketServer
|
||||
var handler = new T(socket);
|
||||
handlers.push(handler);
|
||||
|
||||
Log.debug("Adding to web server handler to list - total: " + handlers.length, handler.id);
|
||||
if (onClientAdded != null) {
|
||||
onClientAdded(handler);
|
||||
}
|
||||
@ -174,7 +172,6 @@ class WebSocketServer
|
||||
|
||||
for (h in toRemove) {
|
||||
handlers.remove(h);
|
||||
Log.debug("Removing web server handler from list - total: " + handlers.length, h.id);
|
||||
if (onClientRemoved != null) {
|
||||
onClientRemoved(h);
|
||||
}
|
||||
@ -186,7 +183,6 @@ class WebSocketServer
|
||||
while (_handlersClosed.length > 0) {
|
||||
var h = _handlersClosed.shift();
|
||||
handlers.remove(h);
|
||||
Log.debug("Removing web server handler from list - total: " + handlers.length, h.id);
|
||||
if (onClientRemoved != null) {
|
||||
onClientRemoved(h);
|
||||
}
|
||||
|
||||
@ -52,8 +52,15 @@ class KromSecureSocket extends KromSocket {
|
||||
}
|
||||
|
||||
override public function connect(host:KromHost, port:Int):Void {
|
||||
setBlocking(true);
|
||||
try {
|
||||
super.connect(host, port);
|
||||
enableSSL();
|
||||
setBlocking(false);
|
||||
} catch (e:Dynamic) {
|
||||
setBlocking(false);
|
||||
trace('Failed Krom Host');
|
||||
}
|
||||
}
|
||||
|
||||
public function isSSLEnabled():Bool {
|
||||
|
||||
@ -43,6 +43,9 @@ class KromSocket {
|
||||
}
|
||||
|
||||
private function setSocketId(id:Int):Void {
|
||||
if (_socketId >= 0) {
|
||||
krom_socket_close(_socketId);
|
||||
}
|
||||
_socketId = id;
|
||||
if (_socketId >= 0) {
|
||||
input = new KromSocketInput(this);
|
||||
@ -247,3 +250,29 @@ class KromHost {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
70
leenkx/Sources/leenkx/network/krom/KromUdpSocket.hx
Normal file
70
leenkx/Sources/leenkx/network/krom/KromUdpSocket.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
209
leenkx/Sources/leenkx/network/torrent/Aegis128x4.hx
Normal file
209
leenkx/Sources/leenkx/network/torrent/Aegis128x4.hx
Normal file
@ -0,0 +1,209 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
class Aegis128x4 {
|
||||
static inline var D = 4;
|
||||
static inline var RATE = 128;
|
||||
|
||||
static var C0 = Bytes.ofHex("000101020305080d1522375990e97962");
|
||||
static var C1 = Bytes.ofHex("db3d18556dc22ff12011314273b528dd");
|
||||
|
||||
var v:Bytes;
|
||||
var nv:Bytes;
|
||||
var t:Bytes;
|
||||
var t2:Bytes;
|
||||
|
||||
public function new(key:Bytes, nonce:Bytes) {
|
||||
v = Bytes.alloc(512);
|
||||
nv = Bytes.alloc(512);
|
||||
t = Bytes.alloc(16);
|
||||
t2 = Bytes.alloc(16);
|
||||
init(key, nonce);
|
||||
}
|
||||
|
||||
static inline function off(j:Int, i:Int):Int {
|
||||
return (j * D + i) * 16;
|
||||
}
|
||||
|
||||
static inline function xtime(x:Int):Int {
|
||||
return ((x << 1) ^ ((x >> 7) * 0x1b)) & 0xff;
|
||||
}
|
||||
|
||||
static function xorInto(a:Bytes, ao:Int, b:Bytes, bo:Int, out:Bytes, oo:Int):Void {
|
||||
for (k in 0...16) out.set(oo + k, a.get(ao + k) ^ b.get(bo + k));
|
||||
}
|
||||
|
||||
function aesRound(inOff:Int, rk:Bytes, rkOff:Int, out:Bytes, outOff:Int):Void {
|
||||
for (c in 0...4) for (r in 0...4)
|
||||
t2.set(4 * c + r, SBOX[v.get(inOff + 4 * ((c + r) & 3) + r)]);
|
||||
for (c in 0...4) {
|
||||
var b0 = t2.get(4 * c);
|
||||
var b1 = t2.get(4 * c + 1);
|
||||
var b2 = t2.get(4 * c + 2);
|
||||
var b3 = t2.get(4 * c + 3);
|
||||
out.set(outOff + 4 * c, (xtime(b0) ^ xtime(b1) ^ b1 ^ b2 ^ b3) ^ rk.get(rkOff + 4 * c));
|
||||
out.set(outOff + 4 * c + 1, (b0 ^ xtime(b1) ^ xtime(b2) ^ b2 ^ b3) ^ rk.get(rkOff + 4 * c + 1));
|
||||
out.set(outOff + 4 * c + 2, (b0 ^ b1 ^ xtime(b2) ^ xtime(b3) ^ b3) ^ rk.get(rkOff + 4 * c + 2));
|
||||
out.set(outOff + 4 * c + 3, (xtime(b0) ^ b0 ^ b1 ^ b2 ^ xtime(b3)) ^ rk.get(rkOff + 4 * c + 3));
|
||||
}
|
||||
}
|
||||
|
||||
function update(m0:Bytes, m0off:Int, m1:Bytes, m1off:Int):Void {
|
||||
for (i in 0...D) {
|
||||
xorInto(v, off(0, i), m0, m0off + i * 16, t, 0);
|
||||
aesRound(off(7, i), t, 0, nv, off(0, i));
|
||||
aesRound(off(0, i), v, off(1, i), nv, off(1, i));
|
||||
aesRound(off(1, i), v, off(2, i), nv, off(2, i));
|
||||
aesRound(off(2, i), v, off(3, i), nv, off(3, i));
|
||||
xorInto(v, off(4, i), m1, m1off + i * 16, t, 0);
|
||||
aesRound(off(3, i), t, 0, nv, off(4, i));
|
||||
aesRound(off(4, i), v, off(5, i), nv, off(5, i));
|
||||
aesRound(off(5, i), v, off(6, i), nv, off(6, i));
|
||||
aesRound(off(6, i), v, off(7, i), nv, off(7, i));
|
||||
}
|
||||
v.blit(0, nv, 0, 512);
|
||||
}
|
||||
|
||||
function init(key:Bytes, nonce:Bytes):Void {
|
||||
var kn = Bytes.alloc(16);
|
||||
var kc0 = Bytes.alloc(16);
|
||||
var kc1 = Bytes.alloc(16);
|
||||
for (k in 0...16) {
|
||||
kn.set(k, key.get(k) ^ nonce.get(k));
|
||||
kc0.set(k, key.get(k) ^ C0.get(k));
|
||||
kc1.set(k, key.get(k) ^ C1.get(k));
|
||||
}
|
||||
for (i in 0...D) {
|
||||
v.blit(off(0, i), kn, 0, 16);
|
||||
v.blit(off(1, i), C1, 0, 16);
|
||||
v.blit(off(2, i), C0, 0, 16);
|
||||
v.blit(off(3, i), C1, 0, 16);
|
||||
v.blit(off(4, i), kn, 0, 16);
|
||||
v.blit(off(5, i), kc0, 0, 16);
|
||||
v.blit(off(6, i), kc1, 0, 16);
|
||||
v.blit(off(7, i), kc0, 0, 16);
|
||||
}
|
||||
var m0 = Bytes.alloc(64);
|
||||
var m1 = Bytes.alloc(64);
|
||||
for (i in 0...D) {
|
||||
m0.blit(i * 16, nonce, 0, 16);
|
||||
m1.blit(i * 16, key, 0, 16);
|
||||
}
|
||||
for (r in 0...10) {
|
||||
for (i in 0...D) {
|
||||
v.set(off(3, i), v.get(off(3, i)) ^ i);
|
||||
v.set(off(3, i) + 1, v.get(off(3, i) + 1) ^ (D - 1));
|
||||
v.set(off(7, i), v.get(off(7, i)) ^ i);
|
||||
v.set(off(7, i) + 1, v.get(off(7, i) + 1) ^ (D - 1));
|
||||
}
|
||||
update(m0, 0, m1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public function absorb(ai:Bytes, aiOff:Int):Void {
|
||||
update(ai, aiOff, ai, aiOff + 64);
|
||||
}
|
||||
|
||||
public function enc(xi:Bytes, xiOff:Int):Bytes {
|
||||
var out = Bytes.alloc(RATE);
|
||||
for (i in 0...D) {
|
||||
for (k in 0...16) {
|
||||
var z0 = v.get(off(1, i) + k) ^ v.get(off(6, i) + k)
|
||||
^ (v.get(off(2, i) + k) & v.get(off(3, i) + k));
|
||||
var z1 = v.get(off(2, i) + k) ^ v.get(off(5, i) + k)
|
||||
^ (v.get(off(6, i) + k) & v.get(off(7, i) + k));
|
||||
out.set(i * 16 + k, xi.get(xiOff + i * 16 + k) ^ z0);
|
||||
out.set(64 + i * 16 + k, xi.get(xiOff + 64 + i * 16 + k) ^ z1);
|
||||
}
|
||||
}
|
||||
update(xi, xiOff, xi, xiOff + 64);
|
||||
return out;
|
||||
}
|
||||
|
||||
public function nextBlock():Bytes {
|
||||
var out = Bytes.alloc(RATE);
|
||||
for (i in 0...D) {
|
||||
for (k in 0...16) {
|
||||
out.set(i * 16 + k, v.get(off(1, i) + k) ^ v.get(off(6, i) + k)
|
||||
^ (v.get(off(2, i) + k) & v.get(off(3, i) + k)));
|
||||
out.set(64 + i * 16 + k, v.get(off(2, i) + k) ^ v.get(off(5, i) + k)
|
||||
^ (v.get(off(6, i) + k) & v.get(off(7, i) + k)));
|
||||
}
|
||||
}
|
||||
for (i in 0...D) {
|
||||
aesRound(off(7, i), v, off(0, i), nv, off(0, i));
|
||||
aesRound(off(0, i), v, off(1, i), nv, off(1, i));
|
||||
aesRound(off(1, i), v, off(2, i), nv, off(2, i));
|
||||
aesRound(off(2, i), v, off(3, i), nv, off(3, i));
|
||||
aesRound(off(3, i), v, off(4, i), nv, off(4, i));
|
||||
aesRound(off(4, i), v, off(5, i), nv, off(5, i));
|
||||
aesRound(off(5, i), v, off(6, i), nv, off(6, i));
|
||||
aesRound(off(6, i), v, off(7, i), nv, off(7, i));
|
||||
}
|
||||
v.blit(0, nv, 0, 512);
|
||||
return out;
|
||||
}
|
||||
|
||||
public static function selfTest():Bool {
|
||||
var key = Bytes.ofHex("000102030405060708090a0b0c0d0e0f");
|
||||
var nonce = Bytes.ofHex("101112131415161718191a1b1c1d1e1f");
|
||||
var a = new Aegis128x4(key, nonce);
|
||||
var expected = Bytes.ofHex(
|
||||
"924eb07635003a37e6c6575ba8ce1929c8b6a5d91475445e936d48e794be0ce2" +
|
||||
"fcd37d050e24084befe3bbb219d647602e9f58cfb893a8800220242c373a8b18" +
|
||||
"1a1f60c4fab64e5471dc72edfcf6fe6bc1e525ebea2d6375a9edd045dce96381" +
|
||||
"97a3e25abd228a44d4a14a6d3fe9185cc2d4cf7f4287a98744645674265d4ca8" +
|
||||
"7bb50c534f6ec4780530ff1cce8a16e87b08d57557da0b5ef7b5f7d98b0ba189" +
|
||||
"6bfcac34ddb68404821a4d665303cb0fd95626f6dfad1aed7467622c38529932" +
|
||||
"af339fd2d50ee45fc47665c647cf6586d0669b39d140f0e118a4a511efe2f95a" +
|
||||
"7a94330f35c194fadda2a87e42cdeccc233b640d1f4d56e2757e72c1a9d8ecb1" +
|
||||
"9f93737d699ba05c11e94f2b201bef5e61caf387cf7cfd3f8300ac7680ccfd76" +
|
||||
"5825a671ecef03b7a9c98a601ae3211587a1fe4d558161a8f4c38731f3223032" +
|
||||
"7a5aca78d636c05bbc702b2980196ab6915d868408495d07eb527789f282c575" +
|
||||
"d0947bfbc1d3309cdffc9be1503aea628834ea57a15b9fbdc0245464a4b8cbef" +
|
||||
"e46f4cf71a95ac45b6f0823e3aba1a868c4ecef682fc44a8eba911b3fc7d99f9" +
|
||||
"a4fb61e2c928a2ca760b8772f2ea5f2e3d34ea89da73caa3016c280500a155a3" +
|
||||
"85075f0080e9d618e7eb40f57c32d9f7d2ab2b320c6e93b155a3787cb83e5281" +
|
||||
"0b3af0250ae36831a1b072e499929bcb5cce4d00329d69f1aae36aa541347512");
|
||||
if (expected.compare(a.v) != 0) {
|
||||
return false;
|
||||
}
|
||||
var ad = Bytes.alloc(128);
|
||||
for (k in 0...8) ad.set(k, [1, 2, 3, 4][k & 3]);
|
||||
a.absorb(ad, 0);
|
||||
var msg = Bytes.alloc(128);
|
||||
for (k in 0...120) msg.set(k, [4, 5, 6, 7][k & 3]);
|
||||
var ct = a.enc(msg, 0);
|
||||
var expectedCt = Bytes.ofHex(
|
||||
"e836118562f4479c9d35c17356a833114c21f9aa39e4dda5e5c87f4152a00fce" +
|
||||
"9a7c38f832eafe8b1c12f8a7cf12a81a1ad8a9c24ba9dedfbdaa586ffea67ddc" +
|
||||
"801ea97d9ab4a872f42d0e352e2713dacd609f9442c17517c5a29daf3e2a3fac" +
|
||||
"4ff6b1380c4e46df7b086af6ce6bc1ed594b8dd64aed2a7e");
|
||||
for (k in 0...120) {
|
||||
if (ct.get(k) != expectedCt.get(k)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static var SBOX = [
|
||||
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
|
||||
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
|
||||
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
|
||||
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
|
||||
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
|
||||
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
|
||||
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
|
||||
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
|
||||
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
|
||||
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
|
||||
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
|
||||
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
|
||||
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
|
||||
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
|
||||
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
|
||||
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
|
||||
];
|
||||
}
|
||||
119
leenkx/Sources/leenkx/network/torrent/Base58.hx
Normal file
119
leenkx/Sources/leenkx/network/torrent/Base58.hx
Normal file
@ -0,0 +1,119 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
class Base58 {
|
||||
|
||||
static var ALPHABET:String = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
static var ALPHABET_MAP:Map<Int, Int> = {
|
||||
var m = new Map<Int, Int>();
|
||||
for (i in 0...ALPHABET.length) {
|
||||
m.set(ALPHABET.charCodeAt(i), i);
|
||||
}
|
||||
m;
|
||||
};
|
||||
|
||||
public static function encode(bytes:Bytes):String {
|
||||
if (bytes.length == 0) return "";
|
||||
|
||||
var zeros = 0;
|
||||
while (zeros < bytes.length && bytes.get(zeros) == 0) zeros++;
|
||||
|
||||
var size = Math.ceil(bytes.length * 138 / 100 + 1);
|
||||
var b58 = [for (i in 0...size) 0];
|
||||
|
||||
for (i in zeros...bytes.length) {
|
||||
var carry = bytes.get(i);
|
||||
for (j in 0...size) {
|
||||
var idx = size - 1 - j;
|
||||
carry += b58[idx] << 8;
|
||||
b58[idx] = carry % 58;
|
||||
carry = Std.int(carry / 58);
|
||||
}
|
||||
}
|
||||
|
||||
var leadingZeros = 0;
|
||||
for (i in 0...size) {
|
||||
if (b58[i] != 0) break;
|
||||
leadingZeros++;
|
||||
}
|
||||
|
||||
var sb = new StringBuf();
|
||||
for (i in 0...zeros) sb.addChar(ALPHABET.charCodeAt(0));
|
||||
for (i in leadingZeros...size) {
|
||||
sb.addChar(ALPHABET.charCodeAt(b58[i]));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static function decode(str:String):Bytes {
|
||||
if (str.length == 0) return Bytes.alloc(0);
|
||||
|
||||
var zeros = 0;
|
||||
while (zeros < str.length && str.charCodeAt(zeros) == ALPHABET.charCodeAt(0)) zeros++;
|
||||
|
||||
var size = Math.ceil(str.length * 733 / 1000 + 1);
|
||||
var bytes = [for (i in 0...size) 0];
|
||||
|
||||
for (i in zeros...str.length) {
|
||||
var ch = str.charCodeAt(i);
|
||||
if (!ALPHABET_MAP.exists(ch)) {
|
||||
throw "Base58: invalid character " + str.charAt(i);
|
||||
}
|
||||
var carry = ALPHABET_MAP.get(ch);
|
||||
for (j in 0...size) {
|
||||
var idx = size - 1 - j;
|
||||
carry += bytes[idx] * 58;
|
||||
bytes[idx] = carry & 0xff;
|
||||
carry = carry >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
var leadingZeros = 0;
|
||||
for (i in 0...size) {
|
||||
if (bytes[i] != 0) break;
|
||||
leadingZeros++;
|
||||
}
|
||||
|
||||
var result = Bytes.alloc(zeros + size - leadingZeros);
|
||||
for (i in 0...zeros) result.set(i, 0);
|
||||
for (i in 0...(size - leadingZeros)) {
|
||||
result.set(zeros + i, bytes[leadingZeros + i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class Base58Check {
|
||||
|
||||
static var CHECKSUM_LEN = 4;
|
||||
|
||||
public static function encode(bytes:Bytes):String {
|
||||
var hash = sha256sha256(bytes);
|
||||
var payload = Bytes.alloc(bytes.length + CHECKSUM_LEN);
|
||||
payload.blit(0, bytes, 0, bytes.length);
|
||||
payload.blit(bytes.length, hash, 0, CHECKSUM_LEN);
|
||||
return Base58.encode(payload);
|
||||
}
|
||||
|
||||
public static function decode(str:String):Bytes {
|
||||
var payload = Base58.decode(str);
|
||||
if (payload.length < CHECKSUM_LEN) {
|
||||
throw "Base58Check: too short";
|
||||
}
|
||||
var data = payload.sub(0, payload.length - CHECKSUM_LEN);
|
||||
var checksum = payload.sub(payload.length - CHECKSUM_LEN, CHECKSUM_LEN);
|
||||
var hash = sha256sha256(data);
|
||||
for (i in 0...CHECKSUM_LEN) {
|
||||
if (hash.get(i) != checksum.get(i)) {
|
||||
throw "Base58Check: checksum mismatch";
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
static function sha256sha256(bytes:Bytes):Bytes {
|
||||
var h1 = haxe.crypto.Sha256.make(bytes);
|
||||
return haxe.crypto.Sha256.make(h1);
|
||||
}
|
||||
}
|
||||
182
leenkx/Sources/leenkx/network/torrent/Bencode.hx
Normal file
182
leenkx/Sources/leenkx/network/torrent/Bencode.hx
Normal file
@ -0,0 +1,182 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
class RawDict {
|
||||
public var pairs:Array<{key:Bytes, val:Dynamic}> = [];
|
||||
public function new() {}
|
||||
}
|
||||
|
||||
class Bencode {
|
||||
|
||||
public static function encode(value:Dynamic):Bytes {
|
||||
var out = new haxe.io.BytesOutput();
|
||||
encodeValue(out, value);
|
||||
return out.getBytes();
|
||||
}
|
||||
|
||||
public static function encodeToString(value:Dynamic):String {
|
||||
return encode(value).toString();
|
||||
}
|
||||
|
||||
static function encodeValue(out:haxe.io.BytesOutput, value:Dynamic):Void {
|
||||
if (Std.isOfType(value, Bool)) {
|
||||
encodeInt(out, value ? 1 : 0);
|
||||
} else if (Std.isOfType(value, Int) || Std.isOfType(value, Float)) {
|
||||
encodeInt(out, value);
|
||||
} else if (Std.isOfType(value, String)) {
|
||||
encodeString(out, value);
|
||||
} else if (Std.isOfType(value, Bytes)) {
|
||||
encodeBytes(out, value);
|
||||
} else if (Std.isOfType(value, RawDict)) {
|
||||
encodeRawDict(out, value);
|
||||
} else if (Std.isOfType(value, Array)) {
|
||||
encodeList(out, value);
|
||||
} else if (value != null && Reflect.isObject(value)) {
|
||||
encodeDict(out, value);
|
||||
} else {
|
||||
throw "Bencode: cannot encode " + Type.typeof(value);
|
||||
}
|
||||
}
|
||||
|
||||
static function encodeInt(out:haxe.io.BytesOutput, value:Float):Void {
|
||||
out.writeString("i");
|
||||
var s = Std.string(value);
|
||||
var dot = s.indexOf(".");
|
||||
if (dot != -1) s = s.substring(0, dot);
|
||||
if (s.indexOf("e") != -1 || s.indexOf("E") != -1) {
|
||||
s = Std.string(Math.floor(value));
|
||||
dot = s.indexOf(".");
|
||||
if (dot != -1) s = s.substring(0, dot);
|
||||
}
|
||||
out.writeString(s);
|
||||
out.writeString("e");
|
||||
}
|
||||
|
||||
static function encodeString(out:haxe.io.BytesOutput, value:String):Void {
|
||||
var bytes = Bytes.ofString(value);
|
||||
encodeBytes(out, bytes);
|
||||
}
|
||||
|
||||
static function encodeBytes(out:haxe.io.BytesOutput, bytes:Bytes):Void {
|
||||
out.writeString(Std.string(bytes.length));
|
||||
out.writeString(":");
|
||||
out.writeBytes(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
static function encodeRawDict(out:haxe.io.BytesOutput, dict:RawDict):Void {
|
||||
out.writeString("d");
|
||||
for (p in dict.pairs) {
|
||||
encodeBytes(out, p.key);
|
||||
encodeValue(out, p.val);
|
||||
}
|
||||
out.writeString("e");
|
||||
}
|
||||
|
||||
static function encodeList(out:haxe.io.BytesOutput, arr:Array<Dynamic>):Void {
|
||||
out.writeString("l");
|
||||
for (item in arr) {
|
||||
encodeValue(out, item);
|
||||
}
|
||||
out.writeString("e");
|
||||
}
|
||||
|
||||
static function encodeDict(out:haxe.io.BytesOutput, obj:Dynamic):Void {
|
||||
out.writeString("d");
|
||||
var keys = Reflect.fields(obj);
|
||||
keys.sort(Reflect.compare);
|
||||
for (key in keys) {
|
||||
if (Reflect.field(obj, key) == null) continue;
|
||||
encodeString(out, key);
|
||||
encodeValue(out, Reflect.field(obj, key));
|
||||
}
|
||||
out.writeString("e");
|
||||
}
|
||||
|
||||
public static function decode(bytes:Bytes):Dynamic {
|
||||
return new Decoder(bytes).value();
|
||||
}
|
||||
|
||||
public static function decodeString(str:String):Dynamic {
|
||||
return decode(Bytes.ofString(str));
|
||||
}
|
||||
}
|
||||
|
||||
private class Decoder {
|
||||
static inline var MAX_DECODE_LEN = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
var data:Bytes;
|
||||
var pos:Int = 0;
|
||||
|
||||
public function new(data:Bytes) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public function value():Dynamic {
|
||||
if (pos >= data.length) throw "Bencode: truncated input";
|
||||
var b = data.get(pos++);
|
||||
switch (b) {
|
||||
case 'i'.code:
|
||||
return decodeInt();
|
||||
case 'l'.code:
|
||||
return decodeList();
|
||||
case 'd'.code:
|
||||
return decodeDict();
|
||||
default:
|
||||
return decodeBytes();
|
||||
}
|
||||
}
|
||||
|
||||
function decodeInt():Float {
|
||||
var start = pos;
|
||||
while (pos < data.length && data.get(pos) != 'e'.code) pos++;
|
||||
if (pos >= data.length) throw "Bencode: unterminated int";
|
||||
var s = data.getString(start, pos - start);
|
||||
pos++;
|
||||
var v = Std.parseFloat(s);
|
||||
if (Math.isNaN(v)) throw "Bencode: bad int " + s;
|
||||
return v;
|
||||
}
|
||||
|
||||
function decodeBytes():Bytes {
|
||||
var start = pos - 1;
|
||||
if (data.get(start) < '0'.code || data.get(start) > '9'.code) {
|
||||
throw "Bencode: bad byte length";
|
||||
}
|
||||
while (pos < data.length && data.get(pos) != ':'.code) pos++;
|
||||
if (pos >= data.length) throw "Bencode: unterminated length";
|
||||
var len = Std.parseInt(data.getString(start, pos - start));
|
||||
pos++;
|
||||
if (len == null || len < 0 || len > MAX_DECODE_LEN
|
||||
|| pos + len > data.length) {
|
||||
throw "Bencode: invalid byte length " + len;
|
||||
}
|
||||
var bytes = Bytes.alloc(len);
|
||||
bytes.blit(0, data, pos, len);
|
||||
pos += len;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function decodeList():Array<Dynamic> {
|
||||
var arr:Array<Dynamic> = [];
|
||||
while (true) {
|
||||
if (pos >= data.length) throw "Bencode: unterminated list";
|
||||
if (data.get(pos) == 'e'.code) break;
|
||||
arr.push(value());
|
||||
}
|
||||
pos++;
|
||||
return arr;
|
||||
}
|
||||
|
||||
function decodeDict():Dynamic {
|
||||
var obj:Dynamic = {};
|
||||
while (true) {
|
||||
if (pos >= data.length) throw "Bencode: unterminated dict";
|
||||
if (data.get(pos) == 'e'.code) break;
|
||||
var key:Bytes = value();
|
||||
Reflect.setField(obj, key.toString(), value());
|
||||
}
|
||||
pos++;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
152
leenkx/Sources/leenkx/network/torrent/Crypto.hx
Normal file
152
leenkx/Sources/leenkx/network/torrent/Crypto.hx
Normal file
@ -0,0 +1,152 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
import leenkx.network.torrent.Base58.Base58Check;
|
||||
|
||||
class Crypto {
|
||||
|
||||
public static var SEEDPREFIX(default, null) = "490a";
|
||||
public static var ADDRESSPREFIX(default, null) = "55";
|
||||
|
||||
static var drbg:Aegis128x4 = null;
|
||||
static var drbgBuf:Bytes = null;
|
||||
static var drbgPos:Int = 0;
|
||||
static var drbgNative:Int = -1;
|
||||
|
||||
public static function randomBytes(length:Int):Bytes {
|
||||
var bytes = Bytes.alloc(length);
|
||||
#if (js && !kha_krom)
|
||||
var webCrypto:Dynamic = js.Syntax.code("(typeof globalThis !== 'undefined' ? globalThis.crypto : null)");
|
||||
if (webCrypto != null && webCrypto.getRandomValues != null) {
|
||||
var view = new js.lib.Uint8Array(length);
|
||||
webCrypto.getRandomValues(view);
|
||||
for (i in 0...length) bytes.set(i, view[i]);
|
||||
return bytes;
|
||||
}
|
||||
#end
|
||||
#if lnx_datachannel
|
||||
if (drbgNative == -1) {
|
||||
var seed = osEntropy(32);
|
||||
try {
|
||||
drbgNative = datachannel.RTC.aegisAvailable()
|
||||
&& datachannel.RTC.aegisInit(seed.sub(0, 16), seed.sub(16, 16)) ? 1 : 0;
|
||||
} catch (e:Dynamic) {
|
||||
drbgNative = 0;
|
||||
}
|
||||
}
|
||||
if (drbgNative == 1) {
|
||||
return datachannel.RTC.aegisRandom(length);
|
||||
}
|
||||
#end
|
||||
if (drbg == null) {
|
||||
var seed = osEntropy(32);
|
||||
drbg = new Aegis128x4(seed.sub(0, 16), seed.sub(16, 16));
|
||||
drbgBuf = Bytes.alloc(0);
|
||||
drbgPos = 0;
|
||||
}
|
||||
var off = 0;
|
||||
while (off < length) {
|
||||
if (drbgPos >= drbgBuf.length) {
|
||||
drbgBuf = drbg.nextBlock();
|
||||
drbgPos = 0;
|
||||
}
|
||||
var n = Std.int(Math.min(drbgBuf.length - drbgPos, length - off));
|
||||
bytes.blit(off, drbgBuf, drbgPos, n);
|
||||
drbgPos += n;
|
||||
off += n;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static function osEntropy(length:Int):Bytes {
|
||||
var bytes = Bytes.alloc(length);
|
||||
#if kha_krom
|
||||
var kromBytes = leenkx.network.torrent.transport.RtcNative.randomBytes(length);
|
||||
if (kromBytes != null) return kromBytes;
|
||||
#end
|
||||
#if sys
|
||||
if (Sys.systemName() != "Windows") {
|
||||
try {
|
||||
var f = sys.io.File.read("/dev/urandom", true);
|
||||
f.readBytes(bytes, 0, length);
|
||||
f.close();
|
||||
return bytes;
|
||||
} catch(e:Dynamic) {}
|
||||
} else {
|
||||
try {
|
||||
var p = new sys.io.Process("powershell", [
|
||||
"-NoProfile", "-Command",
|
||||
"$r = New-Object 'byte[]' " + length + "; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($r); -join ($r | ForEach-Object { $_.ToString('x2') })"
|
||||
]);
|
||||
var hex = StringTools.trim(p.stdout.readAll().toString());
|
||||
p.close();
|
||||
if (hex.length == length * 2) {
|
||||
return Bytes.ofHex(hex);
|
||||
}
|
||||
} catch(e:Dynamic) {
|
||||
trace("Windows RNG via PowerShell failed: " + e);
|
||||
}
|
||||
}
|
||||
#end
|
||||
throw "Crypto: OS entropy unavailable. Refusing to generate predictable keys.";
|
||||
}
|
||||
|
||||
public static function hash(data:Bytes):Bytes {
|
||||
return TweetNaCl.hash(data);
|
||||
}
|
||||
|
||||
public static function signDetached(message:Bytes, secretKey:Bytes):Bytes {
|
||||
return TweetNaCl.signDetached(message, secretKey);
|
||||
}
|
||||
|
||||
public static function signDetachedVerify(message:Bytes, signature:Bytes, publicKey:Bytes):Bool {
|
||||
return TweetNaCl.signDetachedVerify(message, signature, publicKey);
|
||||
}
|
||||
|
||||
public static function box(message:Bytes, nonce:Bytes, theirPublicKey:Bytes, mySecretKey:Bytes):Bytes {
|
||||
return TweetNaCl.box(message, nonce, theirPublicKey, mySecretKey);
|
||||
}
|
||||
|
||||
public static function boxOpen(message:Bytes, nonce:Bytes, theirPublicKey:Bytes, mySecretKey:Bytes):Bytes {
|
||||
return TweetNaCl.boxOpen(message, nonce, theirPublicKey, mySecretKey);
|
||||
}
|
||||
|
||||
public static function boxKeyPair():{publicKey:Bytes, secretKey:Bytes} {
|
||||
return TweetNaCl.boxKeyPair();
|
||||
}
|
||||
|
||||
public static function signKeyPairFromSeed(seed:Bytes):{publicKey:Bytes, secretKey:Bytes} {
|
||||
return TweetNaCl.signKeyPairFromSeed(seed);
|
||||
}
|
||||
|
||||
public static var boxNonceLength(get, null):Int;
|
||||
static function get_boxNonceLength():Int {
|
||||
return TweetNaCl.crypto_box_NONCEBYTES;
|
||||
}
|
||||
|
||||
public static function ripemd160(data:Bytes):Bytes {
|
||||
return Ripemd160.make(data);
|
||||
}
|
||||
|
||||
public static function encodeSeed(material:Bytes):String {
|
||||
var prefix = Bytes.ofHex(SEEDPREFIX);
|
||||
var payload = Bytes.alloc(prefix.length + material.length);
|
||||
payload.blit(0, prefix, 0, prefix.length);
|
||||
payload.blit(prefix.length, material, 0, material.length);
|
||||
return Base58Check.encode(payload);
|
||||
}
|
||||
|
||||
public static function encodeAddress(publicKey:Bytes):String {
|
||||
var prefix = Bytes.ofHex(ADDRESSPREFIX);
|
||||
var hash = hash(publicKey);
|
||||
var ripemd = ripemd160(hash);
|
||||
var payload = Bytes.alloc(prefix.length + ripemd.length);
|
||||
payload.blit(0, prefix, 0, prefix.length);
|
||||
payload.blit(prefix.length, ripemd, 0, ripemd.length);
|
||||
return Base58Check.encode(payload);
|
||||
}
|
||||
|
||||
public static function toHex(bytes:Bytes):String {
|
||||
return bytes.toHex();
|
||||
}
|
||||
}
|
||||
21
leenkx/Sources/leenkx/network/torrent/LICENSE.md
Normal file
21
leenkx/Sources/leenkx/network/torrent/LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Riccardo Mazzucco - All Rights reserved
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
576
leenkx/Sources/leenkx/network/torrent/LeenkxClient.hx
Normal file
576
leenkx/Sources/leenkx/network/torrent/LeenkxClient.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
115
leenkx/Sources/leenkx/network/torrent/Magnet.hx
Normal file
115
leenkx/Sources/leenkx/network/torrent/Magnet.hx
Normal file
@ -0,0 +1,115 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
class Magnet {
|
||||
public var infoHash:Bytes;
|
||||
public var infoHashHex:String;
|
||||
public var infoHashV2:Bytes;
|
||||
public var infoHashV2Hex:String;
|
||||
public var name:String = null;
|
||||
public var trackers:Array<String> = [];
|
||||
public var webSeeds:Array<String> = [];
|
||||
|
||||
public function new() {}
|
||||
|
||||
public static function parse(uri:String):Magnet {
|
||||
if (uri.indexOf("magnet:?") != 0) return null;
|
||||
var m = new Magnet();
|
||||
var query = uri.substr(8);
|
||||
for (param in query.split("&")) {
|
||||
var eq = param.indexOf("=");
|
||||
if (eq < 0) continue;
|
||||
var key = param.substr(0, eq);
|
||||
var value = StringTools.urlDecode(param.substr(eq + 1));
|
||||
switch (key) {
|
||||
case "xt":
|
||||
if (value.indexOf("urn:btih:") == 0) {
|
||||
var hash = value.substr(9);
|
||||
m.infoHash = decodeBtih(hash);
|
||||
if (m.infoHash != null) {
|
||||
m.infoHashHex = Crypto.toHex(m.infoHash);
|
||||
}
|
||||
} else if (value.indexOf("urn:btmh:") == 0) {
|
||||
var hash = value.substr(9);
|
||||
if (hash.length == 68) hash = hash.substr(4);
|
||||
if (hash.length == 64) {
|
||||
try {
|
||||
m.infoHashV2 = Bytes.ofHex(hash);
|
||||
} catch(e:Dynamic) {}
|
||||
if (m.infoHashV2 != null) {
|
||||
m.infoHashV2Hex =
|
||||
Crypto.toHex(m.infoHashV2);
|
||||
}
|
||||
}
|
||||
}
|
||||
case "dn":
|
||||
m.name = value;
|
||||
case "tr":
|
||||
m.trackers.push(value);
|
||||
case "ws":
|
||||
m.webSeeds.push(value);
|
||||
default:
|
||||
}
|
||||
}
|
||||
if (m.infoHash == null && m.infoHashV2 == null) return null;
|
||||
return m;
|
||||
}
|
||||
|
||||
public static function build(infoHash:Bytes, name:String, trackers:Array<String>, ?webSeeds:Array<String>, ?infoHashV2:Bytes):String {
|
||||
var parts:Array<String> = [];
|
||||
if (infoHash != null) {
|
||||
parts.push("xt=urn:btih:" + Crypto.toHex(infoHash));
|
||||
}
|
||||
if (infoHashV2 != null) {
|
||||
parts.push("xt=urn:btmh:1220"
|
||||
+ Crypto.toHex(infoHashV2));
|
||||
}
|
||||
if (name != null) {
|
||||
parts.push("dn=" + StringTools.urlEncode(name));
|
||||
}
|
||||
if (trackers != null) {
|
||||
for (t in trackers) {
|
||||
parts.push("tr=" + StringTools.urlEncode(t));
|
||||
}
|
||||
}
|
||||
if (webSeeds != null) {
|
||||
for (w in webSeeds) {
|
||||
parts.push("ws=" + StringTools.urlEncode(w));
|
||||
}
|
||||
}
|
||||
return "magnet:?" + parts.join("&");
|
||||
}
|
||||
|
||||
static function decodeBtih(s:String):Bytes {
|
||||
if (s.length == 40) {
|
||||
try {
|
||||
return Bytes.ofHex(s);
|
||||
} catch(e:Dynamic) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (s.length == 32) {
|
||||
return base32Decode(s.toUpperCase());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static function base32Decode(s:String):Bytes {
|
||||
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
var out = new haxe.io.BytesBuffer();
|
||||
var buffer = 0;
|
||||
var bits = 0;
|
||||
for (i in 0...s.length) {
|
||||
var v = alphabet.indexOf(s.charAt(i));
|
||||
if (v < 0) return null;
|
||||
buffer = (buffer << 5) | v;
|
||||
bits += 5;
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out.addByte((buffer >> bits) & 0xFF);
|
||||
}
|
||||
}
|
||||
return out.getBytes();
|
||||
}
|
||||
}
|
||||
711
leenkx/Sources/leenkx/network/torrent/MetaInfo.hx
Normal file
711
leenkx/Sources/leenkx/network/torrent/MetaInfo.hx
Normal file
@ -0,0 +1,711 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
typedef TorrentFileEntry = {
|
||||
var path:String;
|
||||
var length:Int;
|
||||
var offset:Int;
|
||||
@:optional var piecesRoot:Bytes;
|
||||
}
|
||||
|
||||
class MetaInfo {
|
||||
public var infoHash:Bytes;
|
||||
public var infoHashHex:String;
|
||||
public var infoHashV2:Bytes = null;
|
||||
public var infoHashV2Hex:String = null;
|
||||
public var metaVersion:Int = 1;
|
||||
public var name:String;
|
||||
public var pieceLength:Int;
|
||||
public var pieces:Bytes;
|
||||
public var files:Array<TorrentFileEntry>;
|
||||
public var totalLength:Int = 0;
|
||||
public var announce:Array<String>;
|
||||
public var rawInfo:Bytes;
|
||||
public var isPrivate:Bool = false;
|
||||
public var comment:String = null;
|
||||
public var createdBy:String = null;
|
||||
public var creationDate:Int = 0;
|
||||
public var urlList:Array<String> = null;
|
||||
public var source:String = null;
|
||||
public var fileHashes:Array<Array<Bytes>> = null;
|
||||
public var storageLength:Int = 0;
|
||||
public var pieceLayers:Array<{root:Bytes, hashes:Bytes}> = null;
|
||||
|
||||
public var numPieces(get, never):Int;
|
||||
function get_numPieces():Int {
|
||||
if (pieces != null && pieces.length > 0) {
|
||||
return Std.int(pieces.length / TorrentInfo.HASH_LENGTH);
|
||||
}
|
||||
if (pieceLength > 0 && files != null) {
|
||||
var end = 0;
|
||||
for (f in files) {
|
||||
var e = f.offset + f.length;
|
||||
if (e > end) end = e;
|
||||
}
|
||||
return end > 0
|
||||
? Std.int(Math.ceil(end / pieceLength)) : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public var protocolVersion(get, never):Int;
|
||||
function get_protocolVersion():Int {
|
||||
return metaVersion == 2 ? (pieces != null ? 3 : 2) : 1;
|
||||
}
|
||||
|
||||
public function new() {}
|
||||
|
||||
public static function fromTorrentBytes(data:Bytes):MetaInfo {
|
||||
var dict:Dynamic = Bencode.decode(data);
|
||||
if (dict == null || Reflect.field(dict, "info") == null) {
|
||||
throw "MetaInfo: no info dict";
|
||||
}
|
||||
var m = new MetaInfo();
|
||||
m.rawInfo = extractInfoBytes(data);
|
||||
var info = Reflect.field(dict, "info");
|
||||
m.parseInfo(info);
|
||||
if (m.metaVersion == 2) {
|
||||
m.infoHashV2 = haxe.crypto.Sha256.make(m.rawInfo);
|
||||
m.infoHashV2Hex = Crypto.toHex(m.infoHashV2);
|
||||
}
|
||||
if (m.pieces != null) {
|
||||
m.infoHash = haxe.crypto.Sha1.make(m.rawInfo);
|
||||
m.infoHashHex = Crypto.toHex(m.infoHash);
|
||||
}
|
||||
m.announce = parseAnnounce(dict);
|
||||
m.initFileHashes();
|
||||
m.loadPieceLayers(data);
|
||||
m.comment = bytesToString(Reflect.field(dict, "comment"));
|
||||
m.createdBy = bytesToString(Reflect.field(dict, "created by"));
|
||||
m.creationDate = Std.int(fieldFloat(dict, "creation date"));
|
||||
var ul = Reflect.field(dict, "url-list");
|
||||
if (Std.isOfType(ul, Array)) {
|
||||
m.urlList = [for (u in (ul:Array<Dynamic>))
|
||||
bytesToString(u)];
|
||||
} else if (ul != null) {
|
||||
m.urlList = [bytesToString(ul)];
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
function parseInfo(info:Dynamic):Void {
|
||||
name = bytesToString(Reflect.field(info, "name"));
|
||||
if (name == null) name = "";
|
||||
pieceLength = Std.int(fieldFloat(info, "piece length"));
|
||||
metaVersion = Std.int(fieldFloat(info, "meta version"));
|
||||
if (metaVersion == 0) metaVersion = 1;
|
||||
var piecesField = Reflect.field(info, "pieces");
|
||||
if (piecesField == null) {
|
||||
pieces = null;
|
||||
} else {
|
||||
pieces = Std.isOfType(piecesField, Bytes) ? piecesField
|
||||
: Bytes.ofString(Std.string(piecesField));
|
||||
}
|
||||
isPrivate = fieldFloat(info, "private") == 1;
|
||||
source = bytesToString(Reflect.field(info, "source"));
|
||||
|
||||
files = [];
|
||||
totalLength = 0;
|
||||
var fileTree = Reflect.field(info, "file tree");
|
||||
if (fileTree != null) {
|
||||
walkFileTree(fileTree, [name]);
|
||||
if (pieces != null) {
|
||||
applyV1Layout(Reflect.field(info, "files"));
|
||||
}
|
||||
storageLength = pieces == null
|
||||
? treeCursor * pieceLength : totalLength;
|
||||
return;
|
||||
}
|
||||
storageLength = 0;
|
||||
var filesList:Array<Dynamic> = Reflect.field(info, "files");
|
||||
if (filesList != null) {
|
||||
for (f in filesList) {
|
||||
var len = Std.int(fieldFloat(f, "length"));
|
||||
var pathList:Array<Dynamic> = Reflect.field(f, "path");
|
||||
var path = name;
|
||||
if (pathList != null) {
|
||||
var parts = [for (p in pathList) bytesToString(p)];
|
||||
path = name + "/" + parts.join("/");
|
||||
}
|
||||
files.push({path: sanitizePath(path), length: len,
|
||||
offset: totalLength});
|
||||
totalLength += len;
|
||||
}
|
||||
} else {
|
||||
var len = Std.int(fieldFloat(info, "length"));
|
||||
files.push({path: sanitizePath(name), length: len,
|
||||
offset: 0});
|
||||
totalLength = len;
|
||||
}
|
||||
storageLength = totalLength;
|
||||
}
|
||||
|
||||
public static function sanitizeComponent(c:String):String {
|
||||
if (c == null || c == "" || c == "." || c == ".."
|
||||
|| c.indexOf("/") >= 0 || c.indexOf("\\") >= 0
|
||||
|| c.indexOf(":") >= 0
|
||||
|| c.indexOf(String.fromCharCode(0)) >= 0) {
|
||||
return "_";
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
public static function sanitizePath(p:String):String {
|
||||
if (p == null) return "_";
|
||||
var parts = p.split("/");
|
||||
return [for (c in parts) sanitizeComponent(c)].join("/");
|
||||
}
|
||||
|
||||
public function fileForPiece(index:Int):Int {
|
||||
for (i in 0...files.length) {
|
||||
var f = files[i];
|
||||
var start = Std.int(f.offset / pieceLength);
|
||||
var np = Std.int(Math.ceil(f.length / pieceLength));
|
||||
if (index >= start && index < start + np) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public function pieceSizeV2(index:Int):Int {
|
||||
var fi = fileForPiece(index);
|
||||
if (fi < 0) return 0;
|
||||
var f = files[fi];
|
||||
var start = Std.int(f.offset / pieceLength);
|
||||
var rem = f.length - (index - start) * pieceLength;
|
||||
return Std.int(Math.min(pieceLength, rem));
|
||||
}
|
||||
|
||||
public function verifyPieceV2(index:Int, data:Bytes):Bool {
|
||||
var fi = fileForPiece(index);
|
||||
if (fi < 0 || fileHashes == null
|
||||
|| fileHashes.length <= fi || fileHashes[fi] == null) {
|
||||
return false;
|
||||
}
|
||||
var f = files[fi];
|
||||
var start = Std.int(f.offset / pieceLength);
|
||||
var expected = fileHashes[fi][index - start];
|
||||
if (expected == null) return false;
|
||||
var actual = f.length <= pieceLength
|
||||
? merkleRoot(data) : pieceRootV2(data);
|
||||
return memcmp(actual, expected);
|
||||
}
|
||||
|
||||
public function pieceRootV2(data:Bytes):Bytes {
|
||||
var base = Std.int(Math.log(pieceLength / 16384)
|
||||
/ Math.log(2));
|
||||
var pieceLeaves = 1 << base;
|
||||
var leaves:Array<Bytes> = [];
|
||||
var pos = 0;
|
||||
while (pos < data.length) {
|
||||
var n = Std.int(Math.min(16384, data.length - pos));
|
||||
leaves.push(haxe.crypto.Sha256.make(data.sub(pos, n)));
|
||||
pos += n;
|
||||
}
|
||||
var zero = leafZero();
|
||||
while (leaves.length < pieceLeaves) leaves.push(zero);
|
||||
return merkleReduce(leaves);
|
||||
}
|
||||
|
||||
public function verifyLayer(fi:Int, hashes:Array<Bytes>):Bool {
|
||||
if (fi < 0 || hashes == null) return false;
|
||||
var root = files[fi].piecesRoot;
|
||||
if (root == null) return false;
|
||||
var base = Std.int(Math.log(pieceLength / 16384)
|
||||
/ Math.log(2));
|
||||
var level = hashes.copy();
|
||||
var pow = 1;
|
||||
while (pow < level.length) pow <<= 1;
|
||||
var zero = layerZero(base);
|
||||
while (level.length < pow) level.push(zero);
|
||||
return memcmp(merkleReduce(level), root);
|
||||
}
|
||||
|
||||
static var layerZeroCache:Map<Int, Bytes> = [];
|
||||
public static function layerZero(level:Int):Bytes {
|
||||
if (level == 0) return leafZero();
|
||||
var half = layerZero(level - 1);
|
||||
return hashPair(half, half);
|
||||
}
|
||||
|
||||
public static function hashPair(a:Bytes, b:Bytes):Bytes {
|
||||
var buf = new haxe.io.BytesBuffer();
|
||||
buf.addBytes(a, 0, 32);
|
||||
buf.addBytes(b, 0, 32);
|
||||
return haxe.crypto.Sha256.make(buf.getBytes());
|
||||
}
|
||||
|
||||
public static function merkleReduce(hashes:Array<Bytes>):Bytes {
|
||||
var level = hashes;
|
||||
while (level.length > 1) {
|
||||
var next:Array<Bytes> = [];
|
||||
for (i in 0...Std.int(level.length / 2)) {
|
||||
next.push(hashPair(level[i * 2], level[i * 2 + 1]));
|
||||
}
|
||||
level = next;
|
||||
}
|
||||
return level.length > 0 ? level[0] : Bytes.alloc(32);
|
||||
}
|
||||
|
||||
static public function memcmp(a:Bytes, b:Bytes):Bool {
|
||||
if (a == null || b == null || a.length != b.length) return false;
|
||||
for (i in 0...a.length) if (a.get(i) != b.get(i)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
var treeCursor:Int = 0;
|
||||
function walkFileTree(node:Dynamic, path:Array<String>):Void {
|
||||
var aligned = pieces == null;
|
||||
var keys = Reflect.fields(node);
|
||||
keys.sort(Reflect.compare);
|
||||
for (key in keys) {
|
||||
var child = Reflect.field(node, key);
|
||||
if (key == "") {
|
||||
var len = Std.int(fieldFloat(child, "length"));
|
||||
var p = path.length == 2 && path[0] == name
|
||||
&& path[1] == name ? name : path.join("/");
|
||||
var offset = aligned ? treeCursor * pieceLength
|
||||
: totalLength;
|
||||
files.push({path: sanitizePath(p), length: len,
|
||||
offset: offset,
|
||||
piecesRoot: toBytes(
|
||||
Reflect.field(child, "pieces root"))});
|
||||
if (aligned) {
|
||||
treeCursor += Std.int(
|
||||
Math.ceil(len / pieceLength));
|
||||
}
|
||||
totalLength += len;
|
||||
} else {
|
||||
var next = path.copy();
|
||||
next.push(key);
|
||||
walkFileTree(child, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyV1Layout(filesList:Dynamic):Void {
|
||||
if (!Std.isOfType(filesList, Array)) return;
|
||||
var byPath = new Map<String, TorrentFileEntry>();
|
||||
for (f in files) byPath.set(f.path, f);
|
||||
var ordered:Array<TorrentFileEntry> = [];
|
||||
var off = 0;
|
||||
for (fl in (filesList:Array<Dynamic>)) {
|
||||
var len = Std.int(fieldFloat(fl, "length"));
|
||||
var pathList:Array<Dynamic> = Reflect.field(fl, "path");
|
||||
var path = name;
|
||||
if (pathList != null) {
|
||||
var parts = [for (p in pathList) bytesToString(p)];
|
||||
path = name + "/" + parts.join("/");
|
||||
}
|
||||
var e = byPath.get(sanitizePath(path));
|
||||
if (e == null) {
|
||||
e = {path: sanitizePath(path), length: len,
|
||||
offset: off};
|
||||
} else {
|
||||
e.offset = off;
|
||||
e.length = len;
|
||||
}
|
||||
off += len;
|
||||
ordered.push(e);
|
||||
}
|
||||
files = ordered;
|
||||
totalLength = off;
|
||||
}
|
||||
|
||||
public function initFileHashes():Void {
|
||||
if (metaVersion != 2 || files == null
|
||||
|| pieceLength <= 0) return;
|
||||
if (fileHashes == null) {
|
||||
fileHashes = [for (f in files) null];
|
||||
}
|
||||
for (fi in 0...files.length) {
|
||||
var f = files[fi];
|
||||
if (f.length > 0 && f.length <= pieceLength
|
||||
&& f.piecesRoot != null) {
|
||||
fileHashes[fi] = [f.piecesRoot];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function loadPieceLayers(data:Bytes):Void {
|
||||
if (metaVersion != 2 || fileHashes == null) return;
|
||||
var pos = findTopKey(data, "piece layers");
|
||||
if (pos < 0 || data.get(pos) != 'd'.code) return;
|
||||
var byRoot = new Map<String, Bytes>();
|
||||
var p = pos + 1;
|
||||
while (p < data.length && data.get(p) != 'e'.code) {
|
||||
var key = rawString(data, p);
|
||||
if (key == null) return;
|
||||
p = skipValue(data, p);
|
||||
var val = rawString(data, p);
|
||||
if (val == null) return;
|
||||
p = skipValue(data, p);
|
||||
byRoot.set(Crypto.toHex(key), val);
|
||||
}
|
||||
for (fi in 0...files.length) {
|
||||
var f = files[fi];
|
||||
if (f.piecesRoot == null) continue;
|
||||
var np = Std.int(Math.ceil(f.length / pieceLength));
|
||||
if (np <= 1) continue;
|
||||
var raw = byRoot.get(Crypto.toHex(f.piecesRoot));
|
||||
if (raw == null || raw.length < np * 32) continue;
|
||||
var hashes = [for (i in 0...np) raw.sub(i * 32, 32)];
|
||||
if (verifyLayer(fi, hashes)) fileHashes[fi] = hashes;
|
||||
}
|
||||
}
|
||||
|
||||
static function rawString(data:Bytes, pos:Int):Bytes {
|
||||
var b = data.get(pos);
|
||||
if (b < '0'.code || b > '9'.code) return null;
|
||||
var len = 0;
|
||||
while (pos < data.length && data.get(pos) != ':'.code) {
|
||||
len = len * 10 + (data.get(pos) - '0'.code);
|
||||
pos++;
|
||||
}
|
||||
if (pos + 1 + len > data.length) return null;
|
||||
return data.sub(pos + 1, len);
|
||||
}
|
||||
|
||||
static function findTopKey(data:Bytes, want:String):Int {
|
||||
if (data.length == 0 || data.get(0) != 'd'.code) return -1;
|
||||
var pos = 1;
|
||||
while (pos < data.length && data.get(pos) != 'e'.code) {
|
||||
var keyEnd = skipValue(data, pos);
|
||||
var key = data.sub(pos, keyEnd - pos).toString();
|
||||
var colon = key.indexOf(":");
|
||||
key = key.substr(colon + 1);
|
||||
var valEnd = skipValue(data, keyEnd);
|
||||
if (key == want) return keyEnd;
|
||||
pos = valEnd;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static function parseAnnounce(dict:Dynamic):Array<String> {
|
||||
var out:Array<String> = [];
|
||||
var seen = new Map<String, Bool>();
|
||||
var list:Array<Dynamic> = Reflect.field(dict, "announce-list");
|
||||
if (list != null) {
|
||||
for (tier in list) {
|
||||
if (Std.isOfType(tier, Array)) {
|
||||
for (u in (tier:Array<Dynamic>)) {
|
||||
var s = bytesToString(u);
|
||||
if (s != null && !seen.exists(s)) {
|
||||
seen.set(s, true);
|
||||
out.push(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var single = bytesToString(Reflect.field(dict, "announce"));
|
||||
if (single != null && !seen.exists(single)) {
|
||||
out.push(single);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static function fromInfoDict(rawInfo:Bytes, infoHash:Bytes, ?announce:Array<String>):MetaInfo {
|
||||
var m = new MetaInfo();
|
||||
m.rawInfo = rawInfo;
|
||||
m.infoHash = infoHash;
|
||||
m.infoHashHex = infoHash != null ? Crypto.toHex(infoHash) : null;
|
||||
var infoDict = Bencode.decode(rawInfo);
|
||||
m.parseInfo(infoDict);
|
||||
if (m.metaVersion == 2) {
|
||||
m.infoHashV2 = haxe.crypto.Sha256.make(rawInfo);
|
||||
m.infoHashV2Hex = Crypto.toHex(m.infoHashV2);
|
||||
}
|
||||
m.announce = announce != null ? announce : [];
|
||||
m.initFileHashes();
|
||||
return m;
|
||||
}
|
||||
|
||||
public static function create(name:String, files:Array<{path:String, data:Bytes}>, ?announce:Array<String>, ?opts:Dynamic):MetaInfo {
|
||||
var m = new MetaInfo();
|
||||
m.name = name;
|
||||
m.announce = announce != null ? announce : [];
|
||||
m.files = [];
|
||||
var pieceLengthOpt = 0;
|
||||
if (opts != null) {
|
||||
m.isPrivate = fieldFloat(opts, "private") == 1
|
||||
|| Reflect.field(opts, "private") == true;
|
||||
m.source = Reflect.field(opts, "source");
|
||||
m.comment = Reflect.field(opts, "comment");
|
||||
m.createdBy = Reflect.field(opts, "createdBy");
|
||||
var u:Dynamic = Reflect.field(opts, "urlList");
|
||||
if (u != null) {
|
||||
m.urlList = Std.isOfType(u, Array)
|
||||
? (u:Array<String>) : [Std.string(u)];
|
||||
}
|
||||
pieceLengthOpt = Std.int(fieldFloat(opts, "pieceLength"));
|
||||
}
|
||||
|
||||
var mvOpt = opts != null
|
||||
? Std.int(fieldFloat(opts, "metaVersion")) : 1;
|
||||
if (mvOpt < 1) mvOpt = 1;
|
||||
var isV1 = mvOpt != 2;
|
||||
var isV2 = mvOpt != 1;
|
||||
m.metaVersion = isV2 ? 2 : 1;
|
||||
|
||||
var aligned = mvOpt == 2;
|
||||
var total = 0;
|
||||
for (f in files) total += f.data.length;
|
||||
m.pieceLength = pieceLengthOpt > 0 ? pieceLengthOpt
|
||||
: TorrentInfo.pieceLength(total);
|
||||
|
||||
var cursor = 0;
|
||||
var content = new haxe.io.BytesBuffer();
|
||||
for (f in files) {
|
||||
var entry:TorrentFileEntry = {path: f.path,
|
||||
length: f.data.length,
|
||||
offset: aligned ? cursor * m.pieceLength
|
||||
: m.totalLength};
|
||||
if (isV2) entry.piecesRoot = merkleRoot(f.data);
|
||||
m.files.push(entry);
|
||||
m.totalLength += f.data.length;
|
||||
cursor += Std.int(Math.ceil(f.data.length
|
||||
/ m.pieceLength));
|
||||
content.addBytes(f.data, 0, f.data.length);
|
||||
}
|
||||
var all = content.getBytes();
|
||||
m.storageLength = aligned ? cursor * m.pieceLength
|
||||
: m.totalLength;
|
||||
m.pieces = isV1 ? TorrentInfo.pieces(all, m.pieceLength)
|
||||
: null;
|
||||
|
||||
var info:Dynamic = {};
|
||||
Reflect.setField(info, "name", name);
|
||||
Reflect.setField(info, "piece length", m.pieceLength);
|
||||
if (m.isPrivate) Reflect.setField(info, "private", 1);
|
||||
if (m.source != null) Reflect.setField(info, "source", m.source);
|
||||
if (isV1) {
|
||||
Reflect.setField(info, "pieces", m.pieces);
|
||||
if (files.length == 1) {
|
||||
Reflect.setField(info, "length", files[0].data.length);
|
||||
} else {
|
||||
var fl:Array<Dynamic> = [];
|
||||
for (f in files) {
|
||||
var fd:Dynamic = {};
|
||||
Reflect.setField(fd, "length", f.data.length);
|
||||
var rel = f.path;
|
||||
if (rel.indexOf(name + "/") == 0) {
|
||||
rel = rel.substr(name.length + 1);
|
||||
}
|
||||
Reflect.setField(fd, "path", rel.split("/"));
|
||||
fl.push(fd);
|
||||
}
|
||||
Reflect.setField(info, "files", fl);
|
||||
}
|
||||
}
|
||||
if (isV2) {
|
||||
Reflect.setField(info, "meta version", 2);
|
||||
Reflect.setField(info, "file tree",
|
||||
buildFileTree(name, files, m.files));
|
||||
}
|
||||
if (isV2) {
|
||||
m.pieceLayers = [];
|
||||
for (i in 0...files.length) {
|
||||
var f = files[i];
|
||||
var entry = m.files[i];
|
||||
if (f.data.length <= m.pieceLength
|
||||
|| entry.piecesRoot == null) continue;
|
||||
var np = Std.int(Math.ceil(f.data.length
|
||||
/ m.pieceLength));
|
||||
var buf = new haxe.io.BytesBuffer();
|
||||
for (p in 0...np) {
|
||||
var off = p * m.pieceLength;
|
||||
var sz = Std.int(Math.min(m.pieceLength,
|
||||
f.data.length - off));
|
||||
buf.addBytes(m.pieceRootV2(
|
||||
f.data.sub(off, sz)), 0, 32);
|
||||
}
|
||||
m.pieceLayers.push({root: entry.piecesRoot,
|
||||
hashes: buf.getBytes()});
|
||||
}
|
||||
if (m.pieceLayers.length == 0) m.pieceLayers = null;
|
||||
}
|
||||
m.rawInfo = Bencode.encode(info);
|
||||
if (isV1) {
|
||||
m.infoHash = haxe.crypto.Sha1.make(m.rawInfo);
|
||||
m.infoHashHex = Crypto.toHex(m.infoHash);
|
||||
}
|
||||
if (isV2) {
|
||||
m.infoHashV2 = haxe.crypto.Sha256.make(m.rawInfo);
|
||||
m.infoHashV2Hex = Crypto.toHex(m.infoHashV2);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
public static function merkleRoot(data:Bytes):Bytes {
|
||||
if (data.length == 0) return Bytes.alloc(32);
|
||||
var leaves:Array<Bytes> = [];
|
||||
var pos = 0;
|
||||
while (pos < data.length) {
|
||||
var n = Std.int(Math.min(16384, data.length - pos));
|
||||
leaves.push(haxe.crypto.Sha256.make(data.sub(pos, n)));
|
||||
pos += n;
|
||||
}
|
||||
var pow = 1;
|
||||
while (pow < leaves.length) pow <<= 1;
|
||||
var zero = leafZero();
|
||||
while (leaves.length < pow) leaves.push(zero);
|
||||
return merkleReduce(leaves);
|
||||
}
|
||||
|
||||
static var leafZeroCache:Bytes = null;
|
||||
static function leafZero():Bytes {
|
||||
if (leafZeroCache == null) leafZeroCache = Bytes.alloc(32);
|
||||
return leafZeroCache;
|
||||
}
|
||||
|
||||
static function buildFileTree(name:String, files:Array<{path:String, data:Bytes}>, entries:Array<TorrentFileEntry>):Dynamic {
|
||||
var root:Dynamic = {};
|
||||
for (i in 0...files.length) {
|
||||
var rel = files[i].path;
|
||||
if (rel.indexOf(name + "/") == 0) {
|
||||
rel = rel.substr(name.length + 1);
|
||||
}
|
||||
var parts = rel.split("/");
|
||||
var node = root;
|
||||
for (p in 0...parts.length - 1) {
|
||||
var nxt = Reflect.field(node, parts[p]);
|
||||
if (nxt == null) {
|
||||
nxt = {};
|
||||
Reflect.setField(node, parts[p], nxt);
|
||||
}
|
||||
node = nxt;
|
||||
}
|
||||
var leaf:Dynamic = {};
|
||||
Reflect.setField(leaf, "length", files[i].data.length);
|
||||
Reflect.setField(leaf, "pieces root",
|
||||
entries[i].piecesRoot);
|
||||
var leafWrap:Dynamic = {};
|
||||
Reflect.setField(leafWrap, "", leaf);
|
||||
Reflect.setField(node, parts[parts.length - 1], leafWrap);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
public function toTorrentBytes(?extra:Dynamic):Bytes {
|
||||
var dict:Dynamic = extra != null ? extra : {};
|
||||
Reflect.setField(dict, "info", Bencode.decode(rawInfo));
|
||||
if (announce.length > 0) {
|
||||
Reflect.setField(dict, "announce", announce[0]);
|
||||
var tiers:Array<Dynamic> = [for (a in announce) [a]];
|
||||
Reflect.setField(dict, "announce-list", tiers);
|
||||
}
|
||||
if (comment != null) Reflect.setField(dict, "comment", comment);
|
||||
if (createdBy != null) {
|
||||
Reflect.setField(dict, "created by", createdBy);
|
||||
}
|
||||
if (urlList != null && urlList.length > 0) {
|
||||
Reflect.setField(dict, "url-list",
|
||||
urlList.length == 1 ? (urlList[0]:Dynamic)
|
||||
: [for (u in urlList) u]);
|
||||
}
|
||||
Reflect.setField(dict, "creation date",
|
||||
creationDate > 0 ? creationDate
|
||||
: Std.int(Date.now().getTime() / 1000));
|
||||
if (pieceLayers != null && pieceLayers.length > 0) {
|
||||
var pl = new Bencode.RawDict();
|
||||
var sorted = pieceLayers.copy();
|
||||
sorted.sort(function(a, b) {
|
||||
var n = a.root.length < b.root.length
|
||||
? a.root.length : b.root.length;
|
||||
for (i in 0...n) {
|
||||
var d = a.root.get(i) - b.root.get(i);
|
||||
if (d != 0) return d;
|
||||
}
|
||||
return a.root.length - b.root.length;
|
||||
});
|
||||
for (e in sorted) {
|
||||
pl.pairs.push({key: e.root, val: e.hashes});
|
||||
}
|
||||
Reflect.setField(dict, "piece layers", pl);
|
||||
}
|
||||
return Bencode.encode(dict);
|
||||
}
|
||||
|
||||
public function pieceHash(index:Int):Bytes {
|
||||
return pieces.sub(index * TorrentInfo.HASH_LENGTH,
|
||||
TorrentInfo.HASH_LENGTH);
|
||||
}
|
||||
|
||||
public function pieceSize(index:Int):Int {
|
||||
if (pieces == null) return pieceSizeV2(index); // v2 aligned
|
||||
if (index == numPieces - 1) {
|
||||
var rem = totalLength % pieceLength;
|
||||
return rem == 0 ? pieceLength : rem;
|
||||
}
|
||||
return pieceLength;
|
||||
}
|
||||
|
||||
static function fieldFloat(obj:Dynamic, name:String):Float {
|
||||
var v = Reflect.field(obj, name);
|
||||
if (v == null) return 0;
|
||||
if (Std.isOfType(v, Float) || Std.isOfType(v, Int)) return v;
|
||||
return Std.parseFloat(Std.string(v));
|
||||
}
|
||||
|
||||
static function bytesToString(v:Dynamic):String {
|
||||
if (v == null) return null;
|
||||
if (Std.isOfType(v, String)) return v;
|
||||
if (Std.isOfType(v, Float) || Std.isOfType(v, Int)) {
|
||||
return Std.string(v);
|
||||
}
|
||||
if (Std.isOfType(v, Bytes)) return (v:Bytes).toString();
|
||||
return Std.string(v);
|
||||
}
|
||||
|
||||
public static function toBytes(v:Dynamic):Bytes {
|
||||
if (v == null) return null;
|
||||
if (Std.isOfType(v, Float) || Std.isOfType(v, Int)
|
||||
|| Std.isOfType(v, String) || Std.isOfType(v, Array)) {
|
||||
return null;
|
||||
}
|
||||
if (Std.isOfType(v, Bytes)) return (v:Bytes);
|
||||
return null;
|
||||
}
|
||||
|
||||
static function extractInfoBytes(data:Bytes):Bytes {
|
||||
var pos = 0;
|
||||
if (data.get(pos) != 'd'.code) throw "MetaInfo: not a dict";
|
||||
pos++;
|
||||
while (pos < data.length && data.get(pos) != 'e'.code) {
|
||||
var keyEnd = skipValue(data, pos);
|
||||
var key = data.sub(pos, keyEnd - pos).toString();
|
||||
var colon = key.indexOf(":");
|
||||
key = key.substr(colon + 1);
|
||||
var valStart = keyEnd;
|
||||
var valEnd = skipValue(data, valStart);
|
||||
if (key == "info") {
|
||||
return data.sub(valStart, valEnd - valStart);
|
||||
}
|
||||
pos = valEnd;
|
||||
}
|
||||
throw "MetaInfo: info key not found";
|
||||
}
|
||||
|
||||
static public function skipValue(data:Bytes, pos:Int):Int {
|
||||
var b = data.get(pos);
|
||||
if (b == 'i'.code) {
|
||||
pos++;
|
||||
while (pos < data.length && data.get(pos) != 'e'.code) pos++;
|
||||
return pos + 1;
|
||||
}
|
||||
if (b == 'l'.code || b == 'd'.code) {
|
||||
pos++;
|
||||
while (pos < data.length && data.get(pos) != 'e'.code) {
|
||||
pos = skipValue(data, pos);
|
||||
}
|
||||
return pos + 1;
|
||||
}
|
||||
var len = 0;
|
||||
while (pos < data.length && data.get(pos) != ':'.code) {
|
||||
len = len * 10 + (data.get(pos) - '0'.code);
|
||||
pos++;
|
||||
}
|
||||
return pos + 1 + len;
|
||||
}
|
||||
}
|
||||
329
leenkx/Sources/leenkx/network/torrent/PieceManager.hx
Normal file
329
leenkx/Sources/leenkx/network/torrent/PieceManager.hx
Normal file
@ -0,0 +1,329 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
import leenkx.network.torrent.storage.Storage.IStorage;
|
||||
|
||||
class PieceManager {
|
||||
public static inline var BLOCK_SIZE = 16384;
|
||||
|
||||
var meta:MetaInfo;
|
||||
var storage:IStorage;
|
||||
|
||||
public var have:Array<Bool>;
|
||||
public var numHave:Int = 0;
|
||||
public var bitfield:Bytes;
|
||||
|
||||
var pending:Map<Int, Map<Int, Bytes>> = [];
|
||||
var pendingCount:Map<Int, Int> = [];
|
||||
var requested:Map<String, Map<Int, Map<Int, Bool>>> = [];
|
||||
var globalRequested:Map<Int, Map<Int, Bool>> = [];
|
||||
var pendingV2:Map<Int, Bytes> = [];
|
||||
|
||||
public var onPieceComplete:Int -> Void;
|
||||
public var onPieceFailed:Int -> Void;
|
||||
public var onComplete:Void -> Void;
|
||||
|
||||
@:allow(leenkx.network.torrent.Torrent)
|
||||
@:allow(leenkx.network.torrent.WebSeed)
|
||||
var selections:Array<{start:Int, end:Int, priority:Int,
|
||||
notify:Void -> Void}> = [];
|
||||
|
||||
public function select(startByte:Int, endByte:Int, ?priority:Int, ?notify:Void -> Void):Void {
|
||||
selections.push({start: pieceIndex(startByte),
|
||||
end: pieceIndex(endByte),
|
||||
priority: priority != null ? priority : 0,
|
||||
notify: notify});
|
||||
selections.sort(function(a, b) return b.priority
|
||||
- a.priority);
|
||||
}
|
||||
|
||||
public function deselect(startByte:Int, endByte:Int, ?priority:Int):Void {
|
||||
var s = pieceIndex(startByte);
|
||||
var e = pieceIndex(endByte);
|
||||
var p = priority != null ? priority : 0;
|
||||
for (i in 0...selections.length) {
|
||||
var sel = selections[i];
|
||||
if (sel.start == s && sel.end == e
|
||||
&& sel.priority == p) {
|
||||
selections.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pieceIndex(byte:Int):Int {
|
||||
var i = Std.int(byte / meta.pieceLength);
|
||||
if (i < 0) i = 0;
|
||||
if (i >= meta.numPieces) i = meta.numPieces - 1;
|
||||
return i;
|
||||
}
|
||||
|
||||
function updateSelections(index:Int):Void {
|
||||
var i = 0;
|
||||
while (i < selections.length) {
|
||||
var sel = selections[i];
|
||||
if (index < sel.start || index > sel.end
|
||||
|| sel.notify == null) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
var done = true;
|
||||
for (p in sel.start...sel.end + 1) {
|
||||
if (!have[p]) {
|
||||
done = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
selections.splice(i, 1);
|
||||
sel.notify();
|
||||
} else i++;
|
||||
}
|
||||
}
|
||||
|
||||
public var complete(get, never):Bool;
|
||||
function get_complete():Bool return numHave == meta.numPieces;
|
||||
|
||||
public function new(meta:MetaInfo, storage:IStorage) {
|
||||
this.meta = meta;
|
||||
this.storage = storage;
|
||||
have = [for (i in 0...meta.numPieces) false];
|
||||
bitfield = Bytes.alloc(Std.int(Math.ceil(meta.numPieces / 8)));
|
||||
}
|
||||
|
||||
public static function forSeeder(meta:MetaInfo, storage:IStorage):PieceManager {
|
||||
var pm = new PieceManager(meta, storage);
|
||||
for (i in 0...meta.numPieces) pm.markHave(i);
|
||||
return pm;
|
||||
}
|
||||
|
||||
public function markHave(index:Int):Void {
|
||||
if (have[index]) return;
|
||||
have[index] = true;
|
||||
numHave++;
|
||||
bitfield.set(index >> 3, bitfield.get(index >> 3)
|
||||
| (0x80 >> (index & 7)));
|
||||
}
|
||||
|
||||
public function addBlock(index:Int, begin:Int, data:Bytes):Bool {
|
||||
if (index < 0 || index >= meta.numPieces || begin < 0
|
||||
|| data == null
|
||||
|| begin + data.length > meta.pieceSize(index)) {
|
||||
return false;
|
||||
}
|
||||
if (have[index]) return true;
|
||||
var blocks = pending.get(index);
|
||||
if (blocks == null) {
|
||||
blocks = [];
|
||||
pending.set(index, blocks);
|
||||
pendingCount.set(index, 0);
|
||||
}
|
||||
if (!blocks.exists(begin)) {
|
||||
blocks.set(begin, data);
|
||||
pendingCount.set(index, pendingCount.get(index) + data.length);
|
||||
var gReq = globalRequested.get(index);
|
||||
if (gReq != null) {
|
||||
gReq.remove(begin);
|
||||
if (!gReq.keys().hasNext()) globalRequested.remove(index);
|
||||
}
|
||||
}
|
||||
if (pendingCount.get(index) < meta.pieceSize(index)) return false;
|
||||
|
||||
var piece = Bytes.alloc(meta.pieceSize(index));
|
||||
var off = 0;
|
||||
var keys = [for (k in blocks.keys()) k];
|
||||
keys.sort(Reflect.compare);
|
||||
for (b in keys) {
|
||||
var d = blocks.get(b);
|
||||
piece.blit(b, d, 0, d.length);
|
||||
}
|
||||
var ok = true;
|
||||
if (meta.pieces == null) {
|
||||
var fi = meta.fileForPiece(index);
|
||||
var layer = fi >= 0 && meta.fileHashes != null
|
||||
&& meta.fileHashes.length > fi
|
||||
? meta.fileHashes[fi] : null;
|
||||
pending.remove(index);
|
||||
pendingCount.remove(index);
|
||||
if (layer == null) {
|
||||
pendingV2.set(index, piece);
|
||||
return false;
|
||||
}
|
||||
ok = meta.verifyPieceV2(index, piece);
|
||||
} else {
|
||||
var hash = haxe.crypto.Sha1.make(piece);
|
||||
ok = MetaInfo.memcmp(hash, meta.pieceHash(index));
|
||||
pending.remove(index);
|
||||
pendingCount.remove(index);
|
||||
}
|
||||
if (!ok) {
|
||||
trace('[PieceManager] piece ' + index
|
||||
+ ' verification FAILED');
|
||||
releaseRequested(index);
|
||||
if (onPieceFailed != null) onPieceFailed(index);
|
||||
return false;
|
||||
}
|
||||
storage.write(index * meta.pieceLength, piece);
|
||||
markHave(index);
|
||||
updateSelections(index);
|
||||
if (onPieceComplete != null) onPieceComplete(index);
|
||||
if (complete && onComplete != null) onComplete();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function flushPendingV2(fi:Int):Bool {
|
||||
if (meta.pieces != null) return false;
|
||||
var f = meta.files[fi];
|
||||
var start = Std.int(f.offset / meta.pieceLength);
|
||||
var np = Std.int(Math.ceil(f.length / meta.pieceLength));
|
||||
var wrote = false;
|
||||
for (i in start...start + np) {
|
||||
var piece = pendingV2.get(i);
|
||||
if (piece == null) continue;
|
||||
pendingV2.remove(i);
|
||||
if (!meta.verifyPieceV2(i, piece)) {
|
||||
trace('[PieceManager] flush piece ' + i
|
||||
+ ' verification FAILED');
|
||||
releaseRequested(i);
|
||||
if (onPieceFailed != null) onPieceFailed(i);
|
||||
continue;
|
||||
}
|
||||
storage.write(i * meta.pieceLength, piece);
|
||||
markHave(i);
|
||||
updateSelections(i);
|
||||
wrote = true;
|
||||
if (onPieceComplete != null) onPieceComplete(i);
|
||||
}
|
||||
if (complete && onComplete != null) onComplete();
|
||||
return wrote;
|
||||
}
|
||||
|
||||
public function untouched(index:Int):Bool {
|
||||
return !have[index] && !pending.exists(index)
|
||||
&& !pendingV2.exists(index)
|
||||
&& !globalRequested.exists(index);
|
||||
}
|
||||
|
||||
public function readBlock(index:Int, begin:Int, length:Int):Bytes {
|
||||
return storage.read(index * meta.pieceLength + begin, length);
|
||||
}
|
||||
|
||||
public function neededBlocks(peerKey:String, index:Int):Array<{begin:Int, length:Int}> {
|
||||
if (pendingV2.exists(index)) return [];
|
||||
var size = meta.pieceSize(index);
|
||||
var blocks = pending.get(index);
|
||||
var peerReq = requested.get(peerKey);
|
||||
var req = peerReq != null ? peerReq.get(index) : null;
|
||||
var globalReq = globalRequested.get(index);
|
||||
var out:Array<{begin:Int, length:Int}> = [];
|
||||
var off = 0;
|
||||
while (off < size) {
|
||||
var len = size - off;
|
||||
if (len > BLOCK_SIZE) len = BLOCK_SIZE;
|
||||
if ((blocks == null || !blocks.exists(off))
|
||||
&& (req == null || !req.exists(off))
|
||||
&& (globalReq == null || !globalReq.exists(off))) {
|
||||
out.push({begin: off, length: len});
|
||||
}
|
||||
off += len;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public function markRequested(key:String, index:Int, begin:Int):Void {
|
||||
var r = requested.get(key);
|
||||
if (r == null) requested.set(key,
|
||||
r = new Map<Int, Map<Int, Bool>>());
|
||||
var m = r.get(index);
|
||||
if (m == null) r.set(index, m = new Map<Int, Bool>());
|
||||
m.set(begin, true);
|
||||
var g = globalRequested.get(index);
|
||||
if (g == null) globalRequested.set(index,
|
||||
g = new Map<Int, Bool>());
|
||||
g.set(begin, true);
|
||||
reqTimes.set(key + "|" + index + "|" + begin,
|
||||
haxe.Timer.stamp());
|
||||
}
|
||||
|
||||
var reqTimes:Map<String, Float> = [];
|
||||
|
||||
public function expireRequests(ttlSec:Float):Map<String, Int> {
|
||||
var now = haxe.Timer.stamp();
|
||||
var dead:Array<String> = [];
|
||||
for (k in reqTimes.keys()) {
|
||||
if (now - reqTimes.get(k) > ttlSec) dead.push(k);
|
||||
}
|
||||
var expired:Map<String, Int> = [];
|
||||
for (k in dead) {
|
||||
var p = k.split("|");
|
||||
var idx = Std.parseInt(p[1]);
|
||||
var beg = Std.parseInt(p[2]);
|
||||
var peerReq = requested.get(p[0]);
|
||||
if (peerReq != null) {
|
||||
var r = peerReq.get(idx);
|
||||
if (r != null && r.exists(beg)) {
|
||||
r.remove(beg);
|
||||
var n = expired.exists(p[0]) ? expired.get(p[0]) : 0;
|
||||
expired.set(p[0], n + 1);
|
||||
}
|
||||
}
|
||||
var g = globalRequested.get(idx);
|
||||
if (g != null) {
|
||||
g.remove(beg);
|
||||
var empty = true;
|
||||
for (b in g.keys()) {
|
||||
empty = false;
|
||||
break;
|
||||
}
|
||||
if (empty) globalRequested.remove(idx);
|
||||
}
|
||||
reqTimes.remove(k);
|
||||
}
|
||||
return expired;
|
||||
}
|
||||
|
||||
public function releaseRequested(index:Int):Void {
|
||||
globalRequested.remove(index);
|
||||
var keys = [for (k in requested.keys()) k];
|
||||
for (key in keys) {
|
||||
var r = requested.get(key);
|
||||
var m = r.get(index);
|
||||
if (m != null) {
|
||||
for (b in m.keys()) {
|
||||
reqTimes.remove(key + "|" + index + "|" + b);
|
||||
}
|
||||
r.remove(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function clearRequested(peerKey:String, index:Int):Void {
|
||||
var peerReq = requested.get(peerKey);
|
||||
if (peerReq != null) {
|
||||
var req = peerReq.get(index);
|
||||
if (req != null) {
|
||||
var gReq = globalRequested.get(index);
|
||||
if (gReq != null) {
|
||||
for (off in req.keys()) gReq.remove(off);
|
||||
if (!gReq.keys().hasNext()) globalRequested.remove(index);
|
||||
}
|
||||
}
|
||||
peerReq.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
public function clearAllRequested(peerKey:String):Void {
|
||||
var peerReq = requested.get(peerKey);
|
||||
if (peerReq != null) {
|
||||
for (index in peerReq.keys()) {
|
||||
var req = peerReq.get(index);
|
||||
var gReq = globalRequested.get(index);
|
||||
if (gReq != null && req != null) {
|
||||
for (off in req.keys()) gReq.remove(off);
|
||||
if (!gReq.keys().hasNext()) globalRequested.remove(index);
|
||||
}
|
||||
}
|
||||
requested.remove(peerKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
334
leenkx/Sources/leenkx/network/torrent/Ripemd160.hx
Normal file
334
leenkx/Sources/leenkx/network/torrent/Ripemd160.hx
Normal file
@ -0,0 +1,334 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
class Ripemd160 {
|
||||
|
||||
static inline function ROL(x:Int, n:Int):Int {
|
||||
return (x << n) | (x >>> (32 - n));
|
||||
}
|
||||
|
||||
static inline function F(x:Int, y:Int, z:Int):Int {
|
||||
return x ^ y ^ z;
|
||||
}
|
||||
|
||||
static inline function G(x:Int, y:Int, z:Int):Int {
|
||||
return (x & y) | (~x & z);
|
||||
}
|
||||
|
||||
static inline function H(x:Int, y:Int, z:Int):Int {
|
||||
return (x | ~y) ^ z;
|
||||
}
|
||||
|
||||
static inline function I(x:Int, y:Int, z:Int):Int {
|
||||
return (x & z) | (y & ~z);
|
||||
}
|
||||
|
||||
static inline function J(x:Int, y:Int, z:Int):Int {
|
||||
return x ^ (y | ~z);
|
||||
}
|
||||
|
||||
static inline function FF(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + F(b, c, d) + x;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline function GG(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + G(b, c, d) + x + 0x5A827999;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline function HH(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + H(b, c, d) + x + 0x6ED9EBA1;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline function II(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + I(b, c, d) + x + 0x8F1BBCDC;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline function JJ(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + J(b, c, d) + x + 0xA953FD4E;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline function FFF(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + F(b, c, d) + x;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline function GGG(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + G(b, c, d) + x + 0x7A6D76E9;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline function HHH(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + H(b, c, d) + x + 0x6D703EF3;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline function III(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + I(b, c, d) + x + 0x5C4DD124;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
static inline function JJJ(a:Int, b:Int, c:Int, d:Int, e:Int, x:Int, s:Int):Int {
|
||||
a = a + J(b, c, d) + x + 0x50A28BE6;
|
||||
a = ROL(a, s) + e;
|
||||
return a;
|
||||
}
|
||||
|
||||
public static function make(data:Bytes):Bytes {
|
||||
var msgLen = data.length;
|
||||
var bitLen = msgLen * 8;
|
||||
|
||||
var padLen = 56 - ((msgLen + 1) % 64);
|
||||
if (padLen < 0) padLen += 64;
|
||||
var totalLen = msgLen + 1 + padLen + 8;
|
||||
|
||||
var padded = Bytes.alloc(totalLen);
|
||||
padded.blit(0, data, 0, msgLen);
|
||||
padded.set(msgLen, 0x80);
|
||||
for (i in (msgLen + 1)...(totalLen - 8)) padded.set(i, 0);
|
||||
padded.set(totalLen - 8, bitLen & 0xFF);
|
||||
padded.set(totalLen - 7, (bitLen >> 8) & 0xFF);
|
||||
padded.set(totalLen - 6, (bitLen >> 16) & 0xFF);
|
||||
padded.set(totalLen - 5, (bitLen >> 24) & 0xFF);
|
||||
padded.set(totalLen - 4, 0);
|
||||
padded.set(totalLen - 3, 0);
|
||||
padded.set(totalLen - 2, 0);
|
||||
padded.set(totalLen - 1, 0);
|
||||
|
||||
var h0 = 0x67452301;
|
||||
var h1 = 0xEFCDAB89;
|
||||
var h2 = 0x98BADCFE;
|
||||
var h3 = 0x10325476;
|
||||
var h4 = 0xC3D2E1F0;
|
||||
|
||||
var numBlocks = Std.int(totalLen / 64);
|
||||
|
||||
for (block in 0...numBlocks) {
|
||||
var base = block * 64;
|
||||
var X = new Array<Int>();
|
||||
for (i in 0...16) {
|
||||
var off = base + i * 4;
|
||||
X.push(padded.get(off) | (padded.get(off + 1) << 8) |
|
||||
(padded.get(off + 2) << 16) | (padded.get(off + 3) << 24));
|
||||
}
|
||||
|
||||
var a = h0, b = h1, c = h2, d = h3, e = h4;
|
||||
var a1 = h0, b1 = h1, c1 = h2, d1 = h3, e1 = h4;
|
||||
|
||||
a = FF(a, b, c, d, e, X[0], 11); c = ROL(c, 10);
|
||||
e = FF(e, a, b, c, d, X[1], 14); b = ROL(b, 10);
|
||||
d = FF(d, e, a, b, c, X[2], 15); a = ROL(a, 10);
|
||||
c = FF(c, d, e, a, b, X[3], 12); e = ROL(e, 10);
|
||||
b = FF(b, c, d, e, a, X[4], 5); d = ROL(d, 10);
|
||||
a = FF(a, b, c, d, e, X[5], 8); c = ROL(c, 10);
|
||||
e = FF(e, a, b, c, d, X[6], 7); b = ROL(b, 10);
|
||||
d = FF(d, e, a, b, c, X[7], 9); a = ROL(a, 10);
|
||||
c = FF(c, d, e, a, b, X[8], 11); e = ROL(e, 10);
|
||||
b = FF(b, c, d, e, a, X[9], 13); d = ROL(d, 10);
|
||||
a = FF(a, b, c, d, e, X[10], 14); c = ROL(c, 10);
|
||||
e = FF(e, a, b, c, d, X[11], 15); b = ROL(b, 10);
|
||||
d = FF(d, e, a, b, c, X[12], 6); a = ROL(a, 10);
|
||||
c = FF(c, d, e, a, b, X[13], 7); e = ROL(e, 10);
|
||||
b = FF(b, c, d, e, a, X[14], 9); d = ROL(d, 10);
|
||||
a = FF(a, b, c, d, e, X[15], 8); c = ROL(c, 10);
|
||||
|
||||
e = GG(e, a, b, c, d, X[7], 7); b = ROL(b, 10);
|
||||
d = GG(d, e, a, b, c, X[4], 6); a = ROL(a, 10);
|
||||
c = GG(c, d, e, a, b, X[13], 8); e = ROL(e, 10);
|
||||
b = GG(b, c, d, e, a, X[1], 13); d = ROL(d, 10);
|
||||
a = GG(a, b, c, d, e, X[10], 11); c = ROL(c, 10);
|
||||
e = GG(e, a, b, c, d, X[6], 9); b = ROL(b, 10);
|
||||
d = GG(d, e, a, b, c, X[15], 7); a = ROL(a, 10);
|
||||
c = GG(c, d, e, a, b, X[3], 15); e = ROL(e, 10);
|
||||
b = GG(b, c, d, e, a, X[12], 7); d = ROL(d, 10);
|
||||
a = GG(a, b, c, d, e, X[0], 12); c = ROL(c, 10);
|
||||
e = GG(e, a, b, c, d, X[9], 15); b = ROL(b, 10);
|
||||
d = GG(d, e, a, b, c, X[5], 9); a = ROL(a, 10);
|
||||
c = GG(c, d, e, a, b, X[2], 11); e = ROL(e, 10);
|
||||
b = GG(b, c, d, e, a, X[14], 7); d = ROL(d, 10);
|
||||
a = GG(a, b, c, d, e, X[11], 13); c = ROL(c, 10);
|
||||
e = GG(e, a, b, c, d, X[8], 12); b = ROL(b, 10);
|
||||
|
||||
d = HH(d, e, a, b, c, X[3], 11); a = ROL(a, 10);
|
||||
c = HH(c, d, e, a, b, X[10], 13); e = ROL(e, 10);
|
||||
b = HH(b, c, d, e, a, X[14], 6); d = ROL(d, 10);
|
||||
a = HH(a, b, c, d, e, X[4], 7); c = ROL(c, 10);
|
||||
e = HH(e, a, b, c, d, X[9], 14); b = ROL(b, 10);
|
||||
d = HH(d, e, a, b, c, X[15], 9); a = ROL(a, 10);
|
||||
c = HH(c, d, e, a, b, X[8], 13); e = ROL(e, 10);
|
||||
b = HH(b, c, d, e, a, X[1], 15); d = ROL(d, 10);
|
||||
a = HH(a, b, c, d, e, X[2], 14); c = ROL(c, 10);
|
||||
e = HH(e, a, b, c, d, X[7], 8); b = ROL(b, 10);
|
||||
d = HH(d, e, a, b, c, X[0], 13); a = ROL(a, 10);
|
||||
c = HH(c, d, e, a, b, X[6], 6); e = ROL(e, 10);
|
||||
b = HH(b, c, d, e, a, X[13], 5); d = ROL(d, 10);
|
||||
a = HH(a, b, c, d, e, X[11], 12); c = ROL(c, 10);
|
||||
e = HH(e, a, b, c, d, X[5], 7); b = ROL(b, 10);
|
||||
d = HH(d, e, a, b, c, X[12], 5); a = ROL(a, 10);
|
||||
|
||||
c = II(c, d, e, a, b, X[1], 11); e = ROL(e, 10);
|
||||
b = II(b, c, d, e, a, X[9], 12); d = ROL(d, 10);
|
||||
a = II(a, b, c, d, e, X[11], 14); c = ROL(c, 10);
|
||||
e = II(e, a, b, c, d, X[10], 15); b = ROL(b, 10);
|
||||
d = II(d, e, a, b, c, X[0], 14); a = ROL(a, 10);
|
||||
c = II(c, d, e, a, b, X[8], 15); e = ROL(e, 10);
|
||||
b = II(b, c, d, e, a, X[12], 9); d = ROL(d, 10);
|
||||
a = II(a, b, c, d, e, X[4], 8); c = ROL(c, 10);
|
||||
e = II(e, a, b, c, d, X[13], 9); b = ROL(b, 10);
|
||||
d = II(d, e, a, b, c, X[3], 14); a = ROL(a, 10);
|
||||
c = II(c, d, e, a, b, X[7], 5); e = ROL(e, 10);
|
||||
b = II(b, c, d, e, a, X[15], 6); d = ROL(d, 10);
|
||||
a = II(a, b, c, d, e, X[14], 8); c = ROL(c, 10);
|
||||
e = II(e, a, b, c, d, X[5], 6); b = ROL(b, 10);
|
||||
d = II(d, e, a, b, c, X[6], 5); a = ROL(a, 10);
|
||||
c = II(c, d, e, a, b, X[2], 12); e = ROL(e, 10);
|
||||
|
||||
b = JJ(b, c, d, e, a, X[4], 9); d = ROL(d, 10);
|
||||
a = JJ(a, b, c, d, e, X[0], 15); c = ROL(c, 10);
|
||||
e = JJ(e, a, b, c, d, X[5], 5); b = ROL(b, 10);
|
||||
d = JJ(d, e, a, b, c, X[9], 11); a = ROL(a, 10);
|
||||
c = JJ(c, d, e, a, b, X[7], 6); e = ROL(e, 10);
|
||||
b = JJ(b, c, d, e, a, X[12], 8); d = ROL(d, 10);
|
||||
a = JJ(a, b, c, d, e, X[2], 13); c = ROL(c, 10);
|
||||
e = JJ(e, a, b, c, d, X[10], 12); b = ROL(b, 10);
|
||||
d = JJ(d, e, a, b, c, X[14], 5); a = ROL(a, 10);
|
||||
c = JJ(c, d, e, a, b, X[1], 12); e = ROL(e, 10);
|
||||
b = JJ(b, c, d, e, a, X[3], 13); d = ROL(d, 10);
|
||||
a = JJ(a, b, c, d, e, X[8], 14); c = ROL(c, 10);
|
||||
e = JJ(e, a, b, c, d, X[11], 11); b = ROL(b, 10);
|
||||
d = JJ(d, e, a, b, c, X[6], 8); a = ROL(a, 10);
|
||||
c = JJ(c, d, e, a, b, X[15], 5); e = ROL(e, 10);
|
||||
b = JJ(b, c, d, e, a, X[13], 6); d = ROL(d, 10);
|
||||
|
||||
a1 = JJJ(a1, b1, c1, d1, e1, X[5], 8); c1 = ROL(c1, 10);
|
||||
e1 = JJJ(e1, a1, b1, c1, d1, X[14], 9); b1 = ROL(b1, 10);
|
||||
d1 = JJJ(d1, e1, a1, b1, c1, X[7], 9); a1 = ROL(a1, 10);
|
||||
c1 = JJJ(c1, d1, e1, a1, b1, X[0], 11); e1 = ROL(e1, 10);
|
||||
b1 = JJJ(b1, c1, d1, e1, a1, X[9], 13); d1 = ROL(d1, 10);
|
||||
a1 = JJJ(a1, b1, c1, d1, e1, X[2], 15); c1 = ROL(c1, 10);
|
||||
e1 = JJJ(e1, a1, b1, c1, d1, X[11], 15); b1 = ROL(b1, 10);
|
||||
d1 = JJJ(d1, e1, a1, b1, c1, X[4], 5); a1 = ROL(a1, 10);
|
||||
c1 = JJJ(c1, d1, e1, a1, b1, X[13], 7); e1 = ROL(e1, 10);
|
||||
b1 = JJJ(b1, c1, d1, e1, a1, X[6], 7); d1 = ROL(d1, 10);
|
||||
a1 = JJJ(a1, b1, c1, d1, e1, X[15], 8); c1 = ROL(c1, 10);
|
||||
e1 = JJJ(e1, a1, b1, c1, d1, X[8], 11); b1 = ROL(b1, 10);
|
||||
d1 = JJJ(d1, e1, a1, b1, c1, X[1], 14); a1 = ROL(a1, 10);
|
||||
c1 = JJJ(c1, d1, e1, a1, b1, X[10], 14); e1 = ROL(e1, 10);
|
||||
b1 = JJJ(b1, c1, d1, e1, a1, X[3], 12); d1 = ROL(d1, 10);
|
||||
a1 = JJJ(a1, b1, c1, d1, e1, X[12], 6); c1 = ROL(c1, 10);
|
||||
|
||||
e1 = III(e1, a1, b1, c1, d1, X[6], 9); b1 = ROL(b1, 10);
|
||||
d1 = III(d1, e1, a1, b1, c1, X[11], 13); a1 = ROL(a1, 10);
|
||||
c1 = III(c1, d1, e1, a1, b1, X[3], 15); e1 = ROL(e1, 10);
|
||||
b1 = III(b1, c1, d1, e1, a1, X[7], 7); d1 = ROL(d1, 10);
|
||||
a1 = III(a1, b1, c1, d1, e1, X[0], 12); c1 = ROL(c1, 10);
|
||||
e1 = III(e1, a1, b1, c1, d1, X[13], 8); b1 = ROL(b1, 10);
|
||||
d1 = III(d1, e1, a1, b1, c1, X[5], 9); a1 = ROL(a1, 10);
|
||||
c1 = III(c1, d1, e1, a1, b1, X[10], 11); e1 = ROL(e1, 10);
|
||||
b1 = III(b1, c1, d1, e1, a1, X[14], 7); d1 = ROL(d1, 10);
|
||||
a1 = III(a1, b1, c1, d1, e1, X[15], 7); c1 = ROL(c1, 10);
|
||||
e1 = III(e1, a1, b1, c1, d1, X[8], 12); b1 = ROL(b1, 10);
|
||||
d1 = III(d1, e1, a1, b1, c1, X[12], 7); a1 = ROL(a1, 10);
|
||||
c1 = III(c1, d1, e1, a1, b1, X[4], 6); e1 = ROL(e1, 10);
|
||||
b1 = III(b1, c1, d1, e1, a1, X[9], 15); d1 = ROL(d1, 10);
|
||||
a1 = III(a1, b1, c1, d1, e1, X[1], 13); c1 = ROL(c1, 10);
|
||||
e1 = III(e1, a1, b1, c1, d1, X[2], 11); b1 = ROL(b1, 10);
|
||||
|
||||
d1 = HHH(d1, e1, a1, b1, c1, X[15], 9); a1 = ROL(a1, 10);
|
||||
c1 = HHH(c1, d1, e1, a1, b1, X[5], 7); e1 = ROL(e1, 10);
|
||||
b1 = HHH(b1, c1, d1, e1, a1, X[1], 15); d1 = ROL(d1, 10);
|
||||
a1 = HHH(a1, b1, c1, d1, e1, X[3], 11); c1 = ROL(c1, 10);
|
||||
e1 = HHH(e1, a1, b1, c1, d1, X[7], 8); b1 = ROL(b1, 10);
|
||||
d1 = HHH(d1, e1, a1, b1, c1, X[14], 6); a1 = ROL(a1, 10);
|
||||
c1 = HHH(c1, d1, e1, a1, b1, X[6], 6); e1 = ROL(e1, 10);
|
||||
b1 = HHH(b1, c1, d1, e1, a1, X[9], 14); d1 = ROL(d1, 10);
|
||||
a1 = HHH(a1, b1, c1, d1, e1, X[11], 12); c1 = ROL(c1, 10);
|
||||
e1 = HHH(e1, a1, b1, c1, d1, X[8], 13); b1 = ROL(b1, 10);
|
||||
d1 = HHH(d1, e1, a1, b1, c1, X[12], 5); a1 = ROL(a1, 10);
|
||||
c1 = HHH(c1, d1, e1, a1, b1, X[2], 14); e1 = ROL(e1, 10);
|
||||
b1 = HHH(b1, c1, d1, e1, a1, X[10], 13); d1 = ROL(d1, 10);
|
||||
a1 = HHH(a1, b1, c1, d1, e1, X[0], 13); c1 = ROL(c1, 10);
|
||||
e1 = HHH(e1, a1, b1, c1, d1, X[4], 7); b1 = ROL(b1, 10);
|
||||
d1 = HHH(d1, e1, a1, b1, c1, X[13], 5); a1 = ROL(a1, 10);
|
||||
|
||||
c1 = GGG(c1, d1, e1, a1, b1, X[8], 15); e1 = ROL(e1, 10);
|
||||
b1 = GGG(b1, c1, d1, e1, a1, X[6], 5); d1 = ROL(d1, 10);
|
||||
a1 = GGG(a1, b1, c1, d1, e1, X[4], 8); c1 = ROL(c1, 10);
|
||||
e1 = GGG(e1, a1, b1, c1, d1, X[1], 11); b1 = ROL(b1, 10);
|
||||
d1 = GGG(d1, e1, a1, b1, c1, X[3], 14); a1 = ROL(a1, 10);
|
||||
c1 = GGG(c1, d1, e1, a1, b1, X[11], 14); e1 = ROL(e1, 10);
|
||||
b1 = GGG(b1, c1, d1, e1, a1, X[15], 6); d1 = ROL(d1, 10);
|
||||
a1 = GGG(a1, b1, c1, d1, e1, X[0], 14); c1 = ROL(c1, 10);
|
||||
e1 = GGG(e1, a1, b1, c1, d1, X[5], 6); b1 = ROL(b1, 10);
|
||||
d1 = GGG(d1, e1, a1, b1, c1, X[12], 9); a1 = ROL(a1, 10);
|
||||
c1 = GGG(c1, d1, e1, a1, b1, X[2], 12); e1 = ROL(e1, 10);
|
||||
b1 = GGG(b1, c1, d1, e1, a1, X[13], 9); d1 = ROL(d1, 10);
|
||||
a1 = GGG(a1, b1, c1, d1, e1, X[9], 12); c1 = ROL(c1, 10);
|
||||
e1 = GGG(e1, a1, b1, c1, d1, X[7], 5); b1 = ROL(b1, 10);
|
||||
d1 = GGG(d1, e1, a1, b1, c1, X[10], 15); a1 = ROL(a1, 10);
|
||||
c1 = GGG(c1, d1, e1, a1, b1, X[14], 8); e1 = ROL(e1, 10);
|
||||
|
||||
b1 = FFF(b1, c1, d1, e1, a1, X[12], 8); d1 = ROL(d1, 10);
|
||||
a1 = FFF(a1, b1, c1, d1, e1, X[15], 5); c1 = ROL(c1, 10);
|
||||
e1 = FFF(e1, a1, b1, c1, d1, X[10], 12); b1 = ROL(b1, 10);
|
||||
d1 = FFF(d1, e1, a1, b1, c1, X[4], 9); a1 = ROL(a1, 10);
|
||||
c1 = FFF(c1, d1, e1, a1, b1, X[1], 12); e1 = ROL(e1, 10);
|
||||
b1 = FFF(b1, c1, d1, e1, a1, X[5], 5); d1 = ROL(d1, 10);
|
||||
a1 = FFF(a1, b1, c1, d1, e1, X[8], 14); c1 = ROL(c1, 10);
|
||||
e1 = FFF(e1, a1, b1, c1, d1, X[7], 6); b1 = ROL(b1, 10);
|
||||
d1 = FFF(d1, e1, a1, b1, c1, X[0], 8); a1 = ROL(a1, 10);
|
||||
c1 = FFF(c1, d1, e1, a1, b1, X[6], 5); e1 = ROL(e1, 10);
|
||||
b1 = FFF(b1, c1, d1, e1, a1, X[11], 12); d1 = ROL(d1, 10);
|
||||
a1 = FFF(a1, b1, c1, d1, e1, X[13], 6); c1 = ROL(c1, 10);
|
||||
e1 = FFF(e1, a1, b1, c1, d1, X[2], 5); b1 = ROL(b1, 10);
|
||||
d1 = FFF(d1, e1, a1, b1, c1, X[14], 15); a1 = ROL(a1, 10);
|
||||
c1 = FFF(c1, d1, e1, a1, b1, X[9], 13); e1 = ROL(e1, 10);
|
||||
b1 = FFF(b1, c1, d1, e1, a1, X[3], 11); d1 = ROL(d1, 10);
|
||||
|
||||
var t = h1 + c + d1;
|
||||
h1 = h2 + d + e1;
|
||||
h2 = h3 + e + a1;
|
||||
h3 = h4 + a + b1;
|
||||
h4 = h0 + b + c1;
|
||||
h0 = t;
|
||||
}
|
||||
|
||||
var result = Bytes.alloc(20);
|
||||
result.set(0, h0 & 0xFF);
|
||||
result.set(1, (h0 >> 8) & 0xFF);
|
||||
result.set(2, (h0 >> 16) & 0xFF);
|
||||
result.set(3, (h0 >> 24) & 0xFF);
|
||||
result.set(4, h1 & 0xFF);
|
||||
result.set(5, (h1 >> 8) & 0xFF);
|
||||
result.set(6, (h1 >> 16) & 0xFF);
|
||||
result.set(7, (h1 >> 24) & 0xFF);
|
||||
result.set(8, h2 & 0xFF);
|
||||
result.set(9, (h2 >> 8) & 0xFF);
|
||||
result.set(10, (h2 >> 16) & 0xFF);
|
||||
result.set(11, (h2 >> 24) & 0xFF);
|
||||
result.set(12, h3 & 0xFF);
|
||||
result.set(13, (h3 >> 8) & 0xFF);
|
||||
result.set(14, (h3 >> 16) & 0xFF);
|
||||
result.set(15, (h3 >> 24) & 0xFF);
|
||||
result.set(16, h4 & 0xFF);
|
||||
result.set(17, (h4 >> 8) & 0xFF);
|
||||
result.set(18, (h4 >> 16) & 0xFF);
|
||||
result.set(19, (h4 >> 24) & 0xFF);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
969
leenkx/Sources/leenkx/network/torrent/Torrent.hx
Normal file
969
leenkx/Sources/leenkx/network/torrent/Torrent.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
407
leenkx/Sources/leenkx/network/torrent/TorrentClient.hx
Normal file
407
leenkx/Sources/leenkx/network/torrent/TorrentClient.hx
Normal 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
|
||||
}
|
||||
}
|
||||
102
leenkx/Sources/leenkx/network/torrent/TorrentFile.hx
Normal file
102
leenkx/Sources/leenkx/network/torrent/TorrentFile.hx
Normal file
@ -0,0 +1,102 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
import leenkx.network.torrent.MetaInfo.TorrentFileEntry;
|
||||
import leenkx.network.torrent.storage.Storage.IStorage;
|
||||
|
||||
class TorrentFile {
|
||||
public var name:String;
|
||||
public var path:String;
|
||||
public var length:Int;
|
||||
public var offset:Int;
|
||||
|
||||
var storage:IStorage;
|
||||
public var torrent:Torrent;
|
||||
|
||||
public function new(entry:TorrentFileEntry, storage:IStorage, ?torrent:Torrent) {
|
||||
this.path = entry.path;
|
||||
this.name = entry.path.split("/").pop();
|
||||
this.length = entry.length;
|
||||
this.offset = entry.offset;
|
||||
this.storage = storage;
|
||||
this.torrent = torrent;
|
||||
}
|
||||
|
||||
public var downloaded(get, never):Float;
|
||||
function get_downloaded():Float {
|
||||
if (torrent == null || torrent.pieces == null) return 0;
|
||||
var pl = torrent.meta.pieceLength;
|
||||
return Math.min(havePieces() * (pl:Float), length);
|
||||
}
|
||||
|
||||
public var progress(get, never):Float;
|
||||
function get_progress():Float {
|
||||
return length > 0 ? downloaded / length : 0;
|
||||
}
|
||||
|
||||
function havePieces():Int {
|
||||
var pl = torrent.meta.pieceLength;
|
||||
var first = Std.int(offset / pl);
|
||||
var last = length > 0
|
||||
? Std.int((offset + length - 1) / pl) : first;
|
||||
var n = 0;
|
||||
for (i in first...last + 1) {
|
||||
if (torrent.pieces.have[i]) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
public function select(?priority:Int):Void {
|
||||
if (torrent == null) return;
|
||||
torrent.select(offset, offset + length - 1, priority);
|
||||
}
|
||||
|
||||
public function deselect(?priority:Int):Void {
|
||||
if (torrent == null) return;
|
||||
torrent.deselect(offset, offset + length - 1, priority);
|
||||
}
|
||||
|
||||
public function critical():Void {
|
||||
if (torrent == null) return;
|
||||
torrent.critical(offset, offset + length - 1);
|
||||
}
|
||||
|
||||
public var type(get, never):String;
|
||||
function get_type():String {
|
||||
var i = name != null ? name.lastIndexOf(".") : -1;
|
||||
var ext = i >= 0 ? name.substr(i + 1).toLowerCase() : "";
|
||||
return switch (ext) {
|
||||
case "mp4" | "m4v": "video/mp4";
|
||||
case "webm": "video/webm";
|
||||
case "mkv": "video/x-matroska";
|
||||
case "mov": "video/quicktime";
|
||||
case "avi": "video/x-msvideo";
|
||||
case "mp3": "audio/mpeg";
|
||||
case "ogg": "audio/ogg";
|
||||
case "ogv": "video/ogg";
|
||||
case "flac": "audio/flac";
|
||||
case "wav": "audio/wav";
|
||||
case "aac": "audio/aac";
|
||||
case "jpg" | "jpeg": "image/jpeg";
|
||||
case "png": "image/png";
|
||||
case "gif": "image/gif";
|
||||
case "webp": "image/webp";
|
||||
case "svg": "image/svg+xml";
|
||||
case "txt" | "log" | "md": "text/plain";
|
||||
case "html" | "htm": "text/html";
|
||||
case "css": "text/css";
|
||||
case "json": "application/json";
|
||||
case "pdf": "application/pdf";
|
||||
case "zip": "application/zip";
|
||||
default: "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
public function getBytes():Bytes {
|
||||
return storage.read(offset, length);
|
||||
}
|
||||
|
||||
public function toString():String {
|
||||
return getBytes().toString();
|
||||
}
|
||||
}
|
||||
72
leenkx/Sources/leenkx/network/torrent/TorrentInfo.hx
Normal file
72
leenkx/Sources/leenkx/network/torrent/TorrentInfo.hx
Normal file
@ -0,0 +1,72 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
class TorrentInfo {
|
||||
|
||||
public static inline var PEERID_PREFIX = "-LX0001-";
|
||||
public static inline var PEERID_LENGTH = 20;
|
||||
public static inline var HASH_LENGTH = 20;
|
||||
public static inline var MIN_PIECE_LENGTH = 16384;
|
||||
|
||||
public static function pieceLength(size:Int):Int {
|
||||
var scaled = size < 1024 ? 1.0 : size / 1024;
|
||||
var exp = Std.int(Math.log(scaled) / Math.log(2) + 0.5);
|
||||
if (exp < 0) exp = 0;
|
||||
if (exp > 30) exp = 30;
|
||||
var len = 1 << exp;
|
||||
return len > MIN_PIECE_LENGTH ? len : MIN_PIECE_LENGTH;
|
||||
}
|
||||
|
||||
public static function pieces(content:Bytes, plen:Int):Bytes {
|
||||
var count = content.length == 0 ? 0 : Math.ceil(content.length / plen);
|
||||
var out = Bytes.alloc(count * HASH_LENGTH);
|
||||
for (i in 0...count) {
|
||||
var off = i * plen;
|
||||
var len = content.length - off;
|
||||
if (len > plen) len = plen;
|
||||
var sha = haxe.crypto.Sha1.make(content.sub(off, len));
|
||||
out.blit(i * HASH_LENGTH, sha, 0, HASH_LENGTH);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static function infoDict(name:String, content:Bytes):Dynamic {
|
||||
var plen = pieceLength(content.length);
|
||||
var info:Dynamic = {};
|
||||
Reflect.setField(info, "length", content.length);
|
||||
Reflect.setField(info, "name", name);
|
||||
Reflect.setField(info, "piece length", plen);
|
||||
Reflect.setField(info, "pieces", pieces(content, plen));
|
||||
return info;
|
||||
}
|
||||
|
||||
public static function infoHash(name:String, content:Bytes):Bytes {
|
||||
return haxe.crypto.Sha1.make(Bencode.encode(infoDict(name, content)));
|
||||
}
|
||||
|
||||
public static function identifierHash(identifier:String):Bytes {
|
||||
return infoHash(identifier, Bytes.ofString(identifier));
|
||||
}
|
||||
|
||||
public static function peerId():Bytes {
|
||||
var prefix = Bytes.ofString(PEERID_PREFIX);
|
||||
var random = Bytes.ofString(Crypto.toHex(Crypto.randomBytes(6)));
|
||||
var out = Bytes.alloc(PEERID_LENGTH);
|
||||
out.blit(0, prefix, 0, prefix.length);
|
||||
out.blit(prefix.length, random, 0, PEERID_LENGTH - prefix.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
public static function toBinaryString(bytes:Bytes):String {
|
||||
var sb = new StringBuf();
|
||||
for (i in 0...bytes.length) sb.addChar(bytes.get(i));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static function fromBinaryString(str:String):Bytes {
|
||||
var bytes = Bytes.alloc(str.length);
|
||||
for (i in 0...str.length) bytes.set(i, str.charCodeAt(i) & 0xFF);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
733
leenkx/Sources/leenkx/network/torrent/TweetNaCl.hx
Normal file
733
leenkx/Sources/leenkx/network/torrent/TweetNaCl.hx
Normal file
@ -0,0 +1,733 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
class TweetNaCl {
|
||||
|
||||
static var _0:Array<Int> = [for (i in 0...16) 0];
|
||||
static var _9:Array<Int> = { var a = [for (i in 0...32) 0]; a[0] = 9; a; };
|
||||
static var sigma:Array<Int> = [101, 120, 112, 97, 110, 100, 32, 51, 50, 45, 98, 121, 116, 101, 32, 107];
|
||||
static var minusp:Array<Int> = [5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252];
|
||||
static var L:Array<Float> = [0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10];
|
||||
|
||||
static inline function gf(?init:Array<Float>):Array<Float> {
|
||||
var r = [for (i in 0...16) 0.0];
|
||||
if (init != null) for (i in 0...init.length) r[i] = init[i];
|
||||
return r;
|
||||
}
|
||||
|
||||
static var gf0 = gf();
|
||||
static var gf1 = gf([1]);
|
||||
static var _121665 = gf([0xdb41, 1]);
|
||||
static var D = gf([0x78a3, 0x1359, 0x4dca, 0x75eb, 0xd8ab, 0x4141, 0x0a4d, 0x0070, 0xe898, 0x7779, 0x4079, 0x8cc7, 0xfe73, 0x2b6f, 0x6cee, 0x5203]);
|
||||
static var D2 = gf([0xf159, 0x26b2, 0x9b94, 0xebd6, 0xb156, 0x8283, 0x149a, 0x00e0, 0xd130, 0xeef3, 0x80f2, 0x198e, 0xfce7, 0x56df, 0xd9dc, 0x2406]);
|
||||
static var X = gf([0xd51a, 0x8f25, 0x2d60, 0xc956, 0xa7b2, 0x9525, 0xc760, 0x692c, 0xdc5c, 0xfdd6, 0xe231, 0xc0a4, 0x53fe, 0xcd6e, 0x36d3, 0x2169]);
|
||||
static var Y = gf([0x6658, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666, 0x6666]);
|
||||
static var I = gf([0xa0b0, 0x4a0e, 0x1b27, 0xc4ee, 0xe478, 0xad2f, 0x1806, 0x2f43, 0xd7a7, 0x3dfb, 0x0099, 0x2b4d, 0xdf0b, 0x4fc1, 0x2480, 0x2b83]);
|
||||
|
||||
static var K:Array<Array<Int>> = [
|
||||
[0x428a2f98, 0xd728ae22], [0x71374491, 0x23ef65cd], [0xb5c0fbcf, 0xec4d3b2f], [0xe9b5dba5, 0x8189dbbc],
|
||||
[0x3956c25b, 0xf348b538], [0x59f111f1, 0xb605d019], [0x923f82a4, 0xaf194f9b], [0xab1c5ed5, 0xda6d8118],
|
||||
[0xd807aa98, 0xa3030242], [0x12835b01, 0x45706fbe], [0x243185be, 0x4ee4b28c], [0x550c7dc3, 0xd5ffb4e2],
|
||||
[0x72be5d74, 0xf27b896f], [0x80deb1fe, 0x3b1696b1], [0x9bdc06a7, 0x25c71235], [0xc19bf174, 0xcf692694],
|
||||
[0xe49b69c1, 0x9ef14ad2], [0xefbe4786, 0x384f25e3], [0x0fc19dc6, 0x8b8cd5b5], [0x240ca1cc, 0x77ac9c65],
|
||||
[0x2de92c6f, 0x592b0275], [0x4a7484aa, 0x6ea6e483], [0x5cb0a9dc, 0xbd41fbd4], [0x76f988da, 0x831153b5],
|
||||
[0x983e5152, 0xee66dfab], [0xa831c66d, 0x2db43210], [0xb00327c8, 0x98fb213f], [0xbf597fc7, 0xbeef0ee4],
|
||||
[0xc6e00bf3, 0x3da88fc2], [0xd5a79147, 0x930aa725], [0x06ca6351, 0xe003826f], [0x14292967, 0x0a0e6e70],
|
||||
[0x27b70a85, 0x46d22ffc], [0x2e1b2138, 0x5c26c926], [0x4d2c6dfc, 0x5ac42aed], [0x53380d13, 0x9d95b3df],
|
||||
[0x650a7354, 0x8baf63de], [0x766a0abb, 0x3c77b2a8], [0x81c2c92e, 0x47edaee6], [0x92722c85, 0x1482353b],
|
||||
[0xa2bfe8a1, 0x4cf10364], [0xa81a664b, 0xbc423001], [0xc24b8b70, 0xd0f89791], [0xc76c51a3, 0x0654be30],
|
||||
[0xd192e819, 0xd6ef5218], [0xd6990624, 0x5565a910], [0xf40e3585, 0x5771202a], [0x106aa070, 0x32bbd1b8],
|
||||
[0x19a4c116, 0xb8d2d0c8], [0x1e376c08, 0x5141ab53], [0x2748774c, 0xdf8eeb99], [0x34b0bcb5, 0xe19b48a8],
|
||||
[0x391c0cb3, 0xc5c95a63], [0x4ed8aa4a, 0xe3418acb], [0x5b9cca4f, 0x7763e373], [0x682e6ff3, 0xd6b2b8a3],
|
||||
[0x748f82ee, 0x5defb2fc], [0x78a5636f, 0x43172f60], [0x84c87814, 0xa1f0ab72], [0x8cc70208, 0x1a6439ec],
|
||||
[0x90befffa, 0x23631e28], [0xa4506ceb, 0xde82bde9], [0xbef9a3f7, 0xb2c67915], [0xc67178f2, 0xe372532b],
|
||||
[0xca273ece, 0xea26619c], [0xd186b8c7, 0x21c0c207], [0xeada7dd6, 0xcde0eb1e], [0xf57d4f7f, 0xee6ed178],
|
||||
[0x06f067aa, 0x72176fba], [0x0a637dc5, 0xa2c898a6], [0x113f9804, 0xbef90dae], [0x1b710b35, 0x131c471b],
|
||||
[0x28db77f5, 0x23047d84], [0x32caab7b, 0x40c72493], [0x3c9ebe0a, 0x15c9bebc], [0x431d67c4, 0x9c100d4c],
|
||||
[0x4cc5d4be, 0xcb3e42b6], [0x597f299c, 0xfc657e2a], [0x5fcb6fab, 0x3ad6faec], [0x6c44198c, 0x4a475817]
|
||||
];
|
||||
|
||||
static var iv:Array<Int> = [
|
||||
0x6a, 0x09, 0xe6, 0x67, 0xf3, 0xbc, 0xc9, 0x08,
|
||||
0xbb, 0x67, 0xae, 0x85, 0x84, 0xca, 0xa7, 0x3b,
|
||||
0x3c, 0x6e, 0xf3, 0x72, 0xfe, 0x94, 0xf8, 0x2b,
|
||||
0xa5, 0x4f, 0xf5, 0x3a, 0x5f, 0x1d, 0x36, 0xf1,
|
||||
0x51, 0x0e, 0x52, 0x7f, 0xad, 0xe6, 0x82, 0xd1,
|
||||
0x9b, 0x05, 0x68, 0x8c, 0x2b, 0x3e, 0x6c, 0x1f,
|
||||
0x1f, 0x83, 0xd9, 0xab, 0xfb, 0x41, 0xbd, 0x6b,
|
||||
0x5b, 0xe0, 0xcd, 0x19, 0x13, 0x7e, 0x21, 0x79
|
||||
];
|
||||
|
||||
static inline function u64(h:Int, l:Int):Array<Int> { return [h, l]; }
|
||||
static inline function u64add(a:Array<Int>, b:Array<Int>):Array<Int> {
|
||||
var a0 = a[1] & 65535, a1 = a[1] >>> 16, a2 = a[0] & 65535, a3 = a[0] >>> 16;
|
||||
var b0 = b[1] & 65535, b1 = b[1] >>> 16, b2 = b[0] & 65535, b3 = b[0] >>> 16;
|
||||
var s0 = a0 + b0; var s1 = a1 + b1 + (s0 >>> 16);
|
||||
var s2 = a2 + b2 + (s1 >>> 16); var s3 = a3 + b3 + (s2 >>> 16);
|
||||
return [(s2 & 65535) | ((s3 & 65535) << 16), (s0 & 65535) | ((s1 & 65535) << 16)];
|
||||
}
|
||||
static function u64addMulti(args:Array<Array<Int>>):Array<Int> {
|
||||
var a0 = 0, a1 = 0, a2 = 0, a3 = 0;
|
||||
for (v in args) {
|
||||
a0 += v[1] & 65535; a1 += v[1] >>> 16;
|
||||
a2 += v[0] & 65535; a3 += v[0] >>> 16;
|
||||
}
|
||||
a1 += a0 >>> 16; a2 += a1 >>> 16; a3 += a2 >>> 16;
|
||||
return [(a2 & 65535) | ((a3 & 65535) << 16), (a0 & 65535) | ((a1 & 65535) << 16)];
|
||||
}
|
||||
static inline function u64xor(a:Array<Int>, b:Array<Int>):Array<Int> {
|
||||
return [a[0] ^ b[0], a[1] ^ b[1]];
|
||||
}
|
||||
static function u64xorMulti(args:Array<Array<Int>>):Array<Int> {
|
||||
var h = 0, l = 0;
|
||||
for (v in args) { h ^= v[0]; l ^= v[1]; }
|
||||
return [h, l];
|
||||
}
|
||||
static function u64R(x:Array<Int>, c:Int):Array<Int> {
|
||||
var h:Int, l:Int, c1 = 32 - c;
|
||||
if (c < 32) {
|
||||
h = (x[0] >>> c) | (x[1] << c1);
|
||||
l = (x[1] >>> c) | (x[0] << c1);
|
||||
} else {
|
||||
h = (x[1] >>> (c - 32)) | (x[0] << (64 - c));
|
||||
l = (x[0] >>> (c - 32)) | (x[1] << (64 - c));
|
||||
}
|
||||
return [h, l];
|
||||
}
|
||||
static inline function u64shr(x:Array<Int>, c:Int):Array<Int> {
|
||||
return [x[0] >>> c, (x[1] >>> c) | (x[0] << (32 - c))];
|
||||
}
|
||||
static inline function u64Ch(x:Array<Int>, y:Array<Int>, z:Array<Int>):Array<Int> {
|
||||
return [(x[0] & y[0]) ^ (~x[0] & z[0]), (x[1] & y[1]) ^ (~x[1] & z[1])];
|
||||
}
|
||||
static inline function u64Maj(x:Array<Int>, y:Array<Int>, z:Array<Int>):Array<Int> {
|
||||
return [(x[0] & y[0]) ^ (x[0] & z[0]) ^ (y[0] & z[0]), (x[1] & y[1]) ^ (x[1] & z[1]) ^ (y[1] & z[1])];
|
||||
}
|
||||
static inline function u64Sigma0(x:Array<Int>):Array<Int> { return u64xorMulti([u64R(x, 28), u64R(x, 34), u64R(x, 39)]); }
|
||||
static inline function u64Sigma1(x:Array<Int>):Array<Int> { return u64xorMulti([u64R(x, 14), u64R(x, 18), u64R(x, 41)]); }
|
||||
static inline function u64sigma0(x:Array<Int>):Array<Int> { return u64xorMulti([u64R(x, 1), u64R(x, 8), u64shr(x, 7)]); }
|
||||
static inline function u64sigma1(x:Array<Int>):Array<Int> { return u64xorMulti([u64R(x, 19), u64R(x, 61), u64shr(x, 6)]); }
|
||||
|
||||
static function dl64(x:Array<Int>, i:Int):Array<Int> {
|
||||
var h = (x[i] << 24) | (x[i + 1] << 16) | (x[i + 2] << 8) | x[i + 3];
|
||||
var l = (x[i + 4] << 24) | (x[i + 5] << 16) | (x[i + 6] << 8) | x[i + 7];
|
||||
return [h, l];
|
||||
}
|
||||
|
||||
static function ts64u(x:Array<Int>, i:Int, v:Array<Int>):Void {
|
||||
x[i] = (v[0] >> 24) & 0xff;
|
||||
x[i + 1] = (v[0] >> 16) & 0xff;
|
||||
x[i + 2] = (v[0] >> 8) & 0xff;
|
||||
x[i + 3] = v[0] & 0xff;
|
||||
x[i + 4] = (v[1] >> 24) & 0xff;
|
||||
x[i + 5] = (v[1] >> 16) & 0xff;
|
||||
x[i + 6] = (v[1] >> 8) & 0xff;
|
||||
x[i + 7] = v[1] & 0xff;
|
||||
}
|
||||
|
||||
static function L32(x:Int, c:Int):Int { return (x << c) | (x >>> (32 - c)); }
|
||||
|
||||
static function ld32(x:Array<Int>, i:Int):Int {
|
||||
var u = x[i + 3] & 0xff;
|
||||
u = (u << 8) | (x[i + 2] & 0xff);
|
||||
u = (u << 8) | (x[i + 1] & 0xff);
|
||||
return (u << 8) | (x[i + 0] & 0xff);
|
||||
}
|
||||
|
||||
static function st32(x:Array<Int>, j:Int, u:Int):Void {
|
||||
for (i in 0...4) { x[j + i] = u & 255; u = u >>> 8; }
|
||||
}
|
||||
|
||||
static function vn(x:Array<Int>, xi:Int, y:Array<Int>, yi:Int, n:Int):Int {
|
||||
var d = 0;
|
||||
for (i in 0...n) d |= x[xi + i] ^ y[yi + i];
|
||||
return (1 & ((d - 1) >>> 8)) - 1;
|
||||
}
|
||||
|
||||
static function crypto_verify_16(x:Array<Int>, xi:Int, y:Array<Int>, yi:Int):Int { return vn(x, xi, y, yi, 16); }
|
||||
static function crypto_verify_32(x:Array<Int>, xi:Int, y:Array<Int>, yi:Int):Int { return vn(x, xi, y, yi, 32); }
|
||||
|
||||
static function core(out:Array<Int>, inp:Array<Int>, k:Array<Int>, c:Array<Int>, h:Bool):Void {
|
||||
var w = [for (i in 0...16) 0], x = [for (i in 0...16) 0], y = [for (i in 0...16) 0], t = [for (i in 0...4) 0];
|
||||
for (i in 0...4) {
|
||||
x[5 * i] = ld32(c, 4 * i); x[1 + i] = ld32(k, 4 * i);
|
||||
x[6 + i] = ld32(inp, 4 * i); x[11 + i] = ld32(k, 16 + 4 * i);
|
||||
}
|
||||
for (i in 0...16) y[i] = x[i];
|
||||
for (i in 0...20) {
|
||||
for (j in 0...4) {
|
||||
for (m in 0...4) t[m] = x[(5 * j + 4 * m) % 16];
|
||||
t[1] ^= L32(t[0] + t[3], 7); t[2] ^= L32(t[1] + t[0], 9);
|
||||
t[3] ^= L32(t[2] + t[1], 13); t[0] ^= L32(t[3] + t[2], 18);
|
||||
for (m in 0...4) w[4 * j + (j + m) % 4] = t[m];
|
||||
}
|
||||
for (m in 0...16) x[m] = w[m];
|
||||
}
|
||||
if (h) {
|
||||
for (i in 0...16) x[i] += y[i];
|
||||
for (i in 0...4) { x[5 * i] -= ld32(c, 4 * i); x[6 + i] -= ld32(inp, 4 * i); }
|
||||
for (i in 0...4) { st32(out, 4 * i, x[5 * i]); st32(out, 16 + 4 * i, x[6 + i]); }
|
||||
} else {
|
||||
for (i in 0...16) st32(out, 4 * i, x[i] + y[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static function crypto_stream_salsa20_xor(c:Array<Int>, cpos:Int, m:Array<Int>, mpos:Int, b:Int, n:Array<Int>, k:Array<Int>):Int {
|
||||
var z = [for (i in 0...16) 0], x = [for (i in 0...64) 0];
|
||||
if (b == 0) return 0;
|
||||
for (i in 0...8) z[i] = n[i];
|
||||
while (b >= 64) {
|
||||
crypto_core_salsa20(x, z, k, sigma);
|
||||
for (i in 0...64) c[cpos + i] = (m != null ? m[mpos + i] : 0) ^ x[i];
|
||||
var u = 1;
|
||||
for (i in 8...16) { u += z[i]; z[i] = u & 0xff; u >>>= 8; }
|
||||
b -= 64; cpos += 64; if (m != null) mpos += 64;
|
||||
}
|
||||
if (b > 0) {
|
||||
crypto_core_salsa20(x, z, k, sigma);
|
||||
for (i in 0...b) c[cpos + i] = (m != null ? m[mpos + i] : 0) ^ x[i];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static function crypto_stream(c:Array<Int>, cpos:Int, d:Int, n:Array<Int>, k:Array<Int>):Int {
|
||||
var np = [for (i in 0...32) 0];
|
||||
for (i in 0...n.length) np[i] = n[i];
|
||||
var s = [for (i in 0...32) 0];
|
||||
crypto_core_hsalsa20(s, np, k, sigma);
|
||||
var n2 = [for (i in 0...16) 0];
|
||||
for (i in 0...16) n2[i] = np[16 + i];
|
||||
return crypto_stream_salsa20(c, cpos, d, n2, s);
|
||||
}
|
||||
|
||||
static function crypto_stream_xor(c:Array<Int>, cpos:Int, m:Array<Int>, mpos:Int, d:Int, n:Array<Int>, k:Array<Int>):Int {
|
||||
var np = [for (i in 0...32) 0];
|
||||
for (i in 0...n.length) np[i] = n[i];
|
||||
var s = [for (i in 0...32) 0];
|
||||
crypto_core_hsalsa20(s, np, k, sigma);
|
||||
var n2 = [for (i in 0...16) 0];
|
||||
for (i in 0...16) n2[i] = np[16 + i];
|
||||
return crypto_stream_salsa20_xor(c, cpos, m, mpos, d, n2, s);
|
||||
}
|
||||
|
||||
static inline function crypto_core_salsa20(out:Array<Int>, inp:Array<Int>, k:Array<Int>, c:Array<Int>):Int { core(out, inp, k, c, false); return 0; }
|
||||
static inline function crypto_core_hsalsa20(out:Array<Int>, inp:Array<Int>, k:Array<Int>, c:Array<Int>):Int { core(out, inp, k, c, true); return 0; }
|
||||
static inline function crypto_stream_salsa20(c:Array<Int>, cpos:Int, d:Int, n:Array<Int>, k:Array<Int>):Int { return crypto_stream_salsa20_xor(c, cpos, null, 0, d, n, k); }
|
||||
|
||||
static function add1305(h:Array<Int>, c:Array<Int>):Void {
|
||||
var u = 0;
|
||||
for (j in 0...17) { u += h[j] + c[j]; h[j] = u & 255; u >>>= 8; }
|
||||
}
|
||||
|
||||
static function crypto_onetimeauth(out:Array<Int>, outpos:Int, m:Array<Int>, mpos:Int, n:Int, k:Array<Int>):Int {
|
||||
var s:Int, i:Int, j:Int, u:Int;
|
||||
var x = [for (i in 0...17) 0], r = [for (i in 0...17) 0], h = [for (i in 0...17) 0], c = [for (i in 0...17) 0], g = [for (i in 0...17) 0];
|
||||
for (j in 0...17) r[j] = h[j] = 0;
|
||||
for (j in 0...16) r[j] = k[j];
|
||||
r[3] &= 15; r[4] &= 252; r[7] &= 15; r[8] &= 252; r[11] &= 15; r[12] &= 252; r[15] &= 15;
|
||||
while (n > 0) {
|
||||
for (j in 0...17) c[j] = 0;
|
||||
j = 0; while (j < 16 && j < n) { c[j] = m[mpos + j]; j++; }
|
||||
c[j] = 1; mpos += j; n -= j;
|
||||
add1305(h, c);
|
||||
for (i in 0...17) {
|
||||
x[i] = 0;
|
||||
for (j in 0...17) x[i] += Std.int(h[j] * ((j <= i) ? r[i - j] : (320 * r[i + 17 - j])));
|
||||
}
|
||||
for (i in 0...17) h[i] = x[i];
|
||||
u = 0;
|
||||
for (j in 0...16) { u += h[j]; h[j] = u & 255; u >>>= 8; }
|
||||
u += h[16]; h[16] = u & 3; u = 5 * (u >>> 2);
|
||||
for (j in 0...16) { u += h[j]; h[j] = u & 255; u >>>= 8; }
|
||||
u += h[16]; h[16] = u;
|
||||
}
|
||||
for (j in 0...17) g[j] = h[j];
|
||||
add1305(h, minusp);
|
||||
s = -(h[16] >>> 7);
|
||||
for (j in 0...17) h[j] ^= s & (g[j] ^ h[j]);
|
||||
for (j in 0...16) c[j] = k[j + 16];
|
||||
c[16] = 0; add1305(h, c);
|
||||
for (j in 0...16) out[outpos + j] = h[j];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static function crypto_secretbox(c:Array<Int>, m:Array<Int>, d:Int, n:Array<Int>, k:Array<Int>):Int {
|
||||
if (d < 32) return -1;
|
||||
crypto_stream_xor(c, 0, m, 0, d, n, k);
|
||||
crypto_onetimeauth(c, 16, c, 32, d - 32, c);
|
||||
for (i in 0...16) c[i] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static function crypto_secretbox_open(m:Array<Int>, c:Array<Int>, d:Int, n:Array<Int>, k:Array<Int>):Int {
|
||||
var x = [for (i in 0...32) 0];
|
||||
if (d < 32) return -1;
|
||||
crypto_stream(x, 0, 32, n, k);
|
||||
if (crypto_onetimeauth_verify(c, 16, c, 32, d - 32, x) != 0) return -1;
|
||||
crypto_stream_xor(m, 0, c, 0, d, n, k);
|
||||
for (i in 0...32) m[i] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline function crypto_onetimeauth_verify(h:Array<Int>, hpos:Int, m:Array<Int>, mpos:Int, n:Int, k:Array<Int>):Int {
|
||||
var x = [for (i in 0...16) 0];
|
||||
crypto_onetimeauth(x, 0, m, mpos, n, k);
|
||||
return crypto_verify_16(h, hpos, x, 0);
|
||||
}
|
||||
|
||||
static function set25519(r:Array<Float>, a:Array<Float>):Void { for (i in 0...16) r[i] = a[i]; }
|
||||
|
||||
static function car25519(o:Array<Float>):Void {
|
||||
var c:Float;
|
||||
for (i in 0...16) {
|
||||
o[i] += 65536;
|
||||
c = Math.floor(o[i] / 65536);
|
||||
o[(i + 1) * (i < 15 ? 1 : 0)] += c - 1 + 37 * (c - 1) * (i == 15 ? 1 : 0);
|
||||
o[i] -= c * 65536;
|
||||
}
|
||||
}
|
||||
|
||||
static function sel25519(p:Array<Float>, q:Array<Float>, b:Int):Void {
|
||||
var c = ~((b - 1));
|
||||
for (i in 0...16) {
|
||||
var t:Int = c & (Std.int(p[i]) ^ Std.int(q[i]));
|
||||
p[i] = Std.int(p[i]) ^ t; q[i] = Std.int(q[i]) ^ t;
|
||||
}
|
||||
}
|
||||
|
||||
static function pack25519(o:Array<Int>, n:Array<Float>):Void {
|
||||
var m = gf(), t = gf();
|
||||
for (i in 0...16) t[i] = n[i];
|
||||
car25519(t); car25519(t); car25519(t);
|
||||
for (j in 0...2) {
|
||||
m[0] = t[0] - 0xffed;
|
||||
for (i in 1...15) {
|
||||
m[i] = t[i] - 0xffff - ((Std.int(m[i - 1]) >> 16) & 1);
|
||||
m[i - 1] = Std.int(m[i - 1]) & 0xffff;
|
||||
}
|
||||
m[15] = t[15] - 0x7fff - ((Std.int(m[14]) >> 16) & 1);
|
||||
var b = (Std.int(m[15]) >> 16) & 1;
|
||||
m[14] = Std.int(m[14]) & 0xffff;
|
||||
sel25519(t, m, 1 - b);
|
||||
}
|
||||
for (i in 0...16) {
|
||||
o[2 * i] = Std.int(t[i]) & 0xff;
|
||||
o[2 * i + 1] = Std.int(t[i]) >> 8;
|
||||
}
|
||||
}
|
||||
|
||||
static function neq25519(a:Array<Float>, b:Array<Float>):Int {
|
||||
var c = [for (i in 0...32) 0], d = [for (i in 0...32) 0];
|
||||
pack25519(c, a); pack25519(d, b);
|
||||
return crypto_verify_32(c, 0, d, 0);
|
||||
}
|
||||
|
||||
static function par25519(a:Array<Float>):Int {
|
||||
var d = [for (i in 0...32) 0];
|
||||
pack25519(d, a);
|
||||
return d[0] & 1;
|
||||
}
|
||||
|
||||
static function unpack25519(o:Array<Float>, n:Array<Int>):Void {
|
||||
for (i in 0...16) o[i] = n[2 * i] + (n[2 * i + 1] << 8);
|
||||
o[15] = Std.int(o[15]) & 0x7fff;
|
||||
}
|
||||
|
||||
static function A(o:Array<Float>, a:Array<Float>, b:Array<Float>):Void { for (i in 0...16) o[i] = a[i] + b[i]; }
|
||||
static function Z(o:Array<Float>, a:Array<Float>, b:Array<Float>):Void { for (i in 0...16) o[i] = a[i] - b[i]; }
|
||||
|
||||
static function M(o:Array<Float>, a:Array<Float>, b:Array<Float>):Void {
|
||||
var t = [for (i in 0...31) 0.0];
|
||||
for (i in 0...16) for (j in 0...16) t[i + j] += a[i] * b[j];
|
||||
for (i in 0...15) t[i] += 38 * t[i + 16];
|
||||
for (i in 0...16) o[i] = t[i];
|
||||
car25519(o); car25519(o);
|
||||
}
|
||||
|
||||
static inline function S(o:Array<Float>, a:Array<Float>):Void { M(o, a, a); }
|
||||
|
||||
static function inv25519(o:Array<Float>, i:Array<Float>):Void {
|
||||
var c = gf();
|
||||
for (a in 0...16) c[a] = i[a];
|
||||
var a = 253; while (a >= 0) {
|
||||
S(c, c);
|
||||
if (a != 2 && a != 4) M(c, c, i);
|
||||
a--;
|
||||
}
|
||||
for (a in 0...16) o[a] = c[a];
|
||||
}
|
||||
|
||||
static function pow2523(o:Array<Float>, i:Array<Float>):Void {
|
||||
var c = gf();
|
||||
for (a in 0...16) c[a] = i[a];
|
||||
var a = 250; while (a >= 0) {
|
||||
S(c, c);
|
||||
if (a != 1) M(c, c, i);
|
||||
a--;
|
||||
}
|
||||
for (a in 0...16) o[a] = c[a];
|
||||
}
|
||||
|
||||
static function crypto_scalarmult(q:Array<Int>, n:Array<Int>, p:Array<Int>):Int {
|
||||
var z = [for (i in 0...32) 0];
|
||||
var x = [for (i in 0...80) 0.0];
|
||||
var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf();
|
||||
for (i in 0...31) z[i] = n[i];
|
||||
z[31] = (n[31] & 127) | 64;
|
||||
z[0] &= 248;
|
||||
unpack25519(x, p);
|
||||
for (i in 0...16) { b[i] = x[i]; d[i] = a[i] = c[i] = 0; }
|
||||
a[0] = d[0] = 1;
|
||||
var r:Int;
|
||||
var i = 254; while (i >= 0) {
|
||||
r = (z[i >>> 3] >>> (i & 7)) & 1;
|
||||
sel25519(a, b, r); sel25519(c, d, r);
|
||||
A(e, a, c); Z(a, a, c); A(c, b, d); Z(b, b, d);
|
||||
S(d, e); S(f, a); M(a, c, a); M(c, b, e);
|
||||
A(e, a, c); Z(a, a, c); S(b, a); Z(c, d, f);
|
||||
M(a, c, _121665); A(a, a, d); M(c, c, a); M(a, d, f); M(d, b, x);
|
||||
S(b, e); sel25519(a, b, r); sel25519(c, d, r);
|
||||
i--;
|
||||
}
|
||||
for (i in 0...16) {
|
||||
x[i + 16] = a[i]; x[i + 32] = c[i]; x[i + 48] = b[i]; x[i + 64] = d[i];
|
||||
}
|
||||
var x32 = [for (i in 0...16) 0.0]; for (i in 0...16) x32[i] = x[i + 32];
|
||||
var x16 = [for (i in 0...16) 0.0]; for (i in 0...16) x16[i] = x[i + 16];
|
||||
inv25519(x32, x32);
|
||||
M(x16, x16, x32);
|
||||
pack25519(q, x16);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static function crypto_scalarmult_base(q:Array<Int>, n:Array<Int>):Int { return crypto_scalarmult(q, n, _9); }
|
||||
|
||||
static function crypto_box_beforenm(k:Array<Int>, y:Array<Int>, x:Array<Int>):Int {
|
||||
var s = [for (i in 0...32) 0];
|
||||
crypto_scalarmult(s, x, y);
|
||||
return crypto_core_hsalsa20(k, _0, s, sigma);
|
||||
}
|
||||
|
||||
static function crypto_box(c:Array<Int>, m:Array<Int>, d:Int, n:Array<Int>, y:Array<Int>, x:Array<Int>):Int {
|
||||
var k = [for (i in 0...32) 0];
|
||||
crypto_box_beforenm(k, y, x);
|
||||
return crypto_secretbox(c, m, d, n, k);
|
||||
}
|
||||
|
||||
static function crypto_box_open(m:Array<Int>, c:Array<Int>, d:Int, n:Array<Int>, y:Array<Int>, x:Array<Int>):Int {
|
||||
var k = [for (i in 0...32) 0];
|
||||
crypto_box_beforenm(k, y, x);
|
||||
return crypto_secretbox_open(m, c, d, n, k);
|
||||
}
|
||||
|
||||
static function crypto_box_keypair(y:Array<Int>, x:Array<Int>, randomFn:Int->Array<Int>):Int {
|
||||
var rb = randomFn(32);
|
||||
for (i in 0...32) x[i] = rb[i];
|
||||
return crypto_scalarmult_base(y, x);
|
||||
}
|
||||
|
||||
static function crypto_hashblocks(x:Array<Int>, m:Array<Int>, mpos:Int, n:Int):Int {
|
||||
var z = [for (i in 0...8) null], b = [for (i in 0...8) null], a = [for (i in 0...8) null], w = [for (i in 0...16) null];
|
||||
for (i in 0...8) z[i] = a[i] = dl64(x, 8 * i);
|
||||
while (n >= 128) {
|
||||
for (i in 0...16) w[i] = dl64(m, 8 * i + mpos);
|
||||
for (i in 0...80) {
|
||||
for (j in 0...8) b[j] = a[j];
|
||||
var t = u64addMulti([a[7], u64Sigma1(a[4]), u64Ch(a[4], a[5], a[6]), u64(K[i][0], K[i][1]), w[i % 16]]);
|
||||
b[7] = u64addMulti([t, u64Sigma0(a[0]), u64Maj(a[0], a[1], a[2])]);
|
||||
b[3] = u64add(b[3], t);
|
||||
for (j in 0...8) a[(j + 1) % 8] = b[j];
|
||||
if (i % 16 == 15) {
|
||||
for (j in 0...16) w[j] = u64addMulti([w[j], w[(j + 9) % 16], u64sigma0(w[(j + 1) % 16]), u64sigma1(w[(j + 14) % 16])]);
|
||||
}
|
||||
}
|
||||
for (i in 0...8) { a[i] = u64add(a[i], z[i]); z[i] = a[i]; }
|
||||
mpos += 128; n -= 128;
|
||||
}
|
||||
for (i in 0...8) ts64u(x, 8 * i, z[i]);
|
||||
return n;
|
||||
}
|
||||
|
||||
static function crypto_hash(out:Array<Int>, m:Array<Int>, n:Int):Int {
|
||||
var h = [for (i in 0...64) 0], x = [for (i in 0...256) 0];
|
||||
var b = n;
|
||||
for (i in 0...64) h[i] = iv[i];
|
||||
crypto_hashblocks(h, m, 0, n);
|
||||
n = n % 128;
|
||||
for (i in 0...256) x[i] = 0;
|
||||
for (i in 0...n) x[i] = m[b - n + i];
|
||||
x[n] = 128;
|
||||
var sz = 256 - 128 * (n < 112 ? 1 : 0);
|
||||
x[sz - 9] = 0;
|
||||
ts64u(x, sz - 8, u64(Std.int(b / 0x20000000), b << 3));
|
||||
crypto_hashblocks(h, x, 0, sz);
|
||||
for (i in 0...64) out[i] = h[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static function edAdd(p:Array<Array<Float>>, q:Array<Array<Float>>):Void {
|
||||
var a = gf(), b = gf(), c = gf(), d = gf(), e = gf(), f = gf(), g = gf(), h = gf(), t = gf();
|
||||
Z(a, p[1], p[0]); Z(t, q[1], q[0]); M(a, a, t);
|
||||
A(b, p[0], p[1]); A(t, q[0], q[1]); M(b, b, t);
|
||||
M(c, p[3], q[3]); M(c, c, D2);
|
||||
M(d, p[2], q[2]); A(d, d, d);
|
||||
Z(e, b, a); Z(f, d, c); A(g, d, c); A(h, b, a);
|
||||
M(p[0], e, f); M(p[1], h, g); M(p[2], g, f); M(p[3], e, h);
|
||||
}
|
||||
|
||||
static function cswap(p:Array<Array<Float>>, q:Array<Array<Float>>, b:Int):Void {
|
||||
for (i in 0...4) sel25519(p[i], q[i], b);
|
||||
}
|
||||
|
||||
static function edPack(r:Array<Int>, p:Array<Array<Float>>):Void {
|
||||
var tx = gf(), ty = gf(), zi = gf();
|
||||
inv25519(zi, p[2]); M(tx, p[0], zi); M(ty, p[1], zi);
|
||||
pack25519(r, ty);
|
||||
r[31] ^= par25519(tx) << 7;
|
||||
}
|
||||
|
||||
static function edScalarmult(p:Array<Array<Float>>, q:Array<Array<Float>>, s:Array<Int>):Void {
|
||||
set25519(p[0], gf0); set25519(p[1], gf1); set25519(p[2], gf1); set25519(p[3], gf0);
|
||||
var i = 255; while (i >= 0) {
|
||||
var b = (s[Std.int(i / 8)] >> (i & 7)) & 1;
|
||||
cswap(p, q, b);
|
||||
edAdd(q, p);
|
||||
edAdd(p, p);
|
||||
cswap(p, q, b);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
static function edScalarbase(p:Array<Array<Float>>, s:Array<Int>):Void {
|
||||
var q = [gf(), gf(), gf(), gf()];
|
||||
set25519(q[0], X); set25519(q[1], Y); set25519(q[2], gf1);
|
||||
M(q[3], X, Y);
|
||||
edScalarmult(p, q, s);
|
||||
}
|
||||
|
||||
static function crypto_sign_keypair(pk:Array<Int>, sk:Array<Int>, seeded:Bool, randomFn:Int->Array<Int>):Int {
|
||||
var d = [for (i in 0...64) 0];
|
||||
var p = [gf(), gf(), gf(), gf()];
|
||||
if (!seeded) {
|
||||
var rb = randomFn(32);
|
||||
for (i in 0...32) sk[i] = rb[i];
|
||||
}
|
||||
crypto_hash(d, sk, 32);
|
||||
d[0] &= 248; d[31] &= 127; d[31] |= 64;
|
||||
edScalarbase(p, d);
|
||||
edPack(pk, p);
|
||||
for (i in 0...32) sk[i + 32] = pk[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
static function modL(r:Array<Int>, roff:Int, x:Array<Float>):Void {
|
||||
var carry:Int, i:Int, j:Int, k:Int;
|
||||
i = 63; while (i >= 32) {
|
||||
carry = 0;
|
||||
j = i - 32; k = i - 12;
|
||||
while (j < k) {
|
||||
x[j] += carry - 16 * x[i] * L[j - (i - 32)];
|
||||
carry = Std.int(Math.floor((x[j] + 128) / 256));
|
||||
x[j] -= carry * 256;
|
||||
j++;
|
||||
}
|
||||
x[j] += carry;
|
||||
x[i] = 0;
|
||||
i--;
|
||||
}
|
||||
carry = 0;
|
||||
for (j in 0...32) {
|
||||
x[j] += carry - (Std.int(x[31]) >> 4) * L[j];
|
||||
carry = Std.int(x[j]) >> 8;
|
||||
x[j] = Std.int(x[j]) & 255;
|
||||
}
|
||||
for (j in 0...32) x[j] -= carry * L[j];
|
||||
for (i in 0...32) {
|
||||
x[i + 1] += Std.int(x[i]) >> 8;
|
||||
r[roff + i] = Std.int(x[i]) & 255;
|
||||
}
|
||||
}
|
||||
|
||||
static function reduce(r:Array<Int>):Void {
|
||||
var x = [for (i in 0...64) 0.0];
|
||||
for (i in 0...64) x[i] = r[i];
|
||||
for (i in 0...64) r[i] = 0;
|
||||
modL(r, 0, x);
|
||||
}
|
||||
|
||||
static function crypto_sign(sm:Array<Int>, m:Array<Int>, n:Int, sk:Array<Int>):Int {
|
||||
var d = [for (i in 0...64) 0], h = [for (i in 0...64) 0], r = [for (i in 0...64) 0];
|
||||
var x = [for (i in 0...64) 0.0];
|
||||
var p = [gf(), gf(), gf(), gf()];
|
||||
crypto_hash(d, sk, 32);
|
||||
d[0] &= 248; d[31] &= 127; d[31] |= 64;
|
||||
for (i in 0...n) sm[64 + i] = m[i];
|
||||
for (i in 0...32) sm[32 + i] = d[32 + i];
|
||||
var sm32 = [for (i in 0...n + 32) 0];
|
||||
for (i in 0...n + 32) sm32[i] = sm[32 + i];
|
||||
crypto_hash(r, sm32, n + 32);
|
||||
reduce(r);
|
||||
edScalarbase(p, r);
|
||||
edPack(sm, p);
|
||||
for (i in 32...64) sm[i] = sk[i];
|
||||
var smFull = [for (i in 0...n + 64) 0];
|
||||
for (i in 0...n + 64) smFull[i] = sm[i];
|
||||
crypto_hash(h, smFull, n + 64);
|
||||
reduce(h);
|
||||
for (i in 0...64) x[i] = 0;
|
||||
for (i in 0...32) x[i] = r[i];
|
||||
for (i in 0...32) for (j in 0...32) x[i + j] += h[i] * d[j];
|
||||
modL(sm, 32, x);
|
||||
return n + 64;
|
||||
}
|
||||
|
||||
static function unpackneg(r:Array<Array<Float>>, p:Array<Int>):Int {
|
||||
var t = gf(), chk = gf(), num = gf(), den = gf(), den2 = gf(), den4 = gf(), den6 = gf();
|
||||
set25519(r[2], gf1);
|
||||
unpack25519(r[1], p);
|
||||
S(num, r[1]); M(den, num, D); Z(num, num, r[2]); A(den, r[2], den);
|
||||
S(den2, den); S(den4, den2); M(den6, den4, den2);
|
||||
M(t, den6, num); M(t, t, den);
|
||||
pow2523(t, t);
|
||||
M(t, t, num); M(t, t, den); M(t, t, den);
|
||||
M(r[0], t, den);
|
||||
S(chk, r[0]); M(chk, chk, den);
|
||||
if (neq25519(chk, num) != 0) M(r[0], r[0], I);
|
||||
S(chk, r[0]); M(chk, chk, den);
|
||||
if (neq25519(chk, num) != 0) return -1;
|
||||
if (par25519(r[0]) == (p[31] >> 7)) Z(r[0], gf0, r[0]);
|
||||
M(r[3], r[0], r[1]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static function crypto_sign_open(m:Array<Int>, sm:Array<Int>, n:Int, pk:Array<Int>):Int {
|
||||
var t = [for (i in 0...32) 0], h = [for (i in 0...64) 0];
|
||||
var p = [gf(), gf(), gf(), gf()], q = [gf(), gf(), gf(), gf()];
|
||||
if (n < 64) return -1;
|
||||
if (unpackneg(q, pk) != 0) return -1;
|
||||
for (i in 0...n) m[i] = sm[i];
|
||||
for (i in 0...32) m[i + 32] = pk[i];
|
||||
crypto_hash(h, m, n);
|
||||
reduce(h);
|
||||
edScalarmult(p, q, h);
|
||||
var sm32 = [for (i in 0...32) 0];
|
||||
for (i in 0...32) sm32[i] = sm[32 + i];
|
||||
edScalarbase(q, sm32);
|
||||
edAdd(p, q);
|
||||
edPack(t, p);
|
||||
n -= 64;
|
||||
if (crypto_verify_32(sm, 0, t, 0) != 0) {
|
||||
for (i in 0...n) m[i] = 0;
|
||||
return -1;
|
||||
}
|
||||
for (i in 0...n) m[i] = sm[i + 64];
|
||||
return n;
|
||||
}
|
||||
|
||||
public static inline var crypto_box_PUBLICKEYBYTES = 32;
|
||||
public static inline var crypto_box_SECRETKEYBYTES = 32;
|
||||
public static inline var crypto_box_NONCEBYTES = 24;
|
||||
public static inline var crypto_box_ZEROBYTES = 32;
|
||||
public static inline var crypto_box_BOXZEROBYTES = 16;
|
||||
public static inline var crypto_sign_BYTES = 64;
|
||||
public static inline var crypto_sign_PUBLICKEYBYTES = 32;
|
||||
public static inline var crypto_sign_SECRETKEYBYTES = 64;
|
||||
public static inline var crypto_sign_SEEDBYTES = 32;
|
||||
public static inline var crypto_hash_BYTES = 64;
|
||||
|
||||
public static function hash(msg:Bytes):Bytes {
|
||||
var m = bytesToArray(msg);
|
||||
var out = [for (i in 0...64) 0];
|
||||
crypto_hash(out, m, msg.length);
|
||||
return arrayToBytes(out);
|
||||
}
|
||||
|
||||
public static function signDetached(msg:Bytes, secretKey:Bytes):Bytes {
|
||||
var m = bytesToArray(msg);
|
||||
var sk = bytesToArray(secretKey);
|
||||
var sm = [for (i in 0...crypto_sign_BYTES + msg.length) 0];
|
||||
crypto_sign(sm, m, msg.length, sk);
|
||||
var sig = [for (i in 0...crypto_sign_BYTES) 0];
|
||||
for (i in 0...crypto_sign_BYTES) sig[i] = sm[i];
|
||||
return arrayToBytes(sig);
|
||||
}
|
||||
|
||||
public static function signDetachedVerify(msg:Bytes, sig:Bytes, publicKey:Bytes):Bool {
|
||||
var sm = [for (i in 0...crypto_sign_BYTES + msg.length) 0];
|
||||
var m = [for (i in 0...crypto_sign_BYTES + msg.length) 0];
|
||||
var sigArr = bytesToArray(sig);
|
||||
var msgArr = bytesToArray(msg);
|
||||
for (i in 0...crypto_sign_BYTES) sm[i] = sigArr[i];
|
||||
for (i in 0...msg.length) sm[i + crypto_sign_BYTES] = msgArr[i];
|
||||
var pk = bytesToArray(publicKey);
|
||||
return crypto_sign_open(m, sm, sm.length, pk) >= 0;
|
||||
}
|
||||
|
||||
public static function signKeyPairFromSeed(seed:Bytes):{publicKey:Bytes, secretKey:Bytes} {
|
||||
var pk = [for (i in 0...32) 0];
|
||||
var sk = [for (i in 0...64) 0];
|
||||
var seedArr = bytesToArray(seed);
|
||||
for (i in 0...32) sk[i] = seedArr[i];
|
||||
crypto_sign_keypair(pk, sk, true, null);
|
||||
return {publicKey: arrayToBytes(pk), secretKey: arrayToBytes(sk)};
|
||||
}
|
||||
|
||||
public static function boxKeyPair():{publicKey:Bytes, secretKey:Bytes} {
|
||||
var pk = [for (i in 0...32) 0];
|
||||
var sk = [for (i in 0...32) 0];
|
||||
crypto_box_keypair(pk, sk, randomBytesArray);
|
||||
return {publicKey: arrayToBytes(pk), secretKey: arrayToBytes(sk)};
|
||||
}
|
||||
|
||||
public static function box(msg:Bytes, nonce:Bytes, publicKey:Bytes, secretKey:Bytes):Bytes {
|
||||
var m = [for (i in 0...crypto_box_ZEROBYTES + msg.length) 0];
|
||||
var c = [for (i in 0...m.length) 0];
|
||||
var msgArr = bytesToArray(msg);
|
||||
for (i in 0...msg.length) m[i + crypto_box_ZEROBYTES] = msgArr[i];
|
||||
var n = bytesToArray(nonce);
|
||||
var pk = bytesToArray(publicKey);
|
||||
var sk = bytesToArray(secretKey);
|
||||
crypto_box(c, m, m.length, n, pk, sk);
|
||||
var result = [for (i in 0...c.length - crypto_box_BOXZEROBYTES) 0];
|
||||
for (i in 0...result.length) result[i] = c[i + crypto_box_BOXZEROBYTES];
|
||||
return arrayToBytes(result);
|
||||
}
|
||||
|
||||
public static function boxOpen(box:Bytes, nonce:Bytes, publicKey:Bytes, secretKey:Bytes):Bytes {
|
||||
var c = [for (i in 0...crypto_box_BOXZEROBYTES + box.length) 0];
|
||||
var m = [for (i in 0...c.length) 0];
|
||||
var boxArr = bytesToArray(box);
|
||||
for (i in 0...box.length) c[i + crypto_box_BOXZEROBYTES] = boxArr[i];
|
||||
if (c.length < 32) return null;
|
||||
var n = bytesToArray(nonce);
|
||||
var pk = bytesToArray(publicKey);
|
||||
var sk = bytesToArray(secretKey);
|
||||
if (crypto_box_open(m, c, c.length, n, pk, sk) != 0) return null;
|
||||
var result = [for (i in 0...m.length - crypto_box_ZEROBYTES) 0];
|
||||
for (i in 0...result.length) result[i] = m[i + crypto_box_ZEROBYTES];
|
||||
return arrayToBytes(result);
|
||||
}
|
||||
|
||||
public static var boxNonceLength(get, null):Int;
|
||||
static inline function get_boxNonceLength():Int { return crypto_box_NONCEBYTES; }
|
||||
|
||||
static function bytesToArray(b:Bytes):Array<Int> {
|
||||
var a = [for (i in 0...b.length) 0];
|
||||
for (i in 0...b.length) a[i] = b.get(i);
|
||||
return a;
|
||||
}
|
||||
|
||||
static function arrayToBytes(a:Array<Int>):Bytes {
|
||||
var b = Bytes.alloc(a.length);
|
||||
for (i in 0...a.length) b.set(i, a[i] & 0xff);
|
||||
return b;
|
||||
}
|
||||
|
||||
static function randomBytesArray(n:Int):Array<Int> {
|
||||
var b = Crypto.randomBytes(n);
|
||||
var a = [for (i in 0...n) 0];
|
||||
for (i in 0...n) a[i] = b.get(i);
|
||||
return a;
|
||||
}
|
||||
}
|
||||
56
leenkx/Sources/leenkx/network/torrent/Types.hx
Normal file
56
leenkx/Sources/leenkx/network/torrent/Types.hx
Normal file
@ -0,0 +1,56 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
typedef LeenkxOptions = {
|
||||
@:optional var seed:String;
|
||||
@:optional var keyPair:{publicKey:Bytes, secretKey:Bytes};
|
||||
@:optional var timeout:Int;
|
||||
@:optional var announce:Array<String>;
|
||||
@:optional var iceServers:Array<String>;
|
||||
@:optional var tracker:Dynamic;
|
||||
@:optional var wtOpts:Dynamic;
|
||||
@:optional var torrentOpts:Dynamic;
|
||||
@:optional var torrent:Dynamic;
|
||||
@:optional var wt:Dynamic;
|
||||
@:optional var heartbeat:Int;
|
||||
@:optional var deferJoin:Bool;
|
||||
@:optional var transport:String;
|
||||
}
|
||||
|
||||
typedef PeerInfo = {
|
||||
var pk:String;
|
||||
var ek:String;
|
||||
var last:Float;
|
||||
}
|
||||
|
||||
typedef Packet = {
|
||||
var t:Float;
|
||||
var i:String;
|
||||
var pk:String;
|
||||
var ek:String;
|
||||
var n:Bytes;
|
||||
@:optional var y:String;
|
||||
@:optional var v:String;
|
||||
}
|
||||
|
||||
enum abstract PacketType(String) from String to String {
|
||||
var Message = "m";
|
||||
var Ping = "p";
|
||||
var Leave = "x";
|
||||
}
|
||||
|
||||
enum abstract LeenkxEvent(String) from String to String {
|
||||
var Torrent = "torrent";
|
||||
var Message = "message";
|
||||
var Seen = "seen";
|
||||
var Left = "left";
|
||||
var Server = "server";
|
||||
var Ping = "ping";
|
||||
var Timeout = "timeout";
|
||||
var Connections = "connections";
|
||||
var WireSeen = "wireseen";
|
||||
var WireLeft = "wireleft";
|
||||
var Tracker = "tracker";
|
||||
var Announce = "announce";
|
||||
}
|
||||
128
leenkx/Sources/leenkx/network/torrent/WebSeed.hx
Normal file
128
leenkx/Sources/leenkx/network/torrent/WebSeed.hx
Normal file
@ -0,0 +1,128 @@
|
||||
package leenkx.network.torrent;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
class WebSeed {
|
||||
public var url:String;
|
||||
public var onError:String -> Void;
|
||||
public var onDone:Void -> Void;
|
||||
|
||||
var t:Torrent;
|
||||
var busy = false;
|
||||
var key:String;
|
||||
|
||||
public function new(url:String, t:Torrent) {
|
||||
this.url = url;
|
||||
this.t = t;
|
||||
this.key = "ws:" + url;
|
||||
}
|
||||
|
||||
public function work():Void {
|
||||
if (busy || !supported() || t.pieces == null
|
||||
|| t.pieces.complete) return;
|
||||
var i = pickPiece();
|
||||
if (i < 0) return;
|
||||
var range = rangeFor(i);
|
||||
if (range == null) return;
|
||||
busy = true;
|
||||
var off = 0;
|
||||
var size = t.meta.pieceSize(i);
|
||||
while (off < size) {
|
||||
t.pieces.markRequested(key, i, off);
|
||||
off += PieceManager.BLOCK_SIZE;
|
||||
}
|
||||
fetch(range.url, range.start, range.end, function(body, err) {
|
||||
busy = false;
|
||||
if (t.pieces != null) t.pieces.clearAllRequested(key);
|
||||
if (body != null && t.pieces != null) {
|
||||
t.pieces.addBlock(i, 0, body);
|
||||
if (onDone != null) onDone();
|
||||
} else if (onError != null) {
|
||||
onError(err != null ? err : "empty response");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function pickPiece():Int {
|
||||
var pieces = t.pieces;
|
||||
for (sel in pieces.selections) {
|
||||
for (i in sel.start...sel.end + 1) {
|
||||
if (pieces.untouched(i) && rangeFor(i) != null) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (i in 0...t.meta.numPieces) {
|
||||
if (pieces.untouched(i) && rangeFor(i) != null) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function fileForByteRange(start:Int, end:Int):MetaInfo.TorrentFileEntry {
|
||||
for (f in t.meta.files) {
|
||||
if (start >= f.offset && end < f.offset + f.length) {
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function rangeFor(i:Int):{url:String, start:Int, end:Int} {
|
||||
var pl = t.meta.pieceLength;
|
||||
var start = i * pl;
|
||||
var end = start + t.meta.pieceSize(i) - 1;
|
||||
var f = fileForByteRange(start, end);
|
||||
if (f == null) return null;
|
||||
var u = url;
|
||||
if (StringTools.endsWith(u, "/") || t.meta.files.length > 1) {
|
||||
if (!StringTools.endsWith(u, "/")) u += "/";
|
||||
u += f.path.split("\\").join("/");
|
||||
}
|
||||
return {url: u, start: start - f.offset,
|
||||
end: end - f.offset};
|
||||
}
|
||||
|
||||
static function supported():Bool {
|
||||
#if kha_krom
|
||||
return false;
|
||||
#elseif (sys || js)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
function fetch(url:String, start:Int, end:Int, cb:Bytes -> String -> Void):Void {
|
||||
#if kha_krom
|
||||
cb(null, "webseeds unsupported on krom");
|
||||
#elseif sys
|
||||
var req = new haxe.Http(url);
|
||||
req.setHeader("Range", "bytes=" + start + "-" + end);
|
||||
req.cnxTimeout = 60;
|
||||
req.onBytes = function(b) {
|
||||
TorrentClient.postMain(function() cb(b, null));
|
||||
};
|
||||
req.onError = function(e) {
|
||||
TorrentClient.postMain(function() cb(null, e));
|
||||
};
|
||||
sys.thread.Thread.create(function() req.request(false));
|
||||
#elseif js
|
||||
var xhr = new js.html.XMLHttpRequest();
|
||||
xhr.open("GET", url, true);
|
||||
xhr.setRequestHeader("Range", "bytes=" + start + "-" + end);
|
||||
xhr.timeout = 60000;
|
||||
xhr.ontimeout = function() cb(null, "request timed out");
|
||||
xhr.responseType = js.html.XMLHttpRequestResponseType
|
||||
.ARRAYBUFFER;
|
||||
xhr.onload = function() {
|
||||
var buf:js.lib.ArrayBuffer = xhr.response;
|
||||
cb(buf != null ? haxe.io.Bytes.ofData(buf) : null,
|
||||
buf != null ? null : "empty response");
|
||||
};
|
||||
xhr.onerror = function() cb(null, "http error");
|
||||
xhr.send();
|
||||
#else
|
||||
cb(null, "webseeds unsupported on this target");
|
||||
#end
|
||||
}
|
||||
}
|
||||
152
leenkx/Sources/leenkx/network/torrent/peer/DataChannelPeer.hx
Normal file
152
leenkx/Sources/leenkx/network/torrent/peer/DataChannelPeer.hx
Normal file
@ -0,0 +1,152 @@
|
||||
package leenkx.network.torrent.peer;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
#if lnx_datachannel
|
||||
import datachannel.RTCDataChannel.RTCDataChannelState;
|
||||
#elseif kha_krom
|
||||
import leenkx.network.torrent.transport.RtcNative;
|
||||
#elseif js
|
||||
import js.html.rtc.DataChannelState;
|
||||
#end
|
||||
|
||||
class DataChannelPeer implements IPeerChannel {
|
||||
public var onOpen:Void -> Void;
|
||||
public var onData:Bytes -> Void;
|
||||
public var onClose:Void -> Void;
|
||||
|
||||
var dc:Dynamic;
|
||||
var open = false;
|
||||
var pending:Array<Bytes> = [];
|
||||
|
||||
public var isOpen(get, never):Bool;
|
||||
function get_isOpen():Bool return open;
|
||||
|
||||
public function new(dc:Dynamic) {
|
||||
this.dc = dc;
|
||||
#if lnx_datachannel
|
||||
dc.onOpen = function(name) {
|
||||
open = true;
|
||||
if (onOpen != null) onOpen();
|
||||
};
|
||||
dc.onMessage = function(msg:Bytes) {
|
||||
emitData(msg);
|
||||
};
|
||||
dc.onClosed = function() {
|
||||
open = false;
|
||||
if (onClose != null) onClose();
|
||||
};
|
||||
dc.onError = function(msg) {
|
||||
open = false;
|
||||
if (onClose != null) onClose();
|
||||
};
|
||||
if (dc.state == RTCDataChannelState.Open) {
|
||||
open = true;
|
||||
if (onOpen != null) onOpen();
|
||||
}
|
||||
#elseif kha_krom
|
||||
var dcId:Int = dc;
|
||||
RtcNative.setOpenCallback(dcId, function() {
|
||||
open = true;
|
||||
if (onOpen != null) onOpen();
|
||||
});
|
||||
RtcNative.setMessageCallback(dcId, function(data:Dynamic) {
|
||||
var msg:Bytes = null;
|
||||
if (Std.isOfType(data, js.lib.ArrayBuffer)) {
|
||||
msg = Bytes.ofData(data);
|
||||
} else if (js.lib.ArrayBuffer.isView(data)) {
|
||||
var view:js.lib.ArrayBufferView = data;
|
||||
msg = Bytes.ofData(view.buffer.slice(
|
||||
view.byteOffset, view.byteOffset + view.byteLength));
|
||||
} else {
|
||||
msg = Bytes.ofString(data);
|
||||
}
|
||||
emitData(msg);
|
||||
});
|
||||
RtcNative.setClosedCallback(dcId, function() {
|
||||
open = false;
|
||||
if (onClose != null) onClose();
|
||||
});
|
||||
RtcNative.setErrorCallback(dcId, function(msg:String) {
|
||||
open = false;
|
||||
if (onClose != null) onClose();
|
||||
});
|
||||
if (RtcNative.isOpen(dcId)) {
|
||||
open = true;
|
||||
if (onOpen != null) onOpen();
|
||||
}
|
||||
#elseif js
|
||||
dc.binaryType = "arraybuffer";
|
||||
dc.onopen = function() {
|
||||
open = true;
|
||||
if (onOpen != null) onOpen();
|
||||
};
|
||||
dc.onmessage = function(e:js.html.MessageEvent) {
|
||||
var msg:Bytes = null;
|
||||
if (Std.isOfType(e.data, js.lib.ArrayBuffer)) {
|
||||
msg = Bytes.ofData(e.data);
|
||||
} else if (Std.isOfType(e.data, js.html.Blob)) {
|
||||
return;
|
||||
} else {
|
||||
msg = Bytes.ofString(e.data);
|
||||
}
|
||||
emitData(msg);
|
||||
};
|
||||
dc.onclose = function() {
|
||||
open = false;
|
||||
if (onClose != null) onClose();
|
||||
};
|
||||
dc.onerror = function(e) {
|
||||
open = false;
|
||||
if (onClose != null) onClose();
|
||||
};
|
||||
if (dc.readyState == DataChannelState.OPEN) {
|
||||
open = true;
|
||||
if (onOpen != null) onOpen();
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
function emitData(msg:Bytes):Void {
|
||||
if (onData == null) {
|
||||
pending.push(msg);
|
||||
return;
|
||||
}
|
||||
flushPending();
|
||||
onData(msg);
|
||||
}
|
||||
|
||||
function flushPending():Void {
|
||||
if (onData == null || pending.length == 0) return;
|
||||
var q = pending;
|
||||
pending = [];
|
||||
for (m in q) onData(m);
|
||||
}
|
||||
|
||||
public function send(data:Bytes):Void {
|
||||
flushPending();
|
||||
if (!open) return;
|
||||
#if lnx_datachannel
|
||||
try {
|
||||
dc.sendBytes(data);
|
||||
} catch (e:Dynamic) {
|
||||
trace("Failed to sendBytes: " + Std.string(e));
|
||||
}
|
||||
#elseif kha_krom
|
||||
RtcNative.sendMessage(dc, data.getData().slice(0, data.length));
|
||||
#elseif js
|
||||
dc.send(new js.lib.Uint8Array(data.getData(), 0, data.length));
|
||||
#end
|
||||
}
|
||||
|
||||
public function close():Void {
|
||||
open = false;
|
||||
#if lnx_datachannel
|
||||
try dc.close() catch(e:Dynamic) {}
|
||||
#elseif kha_krom
|
||||
try RtcNative.close(dc) catch(e:Dynamic) {}
|
||||
#elseif js
|
||||
try dc.close() catch(e:Dynamic) {}
|
||||
#end
|
||||
}
|
||||
}
|
||||
12
leenkx/Sources/leenkx/network/torrent/peer/IPeerChannel.hx
Normal file
12
leenkx/Sources/leenkx/network/torrent/peer/IPeerChannel.hx
Normal file
@ -0,0 +1,12 @@
|
||||
package leenkx.network.torrent.peer;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
interface IPeerChannel {
|
||||
function send(data:Bytes):Void;
|
||||
function close():Void;
|
||||
var isOpen(get, never):Bool;
|
||||
var onOpen:Void -> Void;
|
||||
var onData:Bytes -> Void;
|
||||
var onClose:Void -> Void;
|
||||
}
|
||||
524
leenkx/Sources/leenkx/network/torrent/peer/RtcPeerPool.hx
Normal file
524
leenkx/Sources/leenkx/network/torrent/peer/RtcPeerPool.hx
Normal 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
|
||||
}
|
||||
}
|
||||
179
leenkx/Sources/leenkx/network/torrent/peer/TcpListener.hx
Normal file
179
leenkx/Sources/leenkx/network/torrent/peer/TcpListener.hx
Normal file
@ -0,0 +1,179 @@
|
||||
package leenkx.network.torrent.peer;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
#if (sys || kha_krom)
|
||||
class TcpListener {
|
||||
public var onPeer:IPeerChannel -> String -> String -> Void;
|
||||
public var onError:String -> Void;
|
||||
|
||||
var port:Int;
|
||||
var stopped = false;
|
||||
|
||||
public function new(port:Int = 6881) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
#if sys
|
||||
var server:sys.net.Socket = null;
|
||||
|
||||
public function start():Void {
|
||||
server = new sys.net.Socket();
|
||||
server.bind(new sys.net.Host("0.0.0.0"), port);
|
||||
server.listen(50);
|
||||
var self = this;
|
||||
sys.thread.Thread.create(function() {
|
||||
while (!self.stopped) {
|
||||
var s:sys.net.Socket = null;
|
||||
try {
|
||||
s = self.server.accept();
|
||||
} catch(e:Dynamic) {
|
||||
if (self.stopped) break;
|
||||
if (self.onError != null) {
|
||||
self.onError("accept: " + e);
|
||||
}
|
||||
Sys.sleep(0.05);
|
||||
continue;
|
||||
}
|
||||
self.handleAccepted(s);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function stop():Void {
|
||||
stopped = true;
|
||||
if (server != null) {
|
||||
try server.close() catch(e:Dynamic) {}
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleAccepted(s:sys.net.Socket):Void {
|
||||
var self = this;
|
||||
sys.thread.Thread.create(function() {
|
||||
var head = readHandshake(s);
|
||||
if (head == null) {
|
||||
try s.close() catch(e:Dynamic) {}
|
||||
return;
|
||||
}
|
||||
var ih = head.sub(28, 20);
|
||||
var hex = leenkx.network.torrent.Crypto.toHex(ih);
|
||||
var peer = s.peer();
|
||||
var key = "tcp:" + peer.host.toString() + ":" + peer.port;
|
||||
leenkx.network.torrent.TorrentClient.postMain(function() {
|
||||
if (self.stopped || self.onPeer == null) {
|
||||
try s.close() catch(e:Dynamic) {}
|
||||
return;
|
||||
}
|
||||
var ch = new TcpPeer("", 0, s, head);
|
||||
self.onPeer(ch, hex, key);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function readHandshake(s:sys.net.Socket):Bytes {
|
||||
s.setTimeout(10);
|
||||
var buf = new haxe.io.BytesBuffer();
|
||||
var deadline = haxe.Timer.stamp() + 10;
|
||||
while (buf.length < 68) {
|
||||
try {
|
||||
var tmp = Bytes.alloc(68 - buf.length);
|
||||
var n = s.input.readBytes(tmp, 0, tmp.length);
|
||||
if (n <= 0) return null;
|
||||
buf.addBytes(tmp, 0, n);
|
||||
} catch(e:Dynamic) {
|
||||
if (!TcpPeer.isBlocked(e)) return null;
|
||||
if (haxe.Timer.stamp() > deadline) return null;
|
||||
}
|
||||
}
|
||||
var b = buf.getBytes();
|
||||
if (b.get(0) != 19
|
||||
|| b.sub(1, 19).toString() != "BitTorrent protocol") {
|
||||
return null;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
#elseif kha_krom
|
||||
var server:leenkx.network.krom.KromSocket = null;
|
||||
var pumpRef:Void -> Void = null;
|
||||
var pending:Array<{sock:leenkx.network.krom.KromSocket,
|
||||
buf:haxe.io.BytesBuffer, deadline:Float}> = [];
|
||||
|
||||
public function start():Void {
|
||||
server = new leenkx.network.krom.KromSocket();
|
||||
server.bind(new leenkx.network.krom.KromSocket.KromHost(
|
||||
"0.0.0.0"), port);
|
||||
server.listen(50);
|
||||
server.setBlocking(false);
|
||||
pumpRef = pump;
|
||||
leenkx.network.krom.KromSocket.KromPump.add(pumpRef);
|
||||
}
|
||||
|
||||
public function stop():Void {
|
||||
stopped = true;
|
||||
if (pumpRef != null) {
|
||||
leenkx.network.krom.KromSocket.KromPump.remove(pumpRef);
|
||||
pumpRef = null;
|
||||
}
|
||||
for (p in pending) try p.sock.close() catch(e:Dynamic) {}
|
||||
pending = [];
|
||||
if (server != null) {
|
||||
server.close();
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
||||
function pump():Void {
|
||||
if (stopped || server == null) return;
|
||||
while (true) {
|
||||
var s:leenkx.network.krom.KromSocket = null;
|
||||
try {
|
||||
s = server.accept();
|
||||
} catch(e:Dynamic) {
|
||||
break;
|
||||
}
|
||||
if (s == null) break;
|
||||
s.setBlocking(false);
|
||||
pending.push({
|
||||
sock: s,
|
||||
buf: new haxe.io.BytesBuffer(),
|
||||
deadline: haxe.Timer.stamp() + 10
|
||||
});
|
||||
}
|
||||
var i = pending.length;
|
||||
while (i-- > 0) {
|
||||
var p = pending[i];
|
||||
try {
|
||||
var d = p.sock.recvRaw(68 - p.buf.length);
|
||||
if (d != null) p.buf.addBytes(d, 0, d.length);
|
||||
} catch(e:Dynamic) {
|
||||
p.sock.close();
|
||||
pending.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
if (p.buf.length >= 68) {
|
||||
pending.splice(i, 1);
|
||||
routePeer(p.sock, p.buf.getBytes());
|
||||
} else if (haxe.Timer.stamp() > p.deadline) {
|
||||
p.sock.close();
|
||||
pending.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function routePeer(s:leenkx.network.krom.KromSocket, head:Bytes):Void {
|
||||
if (head.get(0) != 19
|
||||
|| head.sub(1, 19).toString() != "BitTorrent protocol"
|
||||
|| stopped || onPeer == null) {
|
||||
s.close();
|
||||
return;
|
||||
}
|
||||
var ih = head.sub(28, 20);
|
||||
var hex = leenkx.network.torrent.Crypto.toHex(ih);
|
||||
var key = "krom:" + s.getSocketId();
|
||||
var ch = new TcpPeer("", 0, s, head);
|
||||
onPeer(ch, hex, key);
|
||||
}
|
||||
#end
|
||||
}
|
||||
#end
|
||||
406
leenkx/Sources/leenkx/network/torrent/peer/TcpPeer.hx
Normal file
406
leenkx/Sources/leenkx/network/torrent/peer/TcpPeer.hx
Normal file
@ -0,0 +1,406 @@
|
||||
package leenkx.network.torrent.peer;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
#if (sys || kha_krom)
|
||||
class TcpPeer implements IPeerChannel {
|
||||
public var onOpen:Void -> Void;
|
||||
public var onData:Bytes -> Void;
|
||||
public var onClose:Void -> Void;
|
||||
|
||||
static var peers:Array<TcpPeer> = [];
|
||||
#if sys
|
||||
static var peersMutex = new sys.thread.Mutex();
|
||||
static var ioThread:sys.thread.Thread = null;
|
||||
static var wakeRecv:sys.net.UdpSocket = null;
|
||||
static var wakeAddr:sys.net.Address = null;
|
||||
#elseif kha_krom
|
||||
static var pumpTickRef:Void -> Void = null;
|
||||
#end
|
||||
|
||||
var open = false;
|
||||
var dead = false;
|
||||
var pendingSend:Array<Bytes> = [];
|
||||
#if sys
|
||||
var sendMutex = new sys.thread.Mutex();
|
||||
#end
|
||||
var head:Bytes = null;
|
||||
var readBuf:Bytes = Bytes.alloc(65536);
|
||||
#if kha_krom
|
||||
var connectDeadline:Float = 0;
|
||||
#end
|
||||
|
||||
#if kha_krom
|
||||
var socket:leenkx.network.krom.KromSocket;
|
||||
#else
|
||||
var socket:sys.net.Socket;
|
||||
#end
|
||||
|
||||
public var isOpen(get, never):Bool;
|
||||
function get_isOpen():Bool return open;
|
||||
|
||||
public function new(host:String, port:Int, ?acceptedSocket:Dynamic, ?headData:Bytes) {
|
||||
if (acceptedSocket != null) {
|
||||
socket = acceptedSocket;
|
||||
head = headData;
|
||||
socket.setBlocking(false);
|
||||
open = true;
|
||||
startPump();
|
||||
var self = this;
|
||||
leenkx.network.torrent.TorrentClient.postMain(function() {
|
||||
if (self.onOpen != null) self.onOpen();
|
||||
});
|
||||
return;
|
||||
}
|
||||
#if kha_krom
|
||||
socket = new leenkx.network.krom.KromSocket();
|
||||
socket.setBlocking(false);
|
||||
var connectFailed = false;
|
||||
try {
|
||||
socket.connect(
|
||||
new leenkx.network.krom.KromSocket.KromHost(host), port);
|
||||
} catch(e:Dynamic) {
|
||||
connectFailed = true;
|
||||
}
|
||||
if (connectFailed) {
|
||||
dead = true;
|
||||
return;
|
||||
}
|
||||
if (socket.isConnected()) {
|
||||
open = true;
|
||||
startPump();
|
||||
if (onOpen != null) onOpen();
|
||||
} else {
|
||||
connectDeadline = haxe.Timer.stamp() + 10;
|
||||
startPump();
|
||||
}
|
||||
#else
|
||||
var self = this;
|
||||
sys.thread.Thread.create(function() {
|
||||
var s = new sys.net.Socket();
|
||||
var err:Dynamic = null;
|
||||
try {
|
||||
s.connect(new sys.net.Host(host), port);
|
||||
s.setBlocking(false);
|
||||
} catch(e:Dynamic) {
|
||||
err = e;
|
||||
}
|
||||
leenkx.network.torrent.TorrentClient.postMain(function() {
|
||||
if (self.dead) {
|
||||
try s.close() catch(e:Dynamic) {}
|
||||
return;
|
||||
}
|
||||
if (err != null) {
|
||||
self.dead = true;
|
||||
if (self.onClose != null) self.onClose();
|
||||
return;
|
||||
}
|
||||
self.socket = s;
|
||||
self.open = true;
|
||||
self.startPump();
|
||||
if (self.onOpen != null) self.onOpen();
|
||||
});
|
||||
});
|
||||
#end
|
||||
}
|
||||
|
||||
function fail():Void {
|
||||
dead = true;
|
||||
open = false;
|
||||
stopPump();
|
||||
if (onClose != null) onClose();
|
||||
}
|
||||
|
||||
function deliverHead():Void {
|
||||
if (head == null || onData == null) return;
|
||||
var d = head;
|
||||
head = null;
|
||||
onData(d);
|
||||
}
|
||||
|
||||
#if sys
|
||||
function startPump():Void register(this);
|
||||
function stopPump():Void unregister(this);
|
||||
|
||||
static function register(p:TcpPeer):Void {
|
||||
peersMutex.acquire();
|
||||
if (!peers.contains(p)) peers.push(p);
|
||||
if (ioThread == null) {
|
||||
wakeRecv = new sys.net.UdpSocket();
|
||||
wakeRecv.bind(new sys.net.Host("127.0.0.1"), 0);
|
||||
wakeRecv.setBlocking(false);
|
||||
var h = wakeRecv.host();
|
||||
wakeAddr = new sys.net.Address();
|
||||
wakeAddr.host = h.host.ip;
|
||||
wakeAddr.port = h.port;
|
||||
ioThread = sys.thread.Thread.create(ioLoop);
|
||||
}
|
||||
peersMutex.release();
|
||||
ioWake();
|
||||
}
|
||||
|
||||
static function unregister(p:TcpPeer):Void {
|
||||
peersMutex.acquire();
|
||||
peers.remove(p);
|
||||
peersMutex.release();
|
||||
ioWake();
|
||||
}
|
||||
|
||||
static function ioWake():Void {
|
||||
if (wakeAddr == null) return;
|
||||
try {
|
||||
wakeRecv.sendTo(Bytes.alloc(1), 0, 1, wakeAddr);
|
||||
} catch(e:Dynamic) {}
|
||||
}
|
||||
|
||||
static function ioLoop():Void {
|
||||
var wakeBuf = Bytes.alloc(64);
|
||||
while (true) {
|
||||
peersMutex.acquire();
|
||||
var snapshot = peers.copy();
|
||||
peersMutex.release();
|
||||
for (p in snapshot) {
|
||||
try {
|
||||
p.deliverHead();
|
||||
} catch(e:Dynamic) {
|
||||
trace("[TcpPeer.ioLoop] deliverHead threw: " + e);
|
||||
}
|
||||
}
|
||||
var reads:Array<sys.net.Socket> = [wakeRecv];
|
||||
var writes:Array<sys.net.Socket> = [];
|
||||
for (p in snapshot) {
|
||||
if (p.dead || p.socket == null) continue;
|
||||
reads.push(p.socket);
|
||||
p.sendMutex.acquire();
|
||||
var hasPending = p.pendingSend.length > 0;
|
||||
p.sendMutex.release();
|
||||
if (hasPending) writes.push(p.socket);
|
||||
}
|
||||
var res = try {
|
||||
sys.net.Socket.select(reads, writes, null, 0.1);
|
||||
} catch(e:Dynamic) {
|
||||
null;
|
||||
}
|
||||
if (res == null) continue;
|
||||
if (res.read.contains(wakeRecv)) {
|
||||
try {
|
||||
while (wakeRecv.readFrom(wakeBuf, 0, 64,
|
||||
new sys.net.Address()) > 0) {}
|
||||
} catch(e:Dynamic) {}
|
||||
}
|
||||
for (s in res.read) {
|
||||
if (s == wakeRecv) continue;
|
||||
for (p in snapshot) {
|
||||
if (p.socket == s && !p.dead) {
|
||||
try {
|
||||
p.ioRead();
|
||||
} catch(e:Dynamic) {
|
||||
trace("[TcpPeer.ioLoop] ioRead threw: "
|
||||
+ e);
|
||||
p.fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (s in res.write) {
|
||||
for (p in snapshot) {
|
||||
if (p.socket == s && !p.dead) {
|
||||
try {
|
||||
p.ioWrite();
|
||||
} catch(e:Dynamic) {
|
||||
trace("[TcpPeer.ioLoop] ioWrite threw: "
|
||||
+ e);
|
||||
p.fail();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
peersMutex.acquire();
|
||||
var i = peers.length;
|
||||
while (i-- > 0) {
|
||||
if (peers[i].dead) peers.splice(i, 1);
|
||||
}
|
||||
peersMutex.release();
|
||||
}
|
||||
}
|
||||
|
||||
function ioRead():Void {
|
||||
try {
|
||||
while (true) {
|
||||
var read = socket.input.readBytes(readBuf, 0,
|
||||
readBuf.length);
|
||||
if (read <= 0) {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
if (onData != null) onData(readBuf.sub(0, read));
|
||||
}
|
||||
} catch(e:Dynamic) {
|
||||
if (isBlocked(e)) return;
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
function ioWrite():Void {
|
||||
flushPending();
|
||||
}
|
||||
#elseif kha_krom
|
||||
function startPump():Void {
|
||||
if (pumpTickRef == null) {
|
||||
pumpTickRef = pumpAll;
|
||||
leenkx.network.krom.KromSocket.KromPump.add(pumpTickRef);
|
||||
}
|
||||
peers.push(this);
|
||||
}
|
||||
|
||||
function stopPump():Void {
|
||||
peers.remove(this);
|
||||
if (peers.length == 0 && pumpTickRef != null) {
|
||||
leenkx.network.krom.KromSocket.KromPump.remove(pumpTickRef);
|
||||
pumpTickRef = null;
|
||||
}
|
||||
}
|
||||
|
||||
static function pumpAll():Void {
|
||||
var i = peers.length;
|
||||
while (i-- > 0) {
|
||||
if (peers[i].dead) peers.splice(i, 1);
|
||||
}
|
||||
var snapshot = peers.copy();
|
||||
for (p in snapshot) {
|
||||
if (!p.dead) p.pumpTick();
|
||||
}
|
||||
if (peers.length == 0 && pumpTickRef != null) {
|
||||
leenkx.network.krom.KromSocket.KromPump.remove(pumpTickRef);
|
||||
pumpTickRef = null;
|
||||
}
|
||||
}
|
||||
|
||||
function pumpTick():Void {
|
||||
if (dead) return;
|
||||
if (connectDeadline > 0) {
|
||||
if (socket.isConnected()) {
|
||||
connectDeadline = 0;
|
||||
open = true;
|
||||
var queued = pendingSend;
|
||||
pendingSend = [];
|
||||
for (d in queued) send(d);
|
||||
if (onOpen != null) onOpen();
|
||||
} else if (haxe.Timer.stamp() > connectDeadline) {
|
||||
connectDeadline = 0;
|
||||
fail();
|
||||
}
|
||||
return;
|
||||
}
|
||||
deliverHead();
|
||||
flushPending();
|
||||
if (dead) return;
|
||||
try {
|
||||
while (true) {
|
||||
var data = socket.recvRaw(readBuf.length);
|
||||
if (data == null || data.length == 0) break;
|
||||
if (onData != null) onData(data);
|
||||
}
|
||||
} catch(e:Dynamic) {
|
||||
if (isBlocked(e)) return;
|
||||
fail();
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
#if sys
|
||||
public function send(data:Bytes):Void {
|
||||
if (dead) return;
|
||||
sendMutex.acquire();
|
||||
pendingSend.push(data);
|
||||
sendMutex.release();
|
||||
ioWake();
|
||||
}
|
||||
#else
|
||||
public function send(data:Bytes):Void {
|
||||
if (dead) return;
|
||||
if (!open || pendingSend.length > 0) {
|
||||
pendingSend.push(data);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var written = socket.sendRaw(data);
|
||||
if (written < data.length) {
|
||||
pendingSend.push(data.sub(written, data.length - written));
|
||||
}
|
||||
} catch(e:Dynamic) {
|
||||
if (isBlocked(e)) {
|
||||
pendingSend.push(data);
|
||||
return;
|
||||
}
|
||||
dead = true;
|
||||
open = false;
|
||||
stopPump();
|
||||
if (onClose != null) onClose();
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
public static function isBlocked(e:Dynamic):Bool {
|
||||
var msg = Std.string(e);
|
||||
if (msg == "Blocking" || msg == "Blocked") return true;
|
||||
if (Std.isOfType(e, haxe.io.Error)
|
||||
&& (e:haxe.io.Error).match(haxe.io.Error.Blocked)) return true;
|
||||
if (msg.indexOf("Unix_error(37") == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
#if sys
|
||||
function flushPending():Void {
|
||||
while (open && !dead) {
|
||||
sendMutex.acquire();
|
||||
var empty = pendingSend.length == 0;
|
||||
var d = empty ? null : pendingSend[0];
|
||||
sendMutex.release();
|
||||
if (empty) return;
|
||||
try {
|
||||
var written = socket.output.writeBytes(d, 0, d.length);
|
||||
socket.output.flush();
|
||||
sendMutex.acquire();
|
||||
if (written < d.length) {
|
||||
pendingSend[0] = d.sub(written, d.length - written);
|
||||
} else {
|
||||
pendingSend.shift();
|
||||
}
|
||||
sendMutex.release();
|
||||
if (written < d.length) return;
|
||||
} catch(e:Dynamic) {
|
||||
if (isBlocked(e)) return;
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
function flushPending():Void {
|
||||
while (open && !dead && pendingSend.length > 0) {
|
||||
var d = pendingSend[0];
|
||||
try {
|
||||
var written = socket.sendRaw(d);
|
||||
if (written < d.length) {
|
||||
pendingSend[0] = d.sub(written, d.length - written);
|
||||
break;
|
||||
}
|
||||
pendingSend.shift();
|
||||
} catch(e:Dynamic) {
|
||||
if (isBlocked(e)) break;
|
||||
fail();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
public function close():Void {
|
||||
open = false;
|
||||
dead = true;
|
||||
stopPump();
|
||||
try socket.close() catch(e:Dynamic) {}
|
||||
}
|
||||
}
|
||||
#end
|
||||
424
leenkx/Sources/leenkx/network/torrent/peer/WireProtocol.hx
Normal file
424
leenkx/Sources/leenkx/network/torrent/peer/WireProtocol.hx
Normal file
@ -0,0 +1,424 @@
|
||||
package leenkx.network.torrent.peer;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
import leenkx.network.torrent.Bencode;
|
||||
|
||||
class WireProtocol {
|
||||
static inline var PSTR = "BitTorrent protocol";
|
||||
static inline var BLOCK_SIZE = 16384;
|
||||
static inline var MAX_MSG_LEN = 256 * 1024;
|
||||
|
||||
public static inline var MSG_CHOKE = 0;
|
||||
public static inline var MSG_UNCHOKE = 1;
|
||||
public static inline var MSG_INTERESTED = 2;
|
||||
public static inline var MSG_NOT_INTERESTED = 3;
|
||||
public static inline var MSG_HAVE = 4;
|
||||
public static inline var MSG_BITFIELD = 5;
|
||||
public static inline var MSG_REQUEST = 6;
|
||||
public static inline var MSG_PIECE = 7;
|
||||
public static inline var MSG_CANCEL = 8;
|
||||
public static inline var MSG_PORT = 9;
|
||||
public static inline var MSG_EXTENDED = 20;
|
||||
public static inline var MSG_HASH_REQUEST = 21;
|
||||
public static inline var MSG_HASHES = 22;
|
||||
public static inline var MSG_HASH_REJECT = 23;
|
||||
|
||||
var channel:IPeerChannel;
|
||||
var infoHash:Bytes;
|
||||
var peerId:Bytes;
|
||||
var buffer:haxe.io.BytesBuffer = new haxe.io.BytesBuffer();
|
||||
var handshakeDone = false;
|
||||
var handshakeSent = false;
|
||||
var started = false;
|
||||
var closed = false;
|
||||
|
||||
public var extIds:Map<String, Int> = [];
|
||||
public var peerExtendedHandshake:Dynamic = null;
|
||||
public var remotePeerId:String = null;
|
||||
public var peerChoking = true;
|
||||
public var peerInterested = false;
|
||||
public var amChoking = true;
|
||||
public var amInterested = false;
|
||||
|
||||
var localExtNames:Array<String> = [];
|
||||
var localExtIds:Map<Int, String> = [];
|
||||
|
||||
public var onHandshake:String -> Void;
|
||||
public var onExtendedHandshake:Dynamic -> Void;
|
||||
public var onChoke:Void -> Void;
|
||||
public var onUnchoke:Void -> Void;
|
||||
public var onInterested:Void -> Void;
|
||||
public var onNotInterested:Void -> Void;
|
||||
public var onHave:Int -> Void;
|
||||
public var onBitfield:Bytes -> Void;
|
||||
public var onRequest:Int -> Int -> Int -> Void;
|
||||
public var onPiece:Int -> Int -> Bytes -> Void;
|
||||
public var onCancel:Int -> Int -> Int -> Void;
|
||||
public var onExtended:String -> Bytes -> Void;
|
||||
public var onHashRequest:Bytes -> Void;
|
||||
public var onHashes:Bytes -> Void;
|
||||
public var onHashReject:Bytes -> Void;
|
||||
public var onClose:Void -> Void;
|
||||
public var onError:String -> Void;
|
||||
|
||||
public function new(channel:IPeerChannel, infoHash:Bytes, peerId:Bytes) {
|
||||
this.channel = channel;
|
||||
this.infoHash = infoHash;
|
||||
this.peerId = peerId;
|
||||
channel.onData = onChannelData;
|
||||
var prevClose = channel.onClose;
|
||||
channel.onClose = function() {
|
||||
if (prevClose != null) prevClose();
|
||||
closed = true;
|
||||
if (onClose != null) onClose();
|
||||
};
|
||||
}
|
||||
|
||||
public function start(extNames:Array<String>):Void {
|
||||
localExtNames = extNames;
|
||||
started = true;
|
||||
sendHandshake();
|
||||
pump();
|
||||
}
|
||||
|
||||
function sendHandshake():Void {
|
||||
if (handshakeSent) return;
|
||||
handshakeSent = true;
|
||||
var out = new haxe.io.BytesBuffer();
|
||||
out.addByte(PSTR.length);
|
||||
out.addString(PSTR);
|
||||
var reserved = Bytes.alloc(8);
|
||||
reserved.set(5, 0x10);
|
||||
out.addBytes(reserved, 0, 8);
|
||||
out.addBytes(infoHash, 0, 20);
|
||||
out.addBytes(peerId, 0, 20);
|
||||
channel.send(out.getBytes());
|
||||
}
|
||||
|
||||
var parsing = false;
|
||||
|
||||
function onChannelData(data:Bytes):Void {
|
||||
if (closed) return;
|
||||
buffer.addBytes(data, 0, data.length);
|
||||
if (!started) return;
|
||||
pump();
|
||||
}
|
||||
|
||||
function pump():Void {
|
||||
if (parsing) return;
|
||||
parsing = true;
|
||||
while (!closed) {
|
||||
var bytes = buffer.getBytes();
|
||||
buffer = new haxe.io.BytesBuffer();
|
||||
if (bytes.length == 0) break;
|
||||
var consumed = parse(bytes);
|
||||
if (consumed < bytes.length) {
|
||||
var nested = buffer.getBytes();
|
||||
buffer = new haxe.io.BytesBuffer();
|
||||
buffer.addBytes(bytes, consumed, bytes.length - consumed);
|
||||
buffer.addBytes(nested, 0, nested.length);
|
||||
if (consumed == 0 && nested.length == 0) break;
|
||||
}
|
||||
}
|
||||
parsing = false;
|
||||
}
|
||||
|
||||
function parse(data:Bytes):Int {
|
||||
var pos = 0;
|
||||
if (!handshakeDone) {
|
||||
if (data.length < 68) return 0;
|
||||
var pstrLen = data.get(0);
|
||||
if (pstrLen != PSTR.length) {
|
||||
fail("bad pstr length");
|
||||
return data.length;
|
||||
}
|
||||
var pstr = data.sub(1, pstrLen).toString();
|
||||
if (pstr != PSTR) {
|
||||
fail("bad pstr");
|
||||
return data.length;
|
||||
}
|
||||
var ihOff = 1 + pstrLen + 8;
|
||||
var remoteHash = data.sub(ihOff, 20);
|
||||
if (!bytesEqual(remoteHash, infoHash)) {
|
||||
fail("info_hash mismatch");
|
||||
return data.length;
|
||||
}
|
||||
remotePeerId = leenkx.network.torrent.Crypto.toHex(
|
||||
data.sub(ihOff + 20, 20));
|
||||
handshakeDone = true;
|
||||
pos = ihOff + 40;
|
||||
sendHandshake();
|
||||
sendExtendedHandshake();
|
||||
if (onHandshake != null) onHandshake(remotePeerId);
|
||||
}
|
||||
while (pos + 4 <= data.length) {
|
||||
var len = (data.get(pos) << 24) | (data.get(pos + 1) << 16)
|
||||
| (data.get(pos + 2) << 8) | data.get(pos + 3);
|
||||
if (len < 0 || len > MAX_MSG_LEN || pos + 4 + len > data.length) break;
|
||||
var msgStart = pos;
|
||||
pos += 4;
|
||||
if (len > 0) {
|
||||
var mid = data.get(pos);
|
||||
handleMessage(mid, data.sub(pos + 1, len - 1));
|
||||
}
|
||||
pos += len;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
function handleMessage(id:Int, payload:Bytes):Void {
|
||||
switch (id) {
|
||||
case MSG_CHOKE:
|
||||
peerChoking = true;
|
||||
if (onChoke != null) onChoke();
|
||||
case MSG_UNCHOKE:
|
||||
peerChoking = false;
|
||||
if (onUnchoke != null) onUnchoke();
|
||||
case MSG_INTERESTED:
|
||||
peerInterested = true;
|
||||
if (onInterested != null) onInterested();
|
||||
case MSG_NOT_INTERESTED:
|
||||
peerInterested = false;
|
||||
if (onNotInterested != null) onNotInterested();
|
||||
case MSG_HAVE:
|
||||
if (payload.length >= 4 && onHave != null) {
|
||||
onHave(readInt32(payload, 0));
|
||||
}
|
||||
case MSG_BITFIELD:
|
||||
if (onBitfield != null) onBitfield(payload);
|
||||
case MSG_REQUEST:
|
||||
if (payload.length >= 12 && onRequest != null) {
|
||||
onRequest(readInt32(payload, 0), readInt32(payload, 4),
|
||||
readInt32(payload, 8));
|
||||
}
|
||||
case MSG_PIECE:
|
||||
if (payload.length >= 8 && onPiece != null) {
|
||||
onPiece(readInt32(payload, 0), readInt32(payload, 4),
|
||||
payload.sub(8, payload.length - 8));
|
||||
}
|
||||
case MSG_CANCEL:
|
||||
if (payload.length >= 12 && onCancel != null) {
|
||||
onCancel(readInt32(payload, 0), readInt32(payload, 4),
|
||||
readInt32(payload, 8));
|
||||
}
|
||||
case MSG_EXTENDED:
|
||||
handleExtended(payload);
|
||||
case MSG_HASH_REQUEST:
|
||||
if (payload.length >= 48 && onHashRequest != null) {
|
||||
onHashRequest(payload);
|
||||
}
|
||||
case MSG_HASHES:
|
||||
if (payload.length >= 48 && onHashes != null) {
|
||||
onHashes(payload);
|
||||
}
|
||||
case MSG_HASH_REJECT:
|
||||
if (payload.length >= 48 && onHashReject != null) {
|
||||
onHashReject(payload);
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
function handleExtended(payload:Bytes):Void {
|
||||
if (payload.length < 1) return;
|
||||
var extId = payload.get(0);
|
||||
var body = payload.sub(1, payload.length - 1);
|
||||
if (extId == 0) {
|
||||
var dict:Dynamic = null;
|
||||
try {
|
||||
dict = Bencode.decode(body);
|
||||
} catch(e:Dynamic) {
|
||||
return;
|
||||
}
|
||||
peerExtendedHandshake = dict;
|
||||
var m:Dynamic = Reflect.field(dict, "m");
|
||||
if (m != null) {
|
||||
for (name in Reflect.fields(m)) {
|
||||
extIds.set(name, Std.int(Reflect.field(m, name)));
|
||||
}
|
||||
}
|
||||
if (onExtendedHandshake != null) onExtendedHandshake(dict);
|
||||
} else {
|
||||
var name = localExtIds.get(extId);
|
||||
if (name != null) {
|
||||
if (onExtended != null) onExtended(name, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendExtendedHandshake():Void {
|
||||
var m:Dynamic = {};
|
||||
var id = 1;
|
||||
for (name in localExtNames) {
|
||||
Reflect.setField(m, name, id);
|
||||
localExtIds.set(id, name);
|
||||
id++;
|
||||
}
|
||||
var dict:Dynamic = {m: m, v: "Leenkx 0.1"};
|
||||
if (metaSize > 0) Reflect.setField(dict, "metadata_size", metaSize);
|
||||
if (extHandshakeFields != null) {
|
||||
for (f in Reflect.fields(extHandshakeFields)) {
|
||||
Reflect.setField(dict, f, Reflect.field(extHandshakeFields, f));
|
||||
}
|
||||
}
|
||||
sendExtendedMessage(0, Bencode.encode(dict));
|
||||
}
|
||||
|
||||
public var metaSize:Int = 0;
|
||||
public var extHandshakeFields:Dynamic = null;
|
||||
|
||||
public function sendExtended(name:String, payload:Bytes):Bool {
|
||||
var id = extIds.get(name);
|
||||
if (id == null) return false;
|
||||
var out = new haxe.io.BytesBuffer();
|
||||
out.addByte(id);
|
||||
out.addBytes(payload, 0, payload.length);
|
||||
sendExtendedMessage(-1, out.getBytes());
|
||||
return true;
|
||||
}
|
||||
|
||||
function sendExtendedMessage(id:Int, payload:Bytes):Void {
|
||||
var out = new haxe.io.BytesBuffer();
|
||||
if (id >= 0) out.addByte(id);
|
||||
out.addBytes(payload, 0, payload.length);
|
||||
sendMessage(MSG_EXTENDED, out.getBytes());
|
||||
}
|
||||
|
||||
var keepAliveBuf:Bytes = Bytes.alloc(4);
|
||||
|
||||
public function sendKeepAlive():Void {
|
||||
channel.send(keepAliveBuf);
|
||||
}
|
||||
|
||||
public function sendChoke():Void {
|
||||
amChoking = true;
|
||||
sendMessage(MSG_CHOKE, null);
|
||||
}
|
||||
public function sendUnchoke():Void {
|
||||
amChoking = false;
|
||||
sendMessage(MSG_UNCHOKE, null);
|
||||
}
|
||||
public function sendInterested():Void {
|
||||
amInterested = true;
|
||||
sendMessage(MSG_INTERESTED, null);
|
||||
}
|
||||
public function sendNotInterested():Void {
|
||||
amInterested = false;
|
||||
sendMessage(MSG_NOT_INTERESTED, null);
|
||||
}
|
||||
|
||||
public function sendHave(index:Int):Void {
|
||||
var out = Bytes.alloc(9);
|
||||
writeInt32(out, 0, 5);
|
||||
out.set(4, MSG_HAVE);
|
||||
writeInt32(out, 5, index);
|
||||
channel.send(out);
|
||||
}
|
||||
|
||||
public function sendBitfield(bitfield:Bytes):Void {
|
||||
sendMessage(MSG_BITFIELD, bitfield);
|
||||
}
|
||||
|
||||
public function sendRequest(index:Int, begin:Int, length:Int):Void {
|
||||
var out = Bytes.alloc(17);
|
||||
writeInt32(out, 0, 13);
|
||||
out.set(4, MSG_REQUEST);
|
||||
writeInt32(out, 5, index);
|
||||
writeInt32(out, 9, begin);
|
||||
writeInt32(out, 13, length);
|
||||
channel.send(out);
|
||||
}
|
||||
|
||||
public function sendPiece(index:Int, begin:Int, data:Bytes):Void {
|
||||
var total = 13 + data.length;
|
||||
var out = Bytes.alloc(total);
|
||||
writeInt32(out, 0, total - 4);
|
||||
out.set(4, MSG_PIECE);
|
||||
writeInt32(out, 5, index);
|
||||
writeInt32(out, 9, begin);
|
||||
out.blit(13, data, 0, data.length);
|
||||
channel.send(out);
|
||||
}
|
||||
|
||||
static function hashMsg(root:Bytes, base:Int, index:Int, length:Int, proofs:Int, ?hashes:Bytes):Bytes {
|
||||
var n = 48 + (hashes != null ? hashes.length : 0);
|
||||
var out = Bytes.alloc(n);
|
||||
out.blit(0, root, 0, 32);
|
||||
writeInt32(out, 32, base);
|
||||
writeInt32(out, 36, index);
|
||||
writeInt32(out, 40, length);
|
||||
writeInt32(out, 44, proofs);
|
||||
if (hashes != null) out.blit(48, hashes, 0, hashes.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
public function sendHashRequest(root:Bytes, base:Int, index:Int, length:Int, proofs:Int):Void {
|
||||
sendMessage(MSG_HASH_REQUEST,
|
||||
hashMsg(root, base, index, length, proofs));
|
||||
}
|
||||
|
||||
public function sendHashes(root:Bytes, base:Int, index:Int, length:Int, proofs:Int, hashes:Bytes):Void {
|
||||
sendMessage(MSG_HASHES,
|
||||
hashMsg(root, base, index, length, proofs, hashes));
|
||||
}
|
||||
|
||||
public function sendHashReject(root:Bytes, base:Int, index:Int, length:Int, proofs:Int):Void {
|
||||
sendMessage(MSG_HASH_REJECT,
|
||||
hashMsg(root, base, index, length, proofs));
|
||||
}
|
||||
|
||||
public function sendCancel(index:Int, begin:Int, length:Int):Void {
|
||||
var out = Bytes.alloc(17);
|
||||
writeInt32(out, 0, 13);
|
||||
out.set(4, MSG_CANCEL);
|
||||
writeInt32(out, 5, index);
|
||||
writeInt32(out, 9, begin);
|
||||
writeInt32(out, 13, length);
|
||||
channel.send(out);
|
||||
}
|
||||
|
||||
function sendMessage(id:Int, payload:Bytes):Void {
|
||||
if (closed) return;
|
||||
var len = 1 + (payload != null ? payload.length : 0);
|
||||
var total = 4 + 1 + (payload != null ? payload.length : 0);
|
||||
var out = Bytes.alloc(total);
|
||||
writeInt32(out, 0, len);
|
||||
out.set(4, id);
|
||||
if (payload != null) out.blit(5, payload, 0, payload.length);
|
||||
channel.send(out);
|
||||
}
|
||||
|
||||
public function close():Void {
|
||||
closed = true;
|
||||
channel.close();
|
||||
}
|
||||
|
||||
function fail(reason:String):Void {
|
||||
if (onError != null) onError(reason);
|
||||
close();
|
||||
}
|
||||
|
||||
static function bytesEqual(a:Bytes, b:Bytes):Bool {
|
||||
if (a.length != b.length) return false;
|
||||
for (i in 0...a.length) {
|
||||
if (a.get(i) != b.get(i)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static function readInt32(b:Bytes, off:Int):Int {
|
||||
return (b.get(off) << 24) | (b.get(off + 1) << 16)
|
||||
| (b.get(off + 2) << 8) | b.get(off + 3);
|
||||
}
|
||||
|
||||
static public function readU32(b:Bytes, off:Int):Int {
|
||||
return readInt32(b, off);
|
||||
}
|
||||
|
||||
static function writeInt32(b:Bytes, off:Int, v:Int):Void {
|
||||
b.set(off, (v >> 24) & 0xFF);
|
||||
b.set(off + 1, (v >> 16) & 0xFF);
|
||||
b.set(off + 2, (v >> 8) & 0xFF);
|
||||
b.set(off + 3, v & 0xFF);
|
||||
}
|
||||
}
|
||||
145
leenkx/Sources/leenkx/network/torrent/storage/Storage.hx
Normal file
145
leenkx/Sources/leenkx/network/torrent/storage/Storage.hx
Normal file
@ -0,0 +1,145 @@
|
||||
package leenkx.network.torrent.storage;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
interface IStorage {
|
||||
function write(offset:Int, data:Bytes):Void;
|
||||
function read(offset:Int, length:Int):Bytes;
|
||||
function flush():Void;
|
||||
function close():Void;
|
||||
function deleteData():Void;
|
||||
}
|
||||
|
||||
class MemoryStorage implements IStorage {
|
||||
var data:Bytes;
|
||||
|
||||
public function new(totalLength:Int) {
|
||||
data = Bytes.alloc(totalLength);
|
||||
}
|
||||
|
||||
public function write(offset:Int, chunk:Bytes):Void {
|
||||
data.blit(offset, chunk, 0, chunk.length);
|
||||
}
|
||||
|
||||
public function read(offset:Int, length:Int):Bytes {
|
||||
return data.sub(offset, length);
|
||||
}
|
||||
|
||||
public function flush():Void {}
|
||||
public function close():Void {}
|
||||
public function deleteData():Void {
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
|
||||
#if (sys || kha_krom)
|
||||
class FileTreeStorage implements IStorage {
|
||||
var dir:String;
|
||||
var files:Array<{path:String, length:Int, offset:Int}>;
|
||||
#if kha_krom
|
||||
var madeDirs = new Map<String, Bool>();
|
||||
#end
|
||||
|
||||
public function new(dir:String, files:Array<{path:String, length:Int, offset:Int}>) {
|
||||
this.dir = dir;
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
function filePath(f:{path:String, length:Int, offset:Int}):String {
|
||||
return dir + "/"
|
||||
+ leenkx.network.torrent.MetaInfo.sanitizePath(f.path);
|
||||
}
|
||||
|
||||
function writeChunk(f:{path:String, length:Int, offset:Int}, foffset:Int, chunk:Bytes):Void {
|
||||
var p = filePath(f);
|
||||
#if kha_krom
|
||||
var i = Std.int(Math.max(p.lastIndexOf("/"),
|
||||
p.lastIndexOf("\\")));
|
||||
if (i > 0) {
|
||||
var d = p.substr(0, i);
|
||||
if (!madeDirs.exists(d)) {
|
||||
madeDirs.set(d, true);
|
||||
var win = Krom.systemId() == "Windows";
|
||||
Krom.sysCommand(win
|
||||
? "mkdir \"" + StringTools.replace(d, "/", "\\") + "\""
|
||||
: "mkdir -p \"" + d + "\"");
|
||||
}
|
||||
}
|
||||
try {
|
||||
Krom.fileWriteBytes(p, chunk.getData(), foffset,
|
||||
chunk.length);
|
||||
} catch(e:Dynamic) {
|
||||
trace('[FileTreeStorage] WRITE FAILED ' + p
|
||||
+ ' off=' + foffset + ' len=' + chunk.length
|
||||
+ ' err=' + Std.string(e));
|
||||
return;
|
||||
}
|
||||
#else
|
||||
var i = Std.int(Math.max(p.lastIndexOf("/"),
|
||||
p.lastIndexOf("\\")));
|
||||
if (i > 0) {
|
||||
var d = p.substr(0, i);
|
||||
if (!sys.FileSystem.exists(d)) sys.FileSystem.createDirectory(d);
|
||||
}
|
||||
var out = sys.io.File.update(p, true);
|
||||
out.seek(foffset, sys.io.FileSeek.SeekBegin);
|
||||
out.writeBytes(chunk, 0, chunk.length);
|
||||
out.close();
|
||||
#end
|
||||
}
|
||||
|
||||
function readChunk(f:{path:String, length:Int, offset:Int}, foffset:Int, len:Int):Bytes {
|
||||
var p = filePath(f);
|
||||
#if kha_krom
|
||||
return Bytes.ofData(Krom.fileReadBytes(p, foffset, len));
|
||||
#else
|
||||
var buf = Bytes.alloc(len);
|
||||
if (sys.FileSystem.exists(p)) {
|
||||
var inp = sys.io.File.read(p, true);
|
||||
inp.seek(foffset, sys.io.FileSeek.SeekBegin);
|
||||
try inp.readBytes(buf, 0, len) catch(e:haxe.io.Eof) {}
|
||||
inp.close();
|
||||
}
|
||||
return buf;
|
||||
#end
|
||||
}
|
||||
|
||||
public function write(offset:Int, data:Bytes):Void {
|
||||
for (f in files) {
|
||||
var lo = offset > f.offset ? offset : f.offset;
|
||||
var hi = offset + data.length < f.offset + f.length
|
||||
? offset + data.length : f.offset + f.length;
|
||||
if (lo >= hi) continue;
|
||||
writeChunk(f, lo - f.offset,
|
||||
data.sub(lo - offset, hi - lo));
|
||||
}
|
||||
}
|
||||
|
||||
public function read(offset:Int, length:Int):Bytes {
|
||||
var out = Bytes.alloc(length);
|
||||
for (f in files) {
|
||||
var lo = offset > f.offset ? offset : f.offset;
|
||||
var hi = offset + length < f.offset + f.length
|
||||
? offset + length : f.offset + f.length;
|
||||
if (lo >= hi) continue;
|
||||
var chunk = readChunk(f, lo - f.offset, hi - lo);
|
||||
out.blit(lo - offset, chunk, 0, chunk.length);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public function flush():Void {}
|
||||
public function close():Void {}
|
||||
|
||||
public function deleteData():Void {
|
||||
for (f in files) {
|
||||
var p = filePath(f);
|
||||
#if sys
|
||||
if (sys.FileSystem.exists(p)) sys.FileSystem.deleteFile(p);
|
||||
#else
|
||||
Krom.deleteFile(p);
|
||||
#end
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
555
leenkx/Sources/leenkx/network/torrent/tracker/HttpTracker.hx
Normal file
555
leenkx/Sources/leenkx/network/torrent/tracker/HttpTracker.hx
Normal file
@ -0,0 +1,555 @@
|
||||
package leenkx.network.torrent.tracker;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
import leenkx.network.torrent.Bencode;
|
||||
import leenkx.network.torrent.Crypto;
|
||||
import leenkx.network.torrent.TorrentInfo;
|
||||
|
||||
#if (sys || kha_krom)
|
||||
class HttpTracker {
|
||||
public var onPeers:Array<{host:String, port:Int}> -> Void;
|
||||
public var onInterval:Int -> Void;
|
||||
public var onError:String -> Void;
|
||||
public var statsProvider:Void -> {downloaded:Float, uploaded:Float,
|
||||
left:Float} = null;
|
||||
|
||||
var url:String;
|
||||
var infoHash:Bytes;
|
||||
var peerId:Bytes;
|
||||
var port:Int;
|
||||
var intervalSec:Int = 1800;
|
||||
var numwant:Int = 50;
|
||||
var timer:haxe.Timer = null;
|
||||
var destroyed = false;
|
||||
var busy = false;
|
||||
var key:Int = Std.random(0x7FFFFFFF);
|
||||
var trackerId:String = null;
|
||||
|
||||
public function new(url:String, infoHash:Bytes, peerId:Bytes, port:Int = 6881) {
|
||||
this.url = url;
|
||||
this.infoHash = infoHash;
|
||||
this.peerId = peerId;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public function start():Void {
|
||||
announce("started");
|
||||
}
|
||||
|
||||
public function stop():Void {
|
||||
destroyed = true;
|
||||
if (timer != null) {
|
||||
timer.stop();
|
||||
timer = null;
|
||||
}
|
||||
announce("stopped");
|
||||
}
|
||||
|
||||
public function announceEvent(event:String):Void {
|
||||
announce(event);
|
||||
}
|
||||
|
||||
function announce(event:String):Void {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
var stats = statsProvider != null ? statsProvider()
|
||||
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
|
||||
var sep = url.indexOf("?") >= 0 ? "&" : "?";
|
||||
var req = url + sep
|
||||
+ "info_hash=" + percentEncode(infoHash)
|
||||
+ "&peer_id=" + percentEncode(peerId)
|
||||
+ "&port=" + port
|
||||
+ "&uploaded=" + Std.int(stats.uploaded)
|
||||
+ "&downloaded=" + Std.int(stats.downloaded)
|
||||
+ "&left=" + Std.int(stats.left)
|
||||
+ "&compact=1&numwant=" + numwant + "&key="
|
||||
+ StringTools.hex(key, 8);
|
||||
if (trackerId != null) req += "&trackerid=" + trackerId;
|
||||
if (event != null) req += "&event=" + event;
|
||||
var self = this;
|
||||
var finish = function(body:Bytes, err:String) {
|
||||
self.busy = false;
|
||||
if (self.destroyed) return;
|
||||
if (err != null) {
|
||||
if (self.onError != null) self.onError(err);
|
||||
self.schedule();
|
||||
return;
|
||||
}
|
||||
self.parseResponse(body);
|
||||
self.schedule();
|
||||
};
|
||||
#if sys
|
||||
var watchdog = new haxe.Timer(35000);
|
||||
watchdog.run = function() {
|
||||
watchdog.stop();
|
||||
var s = self.currentSock;
|
||||
if (s != null) try s.close() catch(e:Dynamic) {}
|
||||
};
|
||||
sys.thread.Thread.create(function() {
|
||||
self.httpGet(req, function(body:Bytes, err:String) {
|
||||
leenkx.network.torrent.TorrentClient.postMain(
|
||||
function() {
|
||||
watchdog.stop();
|
||||
finish(body, err);
|
||||
});
|
||||
});
|
||||
});
|
||||
#else
|
||||
httpGet(req, finish);
|
||||
#end
|
||||
}
|
||||
|
||||
function schedule():Void {
|
||||
if (destroyed) return;
|
||||
if (timer != null) timer.stop();
|
||||
timer = new haxe.Timer(intervalSec * 1000);
|
||||
timer.run = function() {
|
||||
timer.stop();
|
||||
announce(null);
|
||||
};
|
||||
}
|
||||
|
||||
function parseResponse(body:Bytes):Void {
|
||||
var dict:Dynamic;
|
||||
try {
|
||||
dict = Bencode.decode(body);
|
||||
} catch(e:Dynamic) {
|
||||
if (onError != null) onError("bad tracker response");
|
||||
return;
|
||||
}
|
||||
var failure = Reflect.field(dict, "failure reason");
|
||||
if (failure != null) {
|
||||
if (onError != null) onError(Std.string(failure));
|
||||
return;
|
||||
}
|
||||
var warning = Reflect.field(dict, "warning message");
|
||||
if (warning != null && onError != null) {
|
||||
onError("warning: " + Std.string(warning));
|
||||
}
|
||||
var tid = Reflect.field(dict, "tracker id");
|
||||
if (tid != null) trackerId = Std.string(tid);
|
||||
var iv = Reflect.field(dict, "interval");
|
||||
if (iv == null) iv = Reflect.field(dict, "min interval");
|
||||
if (iv != null) {
|
||||
var ivF = Std.parseFloat(Std.string(iv));
|
||||
if (!Math.isNaN(ivF) && ivF > 0) intervalSec = Std.int(ivF);
|
||||
}
|
||||
var peersField = Reflect.field(dict, "peers");
|
||||
var peers:Array<{host:String, port:Int}> = [];
|
||||
if (Std.isOfType(peersField, Bytes)) {
|
||||
peers = UdpTracker.parsePeers((peersField : Bytes), 0);
|
||||
} else if (Std.isOfType(peersField, Array)) {
|
||||
var arr:Array<Dynamic> = cast peersField;
|
||||
for (pd in arr) {
|
||||
var ip = Reflect.field(pd, "ip");
|
||||
var prt = Reflect.field(pd, "port");
|
||||
if (ip != null && prt != null) {
|
||||
peers.push({
|
||||
host: Std.isOfType(ip, Bytes)
|
||||
? (ip:Bytes).toString() : Std.string(ip),
|
||||
port: Std.int(Std.parseFloat(Std.string(prt)))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (onInterval != null) onInterval(intervalSec);
|
||||
if (peers.length > 0 && onPeers != null) onPeers(peers);
|
||||
}
|
||||
|
||||
function httpGet(reqUrl:String, cb:Bytes -> String -> Void):Void {
|
||||
var rest = reqUrl.substr(reqUrl.indexOf("://") + 3);
|
||||
var secure = reqUrl.indexOf("https://") == 0;
|
||||
var slash = rest.indexOf("/");
|
||||
var hostPort = slash >= 0 ? rest.substr(0, slash) : rest;
|
||||
var path = slash >= 0 ? rest.substr(slash) : "/";
|
||||
var colon = hostPort.indexOf(":");
|
||||
var host = colon >= 0 ? hostPort.substr(0, colon) : hostPort;
|
||||
var port = colon >= 0
|
||||
? Std.parseInt(hostPort.substr(colon + 1))
|
||||
: (secure ? 443 : 80);
|
||||
|
||||
#if kha_krom
|
||||
if (secure) {
|
||||
cb(null, "https trackers not supported on krom");
|
||||
return;
|
||||
}
|
||||
var sock = new leenkx.network.krom.KromSocket();
|
||||
sock.setBlocking(false);
|
||||
var connectFailed = false;
|
||||
try {
|
||||
sock.connect(
|
||||
new leenkx.network.krom.KromSocket.KromHost(host), port);
|
||||
} catch(e:Dynamic) {
|
||||
connectFailed = true;
|
||||
}
|
||||
if (connectFailed) {
|
||||
cb(null, "connect failed");
|
||||
return;
|
||||
}
|
||||
var request = "GET " + path + " HTTP/1.0\r\nHost: " + hostPort
|
||||
+ "\r\nConnection: close\r\n\r\n";
|
||||
var reqBytes = Bytes.ofString(request);
|
||||
var buf = new haxe.io.BytesBuffer();
|
||||
var deadline = haxe.Timer.stamp() + 15;
|
||||
var sentReq = false;
|
||||
var tick:Void -> Void = null;
|
||||
tick = function() {
|
||||
if (haxe.Timer.stamp() > deadline) {
|
||||
leenkx.network.krom.KromSocket.KromPump.remove(tick);
|
||||
sock.close();
|
||||
cb(null, "http tracker timeout");
|
||||
return;
|
||||
}
|
||||
if (!sock.isConnected()) return;
|
||||
if (!sentReq) {
|
||||
sock.sendRaw(reqBytes);
|
||||
sentReq = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var chunk = sock.recvRaw(65536);
|
||||
if (chunk == null) return;
|
||||
if (chunk.length == 0) {
|
||||
leenkx.network.krom.KromSocket.KromPump.remove(tick);
|
||||
sock.close();
|
||||
finishResponse(buf.getBytes(), cb);
|
||||
return;
|
||||
}
|
||||
buf.addBytes(chunk, 0, chunk.length);
|
||||
} catch(e:Dynamic) {
|
||||
leenkx.network.krom.KromSocket.KromPump.remove(tick);
|
||||
sock.close();
|
||||
finishResponse(buf.getBytes(), cb);
|
||||
}
|
||||
};
|
||||
leenkx.network.krom.KromSocket.KromPump.add(tick);
|
||||
#else
|
||||
var sock:sys.net.Socket;
|
||||
if (secure) {
|
||||
#if hl
|
||||
sock = new sys.ssl.Socket();
|
||||
#else
|
||||
cb(null, "https trackers not supported on this target");
|
||||
return;
|
||||
#end
|
||||
} else {
|
||||
sock = new sys.net.Socket();
|
||||
}
|
||||
sock.setTimeout(30);
|
||||
try {
|
||||
sock.connect(new sys.net.Host(host), port);
|
||||
currentSock = sock;
|
||||
var request = "GET " + path + " HTTP/1.0\r\nHost: " + hostPort
|
||||
+ "\r\nConnection: close\r\n\r\n";
|
||||
sock.output.write(Bytes.ofString(request));
|
||||
sock.output.flush();
|
||||
var buf = new haxe.io.BytesBuffer();
|
||||
while (true) {
|
||||
var chunk = Bytes.alloc(65536);
|
||||
var read = 0;
|
||||
try {
|
||||
read = sock.input.readBytes(chunk, 0, chunk.length);
|
||||
} catch(eof:haxe.io.Eof) {
|
||||
break;
|
||||
}
|
||||
if (read <= 0) break;
|
||||
buf.addBytes(chunk, 0, read);
|
||||
}
|
||||
sock.close();
|
||||
currentSock = null;
|
||||
finishResponse(buf.getBytes(), cb);
|
||||
} catch(e:Dynamic) {
|
||||
try sock.close() catch(_) {}
|
||||
currentSock = null;
|
||||
cb(null, Std.string(e));
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
#if sys
|
||||
var currentSock:sys.net.Socket = null;
|
||||
#end
|
||||
|
||||
function finishResponse(raw:Bytes, cb:Bytes -> String -> Void):Void {
|
||||
var body:Bytes = null;
|
||||
for (i in 0...raw.length - 3) {
|
||||
if (raw.get(i) == 0x0D && raw.get(i + 1) == 0x0A
|
||||
&& raw.get(i + 2) == 0x0D && raw.get(i + 3) == 0x0A) {
|
||||
body = raw.sub(i + 4, raw.length - i - 4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (body == null) {
|
||||
cb(null, "bad http response");
|
||||
return;
|
||||
}
|
||||
cb(body, null);
|
||||
}
|
||||
|
||||
static function percentEncode(b:Bytes):String {
|
||||
var sb = new StringBuf();
|
||||
var safe = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
+ "abcdefghijklmnopqrstuvwxyz-_.~";
|
||||
for (i in 0...b.length) {
|
||||
var c = b.get(i);
|
||||
if (safe.indexOf(String.fromCharCode(c)) >= 0) {
|
||||
sb.addChar(c);
|
||||
} else {
|
||||
sb.add("%");
|
||||
sb.add(StringTools.hex(c, 2));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
class HttpTrackerServer {
|
||||
public var onLog:String -> Void = null;
|
||||
public var onError:String -> Void = null;
|
||||
|
||||
var swarm = new UdpTracker.TrackerSwarm();
|
||||
var running = false;
|
||||
#if sys
|
||||
var sock:sys.net.Socket;
|
||||
#elseif kha_krom
|
||||
var sock:leenkx.network.krom.KromSocket;
|
||||
var timer:haxe.Timer = null;
|
||||
var pending:Array<{sock:leenkx.network.krom.KromSocket,
|
||||
buf:haxe.io.BytesBuffer}> = [];
|
||||
#end
|
||||
|
||||
public function new() {}
|
||||
|
||||
public function start(port:Int):Void {
|
||||
if (running) return;
|
||||
running = true;
|
||||
#if sys
|
||||
sock = new sys.net.Socket();
|
||||
sock.bind(new sys.net.Host("127.0.0.1"), port);
|
||||
sock.listen(64);
|
||||
var self = this;
|
||||
sys.thread.Thread.create(function() self.loop());
|
||||
#elseif kha_krom
|
||||
sock = new leenkx.network.krom.KromSocket();
|
||||
sock.bind(new leenkx.network.krom.KromSocket.KromHost(
|
||||
"127.0.0.1"), port);
|
||||
sock.listen(64);
|
||||
sock.setBlocking(false);
|
||||
timer = new haxe.Timer(5);
|
||||
timer.run = pump;
|
||||
#end
|
||||
log("http tracker listening on 127.0.0.1:" + port);
|
||||
}
|
||||
|
||||
public function stop():Void {
|
||||
running = false;
|
||||
#if kha_krom
|
||||
if (timer != null) {
|
||||
timer.stop();
|
||||
timer = null;
|
||||
}
|
||||
for (p in pending) try p.sock.close() catch (_) {}
|
||||
pending = [];
|
||||
#end
|
||||
if (sock != null) {
|
||||
try sock.close() catch (e:Dynamic) {}
|
||||
}
|
||||
}
|
||||
|
||||
#if sys
|
||||
function loop():Void {
|
||||
while (running) {
|
||||
try {
|
||||
var c = sock.accept();
|
||||
if (c != null) serve(c);
|
||||
} catch (e:Dynamic) {
|
||||
if (running) err("accept: " + Std.string(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
#elseif kha_krom
|
||||
function pump():Void {
|
||||
if (!running || sock == null) return;
|
||||
while (true) {
|
||||
var c:leenkx.network.krom.KromSocket = null;
|
||||
try {
|
||||
c = sock.accept();
|
||||
} catch (e:Dynamic) {
|
||||
break;
|
||||
}
|
||||
if (c == null) break;
|
||||
c.setBlocking(false);
|
||||
pending.push({sock: c, buf: new haxe.io.BytesBuffer()});
|
||||
}
|
||||
var i = pending.length;
|
||||
while (i-- > 0) {
|
||||
var p = pending[i];
|
||||
var drop = false;
|
||||
try {
|
||||
var d = p.sock.recvRaw(65536);
|
||||
if (d != null) p.buf.addBytes(d, 0, d.length);
|
||||
} catch (e:Dynamic) {
|
||||
drop = true;
|
||||
}
|
||||
var done = !drop && find(p.buf.getBytes(),
|
||||
"\r\n\r\n") >= 0;
|
||||
if (done) {
|
||||
var body = handleRequest(p.buf.getBytes(),
|
||||
"127.0.0.1");
|
||||
try {
|
||||
var head = "HTTP/1.0 200 OK\r\n"
|
||||
+ "Content-Length: " + body.length
|
||||
+ "\r\n\r\n";
|
||||
p.sock.sendRaw(Bytes.ofString(head));
|
||||
p.sock.sendRaw(body);
|
||||
} catch (e:Dynamic) {}
|
||||
}
|
||||
if (done || drop || p.buf.length > 65536) {
|
||||
try p.sock.close() catch (e:Dynamic) {}
|
||||
pending.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
#if sys
|
||||
function serve(c:sys.net.Socket):Void {
|
||||
try {
|
||||
var raw = new haxe.io.BytesBuffer();
|
||||
var tmp = Bytes.alloc(4096);
|
||||
while (find(raw.getBytes(), "\r\n\r\n") < 0) {
|
||||
var n = c.input.readBytes(tmp, 0, tmp.length);
|
||||
if (n <= 0) break;
|
||||
raw.addBytes(tmp, 0, n);
|
||||
if (raw.length > 65536) break;
|
||||
}
|
||||
var body = handleRequest(raw.getBytes(),
|
||||
c.peer().host.toString());
|
||||
reply(c, 200, body);
|
||||
} catch (e:Dynamic) {
|
||||
try c.close() catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function reply(c:sys.net.Socket, code:Int, body:Bytes):Void {
|
||||
var head = "HTTP/1.0 " + code + " OK\r\nContent-Length: "
|
||||
+ body.length + "\r\n\r\n";
|
||||
c.output.write(Bytes.ofString(head));
|
||||
c.output.write(body);
|
||||
c.output.flush();
|
||||
c.close();
|
||||
}
|
||||
#end
|
||||
|
||||
function handleRequest(req:Bytes, peerHost:String):Bytes {
|
||||
var lineEnd = find(req, "\r\n");
|
||||
var line = lineEnd > 0
|
||||
? req.sub(0, lineEnd).toString() : "";
|
||||
var q = line.indexOf("?");
|
||||
var sp2 = line.indexOf(" HTTP");
|
||||
if (q < 0 || sp2 < 0) {
|
||||
return Bytes.ofString("bad request");
|
||||
}
|
||||
var params = decodeQuery(req.sub(q + 1, sp2 - q - 1));
|
||||
var infoHash = params.get("info_hash");
|
||||
var peerId = params.get("peer_id");
|
||||
if (infoHash == null || peerId == null) {
|
||||
return Bencode.encode({
|
||||
"failure reason": "missing params"});
|
||||
}
|
||||
var port = Std.parseInt(textParam(params, "port"));
|
||||
var left = Std.parseFloat(textParam(params, "left"));
|
||||
var numwant = Std.parseInt(textParam(params, "numwant"));
|
||||
if (numwant == null || numwant <= 0 || numwant > 200) {
|
||||
numwant = 50;
|
||||
}
|
||||
var ipText = textParam(params, "ip");
|
||||
var host = ipText != null && ipText != ""
|
||||
? ipText : peerHost;
|
||||
var ev = textParam(params, "event");
|
||||
var res = swarm.announce(infoHash, peerId, host,
|
||||
port != null ? port : 6881,
|
||||
Math.isNaN(left) ? 0.0 : left,
|
||||
ev == "stopped", numwant);
|
||||
log("announce " + Crypto.toHex(infoHash).substr(0, 8)
|
||||
+ " peer=" + host + ":" + port + " event=" + ev
|
||||
+ " swarm=" + res.complete + "s/"
|
||||
+ res.incomplete + "l reply=" + res.peers.length);
|
||||
var compact = new haxe.io.BytesBuffer();
|
||||
UdpTracker.writePeers(compact, res.peers);
|
||||
return Bencode.encode({
|
||||
interval: swarm.intervalSec,
|
||||
complete: res.complete,
|
||||
incomplete: res.incomplete,
|
||||
"tracker id": "leenkx-local",
|
||||
peers: compact.getBytes()
|
||||
});
|
||||
}
|
||||
|
||||
static function find(b:Bytes, s:String):Int {
|
||||
var n = s.length;
|
||||
for (i in 0...b.length - n + 1) {
|
||||
var ok = true;
|
||||
for (j in 0...n) {
|
||||
if (b.get(i + j) != s.charCodeAt(j)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ok) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static function textParam(p:Map<String, Bytes>, k:String):String {
|
||||
var b = p.get(k);
|
||||
return b != null ? b.toString() : null;
|
||||
}
|
||||
|
||||
static function decodeQuery(raw:Bytes):Map<String, Bytes> {
|
||||
var out = new Map<String, Bytes>();
|
||||
var s = raw.toString();
|
||||
for (pair in s.split("&")) {
|
||||
var eq = pair.indexOf("=");
|
||||
var k = eq >= 0 ? pair.substr(0, eq) : pair;
|
||||
var v = eq >= 0 ? pair.substr(eq + 1) : "";
|
||||
out.set(StringTools.urlDecode(k), pctDecode(v));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static function pctDecode(s:String):Bytes {
|
||||
var out = new haxe.io.BytesBuffer();
|
||||
var i = 0;
|
||||
while (i < s.length) {
|
||||
var c = s.charCodeAt(i);
|
||||
if (c == 0x25 && i + 2 < s.length) {
|
||||
out.addByte(Std.parseInt("0x" + s.substr(i + 1, 2)));
|
||||
i += 3;
|
||||
} else if (c == 0x2B) {
|
||||
out.addByte(0x20);
|
||||
i++;
|
||||
} else {
|
||||
out.addByte(c & 0xff);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return out.getBytes();
|
||||
}
|
||||
|
||||
function log(m:String):Void {
|
||||
if (onLog != null) {
|
||||
var f = onLog;
|
||||
leenkx.network.torrent.TorrentClient.postMain(
|
||||
function() f(m));
|
||||
}
|
||||
}
|
||||
|
||||
function err(m:String):Void {
|
||||
if (onError != null) {
|
||||
var f = onError;
|
||||
leenkx.network.torrent.TorrentClient.postMain(
|
||||
function() f(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
696
leenkx/Sources/leenkx/network/torrent/tracker/UdpTracker.hx
Normal file
696
leenkx/Sources/leenkx/network/torrent/tracker/UdpTracker.hx
Normal file
@ -0,0 +1,696 @@
|
||||
package leenkx.network.torrent.tracker;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
import leenkx.network.torrent.Crypto;
|
||||
|
||||
#if (sys || kha_krom)
|
||||
|
||||
typedef TrackerPeer = {
|
||||
peerId:Bytes,
|
||||
host:String,
|
||||
port:Int,
|
||||
left:Float,
|
||||
stamp:Float
|
||||
}
|
||||
|
||||
typedef SwarmResult = {
|
||||
complete:Int,
|
||||
incomplete:Int,
|
||||
peers:Array<TrackerPeer>
|
||||
}
|
||||
|
||||
class TrackerSwarm {
|
||||
public var intervalSec:Int = 30;
|
||||
|
||||
var swarms:Map<String, Map<String, TrackerPeer>> = [];
|
||||
|
||||
public function new() {}
|
||||
|
||||
public function announce(infoHash:Bytes, peerId:Bytes, host:String, port:Int, left:Float, stopped:Bool, numwant:Int):SwarmResult {
|
||||
var key = Crypto.toHex(infoHash);
|
||||
var swarm = swarms.get(key);
|
||||
if (swarm == null) {
|
||||
swarm = new Map();
|
||||
swarms.set(key, swarm);
|
||||
}
|
||||
var pid = Crypto.toHex(peerId);
|
||||
if (stopped) {
|
||||
swarm.remove(pid);
|
||||
} else {
|
||||
swarm.set(pid, {
|
||||
peerId: peerId,
|
||||
host: host,
|
||||
port: port,
|
||||
left: left,
|
||||
stamp: haxe.Timer.stamp()
|
||||
});
|
||||
}
|
||||
var peers:Array<TrackerPeer> = [];
|
||||
var complete = 0;
|
||||
var incomplete = 0;
|
||||
for (p in swarm) {
|
||||
if (p.left <= 0) complete++; else incomplete++;
|
||||
if (pid == Crypto.toHex(p.peerId)) continue;
|
||||
if (peers.length < numwant) peers.push(p);
|
||||
}
|
||||
return {complete: complete, incomplete: incomplete,
|
||||
peers: peers};
|
||||
}
|
||||
}
|
||||
|
||||
class UdpTracker {
|
||||
static inline var PROTOCOL_ID_HI:Int = 0x41727101;
|
||||
static inline var PROTOCOL_ID_LO:Int = 0x80;
|
||||
public static inline var ACTION_CONNECT:Int = 0;
|
||||
public static inline var ACTION_ANNOUNCE:Int = 1;
|
||||
static inline var ACTION_SCRAPE:Int = 2;
|
||||
public static inline var ACTION_ERROR:Int = 3;
|
||||
public static inline var EVENT_COMPLETED:Int = 1;
|
||||
public static inline var EVENT_STARTED:Int = 2;
|
||||
public static inline var EVENT_STOPPED:Int = 3;
|
||||
|
||||
public var onPeers:Array<{host:String, port:Int}> -> Void;
|
||||
public var onInterval:Int -> Void;
|
||||
public var onError:String -> Void;
|
||||
public var statsProvider:Void -> {downloaded:Float, uploaded:Float,
|
||||
left:Float} = null;
|
||||
|
||||
var url:String;
|
||||
var infoHash:Bytes;
|
||||
var peerId:Bytes;
|
||||
var port:Int;
|
||||
var intervalSec:Int = 1800;
|
||||
var numwant:Int = 50;
|
||||
var timer:haxe.Timer = null;
|
||||
var destroyed = false;
|
||||
var inFlight = false;
|
||||
var key:Int = Std.random(0x7FFFFFFF);
|
||||
var connIdHi:Int = 0;
|
||||
var connIdLo:Int = 0;
|
||||
var connIdTime:Float = 0;
|
||||
|
||||
public function new(url:String, infoHash:Bytes, peerId:Bytes, port:Int = 6881) {
|
||||
this.url = url;
|
||||
this.infoHash = infoHash;
|
||||
this.peerId = peerId;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public function start():Void {
|
||||
announce(EVENT_STARTED);
|
||||
}
|
||||
|
||||
public function stop():Void {
|
||||
if (destroyed) return;
|
||||
announce(EVENT_STOPPED);
|
||||
destroyed = true;
|
||||
if (timer != null) {
|
||||
timer.stop();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function announceEvent(event:String):Void {
|
||||
var code = switch (event) {
|
||||
case "completed": EVENT_COMPLETED;
|
||||
case "started": EVENT_STARTED;
|
||||
case "stopped": EVENT_STOPPED;
|
||||
default: 0;
|
||||
};
|
||||
announce(code);
|
||||
}
|
||||
|
||||
function announce(event:Int):Void {
|
||||
if (inFlight) return;
|
||||
inFlight = true;
|
||||
#if kha_krom
|
||||
kromAnnounce(event);
|
||||
return;
|
||||
#end
|
||||
#if sys
|
||||
var self = this;
|
||||
var watchdog = new haxe.Timer(35000);
|
||||
watchdog.run = function() {
|
||||
watchdog.stop();
|
||||
if (self.currentSock != null) {
|
||||
try self.currentSock.close() catch(e:Dynamic) {}
|
||||
}
|
||||
};
|
||||
sys.thread.Thread.create(function() {
|
||||
var result = try {
|
||||
self.doAnnounce(event);
|
||||
} catch(e:Dynamic) {
|
||||
{peers: [], interval: 0, error: Std.string(e)};
|
||||
}
|
||||
leenkx.network.torrent.TorrentClient.postMain(function() {
|
||||
watchdog.stop();
|
||||
self.inFlight = false;
|
||||
if (self.destroyed) return;
|
||||
if (result.error != null) {
|
||||
if (self.onError != null) self.onError(result.error);
|
||||
} else {
|
||||
if (result.interval > 0) {
|
||||
self.intervalSec = result.interval;
|
||||
}
|
||||
if (self.onInterval != null) {
|
||||
self.onInterval(self.intervalSec);
|
||||
}
|
||||
if (result.peers.length > 0 && self.onPeers != null) {
|
||||
self.onPeers(result.peers);
|
||||
}
|
||||
}
|
||||
self.schedule();
|
||||
});
|
||||
});
|
||||
#end
|
||||
}
|
||||
|
||||
#if kha_krom
|
||||
var ksock:leenkx.network.krom.KromUdpSocket = null;
|
||||
var kHost:String = null;
|
||||
var kPort:Int = 0;
|
||||
var kTick:Void -> Void = null;
|
||||
var kAttempt:Int = 0;
|
||||
var kDeadline:Float = 0;
|
||||
var kState:Int = 0;
|
||||
var kTxId:Int = 0;
|
||||
var kEvent:Int = 0;
|
||||
var kLastError:String = "no response";
|
||||
|
||||
function kromAnnounce(event:Int):Void {
|
||||
var rest = url.substr(url.indexOf("://") + 3);
|
||||
var slash = rest.indexOf("/");
|
||||
var hostPort = slash >= 0 ? rest.substr(0, slash) : rest;
|
||||
var colon = hostPort.lastIndexOf(":");
|
||||
kHost = colon >= 0 ? hostPort.substr(0, colon) : hostPort;
|
||||
kPort = colon >= 0
|
||||
? Std.parseInt(hostPort.substr(colon + 1)) : 80;
|
||||
if (kPort == null) kPort = 80;
|
||||
|
||||
kEvent = event;
|
||||
kAttempt = 0;
|
||||
try {
|
||||
if (ksock != null) ksock.close();
|
||||
ksock = new leenkx.network.krom.KromUdpSocket();
|
||||
} catch(e:Dynamic) {
|
||||
finishAnnounce([], 0, "udp socket: " + Std.string(e));
|
||||
return;
|
||||
}
|
||||
kromRound();
|
||||
}
|
||||
|
||||
function kromRound():Void {
|
||||
kAttempt++;
|
||||
kDeadline = haxe.Timer.stamp() + 15;
|
||||
if (connIdTime == 0
|
||||
|| haxe.Timer.stamp() - connIdTime > 45) {
|
||||
kTxId = Std.random(0x7FFFFFFF);
|
||||
var connReq = new haxe.io.BytesBuffer();
|
||||
writeInt64(connReq, PROTOCOL_ID_HI, PROTOCOL_ID_LO);
|
||||
writeInt32(connReq, ACTION_CONNECT);
|
||||
writeInt32(connReq, kTxId);
|
||||
if (kSend(connReq.getBytes()) < 16) {
|
||||
finishAnnounce([], 0, "connect send failed");
|
||||
return;
|
||||
}
|
||||
kState = 1;
|
||||
} else {
|
||||
if (!kSendAnnounce()) return;
|
||||
kState = 2;
|
||||
}
|
||||
startKTimer();
|
||||
}
|
||||
|
||||
function kSendAnnounce():Bool {
|
||||
var stats = statsProvider != null ? statsProvider()
|
||||
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
|
||||
kTxId = Std.random(0x7FFFFFFF);
|
||||
var ann = new haxe.io.BytesBuffer();
|
||||
writeInt32(ann, connIdHi);
|
||||
writeInt32(ann, connIdLo);
|
||||
writeInt32(ann, ACTION_ANNOUNCE);
|
||||
writeInt32(ann, kTxId);
|
||||
ann.addBytes(infoHash, 0, 20);
|
||||
ann.addBytes(peerId, 0, 20);
|
||||
writeInt64v(ann, stats.downloaded);
|
||||
writeInt64v(ann, stats.left);
|
||||
writeInt64v(ann, stats.uploaded);
|
||||
writeInt32(ann, kEvent);
|
||||
writeInt32(ann, 0);
|
||||
writeInt32(ann, key);
|
||||
writeInt32(ann, numwant);
|
||||
var portBytes = Bytes.alloc(2);
|
||||
portBytes.set(0, (this.port >> 8) & 0xFF);
|
||||
portBytes.set(1, this.port & 0xFF);
|
||||
ann.addBytes(portBytes, 0, 2);
|
||||
if (kSend(ann.getBytes()) < 98) {
|
||||
finishAnnounce([], 0, "announce send failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function kSend(pkt:Bytes):Int {
|
||||
try {
|
||||
return ksock.sendTo(pkt, 0, pkt.length,
|
||||
new leenkx.network.krom.KromSocket.KromHost(kHost),
|
||||
kPort);
|
||||
} catch(e:Dynamic) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
function startKTimer():Void {
|
||||
if (kTick == null) {
|
||||
kTick = kromPoll;
|
||||
leenkx.network.krom.KromSocket.KromPump.add(kTick);
|
||||
}
|
||||
}
|
||||
|
||||
function kromPoll():Void {
|
||||
if ((destroyed && kEvent != EVENT_STOPPED) || ksock == null) {
|
||||
kromCleanup();
|
||||
inFlight = false;
|
||||
return;
|
||||
}
|
||||
var pkt:{data:Bytes, host:String, port:Int} = null;
|
||||
try {
|
||||
pkt = ksock.recvFrom(65536);
|
||||
} catch(e:Dynamic) {
|
||||
finishAnnounce([], 0, "recv: " + Std.string(e));
|
||||
return;
|
||||
}
|
||||
if (pkt != null) {
|
||||
var resp = pkt.data;
|
||||
if (kState == 1 && resp.length >= 16
|
||||
&& readInt32(resp, 0) == ACTION_CONNECT
|
||||
&& readInt32(resp, 4) == kTxId) {
|
||||
connIdHi = readInt32(resp, 8);
|
||||
connIdLo = readInt32(resp, 12);
|
||||
connIdTime = haxe.Timer.stamp();
|
||||
if (!kSendAnnounce()) return;
|
||||
kState = 2;
|
||||
kDeadline = haxe.Timer.stamp() + 15;
|
||||
} else if (kState == 2 && resp.length >= 20) {
|
||||
var action = readInt32(resp, 0);
|
||||
if (action == ACTION_ERROR) {
|
||||
finishAnnounce([], 0, "tracker error: "
|
||||
+ resp.sub(8, resp.length - 8).toString());
|
||||
return;
|
||||
}
|
||||
if (action == ACTION_ANNOUNCE
|
||||
&& readInt32(resp, 4) == kTxId) {
|
||||
var interval = readInt32(resp, 8);
|
||||
finishAnnounce(parsePeers(resp), interval, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (haxe.Timer.stamp() > kDeadline) {
|
||||
kLastError = kState == 1 ? "connect timeout"
|
||||
: "announce timeout";
|
||||
if (kAttempt < 3) {
|
||||
connIdTime = 0;
|
||||
kromRound();
|
||||
} else {
|
||||
finishAnnounce([], 0, kLastError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function finishAnnounce(peers:Array<{host:String, port:Int}>, interval:Int, error:String):Void {
|
||||
kromCleanup();
|
||||
inFlight = false;
|
||||
if (destroyed) return;
|
||||
if (error != null) {
|
||||
if (onError != null) onError(error);
|
||||
} else {
|
||||
if (interval > 0) intervalSec = interval;
|
||||
if (onInterval != null) onInterval(intervalSec);
|
||||
if (peers.length > 0 && onPeers != null) onPeers(peers);
|
||||
}
|
||||
schedule();
|
||||
}
|
||||
|
||||
function kromCleanup():Void {
|
||||
if (kTick != null) {
|
||||
leenkx.network.krom.KromSocket.KromPump.remove(kTick);
|
||||
kTick = null;
|
||||
}
|
||||
if (ksock != null) {
|
||||
try ksock.close() catch(e:Dynamic) {}
|
||||
ksock = null;
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
function schedule():Void {
|
||||
if (destroyed) return;
|
||||
if (timer != null) timer.stop();
|
||||
timer = new haxe.Timer(intervalSec * 1000);
|
||||
timer.run = function() {
|
||||
timer.stop();
|
||||
announce(0);
|
||||
};
|
||||
}
|
||||
|
||||
#if sys
|
||||
function doAnnounce(event:Int):{peers:Array<{host:String, port:Int}>,
|
||||
interval:Int, error:String} {
|
||||
var empty = {peers: [], interval: 0, error: null};
|
||||
var rest = url.substr(url.indexOf("://") + 3);
|
||||
var slash = rest.indexOf("/");
|
||||
var hostPort = slash >= 0 ? rest.substr(0, slash) : rest;
|
||||
var colon = hostPort.lastIndexOf(":");
|
||||
var host = colon >= 0 ? hostPort.substr(0, colon) : hostPort;
|
||||
var port = colon >= 0
|
||||
? Std.parseInt(hostPort.substr(colon + 1)) : 80;
|
||||
if (port == null) port = 80;
|
||||
|
||||
var sock = new sys.net.UdpSocket();
|
||||
var addr = new sys.net.Address();
|
||||
try {
|
||||
addr.host = new sys.net.Host(host).ip;
|
||||
addr.port = port;
|
||||
} catch(e:Dynamic) {
|
||||
return {peers: [], interval: 0,
|
||||
error: "resolve: " + Std.string(e)};
|
||||
}
|
||||
|
||||
var lastError = "no response";
|
||||
for (attempt in 0...3) {
|
||||
var r = tryAnnounce(sock, addr, event);
|
||||
if (r.error == null) {
|
||||
sock.close();
|
||||
return r;
|
||||
}
|
||||
lastError = r.error;
|
||||
connIdTime = 0;
|
||||
}
|
||||
sock.close();
|
||||
return {peers: [], interval: 0, error: lastError};
|
||||
}
|
||||
|
||||
function tryAnnounce(sock:sys.net.UdpSocket, addr:sys.net.Address, event:Int):{peers:Array<{host:String, port:Int}>, interval:Int, error:String} {
|
||||
var now = haxe.Timer.stamp();
|
||||
if (connIdTime == 0 || now - connIdTime > 45) {
|
||||
var txId = Std.random(0x7FFFFFFF);
|
||||
var connReq = new haxe.io.BytesBuffer();
|
||||
writeInt64(connReq, PROTOCOL_ID_HI, PROTOCOL_ID_LO); // protocol id
|
||||
writeInt32(connReq, ACTION_CONNECT);
|
||||
writeInt32(connReq, txId);
|
||||
try {
|
||||
sock.sendTo(connReq.getBytes(), 0, 16, addr);
|
||||
} catch(e:Dynamic) {
|
||||
return {peers: [], interval: 0,
|
||||
error: "send: " + Std.string(e)};
|
||||
}
|
||||
var resp = recvWithTimeout(sock, 15000);
|
||||
if (resp == null || resp.length < 16) {
|
||||
return {peers: [], interval: 0, error: "connect timeout"};
|
||||
}
|
||||
if (readInt32(resp, 0) != ACTION_CONNECT
|
||||
|| readInt32(resp, 4) != txId) {
|
||||
return {peers: [], interval: 0,
|
||||
error: "bad connect response"};
|
||||
}
|
||||
connIdHi = readInt32(resp, 8);
|
||||
connIdLo = readInt32(resp, 12);
|
||||
connIdTime = now;
|
||||
}
|
||||
|
||||
var stats = statsProvider != null ? statsProvider()
|
||||
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
|
||||
|
||||
var txId = Std.random(0x7FFFFFFF);
|
||||
var ann = new haxe.io.BytesBuffer();
|
||||
writeInt32(ann, connIdHi);
|
||||
writeInt32(ann, connIdLo);
|
||||
writeInt32(ann, ACTION_ANNOUNCE);
|
||||
writeInt32(ann, txId);
|
||||
ann.addBytes(infoHash, 0, 20);
|
||||
ann.addBytes(peerId, 0, 20);
|
||||
writeInt64v(ann, stats.downloaded);
|
||||
writeInt64v(ann, stats.left);
|
||||
writeInt64v(ann, stats.uploaded);
|
||||
writeInt32(ann, event);
|
||||
writeInt32(ann, 0);
|
||||
writeInt32(ann, key);
|
||||
writeInt32(ann, numwant);
|
||||
var portBytes = Bytes.alloc(2);
|
||||
portBytes.set(0, (this.port >> 8) & 0xFF);
|
||||
portBytes.set(1, this.port & 0xFF);
|
||||
ann.addBytes(portBytes, 0, 2);
|
||||
try {
|
||||
sock.sendTo(ann.getBytes(), 0, 98, addr);
|
||||
} catch(e:Dynamic) {
|
||||
return {peers: [], interval: 0,
|
||||
error: "announce send: " + Std.string(e)};
|
||||
}
|
||||
var resp = recvWithTimeout(sock, 15000);
|
||||
if (resp == null || resp.length < 20) {
|
||||
return {peers: [], interval: 0, error: "announce timeout"};
|
||||
}
|
||||
if (readInt32(resp, 0) == ACTION_ERROR) {
|
||||
return {peers: [], interval: 0,
|
||||
error: "tracker error: "
|
||||
+ resp.sub(8, resp.length - 8).toString()};
|
||||
}
|
||||
if (readInt32(resp, 0) != ACTION_ANNOUNCE
|
||||
|| readInt32(resp, 4) != txId) {
|
||||
return {peers: [], interval: 0, error: "bad announce response"};
|
||||
}
|
||||
var interval = readInt32(resp, 8);
|
||||
return {peers: parsePeers(resp), interval: interval,
|
||||
error: null};
|
||||
}
|
||||
|
||||
var currentSock:sys.net.UdpSocket = null;
|
||||
|
||||
function recvWithTimeout(sock:sys.net.UdpSocket, ms:Int):Bytes {
|
||||
currentSock = sock;
|
||||
sock.setTimeout(ms / 1000);
|
||||
var buf = Bytes.alloc(65536);
|
||||
var addr = new sys.net.Address();
|
||||
try {
|
||||
var r = sock.readFrom(buf, 0, buf.length, addr);
|
||||
if (r > 0) return buf.sub(0, r);
|
||||
} catch(e:Dynamic) {
|
||||
var m = Std.string(e);
|
||||
if (m != "Blocked" && m != "Eof") trace("Error: " + e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
#end
|
||||
|
||||
static public function parsePeers(resp:Bytes, off:Int = 20):Array<{host:String, port:Int}> {
|
||||
var peers:Array<{host:String, port:Int}> = [];
|
||||
while (off + 6 <= resp.length) {
|
||||
peers.push({
|
||||
host: resp.get(off) + "." + resp.get(off + 1) + "."
|
||||
+ resp.get(off + 2) + "." + resp.get(off + 3),
|
||||
port: (resp.get(off + 4) << 8) | resp.get(off + 5)
|
||||
});
|
||||
off += 6;
|
||||
}
|
||||
return peers;
|
||||
}
|
||||
|
||||
static public function writePeers(buf:haxe.io.BytesBuffer, peers:Array<{host:String, port:Int}>):Void {
|
||||
for (p in peers) {
|
||||
for (o in p.host.split(".")) {
|
||||
buf.addByte(Std.parseInt(o));
|
||||
}
|
||||
buf.addByte((p.port >> 8) & 0xff);
|
||||
buf.addByte(p.port & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
static public function readInt32(b:Bytes, pos:Int):Int {
|
||||
return (b.get(pos) << 24) | (b.get(pos + 1) << 16)
|
||||
| (b.get(pos + 2) << 8) | b.get(pos + 3);
|
||||
}
|
||||
|
||||
static public function writeInt32(buf:haxe.io.BytesBuffer, v:Int):Void {
|
||||
buf.addByte((v >> 24) & 0xFF);
|
||||
buf.addByte((v >> 16) & 0xFF);
|
||||
buf.addByte((v >> 8) & 0xFF);
|
||||
buf.addByte(v & 0xFF);
|
||||
}
|
||||
|
||||
static function writeInt64(buf:haxe.io.BytesBuffer, hi:Int, lo:Int):Void {
|
||||
writeInt32(buf, hi);
|
||||
writeInt32(buf, lo);
|
||||
}
|
||||
|
||||
static function writeInt64v(buf:haxe.io.BytesBuffer, v:Float):Void {
|
||||
if (v < 0) v = 0;
|
||||
var hi = Std.int(v / 4294967296);
|
||||
var lo = Std.int(v - hi * 4294967296);
|
||||
writeInt32(buf, hi);
|
||||
writeInt32(buf, lo);
|
||||
}
|
||||
}
|
||||
|
||||
class UdpTrackerServer {
|
||||
public var onLog:String -> Void = null;
|
||||
public var onError:String -> Void = null;
|
||||
|
||||
var swarm = new TrackerSwarm();
|
||||
var running = false;
|
||||
#if sys
|
||||
var sock:sys.net.UdpSocket;
|
||||
#elseif kha_krom
|
||||
var sock:leenkx.network.krom.KromUdpSocket;
|
||||
var timer:haxe.Timer = null;
|
||||
#end
|
||||
|
||||
public function new() {}
|
||||
|
||||
public function start(port:Int):Void {
|
||||
if (running) return;
|
||||
running = true;
|
||||
#if sys
|
||||
sock = new sys.net.UdpSocket();
|
||||
sock.bind(new sys.net.Host("127.0.0.1"), port);
|
||||
var self = this;
|
||||
sys.thread.Thread.create(function() self.loop());
|
||||
#elseif kha_krom
|
||||
sock = new leenkx.network.krom.KromUdpSocket();
|
||||
sock.bind(new leenkx.network.krom.KromSocket.KromHost(
|
||||
"127.0.0.1"), port);
|
||||
timer = new haxe.Timer(5);
|
||||
timer.run = pump;
|
||||
#end
|
||||
log("udp tracker listening on 127.0.0.1:" + port);
|
||||
}
|
||||
|
||||
public function stop():Void {
|
||||
running = false;
|
||||
#if kha_krom
|
||||
if (timer != null) {
|
||||
timer.stop();
|
||||
timer = null;
|
||||
}
|
||||
#end
|
||||
if (sock != null) {
|
||||
try sock.close() catch (e:Dynamic) {}
|
||||
}
|
||||
}
|
||||
|
||||
#if sys
|
||||
function loop():Void {
|
||||
var buf = Bytes.alloc(65536);
|
||||
var from = new sys.net.Address();
|
||||
while (running) {
|
||||
try {
|
||||
var n = sock.readFrom(buf, 0, buf.length, from);
|
||||
if (n > 0) {
|
||||
var resp = handle(buf.sub(0, n),
|
||||
from.getHost().toString(), from.port);
|
||||
if (resp != null) {
|
||||
sock.sendTo(resp, 0, resp.length, from);
|
||||
}
|
||||
}
|
||||
} catch (e:Dynamic) {
|
||||
if (running) err("recv: " + Std.string(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
#elseif kha_krom
|
||||
function pump():Void {
|
||||
if (!running || sock == null) return;
|
||||
while (true) {
|
||||
var r:{data:Bytes, host:String, port:Int} = null;
|
||||
try {
|
||||
r = sock.recvFrom(65536);
|
||||
} catch (e:Dynamic) {
|
||||
err("recv: " + Std.string(e));
|
||||
break;
|
||||
}
|
||||
if (r == null) break;
|
||||
var resp = handle(r.data, r.host, r.port);
|
||||
if (resp != null) {
|
||||
try {
|
||||
sock.sendTo(resp, 0, resp.length,
|
||||
new leenkx.network.krom.KromSocket
|
||||
.KromHost(r.host), r.port);
|
||||
} catch (e:Dynamic) {
|
||||
err("send: " + Std.string(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
||||
function handle(pkt:Bytes, host0:String, port0:Int):Bytes {
|
||||
if (pkt.length < 16) return null;
|
||||
var action = UdpTracker.readInt32(pkt, 8);
|
||||
var tx = UdpTracker.readInt32(pkt, 12);
|
||||
if (action == UdpTracker.ACTION_CONNECT) {
|
||||
var r = new haxe.io.BytesBuffer();
|
||||
UdpTracker.writeInt32(r, UdpTracker.ACTION_CONNECT);
|
||||
UdpTracker.writeInt32(r, tx);
|
||||
UdpTracker.writeInt32(r, Std.random(0x7FFFFFFF));
|
||||
UdpTracker.writeInt32(r, Std.random(0x7FFFFFFF));
|
||||
return r.getBytes();
|
||||
}
|
||||
if (action != UdpTracker.ACTION_ANNOUNCE
|
||||
|| pkt.length < 98) {
|
||||
var r = new haxe.io.BytesBuffer();
|
||||
UdpTracker.writeInt32(r, 3);
|
||||
UdpTracker.writeInt32(r, tx);
|
||||
r.addString("bad announce");
|
||||
return r.getBytes();
|
||||
}
|
||||
var infoHash = pkt.sub(16, 20);
|
||||
var peerId = pkt.sub(36, 20);
|
||||
var hi = UdpTracker.readInt32(pkt, 64);
|
||||
var lo = UdpTracker.readInt32(pkt, 68);
|
||||
var left = hi * 4294967296.0
|
||||
+ (lo < 0 ? lo + 4294967296.0 : lo);
|
||||
var event = UdpTracker.readInt32(pkt, 80);
|
||||
var ipParam = UdpTracker.readInt32(pkt, 84);
|
||||
var numwant = UdpTracker.readInt32(pkt, 92);
|
||||
var port = (pkt.get(96) << 8) | pkt.get(97);
|
||||
if (numwant <= 0 || numwant > 200) numwant = 50;
|
||||
var host = host0 != null ? host0 : "127.0.0.1";
|
||||
if (ipParam != 0) {
|
||||
host = ((ipParam >> 24) & 0xff) + "."
|
||||
+ ((ipParam >> 16) & 0xff) + "."
|
||||
+ ((ipParam >> 8) & 0xff) + "."
|
||||
+ (ipParam & 0xff);
|
||||
}
|
||||
var res = swarm.announce(infoHash, peerId, host, port,
|
||||
left, event == 3, numwant);
|
||||
log("announce " + Crypto.toHex(infoHash).substr(0, 8)
|
||||
+ " peer=" + host + ":" + port + " event=" + event
|
||||
+ " swarm=" + res.complete + "s/"
|
||||
+ res.incomplete + "l reply=" + res.peers.length);
|
||||
var r = new haxe.io.BytesBuffer();
|
||||
UdpTracker.writeInt32(r, UdpTracker.ACTION_ANNOUNCE);
|
||||
UdpTracker.writeInt32(r, tx);
|
||||
UdpTracker.writeInt32(r, swarm.intervalSec);
|
||||
UdpTracker.writeInt32(r, res.incomplete);
|
||||
UdpTracker.writeInt32(r, res.complete);
|
||||
UdpTracker.writePeers(r, res.peers);
|
||||
return r.getBytes();
|
||||
}
|
||||
|
||||
function log(m:String):Void {
|
||||
if (onLog != null) {
|
||||
var f = onLog;
|
||||
leenkx.network.torrent.TorrentClient.postMain(
|
||||
function() f(m));
|
||||
}
|
||||
}
|
||||
|
||||
function err(m:String):Void {
|
||||
if (onError != null) {
|
||||
var f = onError;
|
||||
leenkx.network.torrent.TorrentClient.postMain(
|
||||
function() f(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
#end
|
||||
@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package leenkx.network.torrent.transport;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
|
||||
typedef TransportOpts = {
|
||||
@:optional var announce:Array<String>;
|
||||
@:optional var name:String;
|
||||
@:optional var iceServers:Array<String>;
|
||||
@:optional var tracker:Dynamic;
|
||||
@:optional var wtOpts:Dynamic;
|
||||
@:optional var torrentOpts:Dynamic;
|
||||
@:optional var torrent:Dynamic;
|
||||
@:optional var pk:String;
|
||||
@:optional var ek:String;
|
||||
@:optional var infoHashBinary:String;
|
||||
@:optional var peerIdBinary:String;
|
||||
}
|
||||
|
||||
interface ITransport {
|
||||
function join(identifier:String, opts:TransportOpts):Void;
|
||||
function broadcast(data:Bytes):Void;
|
||||
function sendTo(peerPk:String, data:Bytes):Bool;
|
||||
function getPeerCount():Int;
|
||||
function destroy(?cb:Void -> Void):Void;
|
||||
|
||||
var onReady:Void -> Void;
|
||||
var onPeerConnected:String -> Int -> Void;
|
||||
var onPeerDisconnected:Int -> Void;
|
||||
var onPeerData:String -> Bytes -> Void;
|
||||
var onPeerHandshake:String -> String -> Void;
|
||||
var onTrackerUpdate:Dynamic -> Void;
|
||||
var onTrackerAnnounce:Void -> Void;
|
||||
var onError:String -> Void;
|
||||
}
|
||||
159
leenkx/Sources/leenkx/network/torrent/transport/RtcNative.hx
Normal file
159
leenkx/Sources/leenkx/network/torrent/transport/RtcNative.hx
Normal file
@ -0,0 +1,159 @@
|
||||
package leenkx.network.torrent.transport;
|
||||
|
||||
#if kha_krom
|
||||
|
||||
@:native("krom_rtc_init") extern function krom_rtc_init():Int;
|
||||
@:native("krom_rtc_cleanup") extern function krom_rtc_cleanup():Void;
|
||||
@:native("krom_rtc_create_peer_connection") extern function krom_rtc_create_peer_connection(iceServers:Array<String>):Int;
|
||||
@:native("krom_rtc_close_peer_connection") extern function krom_rtc_close_peer_connection(pc:Int):Void;
|
||||
@:native("krom_rtc_delete_peer_connection") extern function krom_rtc_delete_peer_connection(pc:Int):Void;
|
||||
@:native("krom_rtc_set_local_description") extern function krom_rtc_set_local_description(pc:Int, ?type:String):Void;
|
||||
@:native("krom_rtc_set_remote_description") extern function krom_rtc_set_remote_description(pc:Int, sdp:String, type:String):Int;
|
||||
@:native("krom_rtc_add_remote_candidate") extern function krom_rtc_add_remote_candidate(pc:Int, cand:String, mid:String):Int;
|
||||
@:native("krom_rtc_get_local_description") extern function krom_rtc_get_local_description(pc:Int):String;
|
||||
@:native("krom_rtc_get_local_description_type") extern function krom_rtc_get_local_description_type(pc:Int):String;
|
||||
@:native("krom_rtc_create_data_channel") extern function krom_rtc_create_data_channel(pc:Int, label:String, ?unordered:Bool, ?maxRetransmits:Int):Int;
|
||||
@:native("krom_rtc_delete_data_channel") extern function krom_rtc_delete_data_channel(dc:Int):Void;
|
||||
@:native("krom_rtc_send_message") extern function krom_rtc_send_message(id:Int, data:Dynamic):Int;
|
||||
@:native("krom_rtc_close") extern function krom_rtc_close(id:Int):Void;
|
||||
@:native("krom_rtc_delete") extern function krom_rtc_delete(id:Int):Void;
|
||||
@:native("krom_rtc_is_open") extern function krom_rtc_is_open(id:Int):Bool;
|
||||
@:native("krom_rtc_get_buffered_amount") extern function krom_rtc_get_buffered_amount(id:Int):Int;
|
||||
@:native("krom_rtc_set_local_description_callback") extern function krom_rtc_set_local_description_callback(pc:Int, fn:String -> String -> Void):Void;
|
||||
@:native("krom_rtc_set_local_candidate_callback") extern function krom_rtc_set_local_candidate_callback(pc:Int, fn:String -> String -> Void):Void;
|
||||
@:native("krom_rtc_set_state_change_callback") extern function krom_rtc_set_state_change_callback(pc:Int, fn:Int -> Void):Void;
|
||||
@:native("krom_rtc_set_gathering_state_callback") extern function krom_rtc_set_gathering_state_callback(pc:Int, fn:Int -> Void):Void;
|
||||
@:native("krom_rtc_set_data_channel_callback") extern function krom_rtc_set_data_channel_callback(pc:Int, fn:Int -> Void):Void;
|
||||
@:native("krom_rtc_set_open_callback") extern function krom_rtc_set_open_callback(id:Int, fn:Void -> Void):Void;
|
||||
@:native("krom_rtc_set_closed_callback") extern function krom_rtc_set_closed_callback(id:Int, fn:Void -> Void):Void;
|
||||
@:native("krom_rtc_set_error_callback") extern function krom_rtc_set_error_callback(id:Int, fn:String -> Void):Void;
|
||||
@:native("krom_rtc_set_message_callback") extern function krom_rtc_set_message_callback(id:Int, fn:Dynamic -> Void):Void;
|
||||
@:native("krom_rtc_random_bytes") extern function krom_rtc_random_bytes(len:Int):js.lib.ArrayBuffer;
|
||||
|
||||
class RtcNative {
|
||||
public static inline var RTC_NEW:Int = 0;
|
||||
public static inline var RTC_CONNECTING:Int = 1;
|
||||
public static inline var RTC_CONNECTED:Int = 2;
|
||||
public static inline var RTC_DISCONNECTED:Int = 3;
|
||||
public static inline var RTC_FAILED:Int = 4;
|
||||
public static inline var RTC_CLOSED:Int = 5;
|
||||
|
||||
public static inline var RTC_GATHERING_NEW:Int = 0;
|
||||
public static inline var RTC_GATHERING_INPROGRESS:Int = 1;
|
||||
public static inline var RTC_GATHERING_COMPLETE:Int = 2;
|
||||
|
||||
public static function init():Int {
|
||||
return krom_rtc_init();
|
||||
}
|
||||
|
||||
public static function cleanup():Void {
|
||||
krom_rtc_cleanup();
|
||||
}
|
||||
|
||||
public static function createPeerConnection(iceServers:Array<String>):Int {
|
||||
return krom_rtc_create_peer_connection(iceServers);
|
||||
}
|
||||
|
||||
public static function closePeerConnection(pc:Int):Void {
|
||||
krom_rtc_close_peer_connection(pc);
|
||||
}
|
||||
|
||||
public static function deletePeerConnection(pc:Int):Void {
|
||||
krom_rtc_delete_peer_connection(pc);
|
||||
}
|
||||
|
||||
public static function setLocalDescription(pc:Int, ?type:String):Void {
|
||||
krom_rtc_set_local_description(pc, type);
|
||||
}
|
||||
|
||||
public static function setRemoteDescription(pc:Int, sdp:String, type:String):Int {
|
||||
return krom_rtc_set_remote_description(pc, sdp, type);
|
||||
}
|
||||
|
||||
public static function addRemoteCandidate(pc:Int, cand:String, mid:String):Int {
|
||||
return krom_rtc_add_remote_candidate(pc, cand, mid);
|
||||
}
|
||||
|
||||
public static function getLocalDescription(pc:Int):String {
|
||||
return krom_rtc_get_local_description(pc);
|
||||
}
|
||||
|
||||
public static function getLocalDescriptionType(pc:Int):String {
|
||||
return krom_rtc_get_local_description_type(pc);
|
||||
}
|
||||
|
||||
public static function createDataChannel(pc:Int, label:String, unordered:Bool = true, maxRetransmits:Int = 0):Int {
|
||||
return krom_rtc_create_data_channel(pc, label, unordered, maxRetransmits);
|
||||
}
|
||||
|
||||
public static function deleteDataChannel(dc:Int):Void {
|
||||
krom_rtc_delete_data_channel(dc);
|
||||
}
|
||||
|
||||
public static function sendMessage(id:Int, data:Dynamic):Int {
|
||||
return krom_rtc_send_message(id, data);
|
||||
}
|
||||
|
||||
public static function close(id:Int):Void {
|
||||
krom_rtc_close(id);
|
||||
}
|
||||
|
||||
public static function delete(id:Int):Void {
|
||||
krom_rtc_delete(id);
|
||||
}
|
||||
|
||||
public static function isOpen(id:Int):Bool {
|
||||
return krom_rtc_is_open(id);
|
||||
}
|
||||
|
||||
public static function getBufferedAmount(id:Int):Int {
|
||||
return krom_rtc_get_buffered_amount(id);
|
||||
}
|
||||
|
||||
public static function setLocalDescriptionCallback(pc:Int, fn:String -> String -> Void):Void {
|
||||
krom_rtc_set_local_description_callback(pc, fn);
|
||||
}
|
||||
|
||||
public static function setLocalCandidateCallback(pc:Int, fn:String -> String -> Void):Void {
|
||||
krom_rtc_set_local_candidate_callback(pc, fn);
|
||||
}
|
||||
|
||||
public static function setStateChangeCallback(pc:Int, fn:Int -> Void):Void {
|
||||
krom_rtc_set_state_change_callback(pc, fn);
|
||||
}
|
||||
|
||||
public static function setGatheringStateCallback(pc:Int, fn:Int -> Void):Void {
|
||||
krom_rtc_set_gathering_state_callback(pc, fn);
|
||||
}
|
||||
|
||||
public static function setDataChannelCallback(pc:Int, fn:Int -> Void):Void {
|
||||
krom_rtc_set_data_channel_callback(pc, fn);
|
||||
}
|
||||
|
||||
public static function setOpenCallback(id:Int, fn:Void -> Void):Void {
|
||||
krom_rtc_set_open_callback(id, fn);
|
||||
}
|
||||
|
||||
public static function setClosedCallback(id:Int, fn:Void -> Void):Void {
|
||||
krom_rtc_set_closed_callback(id, fn);
|
||||
}
|
||||
|
||||
public static function setErrorCallback(id:Int, fn:String -> Void):Void {
|
||||
krom_rtc_set_error_callback(id, fn);
|
||||
}
|
||||
|
||||
public static function setMessageCallback(id:Int, fn:Dynamic -> Void):Void {
|
||||
krom_rtc_set_message_callback(id, fn);
|
||||
}
|
||||
|
||||
public static function randomBytes(length:Int):haxe.io.Bytes {
|
||||
try {
|
||||
var buf = krom_rtc_random_bytes(length);
|
||||
return buf != null ? haxe.io.Bytes.ofData(buf) : null;
|
||||
} catch (e:Dynamic) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#end
|
||||
268
leenkx/Sources/leenkx/network/torrent/transport/RtcSignaling.hx
Normal file
268
leenkx/Sources/leenkx/network/torrent/transport/RtcSignaling.hx
Normal file
@ -0,0 +1,268 @@
|
||||
package leenkx.network.torrent.transport;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
import leenkx.network.torrent.Crypto;
|
||||
import leenkx.network.torrent.TorrentInfo;
|
||||
|
||||
class RtcSignaling {
|
||||
|
||||
static inline var OFFER_TIMEOUT = 50 * 1000;
|
||||
static inline var NUM_WANT = 5;
|
||||
|
||||
public var numwant:Int = NUM_WANT;
|
||||
|
||||
var client:WsTrackerClient;
|
||||
var infoHashBinary:String;
|
||||
var peerIdBinary:String;
|
||||
var destroyed:Bool = false;
|
||||
var trackerId:String = null;
|
||||
|
||||
var intervalTimer:haxe.Timer = null;
|
||||
var pendingOffers:Map<String, haxe.Timer> = [];
|
||||
|
||||
public var onConnected:Void -> Void;
|
||||
public var onTrackerUpdate:Dynamic -> Void;
|
||||
public var onCreateOffer:String -> (String -> Void) -> Void;
|
||||
public var onRemoteOffer:String -> String -> Dynamic -> Void;
|
||||
public var onRemoteAnswer:String -> String -> Dynamic -> Void;
|
||||
public var onOfferTimeout:String -> Void;
|
||||
public var onPeerLeft:String -> Void;
|
||||
public var onError:String -> Void;
|
||||
public var statsProvider:Void -> {downloaded:Float, uploaded:Float,
|
||||
left:Float} = null;
|
||||
|
||||
public function new(url:String, infoHashBinary:String, peerIdBinary:String) {
|
||||
this.infoHashBinary = infoHashBinary;
|
||||
this.peerIdBinary = peerIdBinary;
|
||||
client = new WsTrackerClient(url);
|
||||
client.onOpen = function() {
|
||||
if (destroyed) return;
|
||||
announce(null);
|
||||
if (onConnected != null) onConnected();
|
||||
};
|
||||
client.onMessage = onMessage;
|
||||
client.onClose = cleanupTimers;
|
||||
client.onError = function(err) {
|
||||
if (destroyed) return;
|
||||
cleanupTimers();
|
||||
if (onError != null) onError(err);
|
||||
};
|
||||
}
|
||||
|
||||
public function start():Void {
|
||||
client.start();
|
||||
}
|
||||
|
||||
function onMessage(data:String):Void {
|
||||
if (destroyed) return;
|
||||
var msg:Dynamic;
|
||||
try {
|
||||
msg = haxe.Json.parse(data);
|
||||
} catch(e) {
|
||||
return;
|
||||
}
|
||||
if (msg.action == "announce") {
|
||||
onAnnounceResponse(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupTimers():Void {
|
||||
if (intervalTimer != null) {
|
||||
intervalTimer.stop();
|
||||
intervalTimer = null;
|
||||
}
|
||||
for (offerId => timer in pendingOffers) {
|
||||
timer.stop();
|
||||
}
|
||||
pendingOffers.clear();
|
||||
}
|
||||
|
||||
public function setAnnounceInterval(intervalMs:Int):Void {
|
||||
if (intervalTimer != null) {
|
||||
intervalTimer.stop();
|
||||
intervalTimer = null;
|
||||
}
|
||||
if (intervalMs > 0) {
|
||||
intervalTimer = new haxe.Timer(intervalMs);
|
||||
intervalTimer.run = function() {
|
||||
announce(null);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public function announceEvent(event:String):Void {
|
||||
announce(event);
|
||||
}
|
||||
|
||||
function announce(event:String):Void {
|
||||
if (destroyed || !client.isConnected) return;
|
||||
var stats = statsProvider != null ? statsProvider()
|
||||
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
|
||||
var params:Dynamic = {
|
||||
action: "announce",
|
||||
info_hash: infoHashBinary,
|
||||
peer_id: peerIdBinary,
|
||||
numwant: numwant,
|
||||
uploaded: Std.int(stats.uploaded),
|
||||
downloaded: Std.int(stats.downloaded),
|
||||
left: Std.int(stats.left)
|
||||
};
|
||||
if (event != null) {
|
||||
Reflect.setField(params, "event", event);
|
||||
}
|
||||
if (trackerId != null) {
|
||||
Reflect.setField(params, "trackerid", trackerId);
|
||||
}
|
||||
if (event == "stopped" || event == "completed") {
|
||||
client.send(params);
|
||||
} else if (onCreateOffer != null) {
|
||||
var nw = numwant;
|
||||
var offers:Array<Dynamic> = [];
|
||||
var completed = 0;
|
||||
var sent = false;
|
||||
var announceTimer = new haxe.Timer(Std.int(OFFER_TIMEOUT));
|
||||
announceTimer.run = function() {
|
||||
announceTimer.stop();
|
||||
if (!sent) {
|
||||
sent = true;
|
||||
if (offers.length > 0) {
|
||||
Reflect.setField(params, "numwant", offers.length);
|
||||
Reflect.setField(params, "offers", offers);
|
||||
}
|
||||
client.send(params);
|
||||
}
|
||||
};
|
||||
for (i in 0...nw) {
|
||||
var offerIdHex = Crypto.toHex(Crypto.randomBytes(20));
|
||||
createSingleOffer(offerIdHex, function(sdp:String) {
|
||||
offers.push({
|
||||
offer: {type: "offer", sdp: sdp},
|
||||
offer_id: hexToBinaryString(offerIdHex)
|
||||
});
|
||||
completed++;
|
||||
if (completed == nw && !sent) {
|
||||
announceTimer.stop();
|
||||
sent = true;
|
||||
Reflect.setField(params, "numwant", nw);
|
||||
Reflect.setField(params, "offers", offers);
|
||||
client.send(params);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
client.send(params);
|
||||
}
|
||||
}
|
||||
|
||||
function createSingleOffer(offerIdHex:String, cb:String -> Void):Void {
|
||||
onCreateOffer(offerIdHex, cb);
|
||||
var timer = new haxe.Timer(Std.int(OFFER_TIMEOUT));
|
||||
timer.run = function() {
|
||||
timer.stop();
|
||||
pendingOffers.remove(offerIdHex);
|
||||
if (onOfferTimeout != null) onOfferTimeout(offerIdHex);
|
||||
};
|
||||
pendingOffers.set(offerIdHex, timer);
|
||||
}
|
||||
|
||||
function onAnnounceResponse(data:Dynamic):Void {
|
||||
var dataIh = Reflect.field(data, "info_hash");
|
||||
if (Std.string(dataIh) != infoHashBinary) {
|
||||
return;
|
||||
}
|
||||
var dataPeerId = Reflect.field(data, "peer_id");
|
||||
if (dataPeerId != null && Std.string(dataPeerId) == peerIdBinary) {
|
||||
return;
|
||||
}
|
||||
var failure = Reflect.field(data, "failure reason");
|
||||
if (failure != null) {
|
||||
trace('[RtcSignaling] FAILURE: ' + Std.string(failure));
|
||||
return;
|
||||
}
|
||||
var warning = Reflect.field(data, "warning message");
|
||||
if (warning != null) {
|
||||
trace('[RtcSignaling] WARNING: ' + Std.string(warning));
|
||||
return;
|
||||
}
|
||||
var interval = Reflect.field(data, "interval");
|
||||
if (interval == null) {
|
||||
interval = Reflect.field(data, "min interval");
|
||||
}
|
||||
if (interval != null) {
|
||||
setAnnounceInterval(Std.int(Std.parseFloat(Std.string(interval)) * 1000));
|
||||
}
|
||||
var tid = Reflect.field(data, "tracker id");
|
||||
if (tid != null) {
|
||||
trackerId = Std.string(tid);
|
||||
}
|
||||
if (Reflect.field(data, "complete") != null) {
|
||||
if (onTrackerUpdate != null) onTrackerUpdate(data);
|
||||
}
|
||||
var offer = Reflect.field(data, "offer");
|
||||
var peerId = Reflect.field(data, "peer_id");
|
||||
if (offer != null && peerId != null) {
|
||||
var peerIdHex = binaryStringToHex(Std.string(peerId));
|
||||
var offerIdHex = binaryStringToHex(
|
||||
Std.string(Reflect.field(data, "offer_id")));
|
||||
if (onRemoteOffer != null) {
|
||||
onRemoteOffer(peerIdHex, offerIdHex, offer);
|
||||
}
|
||||
}
|
||||
var answer = Reflect.field(data, "answer");
|
||||
if (answer != null && peerId != null) {
|
||||
var offerIdHex = binaryStringToHex(
|
||||
Std.string(Reflect.field(data, "offer_id")));
|
||||
var peerIdHex = binaryStringToHex(Std.string(peerId));
|
||||
var timer = pendingOffers.get(offerIdHex);
|
||||
if (timer != null) {
|
||||
timer.stop();
|
||||
pendingOffers.remove(offerIdHex);
|
||||
}
|
||||
if (onRemoteAnswer != null) {
|
||||
onRemoteAnswer(peerIdHex, offerIdHex, answer);
|
||||
}
|
||||
}
|
||||
var event = Reflect.field(data, "event");
|
||||
if (peerId != null && Std.string(event) == "stopped") {
|
||||
var peerIdHex = binaryStringToHex(Std.string(peerId));
|
||||
if (onPeerLeft != null) onPeerLeft(peerIdHex);
|
||||
}
|
||||
}
|
||||
|
||||
public function sendAnswer(toPeerIdHex:String, offerIdHex:String, sdp:String):Void {
|
||||
if (destroyed || !client.isConnected) return;
|
||||
var stats = statsProvider != null ? statsProvider()
|
||||
: {downloaded: 0.0, uploaded: 0.0, left: 0.0};
|
||||
var params:Dynamic = {
|
||||
action: "announce",
|
||||
info_hash: infoHashBinary,
|
||||
peer_id: peerIdBinary,
|
||||
left: Std.int(stats.left),
|
||||
to_peer_id: hexToBinaryString(toPeerIdHex),
|
||||
answer: {type: "answer", sdp: sdp},
|
||||
offer_id: hexToBinaryString(offerIdHex)
|
||||
};
|
||||
if (trackerId != null) {
|
||||
Reflect.setField(params, "trackerid", trackerId);
|
||||
}
|
||||
client.send(params);
|
||||
}
|
||||
|
||||
public function close():Void {
|
||||
if (destroyed) return;
|
||||
if (client.isConnected) {
|
||||
announce("stopped");
|
||||
}
|
||||
destroyed = true;
|
||||
cleanupTimers();
|
||||
client.close();
|
||||
}
|
||||
|
||||
function hexToBinaryString(hex:String):String {
|
||||
return TorrentInfo.toBinaryString(Bytes.ofHex(hex));
|
||||
}
|
||||
|
||||
function binaryStringToHex(bin:String):String {
|
||||
return Crypto.toHex(TorrentInfo.fromBinaryString(bin));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package leenkx.network.torrent.transport;
|
||||
|
||||
#if (js && !kha_krom)
|
||||
@:native("WebTorrent")
|
||||
extern class WebTorrent {
|
||||
function new(?opts:Dynamic);
|
||||
function seed(input:Dynamic, ?opts:Dynamic, ?cb:Dynamic -> Void):Torrent;
|
||||
function add(torrentId:Dynamic, ?opts:Dynamic, ?cb:Torrent -> Void):Torrent;
|
||||
function remove(torrent:Dynamic, ?cb:Void -> Void):Void;
|
||||
function destroy(?cb:Void -> Void):Void;
|
||||
function on(event:String, cb:Dynamic -> Void):Void;
|
||||
var torrents:Array<Torrent>;
|
||||
}
|
||||
|
||||
@:native("Torrent")
|
||||
extern class Torrent {
|
||||
var ready:Bool;
|
||||
var wires:Array<Wire>;
|
||||
var files:Array<TorrentFile>;
|
||||
var magnetURI:String;
|
||||
var infoHash:String;
|
||||
var length:Int;
|
||||
var downloaded:Int;
|
||||
var uploaded:Int;
|
||||
var numPeers:Int;
|
||||
var discovery:Dynamic;
|
||||
function on(event:String, cb:Dynamic -> Void):Void;
|
||||
function destroy(?cb:Void -> Void):Void;
|
||||
}
|
||||
|
||||
@:native("Wire")
|
||||
extern class Wire {
|
||||
var peerId:String;
|
||||
var addr:String;
|
||||
var peerExtendedHandshake:Dynamic;
|
||||
var extendedHandshake:Dynamic;
|
||||
function use(ext:Dynamic):Void;
|
||||
function extended(name:String, message:Dynamic):Void;
|
||||
function on(event:String, cb:Dynamic -> Void):Void;
|
||||
}
|
||||
|
||||
extern class TorrentFile {
|
||||
var name:String;
|
||||
var path:String;
|
||||
var length:Int;
|
||||
function getBuffer(cb:Dynamic -> Void):Void;
|
||||
function appendTo(elem:Dynamic):Void;
|
||||
function streamTo(elem:Dynamic):Void;
|
||||
}
|
||||
#end
|
||||
@ -0,0 +1,221 @@
|
||||
package leenkx.network.torrent.transport;
|
||||
|
||||
import haxe.io.Bytes;
|
||||
import leenkx.network.torrent.transport.ITransport.TransportOpts;
|
||||
|
||||
#if (js && !kha_krom)
|
||||
import leenkx.network.torrent.transport.WebTorrent;
|
||||
import js.lib.Uint8Array;
|
||||
|
||||
class WebTorrentTransport implements ITransport {
|
||||
public var onReady:Void -> Void;
|
||||
public var onPeerConnected:String -> Int -> Void;
|
||||
public var onPeerDisconnected:Int -> Void;
|
||||
public var onPeerData:String -> Bytes -> Void;
|
||||
public var onPeerHandshake:String -> String -> Void;
|
||||
public var onTrackerUpdate:Dynamic -> Void;
|
||||
public var onTrackerAnnounce:Void -> Void;
|
||||
public var onError:String -> Void;
|
||||
|
||||
public var wt:WebTorrent;
|
||||
public var torrent:Torrent;
|
||||
var torrentCreated:Bool = false;
|
||||
var extName:String = "lx_channel";
|
||||
var lastWireCount:Int = 0;
|
||||
var pk:String;
|
||||
var ek:String;
|
||||
var identifier:String;
|
||||
|
||||
public function new(?existingWt:WebTorrent) {
|
||||
#if js
|
||||
this.wt = existingWt;
|
||||
#end
|
||||
}
|
||||
|
||||
public function join(identifier:String, opts:TransportOpts):Void {
|
||||
#if js
|
||||
var trackerOpts = opts.tracker != null ? opts.tracker : {};
|
||||
if (opts.iceServers != null) {
|
||||
Reflect.setField(trackerOpts, "rtcConfig", {iceServers: opts.iceServers});
|
||||
}
|
||||
if (this.wt == null) {
|
||||
var wtOpts:Dynamic = {tracker: trackerOpts};
|
||||
if (opts.wtOpts != null) {
|
||||
wtOpts = js.lib.Object.assign(js.lib.Object.assign({}, wtOpts), opts.wtOpts);
|
||||
}
|
||||
this.wt = new WebTorrent(wtOpts);
|
||||
}
|
||||
var blob:Dynamic = createBlob(identifier);
|
||||
var torrentOpts:Dynamic = {
|
||||
name: identifier,
|
||||
announce: opts.announce != null ? opts.announce : 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
|
||||
149
leenkx/Sources/leenkx/network/torrent/transport/WireTransport.hx
Normal file
149
leenkx/Sources/leenkx/network/torrent/transport/WireTransport.hx
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -669,8 +669,10 @@ class Inc {
|
||||
#end
|
||||
|
||||
#if rp_ssrs
|
||||
#if kha_opengl
|
||||
path.bindTarget("_main", "gbufferD");
|
||||
#end
|
||||
#end
|
||||
|
||||
path.drawMeshes("translucent");
|
||||
|
||||
@ -1314,7 +1316,11 @@ class ShadowMapAtlas {
|
||||
|
||||
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);
|
||||
#end
|
||||
this.target = shadowMapAtlasName(light.data.raw.type, transparent);
|
||||
this.sizew = this.sizeh = this.baseTileSizeConst = maxTileSize;
|
||||
this.depth = getSubdivisions();
|
||||
@ -1418,7 +1424,7 @@ class ShadowMapAtlas {
|
||||
|
||||
public static inline function shadowMapAtlasName(type: String, transparent: Bool): String {
|
||||
#if lnx_shadowmap_atlas_single_map
|
||||
return "shadowMapAtlas";
|
||||
return transparent ? "shadowMapAtlasTransparent" : "shadowMapAtlas";
|
||||
#else
|
||||
switch (type) {
|
||||
case "point":
|
||||
|
||||
@ -116,6 +116,11 @@ class Postprocess {
|
||||
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 = [
|
||||
1, //0: Auto Exposure Strength
|
||||
1 //1: Auto Exposure Speed
|
||||
@ -406,6 +411,10 @@ class Postprocess {
|
||||
v.x = auto_focus[0];
|
||||
v.y = auto_focus[1];
|
||||
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;
|
||||
|
||||
@ -560,6 +560,7 @@ class RenderPathDeferred {
|
||||
@:access(iron.RenderPath)
|
||||
public static function commands() {
|
||||
path.setTarget("gbuffer0"); // Only clear gbuffer0
|
||||
//path.setTarget("gbuffer0", ["gbuffer1"]);
|
||||
#if (rp_background == "Clear")
|
||||
{
|
||||
path.clearTarget(-1, 1.0);
|
||||
@ -649,7 +650,11 @@ class RenderPathDeferred {
|
||||
{
|
||||
if (leenkx.data.Config.raw.rp_ssao != false) {
|
||||
path.setTarget("singlea");
|
||||
#if rp_ssao_half
|
||||
path.bindTarget("half", "gbufferD");
|
||||
#else
|
||||
path.bindTarget("_main", "gbufferD");
|
||||
#end
|
||||
path.bindTarget("gbuffer0", "gbuffer0");
|
||||
path.drawShader("shader_datas/ssao_pass/ssao_pass");
|
||||
|
||||
@ -670,7 +675,11 @@ class RenderPathDeferred {
|
||||
{
|
||||
if (leenkx.data.Config.raw.rp_ssgi != false) {
|
||||
path.setTarget("ssgi_a");
|
||||
#if rp_ssgi_half
|
||||
path.bindTarget("half", "gbufferD");
|
||||
#else
|
||||
path.bindTarget("_main", "gbufferD");
|
||||
#end
|
||||
path.bindTarget("gbuffer0", "gbuffer0");
|
||||
path.bindTarget("gbuffer1", "gbuffer1");
|
||||
#if rp_gbuffer_emission
|
||||
@ -684,13 +693,21 @@ class RenderPathDeferred {
|
||||
path.setTarget("ssgi_b");
|
||||
path.bindTarget("ssgi_a", "tex");
|
||||
path.bindTarget("gbuffer0", "gbuffer0");
|
||||
#if rp_ssgi_half
|
||||
path.bindTarget("half", "gbufferD");
|
||||
#else
|
||||
path.bindTarget("_main", "gbufferD");
|
||||
#end
|
||||
path.drawShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_x");
|
||||
|
||||
path.setTarget("ssgi_a");
|
||||
path.bindTarget("ssgi_b", "tex");
|
||||
path.bindTarget("gbuffer0", "gbuffer0");
|
||||
#if rp_ssgi_half
|
||||
path.bindTarget("half", "gbufferD");
|
||||
#else
|
||||
path.bindTarget("_main", "gbufferD");
|
||||
#end
|
||||
path.drawShader("shader_datas/ssgi_blur_pass/ssgi_blur_pass_y");
|
||||
}
|
||||
}
|
||||
@ -960,7 +977,7 @@ class RenderPathDeferred {
|
||||
path.bindTarget("tex", "tex");
|
||||
path.drawShader("shader_datas/copy_pass/copy_pass");
|
||||
|
||||
path.setTarget("gbuffer0", ["gbuffer1", "gbuffer_refraction"]);
|
||||
setTargetMeshes();
|
||||
|
||||
#if (rp_voxels != "Off")
|
||||
path.bindTarget("voxelsOut", "voxels");
|
||||
@ -980,7 +997,7 @@ class RenderPathDeferred {
|
||||
#end
|
||||
|
||||
#if rp_ssrs
|
||||
path.bindTarget("_main", "gbufferD");
|
||||
path.bindTarget("gbufferD1", "gbufferD");
|
||||
#end
|
||||
|
||||
path.drawMeshes("refraction");
|
||||
|
||||
@ -223,6 +223,9 @@ class LeenkxExporter:
|
||||
cls.export_network = False
|
||||
if wrd.lnx_network == 'Enabled':
|
||||
cls.export_network = True
|
||||
cls.export_torrent = False
|
||||
if wrd.lnx_torrent == 'Enabled' and wrd.lnx_network != 'Disabled':
|
||||
cls.export_torrent = True
|
||||
|
||||
@staticmethod
|
||||
def write_matrix(matrix):
|
||||
|
||||
@ -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()
|
||||
@ -4,19 +4,71 @@ class CreateLeenkxNode(LnxLogicTreeNode):
|
||||
"""Create Leenkx"""
|
||||
bl_idname = 'LNCreateLeenkxNode'
|
||||
bl_label = 'Create Leenkx'
|
||||
bl_description = 'Create Leenkx'
|
||||
bl_description = ('Open a Leenkx peer connection on the given swarm identifier')
|
||||
lnx_category = 'Leenkx'
|
||||
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):
|
||||
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('LnxDynamicSocket', 'Connection')
|
||||
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():
|
||||
add_category('Leenkx', icon='ORIENTATION_CURSOR')
|
||||
|
||||
@ -5,7 +5,7 @@ class CreateTorrentNode(LnxLogicTreeNode):
|
||||
"""Create Torrent"""
|
||||
bl_idname = 'LNCreateTorrentNode'
|
||||
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_version = 1
|
||||
|
||||
@ -13,15 +13,65 @@ class CreateTorrentNode(LnxLogicTreeNode):
|
||||
super(CreateTorrentNode, 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_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):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxDynamicSocket', 'Connection')
|
||||
self.add_input('LnxDynamicSocket', 'Data')
|
||||
self._add_data_inputs()
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
self.add_output('LnxDynamicSocket', 'Torrent')
|
||||
self.add_output('LnxNodeSocketAction', 'Done')
|
||||
self.add_output('LnxDynamicSocket', 'Meta')
|
||||
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():
|
||||
|
||||
61
leenkx/blender/lnx/logicnode/custom/LN_create_tracker.py
Normal file
61
leenkx/blender/lnx/logicnode/custom/LN_create_tracker.py
Normal 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()
|
||||
76
leenkx/blender/lnx/logicnode/custom/LN_download_torrent.py
Normal file
76
leenkx/blender/lnx/logicnode/custom/LN_download_torrent.py
Normal 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()
|
||||
77
leenkx/blender/lnx/logicnode/custom/LN_get_torrent_file.py
Normal file
77
leenkx/blender/lnx/logicnode/custom/LN_get_torrent_file.py
Normal 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()
|
||||
@ -5,63 +5,40 @@ class LeenkxCloseConnectionNode(LnxLogicTreeNode):
|
||||
"""Close connection on the network"""
|
||||
bl_idname = 'LNLeenkxCloseConnectionNode'
|
||||
bl_label = 'Close Leenkx Connection'
|
||||
bl_description = ('Tear down a Leenkx connection, or drop a single peer from it')
|
||||
lnx_version = 1
|
||||
lnx_category = 'Leenkx'
|
||||
ind = 2
|
||||
|
||||
@staticmethod
|
||||
def get_enum_id_value(obj, prop_name, value):
|
||||
return obj.bl_rna.properties[prop_name].enum_items[value].identifier
|
||||
|
||||
@staticmethod
|
||||
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')
|
||||
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 == '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)
|
||||
|
||||
self['property0'] = value
|
||||
|
||||
|
||||
property0: HaxeEnumProperty(
|
||||
'property1',
|
||||
property0: HaxeEnumProperty('property0',
|
||||
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',
|
||||
set=set_enum,
|
||||
get=get_enum)
|
||||
update=update_sockets)
|
||||
|
||||
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxDynamicSocket', 'Connection')
|
||||
self.add_input('LnxStringSocket', 'Url')
|
||||
if self.property0 == 'peer':
|
||||
self.add_input('LnxStringSocket', 'ID')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
|
||||
|
||||
@ -9,84 +9,26 @@ class LeenkxEventNode(LnxLogicTreeNode):
|
||||
lnx_category = 'Leenkx'
|
||||
lnx_version = 1
|
||||
|
||||
@staticmethod
|
||||
def get_enum_id_value(obj, prop_name, value):
|
||||
return obj.bl_rna.properties[prop_name].enum_items[value].identifier
|
||||
|
||||
@staticmethod
|
||||
def get_count_in(type_name):
|
||||
return {
|
||||
'client': 0,
|
||||
'host': 1,
|
||||
'securehost': 2
|
||||
}.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 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')
|
||||
property1: HaxeEnumProperty('property1',
|
||||
items = [('onopen', 'OnOpen', 'The connection to the server is open'),
|
||||
('onmessage', 'OnMessage', 'A message arrived; ID is the sender peer id, Data the payload'),
|
||||
('onerror', 'OnError', 'A connection error occurred'),
|
||||
('onclose', 'OnClose', 'The connection was closed'),
|
||||
('onseen', 'OnSeen', 'A peer was discovered; ID is the peer id'),
|
||||
('onserver', 'OnServer', 'The signaling server responded'),
|
||||
('onconnections', 'OnConnections', 'The server sent the peer list; Data is the connection list'),
|
||||
('onping', 'OnPing', 'Heartbeat ping roundtrip completed'),
|
||||
('onleft', 'OnLeft', 'A peer left; ID is the peer id'),
|
||||
('ontimeout', 'OnTimeout', 'A handshake timed out'),
|
||||
('onwireleft', 'OnWireLeft', 'A BitTorrent wire peer disconnected; ID is the peer id'),
|
||||
('onwireseen', 'OnWireSeen', 'A BitTorrent wire peer connected; ID is the peer id'),
|
||||
('ontorrent', 'OnTorrent', 'Torrent activity on the connection; Data is the torrent'),
|
||||
('ontorrentadded', 'OnTorrentAdded', 'A torrent registered on the connection; Data is the Torrent handle'),
|
||||
('ontracker', 'OnTracker', 'A tracker event on the connection'),
|
||||
('onannounce', 'OnAnnounce', 'A tracker announce completed'),
|
||||
('ontorrentdone', 'OnTorrentDone', 'A torrent finished downloading; Data is the torrent'),
|
||||
('onload', 'OnLoad', 'The connection was registered; fires once at startup')],
|
||||
name='', default='onopen')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LeenkxEventNode, self).__init__(*args, **kwargs)
|
||||
@ -95,7 +37,7 @@ class LeenkxEventNode(LnxLogicTreeNode):
|
||||
|
||||
def lnx_init(self, context):
|
||||
#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('LnxDynamicSocket', 'ID')
|
||||
@ -103,7 +45,6 @@ class LeenkxEventNode(LnxLogicTreeNode):
|
||||
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
layout.prop(self, 'property0')
|
||||
layout.prop(self, 'property1')
|
||||
|
||||
def register():
|
||||
|
||||
@ -9,16 +9,15 @@ class LeenkxMessageParserNode(LnxLogicTreeNode):
|
||||
lnx_category = 'Leenkx'
|
||||
lnx_version = 1
|
||||
|
||||
property0: HaxeEnumProperty(
|
||||
'property0',
|
||||
items = [('string', 'String', 'Event for a string over the network'),
|
||||
('vector', 'Vector', 'Event for a vector over the network'),
|
||||
('float', 'Float', 'Event for a float over the network'),
|
||||
('integer', 'Integer', 'Event for an integer over the network'),
|
||||
('boolean', 'Boolean', 'Event for a boolean over the network'),
|
||||
('transform', 'Transform', 'Event for a transform over the network'),
|
||||
('rotation', 'Rotation', 'Event for a rotation over the network')],
|
||||
name='', default='string')
|
||||
property0: HaxeEnumProperty('property0', items=[
|
||||
('string', 'String', 'Parse the message as a string'),
|
||||
('vector', 'Vector', 'Parse the message as a vector (Vec4)'),
|
||||
('float', 'Float', 'Parse the message as a float'),
|
||||
('integer', 'Integer', 'Parse the message as an integer'),
|
||||
('boolean', 'Boolean', 'Parse the message as a boolean'),
|
||||
('transform', 'Transform', 'Parse the message as a transform (Mat4)'),
|
||||
('rotation', 'Rotation', 'Parse the message as a rotation (Quat)')
|
||||
], name='', default='string')
|
||||
|
||||
|
||||
def lnx_init(self, context):
|
||||
|
||||
@ -9,77 +9,55 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
|
||||
lnx_category = 'Leenkx'
|
||||
lnx_version = 1
|
||||
|
||||
@staticmethod
|
||||
def get_enum_id_value(obj, prop_name, value):
|
||||
return obj.bl_rna.properties[prop_name].enum_items[value].identifier
|
||||
def update_sockets(self, context):
|
||||
if not self.inputs:
|
||||
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 get_count_in(type_name):
|
||||
return {
|
||||
'client': 0,
|
||||
'host': 1,
|
||||
'securehost': 2
|
||||
}.get(type_name, 0)
|
||||
def _add_data_inputs(self, send_all=True):
|
||||
data_sockets = {
|
||||
'string': ('LnxStringSocket', 'String payload to send'),
|
||||
'vector': ('LnxVectorSocket', 'Vector payload to send'),
|
||||
'float': ('LnxFloatSocket', 'Float payload to send'),
|
||||
'integer': ('LnxIntSocket', 'Integer payload to send'),
|
||||
'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):
|
||||
return self.get('property0', 0)
|
||||
|
||||
def set_enum(self, value):
|
||||
# Checking the selection of each type
|
||||
select_current = self.get_enum_id_value(self, 'property0', value)
|
||||
select_prev = self.property0
|
||||
|
||||
#Check if type changed
|
||||
if select_prev != select_current:
|
||||
|
||||
for i in self.inputs:
|
||||
self.inputs.remove(i)
|
||||
|
||||
# Arguements for type Client
|
||||
if (self.get_count_in(select_current) == 0):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxDynamicSocket', 'Connection')
|
||||
self.add_input('LnxStringSocket', 'API')
|
||||
self.add_input('LnxDynamicSocket', 'Data')
|
||||
def _add_id_input(self):
|
||||
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')
|
||||
def on_socket_val_update(self, context, socket):
|
||||
if socket.name == 'Send All':
|
||||
if (len(self.inputs) > 5
|
||||
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)
|
||||
|
||||
|
||||
# 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',
|
||||
property1: HaxeEnumProperty('property1',
|
||||
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'),
|
||||
('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'),
|
||||
('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')],
|
||||
name='',
|
||||
default='string')
|
||||
name='', default='string', update=update_sockets)
|
||||
|
||||
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):
|
||||
@ -100,17 +82,15 @@ class LeenkxSendMessageNode(LnxLogicTreeNode):
|
||||
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._add_data_inputs()
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
|
||||
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
layout.prop(self, 'property0')
|
||||
layout.prop(self, 'property1')
|
||||
layout.prop(self, 'property2')
|
||||
|
||||
def register():
|
||||
add_category('Leenkx', icon='ORIENTATION_CURSOR')
|
||||
|
||||
68
leenkx/blender/lnx/logicnode/custom/LN_parse_torrent.py
Normal file
68
leenkx/blender/lnx/logicnode/custom/LN_parse_torrent.py
Normal 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()
|
||||
@ -17,12 +17,14 @@ class SeedTorrentNode(LnxLogicTreeNode):
|
||||
def lnx_init(self, context):
|
||||
self.add_input('LnxNodeSocketAction', 'In')
|
||||
self.add_input('LnxDynamicSocket', 'Connection')
|
||||
self.add_input('LnxDynamicSocket', 'Opts [Map]')
|
||||
self.add_input('LnxDynamicSocket', 'Torrent')
|
||||
self.add_input('LnxDynamicSocket', 'Data')
|
||||
|
||||
self.add_output('LnxNodeSocketAction', 'Out')
|
||||
self.add_output('LnxDynamicSocket', 'Torrent')
|
||||
self.add_output('LnxNodeSocketAction', 'Done')
|
||||
self.add_output('LnxStringSocket', 'InfoHash')
|
||||
self.add_output('LnxStringSocket', 'Magnet')
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
75
leenkx/blender/lnx/logicnode/custom/LN_set_trackers.py
Normal file
75
leenkx/blender/lnx/logicnode/custom/LN_set_trackers.py
Normal 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()
|
||||
@ -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()
|
||||
71
leenkx/blender/lnx/logicnode/custom/LN_torrent_control.py
Normal file
71
leenkx/blender/lnx/logicnode/custom/LN_torrent_control.py
Normal 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()
|
||||
40
leenkx/blender/lnx/logicnode/custom/LN_torrent_event.py
Normal file
40
leenkx/blender/lnx/logicnode/custom/LN_torrent_event.py
Normal 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()
|
||||
46
leenkx/blender/lnx/logicnode/custom/LN_torrent_info.py
Normal file
46
leenkx/blender/lnx/logicnode/custom/LN_torrent_info.py
Normal 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()
|
||||
@ -248,6 +248,7 @@ def export_data_impl(fp, sdk_path):
|
||||
export_navigation = bpy.data.worlds['Lnx'].lnx_navigation != 'Disabled'
|
||||
export_ui = bpy.data.worlds['Lnx'].lnx_ui != '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()
|
||||
mat_state.material_cache.clear()
|
||||
@ -264,6 +265,7 @@ def export_data_impl(fp, sdk_path):
|
||||
navigation_found = False
|
||||
ui_found = False
|
||||
network_found = False
|
||||
torrent_found = False
|
||||
LeenkxExporter.compress_enabled = wrd.lnx_asset_compression
|
||||
LeenkxExporter.optimize_enabled = state.is_publish and wrd.lnx_optimize_data
|
||||
if not os.path.exists(build_dir + '/compiled/Assets'):
|
||||
@ -326,6 +328,8 @@ def export_data_impl(fp, sdk_path):
|
||||
ui_found = True
|
||||
if LeenkxExporter.export_network:
|
||||
network_found = True
|
||||
if LeenkxExporter.export_torrent:
|
||||
torrent_found = True
|
||||
assets.add(asset_path)
|
||||
|
||||
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:
|
||||
export_network = False
|
||||
|
||||
if torrent_found == False:
|
||||
export_torrent = False
|
||||
|
||||
# Ugly workaround: some logic nodes require Zui code even if no UI is used,
|
||||
# for now enable UI export unless explicitly disabled.
|
||||
export_ui = True
|
||||
@ -349,6 +356,9 @@ def export_data_impl(fp, sdk_path):
|
||||
if wrd.lnx_network == 'Enabled':
|
||||
export_network = True
|
||||
|
||||
if wrd.lnx_torrent == 'Enabled' and wrd.lnx_network != 'Disabled':
|
||||
export_torrent = True
|
||||
|
||||
modules = []
|
||||
if wrd.lnx_audio == 'Enabled':
|
||||
modules.append('audio')
|
||||
@ -360,6 +370,8 @@ def export_data_impl(fp, sdk_path):
|
||||
modules.append('ui')
|
||||
if export_network:
|
||||
modules.append('network')
|
||||
if export_torrent:
|
||||
modules.append('torrent')
|
||||
|
||||
defs = lnx.utils.def_strings_to_array(wrd.world_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)
|
||||
|
||||
# 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': []}
|
||||
|
||||
for ref in assets.shader_passes:
|
||||
@ -431,6 +444,7 @@ def export_data_impl(fp, sdk_path):
|
||||
if not os.path.exists(target):
|
||||
shutil.copy(file, target)
|
||||
state.last_world_defs = wrd.world_defs
|
||||
state.last_shader_passes = current_passes
|
||||
|
||||
# Reset path
|
||||
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)
|
||||
|
||||
# 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
|
||||
scene_name = lnx.utils.get_project_scene_name()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user