Compare commits

..

48 Commits

Author SHA1 Message Date
4ab646015d P2P Export - Nodes - Patch 2026-10-06 02:10:08 -07:00
9fee67d231 Haxe Lib Datachannel 2026-10-04 17:14:28 -07:00
860b73a8f4 hxWebsocket Upstream and Network Updates 2026-10-03 22:05:53 -07:00
da38631584 Leenkx Network Torrent 2026-10-03 17:40:22 -07:00
6839203dc5 P2P RunT + Static WebView 2026-10-03 00:02:30 -07:00
d9ac74101f Merge pull request 'Fix background color intensity for 'Filmic2' tonemap' (#146) from moisesjpelaez/LNXSDK:filmic into main
Reviewed-on: LeenkxTeam/LNXSDK#146
2026-09-23 22:45:26 +00:00
e20400654c Merge pull request 'Add missing dll to prevent error 3221225781' (#145) from moisesjpelaez/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#145
2026-09-23 22:44:13 +00:00
058cbed20f Fix background color intensity for 'Filmic2' tonemap 2026-09-21 19:44:07 -04:00
e0c10318e5 Add missing dll to prevent error 3221225781 2026-09-21 18:27:29 -04:00
7d609b126d Merge pull request 'main' (#144) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#144
2026-09-20 18:30:58 +00:00
36ecaea919 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-20 11:15:57 -07:00
fac01a2849 Group Nodes, Shaders, Textures, Displacement 2026-09-20 11:15:14 -07:00
c71c1a98e4 Merge pull request 'MRT Index' (#143) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#143
2026-09-17 23:43:07 +00:00
f6d5f142e9 MRT Index 2026-09-17 16:38:36 -07:00
70961b0336 Merge pull request 'Fix thinWall and copy_pass in Forward rendering' (#141) from Moises/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#141
2026-09-17 21:16:33 +00:00
9c50274f29 Merge remote-tracking branch 'upstream/main' 2026-09-17 17:15:55 -04:00
a94b95afc8 Merge pull request 'main' (#142) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#142
2026-09-17 21:09:32 +00:00
cee36635e0 Cleanup 2026-09-17 16:07:10 -04:00
d0337f5dae Fix thinWall and passes 2026-09-17 15:59:45 -04:00
fca51c7e61 Water Level/Reflectopm 2026-09-16 22:44:16 -07:00
0bb4bceb57 Water Foam Fix 2026-09-16 18:12:12 -07:00
af9a259fbb Water Sun Direction 2026-09-16 17:36:31 -07:00
f1d8522f51 Bloom Isnan Isinf 2026-09-16 16:04:32 -07:00
56d1b0e4a7 Forward / Mobile / Solid 2026-09-16 12:40:39 -07:00
73e4a0ad30 Scene Gamma / Exposure 2026-09-13 23:34:36 -07:00
67d5e6b2c7 SSRS Fix 2026-09-13 21:52:13 -07:00
635f8f6645 SSRS WebGL Fix 2026-09-13 20:18:00 -07:00
b2444be7e2 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-13 20:16:57 -07:00
7d00036973 Version Bump 2026-09-13 23:11:41 +00:00
46814444af Merge pull request 'Update Theme' (#140) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#140
2026-09-13 23:09:55 +00:00
b4de377433 Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-13 15:23:25 -07:00
2abb6efe4f Update Theme 2026-09-13 22:20:26 +00:00
d787f9a7fc Update Theme 2026-09-13 12:10:51 -07:00
75e95387bb Merge pull request 'main' (#139) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#139
2026-09-13 08:58:09 +00:00
0067459e9a 3D_Audio numeric menu fix 2026-09-12 20:08:05 -07:00
cf68067343 Leenkx Theme 2026-09-12 19:57:45 -07:00
365c624d73 Tilesheets To Objects 2026-09-09 02:33:00 -07:00
384a8a7ee8 Voxel Shadow - No Temporal 2026-09-09 00:35:34 -07:00
4e2b59a1d6 Merge pull request 'main' (#138) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#138
2026-09-09 05:42:48 +00:00
6d07e70b11 Hashlink Compile - Curve/Console 2026-09-08 22:34:51 -07:00
42fdff4757 TriVoxel: Burley + (EON, Gotanda, Chan) Diffuse Models 2026-09-08 22:02:48 -07:00
d44fd9993a Get Transform Update 2026-09-07 11:33:15 -07:00
3fd1b395fb Merge branch 'main' of https://dev.leenkx.com/Onek8/LNXSDK 2026-09-07 11:13:11 -07:00
40bca62f96 Get Transform Node Replacement Update 2026-09-07 11:12:57 -07:00
e5082835c4 Merge pull request 'Reflections / Refractions' (#137) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#137
2026-09-06 08:39:04 +00:00
bc700a6374 Reflections / Refractions 2026-09-06 01:37:13 -07:00
1e500993d9 Merge pull request 'main' (#136) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#136
2026-09-04 21:02:18 +00:00
d842744aeb 9 Tap PCF 2026-09-04 13:59:07 -07:00
315 changed files with 77167 additions and 2034 deletions

View File

@ -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;

Binary file not shown.

Binary file not shown.

View File

@ -6,12 +6,13 @@ bl_info = {
"location": "Properties -> Render -> Leenkx Player",
"description": "Full Stack SDK",
"author": "Leenkx.com",
"version": (2026, 5, 0),
"version": (2026, 9, 0),
"blender": (5, 2, 0),
"doc_url": "https://leenkx.com/",
"tracker_url": "https://leenkx.com/support"
}
from enum import IntEnum
import json
import os
from pathlib import Path
import platform
@ -32,6 +33,41 @@ from bpy.app.handlers import persistent
from bpy.props import *
from bpy.types import Operator, AddonPreferences
LEENKX_PREFS_ENV = 'LEENKX_ADDON_PREFS'
persist_props = [
'apply_theme', 'show_advanced', 'sdk_path', 'tabs',
'code_editor', 'ide_bin', 'ui_scale', 'viewport_controls',
'khamake_threads', 'khamake_threads_use_auto', 'compilation_server',
'renderdoc_path', 'ffmpeg_path', 'save_on_build', 'open_build_directory',
'cmft_use_opencl', 'legacy_shaders', 'relative_paths',
'debug_console_auto', 'debug_console_visible_sc',
'debug_console_scale_in_sc', 'debug_console_scale_out_sc',
'android_sdk_root_path', 'android_open_build_apk_directory',
'android_apk_copy_path', 'android_apk_copy_open_directory',
'html5_copy_path', 'html5_server_port',
'html5_server_log', 'profile_exporter', 'khamake_debug',
'haxe_times', 'use_leenkx_py_symlink', 'update_submodules',
]
def save_prefs():
prefs = LeenkxAddonPreferences.get_prefs()
data = {k: getattr(prefs, k) for k in persist_props}
os.environ[LEENKX_PREFS_ENV] = json.dumps(data)
def restore_prefs():
raw = os.environ.get(LEENKX_PREFS_ENV)
if not raw:
return
data = json.loads(raw)
prefs = LeenkxAddonPreferences.get_prefs()
prefs.skip_update = True
for k, v in data.items():
if hasattr(prefs, k):
setattr(prefs, k, v)
if bpy.app.version < (2, 90, 0):
ListType = List
@ -116,7 +152,9 @@ class LeenkxAddonPreferences(AddonPreferences):
return
self.skip_update = True
self.sdk_path = bpy.path.reduce_dirs([bpy.path.abspath(self.sdk_path)])[0] + '/'
save_prefs()
restart_leenkx(context)
update_theme(context)
def ide_bin_update(self, context):
if self.skip_update:
@ -311,6 +349,11 @@ class LeenkxAddonPreferences(AddonPreferences):
" development. Warning: this will invalidate the installation if the SDK is removed"),
update=lambda self, context: update_leenkx_py(get_sdk_path(context)),
)
apply_theme: BoolProperty(
name="Apply Leenkx Theme", default=True,
description="Automatically apply the Leenkx Blender theme on addon registration",
update=lambda self, context: update_theme(context),
)
def draw(self, context):
self.skip_update = False
@ -450,6 +493,7 @@ class LeenkxAddonPreferences(AddonPreferences):
col = box.column(align=True)
col.prop(self, "use_leenkx_py_symlink")
col.prop(self, "apply_theme")
@staticmethod
def get_prefs() -> 'LeenkxAddonPreferences':
@ -458,9 +502,10 @@ class LeenkxAddonPreferences(AddonPreferences):
def get_fp():
if bpy.data.filepath == '':
filepath = getattr(bpy.data, 'filepath', '')
if filepath == '':
return ''
s = bpy.data.filepath.split(os.path.sep)
s = filepath.split(os.path.sep)
s.pop()
return os.path.sep.join(s)
@ -934,13 +979,47 @@ def on_load_post(context):
bpy.app.timers.register(lambda: restart_leenkx(bpy.context), first_interval=0.1)
def remove_leenkx_theme():
if "leenkx.theme" in sys.modules:
sys.modules["leenkx.theme"].unregister()
del sys.modules["leenkx.theme"]
def apply_leenkx_theme(sdk_path: str):
remove_leenkx_theme()
import importlib.util
theme_path = os.path.join(sdk_path, "leenkx", "blender", "theme", "theme.py")
if os.path.exists(theme_path):
spec = importlib.util.spec_from_file_location(
"leenkx.theme", theme_path)
theme = importlib.util.module_from_spec(spec)
sys.modules["leenkx.theme"] = theme
spec.loader.exec_module(theme)
theme.register(sdk_path)
else:
print("Leenkx theme: theme.py not found at", theme_path)
def update_theme(context):
prefs = LeenkxAddonPreferences.get_prefs()
if prefs.apply_theme:
sdk_path = get_sdk_path(context)
if sdk_path != "":
apply_leenkx_theme(sdk_path)
else:
remove_leenkx_theme()
def on_register_post():
detect_sdk_path()
save_prefs()
restart_leenkx(bpy.context)
update_theme(bpy.context)
def register():
bpy.utils.register_class(LeenkxAddonPreferences)
restore_prefs()
bpy.utils.register_class(LnxAddonPrintVersionInfoButton)
bpy.utils.register_class(LnxAddonInstallButton)
bpy.utils.register_class(LnxAddonUpdateButton)
@ -953,7 +1032,11 @@ def register():
def unregister():
if bpy.app.timers.is_registered(on_register_post):
bpy.app.timers.unregister(on_register_post)
remove_leenkx_theme()
stop_leenkx()
save_prefs()
bpy.utils.unregister_class(LeenkxAddonPreferences)
bpy.utils.unregister_class(LnxAddonInstallButton)
bpy.utils.unregister_class(LnxAddonPrintVersionInfoButton)

View File

@ -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

View File

@ -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;

View File

@ -420,7 +420,7 @@ void main() {
#endif
#endif
envl.rgb *= albedo;
envl.rgb *= diffuseIBL(albedo, roughness, f0, dotNV);
#ifdef _Brdf
envl.rgb *= 1.0 - F; //LV: We should take refracted light into account
@ -581,15 +581,15 @@ void main() {
vec3 sdirect;
if (abs(matp0.x) > 0.001 && dot(wTangent, wTangent) > 0.001) {
vec3 sbitangent = normalize(cross(n, wTangent));
sdirect = lambertDiffuseBRDF(albedo, sdotNL) +
sdirect = diffuseBRDF(albedo, roughness, f0, sdotNL, dotNV, sdotVH) +
anisotropicBRDF(f0, roughness, matp0.x, matp0.y,
wTangent, sbitangent, n, sunDir, v, sdotNL, dotNV) * occspec.y;
} else {
sdirect = lambertDiffuseBRDF(albedo, sdotNL) +
sdirect = diffuseBRDF(albedo, roughness, f0, sdotNL, dotNV, sdotVH) +
specularBRDF(f0, roughness, sdotNL, sdotNH, dotNV, sdotVH) * occspec.y;
}
#else
vec3 sdirect = lambertDiffuseBRDF(albedo, sdotNL) +
vec3 sdirect = diffuseBRDF(albedo, roughness, f0, sdotNL, dotNV, sdotVH) +
specularBRDF(f0, roughness, sdotNL, sdotNH, dotNV, sdotVH) * occspec.y;
#endif
@ -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

View File

@ -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
envl.rgb *= albedo;
#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,20 +380,20 @@ 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) {
vec3 sbitangent = normalize(cross(n, wTangent));
sdirect = lambertDiffuseBRDF(albedo, sdotNL) +
sdirect = diffuseBRDF(albedo, roughness, f0, sdotNL, dotNV, sdotVH) +
anisotropicBRDF(f0, roughness, matp0.x, matp0.y,
wTangent, sbitangent, n, sunDir, v, sdotNL, dotNV) * occspec.y;
} else {
sdirect = lambertDiffuseBRDF(albedo, sdotNL) +
sdirect = diffuseBRDF(albedo, roughness, f0, sdotNL, dotNV, sdotVH) +
specularBRDF(f0, roughness, sdotNL, sdotNH, dotNV, sdotVH) * occspec.y;
}
#else
vec3 sdirect = lambertDiffuseBRDF(albedo, sdotNL) +
vec3 sdirect = diffuseBRDF(albedo, roughness, f0, sdotNL, dotNV, sdotVH) +
specularBRDF(f0, roughness, sdotNL, sdotNH, dotNV, sdotVH) * occspec.y;
#endif
@ -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
shadowMap
#ifdef _ShadowMapTransparent
shadowMap, shadowMapTransparent
#else
shadowMap
#endif
#endif
, eye, p + n * shadowsBias * 2, shadowsBias
#ifdef _ShadowMapTransparent
, false
#endif
, eye, p + n * shadowsBias * 10, shadowsBias
);
#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
shadowMap
#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

View File

@ -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",

View File

@ -88,6 +88,199 @@ vec3 lambertDiffuseBRDF(const vec3 albedo, const float nl) {
return albedo * INV_PI * nl;
}
#ifdef _BurleyDiffuse
vec3 burleyDiffuseBRDF(const vec3 albedo, const float roughness,
const float dotNL, const float dotNV, const float dotVH) {
float nl = clamp(dotNL, 0.0, 1.0);
float nv = clamp(dotNV, 0.0, 1.0);
float energyBias = mix(0.0, 0.5, roughness);
float energyFactor = mix(1.0, 1.0 / 1.51, roughness);
float fd90 = energyBias + 2.0 * roughness * dotVH * dotVH;
float lightScatter = 1.0 + (fd90 - 1.0) * pow(1.0 - nl, 5.0);
float viewScatter = 1.0 + (fd90 - 1.0) * pow(1.0 - nv, 5.0);
return albedo * INV_PI * lightScatter * viewScatter * energyFactor * nl;
}
#endif
#ifdef _EONDiffuse
const float constant1_FON = 0.5 - 2.0 / (3.0 * PI);
const float constant2_FON = 2.0 / 3.0 - 28.0 / (15.0 * PI);
float E_FON_approx(float mu, float r) {
float mucomp = 1.0 - mu;
const float g1 = 0.0571085289;
const float g2 = 0.491881867;
const float g3 = -0.332181442;
const float g4 = 0.0714429953;
float GoverPi = mucomp * (g1 + mucomp * (g2 + mucomp * (g3 + mucomp * g4)));
return (1.0 + r * GoverPi) / (1.0 + constant1_FON * r);
}
vec3 eonDiffuseBRDF(const vec3 albedo, const float roughness,
const float dotNL, const float dotNV, const float dotVH) {
float r = roughness;
float mu_i = clamp(dotNL, 0.0, 1.0);
float mu_o = clamp(dotNV, 0.0, 1.0);
if (mu_i < 1.0e-7 || mu_o < 1.0e-7) return vec3(0.0);
float dotLV = 2.0 * dotVH * dotVH - 1.0;
float s = dotLV - mu_i * mu_o;
float sovertF = s > 0.0 ? s / max(mu_i, mu_o) : s;
float AF = 1.0 / (1.0 + constant1_FON * r);
vec3 f_ss = (albedo * INV_PI) * AF * (1.0 + r * sovertF);
float EFo = E_FON_approx(mu_o, r);
float EFi = E_FON_approx(mu_i, r);
float avgEF = AF * (1.0 + constant2_FON * r);
vec3 rho_ms = (albedo * albedo) * avgEF
/ max(vec3(1.0) - albedo * (1.0 - avgEF), vec3(1.0e-7));
const float eps = 1.0e-7;
vec3 f_ms = (rho_ms * INV_PI)
* max(eps, 1.0 - EFo)
* max(eps, 1.0 - EFi)
/ max(eps, 1.0 - avgEF);
return (f_ss + f_ms) * mu_i;
}
#endif
#ifdef _GotandaDiffuse
vec3 gotandaDiffuseBRDF(const vec3 albedo, const float roughness, const vec3 f0,
const float dotNL, const float dotNV, const float dotVH) {
float nl = clamp(dotNL, 0.0, 1.0);
float nv = clamp(dotNV, 0.0, 1.0);
float a = roughness * roughness;
float a2 = a * a;
float dotLV = 2.0 * dotVH * dotVH - 1.0;
float Cosri = dotLV - nv * nl;
float a2_13 = a2 + 1.36053;
float Fr = (1.0 - (0.542026 * a2 + 0.303573 * a) / a2_13)
* (1.0 - pow(1.0 - nv, 5.0 - 4.0 * a2) / a2_13)
* ((-0.733996 * a2 * a + 1.50912 * a2 - 1.16402 * a)
* pow(1.0 - nv, 1.0 + 1.0 / (39.0 * a2 * a2 + 1.0)) + 1.0);
float Lm = (max(1.0 - 2.0 * a, 0.0) * (1.0 - pow(1.0 - nl, 5.0))
+ min(2.0 * a, 1.0)) * (1.0 - 0.5 * a * (nl - 1.0)) * nl;
float Vd = (a2 / ((a2 + 0.09) * (1.31072 + 0.995584 * nv)))
* (1.0 - pow(1.0 - nl,
(1.0 - 0.3726732 * nv * nv)
/ (0.188566 + 0.38841 * nv)));
float Bp = Cosri < 0.0 ? 1.4 * nv * nl * Cosri : Cosri;
vec3 Lr = (21.0 / 20.0) * (1.0 - f0) * (Fr * Lm + Vd + Bp);
return max(albedo * INV_PI * Lr, vec3(0.0));
}
#endif
#ifdef _ChanDiffuse
vec3 chanDiffuseBRDF(const vec3 albedo, const float roughness,
const float dotNL, const float dotNV, const float dotVH) {
float nl = clamp(dotNL, 0.0, 1.0);
float nv = clamp(dotNV, 0.0, 1.0);
float vh = clamp(dotVH, 0.0, 1.0);
float a = roughness * roughness;
float a2 = a * a;
float g = clamp((1.0 / 18.0) * log2(2.0 / max(a2, 1e-7) - 1.0), 0.0, 1.0);
float dotNH = clamp((nl + nv) / max(2.0 * vh, 1e-5), 0.0, 1.0);
float F0 = vh + pow(1.0 - vh, 5.0);
float FdV = 1.0 - 0.75 * pow(1.0 - nv, 5.0);
float FdL = 1.0 - 0.75 * pow(1.0 - nl, 5.0);
float Fd = mix(F0, FdV * FdL, clamp(2.2 * g - 0.5, 0.0, 1.0));
float Fb = ((34.5 * g - 59.0) * g + 24.5) * vh
* exp2(-max(73.2 * g - 21.2, 8.9) * sqrt(dotNH));
float Lobe = clamp(Fd + Fb, 0.0, 1.0);
return albedo * INV_PI * Lobe * nl;
}
#endif
vec3 diffuseBRDF(const vec3 albedo, const float roughness, const vec3 f0,
const float dotNL, const float dotNV, const float dotVH) {
#ifdef _BurleyDiffuse
return burleyDiffuseBRDF(albedo, roughness, dotNL, dotNV, dotVH);
#elif defined(_EONDiffuse)
return eonDiffuseBRDF(albedo, roughness, dotNL, dotNV, dotVH);
#elif defined(_GotandaDiffuse)
return gotandaDiffuseBRDF(albedo, roughness, f0, dotNL, dotNV, dotVH);
#elif defined(_ChanDiffuse)
return chanDiffuseBRDF(albedo, roughness, dotNL, dotNV, dotVH);
#else
return lambertDiffuseBRDF(albedo, dotNL);
#endif
}
vec3 lambertDiffuseIBL(const vec3 albedo) {
return albedo;
}
#ifdef _BurleyDiffuse
vec3 burleyDiffuseIBL(const vec3 albedo, const float roughness, const float dotNV) {
float nv = clamp(dotNV, 0.0, 1.0);
float energyBias = mix(0.0, 0.5, roughness);
float energyFactor = mix(1.0, 1.0 / 1.51, roughness);
float fd90 = energyBias + 2.0 * roughness * nv * nv;
float viewScatter = 1.0 + (fd90 - 1.0) * pow(1.0 - nv, 5.0);
return albedo * viewScatter * energyFactor;
}
#endif
#ifdef _EONDiffuse
vec3 eonDiffuseIBL(const vec3 albedo, const float roughness, const float dotNV) {
float r = roughness;
float AF = 1.0 / (1.0 + constant1_FON * r);
float EF = E_FON_approx(clamp(dotNV, 0.0, 1.0), r);
float avgEF = AF * (1.0 + constant2_FON * r);
vec3 rho_ms = (albedo * albedo) * avgEF
/ max(vec3(1.0) - albedo * (1.0 - avgEF), vec3(1.0e-7));
return max(albedo * EF + rho_ms * (1.0 - EF), vec3(0.0));
}
#endif
#ifdef _GotandaDiffuse
vec3 gotandaDiffuseIBL(const vec3 albedo, const float roughness, const vec3 f0, const float dotNV) {
float nv = clamp(dotNV, 0.0, 1.0);
float a = roughness * roughness;
float a2 = a * a;
float a2_13 = a2 + 1.36053;
float Fr = (1.0 - (0.542026 * a2 + 0.303573 * a) / a2_13)
* (1.0 - pow(1.0 - nv, 5.0 - 4.0 * a2) / a2_13)
* ((-0.733996 * a2 * a + 1.50912 * a2 - 1.16402 * a)
* pow(1.0 - nv, 1.0 + 1.0 / (39.0 * a2 * a2 + 1.0)) + 1.0);
float Lm = (max(1.0 - 2.0 * a, 0.0) * (1.0 - pow(1.0 - nv, 5.0))
+ min(2.0 * a, 1.0)) * (1.0 - 0.5 * a * (nv - 1.0)) * nv;
float Vd = (a2 / ((a2 + 0.09) * (1.31072 + 0.995584 * nv)))
* (1.0 - pow(1.0 - nv,
(1.0 - 0.3726732 * nv * nv)
/ (0.188566 + 0.38841 * nv)));
float Cosri = 1.0 - nv * nv;
float Bp = Cosri;
vec3 Lr = (21.0 / 20.0) * (1.0 - f0) * (Fr * Lm + Vd + Bp);
return max(albedo * Lr, vec3(0.0));
}
#endif
#ifdef _ChanDiffuse
vec3 chanDiffuseIBL(const vec3 albedo, const float roughness, const float dotNV) {
float nv = clamp(dotNV, 0.0, 1.0);
float a = roughness * roughness;
float a2 = a * a;
float g = clamp((1.0 / 18.0) * log2(2.0 / max(a2, 1e-7) - 1.0), 0.0, 1.0);
float FdV = 1.0 - 0.75 * pow(1.0 - nv, 5.0);
float Fd = mix(1.0, FdV * FdV, clamp(2.2 * g - 0.5, 0.0, 1.0));
float Fb = ((34.5 * g - 59.0) * g + 24.5)
* exp2(-max(73.2 * g - 21.2, 8.9) * sqrt(nv));
return albedo * clamp(Fd + Fb, 0.0, 1.0);
}
#endif
vec3 diffuseIBL(const vec3 albedo, const float roughness, const vec3 f0, const float dotNV) {
#ifdef _BurleyDiffuse
return burleyDiffuseIBL(albedo, roughness, dotNV);
#elif defined(_EONDiffuse)
return eonDiffuseIBL(albedo, roughness, dotNV);
#elif defined(_GotandaDiffuse)
return gotandaDiffuseIBL(albedo, roughness, f0, dotNV);
#elif defined(_ChanDiffuse)
return chanDiffuseIBL(albedo, roughness, dotNV);
#else
return lambertDiffuseIBL(albedo);
#endif
}
vec3 surfaceAlbedo(const vec3 baseColor, const float metalness) {
return mix(baseColor, vec3(0.0), metalness);
}

View File

@ -131,15 +131,15 @@ vec3 sampleLightCore(const vec3 p, const vec3 n, const vec3 v, const float dotNV
#ifdef _Anisotropy
if (abs(anisotropy) > 0.001 && dot(tangent, tangent) > 0.001) {
vec3 bitangent = normalize(cross(n, tangent));
standard = lambertDiffuseBRDF(albedo, dotNL) +
standard = diffuseBRDF(albedo, rough, f0, dotNL, dotNV, dotVH) +
anisotropicBRDF(f0, rough, anisotropy, anisoRot,
tangent, bitangent, n, l, v, dotNL, dotNV) * spec;
} else {
standard = lambertDiffuseBRDF(albedo, dotNL) +
standard = diffuseBRDF(albedo, rough, f0, dotNL, dotNV, dotVH) +
specularBRDF(f0, rough, dotNL, dotNH, dotNV, dotVH) * spec;
}
#else
standard = lambertDiffuseBRDF(albedo, dotNL) +
standard = diffuseBRDF(albedo, rough, f0, dotNL, dotNV, dotVH) +
specularBRDF(f0, rough, dotNL, dotNH, dotNV, dotVH) * spec;
#endif

View File

@ -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
@ -83,16 +107,16 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
vec3 direct;
if (abs(anisotropy) > 0.001 && dot(tangent, tangent) > 0.001) {
vec3 bitangent = normalize(cross(n, tangent));
direct = lambertDiffuseBRDF(albedo, dotNL) +
direct = diffuseBRDF(albedo, rough, f0, dotNL, dotNV, dotVH) +
anisotropicBRDF(f0, rough, anisotropy, anisoRot,
tangent, bitangent, n, l, v, dotNL, dotNV) * spec;
} else {
direct = lambertDiffuseBRDF(albedo, dotNL) +
direct = diffuseBRDF(albedo, rough, f0, dotNL, dotNV, dotVH) +
specularBRDF(f0, rough, dotNL, dotNH, dotNV, dotVH) * spec;
}
#else
vec3 direct = lambertDiffuseBRDF(albedo, dotNL) +
specularBRDF(f0, rough, dotNL, dotNH, dotNV, dotVH) * spec;
vec3 direct = diffuseBRDF(albedo, rough, f0, dotNL, dotNV, dotVH) +
specularBRDF(f0, rough, dotNL, dotNH, dotNV, dotVH) * spec;
#endif
#ifdef _ExtBRDF
@ -121,26 +145,77 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
#ifdef _ShadowMap
if (receiveShadow) {
#ifdef _SinglePoint
vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 10, 1.0);
direct *= shadowTest(shadowMapSpot[0], lPos.xyz / lPos.w, bias);
vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 10, 1.0);
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(
#ifndef _SingleAtlas
shadowMapAtlasSpot
#else
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],
#ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0],
#endif
, lPos.xyz / lPos.w, bias
);
#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);
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
}
@ -153,25 +228,76 @@ 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);
#ifndef _Spot
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(
#ifndef _SingleAtlas
shadowMapAtlasPoint
#else
shadowMapAtlas
#endif
, ld, -l, bias, lightProj, n, index
);
direct *= PCFFakeCube(
#ifdef _ShadowMapTransparent
#ifndef _SingleAtlas
shadowMapAtlasPoint, shadowMapAtlasPointTransparent
#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);
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],
#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

View File

@ -75,9 +75,15 @@ vec3 PCFTileAware(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
if (transparent == false) {
vec4 shadowmap_transparent = texture(shadowMapTransparent, uv);
if (shadowmap_transparent.a < compare)
result *= shadowmap_transparent.rgb;
vec3 transResult = vec3(0.0);
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
vec4 smt = texture(shadowMapTransparent,
clamp(uv + vec2(x, y) / smSize, tileMin, tileMax));
transResult += (smt.a < compare) ? smt.rgb : vec3(1.0);
}
}
result *= transResult / 9.0;
}
#endif
@ -142,9 +148,15 @@ vec3 PCF(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
if (transparent == false) {
vec4 shadowmap_transparent = texture(shadowMapTransparent, uv);
if (shadowmap_transparent.a < compare)
result *= shadowmap_transparent.rgb;
vec3 transResult = vec3(0.0);
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
vec4 smt = texture(shadowMapTransparent,
uv + vec2(x, y) / smSize);
transResult += (smt.a < compare) ? smt.rgb : vec3(1.0);
}
}
result *= transResult / 9.0;
}
#endif
@ -187,9 +199,19 @@ vec3 PCFCube(samplerCubeShadow shadowMapCube,
#ifdef _ShadowMapTransparent
if (transparent == false) {
vec4 shadowmap_transparent = texture(shadowMapCubeTransparent, ml);
if (shadowmap_transparent.a < compare)
result *= shadowmap_transparent.rgb;
vec3 transResult = vec3(0.0);
vec4 smt = texture(shadowMapCubeTransparent, ml);
transResult += (smt.a < compare) ? smt.rgb : vec3(1.0);
for (int x = -1; x <= 1; x += 2) {
for (int y = -1; y <= 1; y += 2) {
for (int z = -1; z <= 1; z += 2) {
smt = texture(shadowMapCubeTransparent,
ml + vec3(x, y, z) * s);
transResult += (smt.a < compare) ? smt.rgb : vec3(1.0);
}
}
}
result *= transResult / 9.0;
}
#endif
@ -385,9 +407,15 @@ vec3 PCFFakeCube(sampler2DShadow shadowMap,
#ifdef _ShadowMapTransparent
if (transparent == false) {
vec4 shadowmap_transparent = texture(shadowMapTransparent, uvtiled);
if (shadowmap_transparent.a < compare)
result *= shadowmap_transparent.rgb;
vec3 transResult = vec3(0.0);
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
vec4 smt = texture(shadowMapTransparent,
clamp(uvtiled + vec2(x, y) / smSize, 0.0, 1.0));
transResult += (smt.a < compare) ? smt.rgb : vec3(1.0);
}
}
result *= transResult / 9.0;
}
#endif

View File

@ -151,7 +151,7 @@ void main() {
#endif
#endif
envl.rgb *= albedo;
envl.rgb *= diffuseIBL(albedo, roughness, f0, dotNV);
#ifdef _Brdf
envl.rgb *= 1.0 - F; //LV: We should take refracted light into account

View File

@ -137,7 +137,7 @@ void main() {
envl.rgb = pow(envl.rgb, vec3(2.2));
#endif
envl.rgb *= albedo;
envl.rgb *= diffuseIBL(albedo, roughness, f0, dotNV);
#ifdef _Brdf
envl.rgb *= 1.0 - F; //LV: We should take refracted light into account
@ -146,7 +146,7 @@ void main() {
envl.rgb *= envmapStrength * voxelgiEnv * occspec.x;
vec4 trace = traceDiffuse(P, n, voxels, clipmaps);
vec3 color = trace.rgb * albedo * (1.0 - F);
vec3 color = trace.rgb * diffuseIBL(albedo, roughness, f0, dotNV) * (1.0 - F);
color += envl * (1.0 - trace.a);
imageStore(voxels_diffuse, ivec2(pixel), vec4(color, 1.0));

View File

@ -69,7 +69,7 @@ void main() {
n.xy = n.z >= 0.0 ? g0.xy : octahedronWrap(g0.xy);
n = normalize(n);
float occ = 1.0 - traceShadow(P, n, voxels, voxelsSDF, normalize(lPos - P), clipmaps, pixel);
float occ = 1.0 - traceShadow(P, n, voxels, voxelsSDF, normalize(lPos - P), clipmaps, pixel, vec2(0.0));
imageStore(voxels_shadows, ivec2(pixel), vec4(occ));
}

View File

@ -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

View File

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

View File

@ -34,9 +34,9 @@ class RenderPath {
public static var vrCalibrationPosition:Vec4 = null;
public static var vrCalibrationRotation:iron.math.Quat = null;
public static var vrCalibrationSaved:Bool = false;
public static var vrCenterCameraWorld:Mat4 = null;
static var vrOriginalSuperSample:Float = -1.0;
public static inline function isVRPresenting(): Bool {
#if (kha_webgl && lnx_vr)
@ -45,7 +45,7 @@ class RenderPath {
return false;
#end
}
public static inline function isVRSimulateMode(): Bool {
return vrSimulateMode;
}
@ -162,24 +162,24 @@ class RenderPath {
var appW = iron.App.w();
var appH = iron.App.h();
// use native XR framebuffer dimensions
#if (kha_webgl && lnx_vr)
if (kha.vr.VrInterface.instance != null) {
var vr = kha.vr.VrInterface.instance;
var isPresenting = vr != null && vr.IsPresenting();
// save/restore camera position between modes
if (!wasVRPresenting && isPresenting) {
if (Scene.active != null && Scene.active.camera != null) {
if (vrCalibrationPosition == null) vrCalibrationPosition = new Vec4();
if (vrCalibrationRotation == null) vrCalibrationRotation = new Quat();
vrCalibrationPosition.setFrom(Scene.active.camera.transform.loc);
vrCalibrationRotation.setFrom(Scene.active.camera.transform.rot);
vrCalibrationSaved = true;
}
// save original super sampling for later
vrOriginalSuperSample = leenkx.renderpath.Inc.superSample;
@ -196,18 +196,18 @@ class RenderPath {
#if (kha_webgl && lnx_vr)
iron.system.Time.vrFrameTime = -1.0;
#end
if (vrCalibrationSaved && Scene.active != null && Scene.active.camera != null) {
Scene.active.camera.transform.loc.setFrom(vrCalibrationPosition);
Scene.active.camera.transform.rot.setFrom(vrCalibrationRotation);
Scene.active.camera.buildMatrix();
Scene.active.camera.buildProjection();
}
// restore original super sampling from simulate mode
if (vrOriginalSuperSample >= 0.0) {
leenkx.renderpath.Inc.superSample = vrOriginalSuperSample;
for (rt in renderTargets) {
if (rt.raw.width == 0 && rt.raw.scale != null) {
rt.raw.scale = vrOriginalSuperSample;
@ -216,13 +216,13 @@ class RenderPath {
resize();
vrOriginalSuperSample = -1.0;
}
// reset offset for next session
vrCameraOffsetSet = false;
vrCameraOffset = null;
}
wasVRPresenting = isPresenting;
if (isPresenting) {
// TODO: re-investigate using super sampling to avoid pixelation in simulate mode while giving max quality in headset
if (vrOriginalSuperSample >= 0.0 && leenkx.renderpath.Inc.superSample != 4.0) {
@ -234,7 +234,7 @@ class RenderPath {
}
resize();
}
var xrVr: kha.js.vr.VrInterface = cast vr;
if (xrVr.xrGLLayer != null) {
appW = xrVr.xrGLLayer.framebufferWidth;
@ -384,7 +384,7 @@ class RenderPath {
if (currentG != null) end();
currentG = g;
additionalTargets = additionalRenderTargets;
// we still bind but skip begin() when explicitly rendering to XR framebuffer (renderToXRFramebuffer flag)
#if lnx_vr
if (!renderToXRFramebuffer) {
@ -405,7 +405,7 @@ class RenderPath {
currentG.disableScissor();
scissorSet = false;
}
#if lnx_vr
if (beginCalled) {
currentG.end();
@ -692,7 +692,7 @@ class RenderPath {
if (vr == null || vr._glContext == null || vr.xrGLLayer == null) {
return;
}
var gl: js.html.webgl.WebGL2RenderingContext = cast vr._glContext;
var source = renderTargets.get(sourceTarget);
if (source == null) {
@ -703,21 +703,21 @@ class RenderPath {
return;
}
// trace('Framebuffer OK');
renderToXRFramebuffer = true;
gl.bindFramebuffer(js.html.webgl.WebGL2RenderingContext.DRAW_FRAMEBUFFER, vr.xrGLLayer.framebuffer);
gl.bindFramebuffer(js.html.webgl.WebGL2RenderingContext.READ_FRAMEBUFFER, sourceFB);
var readStatus = gl.checkFramebufferStatus(js.html.webgl.WebGL2RenderingContext.READ_FRAMEBUFFER);
var drawStatus = gl.checkFramebufferStatus(js.html.webgl.WebGL2RenderingContext.DRAW_FRAMEBUFFER);
if (readStatus != js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER_COMPLETE ||
if (readStatus != js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER_COMPLETE ||
drawStatus != js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER_COMPLETE) {
return;
}
var halfWidth = Std.int(source.image.width / 2);
var fullHeight = source.image.height;
if (vr._leftViewport != null) {
var vp = vr._leftViewport;
gl.blitFramebuffer(
@ -736,7 +736,7 @@ class RenderPath {
js.html.webgl.WebGL2RenderingContext.LINEAR
);
}
gl.bindFramebuffer(js.html.webgl.WebGL2RenderingContext.FRAMEBUFFER, null);
renderToXRFramebuffer = false;
#end
@ -749,24 +749,24 @@ class RenderPath {
if (currentG == null && frameG != null) {
currentG = frameG;
}
var appw = iron.App.w();
var apph = iron.App.h();
var g = currentG;
// get render target dimensions not App.w/h gbuffer is scaled in simulate mode with supersampling
var gbuffer0 = renderTargets.get("gbuffer0");
var actualWidth = (gbuffer0 != null && gbuffer0.image != null) ? gbuffer0.image.width : appw;
var actualHeight = (gbuffer0 != null && gbuffer0.image != null) ? gbuffer0.image.height : apph;
var actualHalfWidth = Std.int(actualWidth / 2);
var vrFBWidth = actualWidth;
var vrFBHeight = actualHeight;
var vrHalfWidth = actualHalfWidth;
var isVRPresenting = false;
vrSimulateMode = false;
var vr:Dynamic = null;
var vrExists = false;
#if (kha_webgl && lnx_vr)
@ -779,7 +779,7 @@ class RenderPath {
if (vrExists && vr != null && vr.IsPresenting()) {
vrSimulateMode = false;
isVRPresenting = true;
// get framebuffer dimensions from XR layer
#if (kha_webgl && lnx_vr)
var xrVr: kha.js.vr.VrInterface = cast vr;
@ -789,16 +789,16 @@ class RenderPath {
vrHalfWidth = Std.int(vrFBWidth / 2);
}
#end
if (Scene.active == null || Scene.active.camera == null) {
return;
}
#if (kha_webgl && lnx_vr)
if (vrCenterCameraWorld == null) vrCenterCameraWorld = Mat4.identity();
vrCenterCameraWorld.setFrom(Scene.active.camera.transform.world);
#end
// LEFT EYE
// HMD center for room scale position tracking
#if (kha_webgl && lnx_vr)
@ -808,10 +808,10 @@ class RenderPath {
if (viewerTransform != null && viewerTransform.position != null) {
// VR present calibration is used to position objects in world space not the camera
var pos = viewerTransform.position;
// camera follows headset directly in local floor space
Scene.active.camera.transform.loc.set(pos.x, pos.y, pos.z);
if (viewerTransform.orientation != null) {
Scene.active.camera.transform.rot.set(
viewerTransform.orientation.x,
@ -825,7 +825,7 @@ class RenderPath {
}
iron.system.VRController.updatePoses();
#end
Scene.active.camera.V.self = vr.GetViewMatrix(0);
Scene.active.camera.P.self = vr.GetProjectionMatrix(0);
Scene.active.camera.VP.setFrom(Scene.active.camera.P);
@ -835,7 +835,7 @@ class RenderPath {
var renderWidth = actualWidth;
var renderHeight = actualHeight;
var renderHalfWidth = actualHalfWidth;
// left half of render target
g.viewport(0, 0, renderHalfWidth, renderHeight);
g.scissor(0, 0, renderHalfWidth, renderHeight);
@ -847,12 +847,12 @@ class RenderPath {
Scene.active.camera.VP.setFrom(Scene.active.camera.P);
Scene.active.camera.VP.multmat(Scene.active.camera.V);
Scene.active.camera.buildMatrix();
// right half of render target
g.viewport(renderHalfWidth, 0, renderHalfWidth, renderHeight);
g.scissor(renderHalfWidth, 0, renderHalfWidth, renderHeight);
drawMeshes();
// restore for post-processing
g.disableScissor();
g.viewport(0, 0, renderWidth, renderHeight);
@ -865,7 +865,7 @@ class RenderPath {
if (vrCenterCameraWorld == null) vrCenterCameraWorld = Mat4.identity();
vrCenterCameraWorld.setFrom(Scene.active.camera.transform.world);
#end
Scene.active.camera.buildProjection(actualHalfWidth / actualHeight);
Scene.active.camera.transform.move(Scene.active.camera.right(), -ipd_offset);
@ -908,7 +908,9 @@ class RenderPath {
#end
Data.getShader(shaderPath[0], shaderPath[1], function(res: ShaderData) {
cc.context = res.getContext(shaderPath[2]);
if (res != null) {
cc.context = res.getContext(shaderPath[2]);
}
loading--;
});
}

View File

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

View File

@ -84,7 +84,7 @@ class CurveObject extends Object {
};
for (spline in data.splines) {
var newSpline = {
var newSpline: TSpline = {
points: [],
closed: spline.closed,
resolution: spline.resolution,
@ -441,7 +441,7 @@ class CurveObject extends Object {
for (splineIndex in 0...splinesLength) {
var spline = data.splines[splineIndex];
var matIdx = spline.material_index != null ? spline.material_index : 0;
var matIdx = spline.material_index;
if (!indicesByMaterial.exists(matIdx)) indicesByMaterial.set(matIdx, []);
var indices = indicesByMaterial.get(matIdx);
@ -704,7 +704,7 @@ class CurveObject extends Object {
for (splineIndex in 0...splinesLength) {
var spline = data.splines[splineIndex];
var matIdx = spline.material_index != null ? spline.material_index : 0;
var matIdx = spline.material_index;
if (!indicesByMaterial.exists(matIdx)) indicesByMaterial.set(matIdx, []);
var indices = indicesByMaterial.get(matIdx);
@ -1032,7 +1032,8 @@ class CurveObject extends Object {
if (basePos == null) return null;
var baseVertCount = Std.int(basePos.length / 4);
var baseScale = baseRaw.scale_pos;
var baseScale: kha.FastFloat = 1.0;
if (baseRaw.scale_pos != null) baseScale = baseRaw.scale_pos;
var minVal = 1e10;
var maxVal = -1e10;

View File

@ -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

View File

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

View File

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

View File

@ -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();
if (data == null) return;
net_Url = connection._url;
var data = inputs[1].get();
if (data == null) return;
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 {
return switch (from) {
case 1: Leenkx.file.get(net_Url);
default: throw "Unreachable";
}
}
override function get(from:Int):Dynamic {
#if lnx_torrent
return switch (from) {
case 1: meta;
case 2: metaBytes;
case 3: infoHashHex;
case 4: magnetUri;
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package leenkx.logicnode;
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;
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);
}
}
';
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());
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;
}
if (lnxCn != null) {
lnxCn.destroy();
socket.client = null;
}
Leenkx.connections.remove(connection._url);
socket.onclose();
#end
runOutput(0);
}
}

View File

@ -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();
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.";
}
#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
#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;
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
}
function onEvent() {
runOutput(0);
}
override function get(from: Int): Dynamic {
if (property0 == "host") {
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";
}
function onRemove() {
if (listener != null) {
Event.removeListener(listener);
listener = null;
}
}
override function get(from: Int): Dynamic {
#if lnx_torrent
return switch (from) {
case 1: Leenkx.id.get(net_Url);
case 2: Leenkx.data.get(net_Url);
default: throw "Unreachable";
}
#else
return null;
#end
}
}

View File

@ -1,8 +1,6 @@
package leenkx.logicnode;
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,140 +20,107 @@ 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":
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));
case "vector":
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));
case "float":
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));
case "integer":
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);
case "boolean":
case "boolean":
var bytes = buffer.readAllAvailableBytes();
var boolean = bytes.toString().substr(0, bytes.length - api.length);
if(boolean == "true"){
if (boolean == "true") {
parsed = true;
} else {
parsed = false;
}
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());
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;
}
for (f in Reflect.fields(rot)) {
Reflect.setField(rot, f, bytes.readFloat());
}
for (f in Reflect.fields(scl)) {
Reflect.setField(scl, f, bytes.readFloat());
}
var transform = Mat4.identity();
var transform = Mat4.identity();
parsed = transform.compose(loc, rot, scl);
runOutput(0);
case "rotation":
var bytes = new haxe.io.BytesInput(buffer.readUntil(api));
case "rotation":
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 {
return switch (from) {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,52 +1,124 @@
package leenkx.logicnode;
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 torrent: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();
if (connection == null) return;
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
}
override function get(from: Int): Dynamic {
return switch (from) {
case 1: torrent;
default: throw "Unreachable";
}
}
#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
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -63,10 +63,10 @@ class Buffer {
public function readUntil(delimiter:String):Bytes {
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;
}

View File

@ -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;

View File

@ -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";

View File

@ -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,24 +22,20 @@ 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
Leenkx.connections[connectionUrl].onload();
}
}
}
@ -281,11 +120,11 @@ 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;
public var onmessage:Void->Void;
public var onmessage:Void->Void;
public var onerror:Void->Void;
public var onseen:Void->Void;
public var onserver: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

View File

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

View File

@ -136,18 +136,43 @@ import haxe.io.Bytes;
class WebSocket {
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() {
return new js.html.WebSocket(_url);
if (_protocols == null) {
return new js.html.WebSocket(_url);
}
return new js.html.WebSocket(_url, _protocols);
}
public function open() {
@ -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));
if (parsedPort > 0 ) {
_port = parsedPort;
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;
}
} else {
_host = hostPort;
}
_path = urlRegExp.matched(5);
if (_path == null || _path.length == 0) {
_path = "/";
_path = pathAndSearch;
_search = null;
var searchIdx = pathAndSearch.indexOf("?");
if (searchIdx >= 0) {
_path = pathAndSearch.substr(0, searchIdx);
_search = pathAndSearch.substr(searchIdx);
}
**/
var urlArr = url.split(":");
if ( urlArr.length < 3) {
throw 'Uri not matching websocket URL "${url}"';
}
_protocol = urlArr[0];
_host = urlArr[1].substr(2, urlArr[1].length);
var parsedPort = Std.parseInt(urlArr[2].split("/")[0]);
if (parsedPort > 0 ) {
_port = parsedPort;
}
_path = urlArr[2].substr(urlArr[2].split("/")[0].length, urlArr[2].length);
if (_path == null || _path.length == 0) {
_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));
}
}

View File

@ -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;
}

View File

@ -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);
}
sendHttpResponse(httpResponse);
function callback(httpResponse: HttpResponse) {
sendHttpResponse(httpResponse);
if (httpResponse.code == 101) {
_onopenCalled = false;
state = State.Head;
Log.debug('Connected', id);
if (httpResponse.code == 101) {
_onopenCalled = false;
state = State.Head;
} else {
close();
}
}
if (validateHandshake != null) {
validateHandshake(httpRequest, httpResponse, callback);
} else {
close();
callback(httpResponse);
}
}
}

View File

@ -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);
}

View File

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

View File

@ -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();
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -669,8 +669,10 @@ class Inc {
#end
#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,8 +1424,8 @@ class ShadowMapAtlas {
public static inline function shadowMapAtlasName(type: String, transparent: Bool): String {
#if lnx_shadowmap_atlas_single_map
return "shadowMapAtlas";
#else
return transparent ? "shadowMapAtlasTransparent" : "shadowMapAtlas";
#else
switch (type) {
case "point":
return transparent ? "shadowMapAtlasPointTransparent" : "shadowMapAtlasPoint";

View File

@ -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;

View File

@ -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");
}
}
@ -936,6 +953,80 @@ class RenderPathDeferred {
}
#end
#if rp_ssrefr
{
if (leenkx.data.Config.raw.rp_ssrefr != false)
{
#if (!kha_opengl)
path.setDepthFrom("gbuffer0", "gbuffer1"); // Unbind depth so we can read it
path.depthToRenderTarget.set("main", path.renderTargets.get("tex"));
#end
//save depth
path.setTarget("gbufferD1");
path.bindTarget("_main", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass");
#if (!kha_opengl)
path.setDepthFrom("gbuffer0", "tex"); // Re-bind depth
path.depthToRenderTarget.set("main", path.renderTargets.get("gbuffer0"));
#end
//save background color
path.setTarget("refr");
path.bindTarget("tex", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass");
setTargetMeshes();
#if (rp_voxels != "Off")
path.bindTarget("voxelsOut", "voxels");
#if (rp_voxels == "Voxel GI" || lnx_voxelgi_shadows)
path.bindTarget("voxelsSDF", "voxelsSDF");
#end
#end
#if rp_shadowmap
{
#if lnx_shadowmap_atlas
Inc.bindShadowMapAtlas();
#else
Inc.bindShadowMap();
#end
}
#end
#if rp_ssrs
path.bindTarget("gbufferD1", "gbufferD");
#end
path.drawMeshes("refraction");
path.setTarget("buf");
path.bindTarget("tex", "tex");
path.bindTarget("gbufferD1", "gbufferD1");
path.bindTarget("gbuffer0", "gbuffer0");
path.bindTarget("gbuffer1", "gbuffer1");
path.bindTarget("refr", "tex1");
path.bindTarget("_main", "gbufferD");
path.bindTarget("gbuffer_refraction", "gbuffer_refraction");
path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass");
path.setTarget("tex");
path.bindTarget("buf", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass");
}
}
#end
#if (rp_translucency && !rp_ssrefr && rp_ssr)
{
path.setTarget("tex");
path.drawMeshes("depth");
}
#end
#if rp_ssr
{
if (leenkx.data.Config.raw.rp_ssr != false) {
@ -1016,73 +1107,6 @@ class RenderPathDeferred {
}
#end
#if rp_ssrefr
{
if (leenkx.data.Config.raw.rp_ssrefr != false)
{
#if (!kha_opengl)
path.setDepthFrom("gbuffer0", "gbuffer1"); // Unbind depth so we can read it
path.depthToRenderTarget.set("main", path.renderTargets.get("tex"));
#end
//save depth
path.setTarget("gbufferD1");
path.bindTarget("_main", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass");
#if (!kha_opengl)
path.setDepthFrom("gbuffer0", "tex"); // Re-bind depth
path.depthToRenderTarget.set("main", path.renderTargets.get("gbuffer0"));
#end
//save background color
path.setTarget("refr");
path.bindTarget("tex", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass");
path.setTarget("gbuffer0", ["gbuffer1", "gbuffer_refraction"]);
#if (rp_voxels != "Off")
path.bindTarget("voxelsOut", "voxels");
#if (rp_voxels == "Voxel GI" || lnx_voxelgi_shadows)
path.bindTarget("voxelsSDF", "voxelsSDF");
#end
#end
#if rp_shadowmap
{
#if lnx_shadowmap_atlas
Inc.bindShadowMapAtlas();
#else
Inc.bindShadowMap();
#end
}
#end
#if rp_ssrs
path.bindTarget("_main", "gbufferD");
#end
path.drawMeshes("refraction");
path.setTarget("buf");
path.bindTarget("tex", "tex");
path.bindTarget("gbufferD1", "gbufferD1");
path.bindTarget("gbuffer0", "gbuffer0");
path.bindTarget("gbuffer1", "gbuffer1");
path.bindTarget("refr", "tex1");
path.bindTarget("_main", "gbufferD");
path.bindTarget("gbuffer_refraction", "gbuffer_refraction");
path.drawShader("shader_datas/ssrefr_pass/ssrefr_pass");
path.setTarget("tex");
path.bindTarget("buf", "tex");
path.drawShader("shader_datas/copy_pass/copy_pass");
}
}
#end
#if rp_chromatic_aberration
{
path.setTarget("buf");

View File

@ -968,6 +968,7 @@ class DebugConsole extends Trait {
ui.unindent();
}
#if js
if (ui.panel(Id.handle({selected: false}), "Console")) {
ui.indent();
var t = ui.textInput(Id.handle());
@ -977,6 +978,7 @@ class DebugConsole extends Trait {
}
ui.unindent();
}
#end
}
if (ui.tab(htab, lastTraces[0] == "" ? "Console" : lastTraces[0].substr(0, 20))) {
#if js

View File

@ -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':
cls.export_torrent = True
@staticmethod
def write_matrix(matrix):

View File

@ -71,7 +71,7 @@ def init_categories():
lnx_nodes.add_category('Renderpath', icon='STICKY_UVS_LOC', section="graphics")
lnx_nodes.add_category('Sound', icon='OUTLINER_OB_SPEAKER', section="sound")
lnx_nodes.add_category('3D_Audio', icon='SPEAKER', section="sound")
lnx_nodes.add_category('Audio_3D', icon='SPEAKER', section="sound")
lnx_nodes.add_category('Miscellaneous', icon='RESTRICT_COLOR_ON', section="misc")
lnx_nodes.add_category('Custom', icon='PLUGIN', section="misc")

View File

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

View File

@ -6,7 +6,7 @@ class AudioDSPFilterNode(LnxLogicTreeNode):
bl_idname = 'LNAudioDSPFilterNode'
bl_label = 'DSP Filter'
bl_description = 'DSP Audio Filter for lowpass/bandpass/highpass '
lnx_category = '3D_Audio'
lnx_category = 'Audio_3D'
lnx_version = 1
property0: HaxeEnumProperty(
@ -27,5 +27,5 @@ class AudioDSPFilterNode(LnxLogicTreeNode):
layout.prop(self, 'property0')
def register():
add_category('3D_Audio', icon='SOUND')
add_category('Audio_3D', icon='SOUND')
AudioDSPFilterNode.on_register()

View File

@ -6,7 +6,7 @@ class AudioDSPHaasEffectNode(LnxLogicTreeNode):
bl_idname = 'LNAudioDSPHaasEffectNode'
bl_label = 'DSP Haas Effect'
bl_description = 'DSP Audio Haas Effect'
lnx_category = '3D_Audio'
lnx_category = 'Audio_3D'
lnx_version = 1
@ -19,5 +19,5 @@ class AudioDSPHaasEffectNode(LnxLogicTreeNode):
def register():
add_category('3D_Audio', icon='SOUND')
add_category('Audio_3D', icon='SOUND')
AudioDSPHaasEffectNode.on_register()

View File

@ -6,14 +6,14 @@ class AudioHRTFPannerNode(LnxLogicTreeNode):
bl_idname = 'LNAudioHRTFPannerNode'
bl_label = 'HRTF Panner'
bl_description = 'Create 3D audio HRTF Panner'
lnx_category = '3D_Audio'
lnx_category = 'Audio_3D'
lnx_version = 1
def update_input(self, context):
while len(self.inputs) > 5:
self.inputs.remove(self.inputs[-1])
if self.property0 == 'Custom':
self.add_input('LnxStringSocket', 'File Name', default_value=0)
self.add_input('LnxStringSocket', 'File Name', default_value='')
property0: HaxeEnumProperty(
'property0',
@ -35,5 +35,5 @@ class AudioHRTFPannerNode(LnxLogicTreeNode):
layout.prop(self, 'property0')
def register():
add_category('3D_Audio', icon='SOUND')
add_category('Audio_3D', icon='SOUND')
AudioHRTFPannerNode.on_register()

View File

@ -6,7 +6,7 @@ class AudioLoadNode(LnxLogicTreeNode):
bl_idname = 'LNAudioLoadNode'
bl_label = 'Load Audio'
bl_description = 'Load audio'
lnx_category = '3D_Audio'
lnx_category = 'Audio_3D'
lnx_version = 1
def update_input(self, context):
@ -39,5 +39,5 @@ class AudioLoadNode(LnxLogicTreeNode):
#col.prop_search(self, 'property0', bpy.data, 'sounds', icon='NONE', text='')
def register():
add_category('3D_Audio', icon='SOUND')
add_category('Audio_3D', icon='SOUND')
AudioLoadNode.on_register()

View File

@ -6,7 +6,7 @@ class AudioPauseNode(LnxLogicTreeNode):
bl_idname = 'LNAudioPauseNode'
bl_label = 'Pause Audio'
bl_description = 'Trigger the pause function on audio'
lnx_category = '3D_Audio'
lnx_category = 'Audio_3D'
lnx_version = 1
@ -18,5 +18,5 @@ class AudioPauseNode(LnxLogicTreeNode):
self.add_output('LnxDynamicSocket', 'Audio')
def register():
add_category('3D_Audio', icon='SOUND')
add_category('Audio_3D', icon='SOUND')
AudioPauseNode.on_register()

View File

@ -6,7 +6,7 @@ class AudioPlayNode(LnxLogicTreeNode):
bl_idname = 'LNAudioPlayNode'
bl_label = 'Play Audio'
bl_description = 'Trigger the play function on audio'
lnx_category = '3D_Audio'
lnx_category = 'Audio_3D'
lnx_version = 1
@ -19,5 +19,5 @@ class AudioPlayNode(LnxLogicTreeNode):
self.add_output('LnxDynamicSocket', 'Audio')
def register():
add_category('3D_Audio', icon='SOUND')
add_category('Audio_3D', icon='SOUND')
AudioPlayNode.on_register()

View File

@ -6,7 +6,7 @@ class AudioStereoPannerNode(LnxLogicTreeNode):
bl_idname = 'LNAudioStereoPannerNode'
bl_label = 'Stereo Panner'
bl_description = 'Create 2D audio Stereo Panner'
lnx_category = '3D_Audio'
lnx_category = 'Audio_3D'
lnx_version = 1
def update_input(self, context):
@ -34,5 +34,5 @@ class AudioStereoPannerNode(LnxLogicTreeNode):
layout.prop(self, 'property0')
def register():
add_category('3D_Audio', icon='SOUND')
add_category('Audio_3D', icon='SOUND')
AudioStereoPannerNode.on_register()

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