From 69df7302d5923521d9c498ecfa4d0d2a9224236b Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Thu, 6 Mar 2025 21:18:05 -0600 Subject: [PATCH] initial attempt at portal and contact --- meson.build | 4 +- scripts/core/engine.js | 236 +++++++++++++++++++++-------------------- scripts/modules/cmd.js | 24 +++-- source/jsffi.c | 5 - source/qjs_enet.c | 138 ++++++++++++++++++------ tests/contact.js | 8 ++ tests/overling.js | 21 ++-- tests/portal.js | 9 ++ tests/portalspawner.js | 19 ++++ tests/underling.js | 4 +- 10 files changed, 298 insertions(+), 170 deletions(-) create mode 100644 tests/contact.js create mode 100644 tests/portal.js create mode 100644 tests/portalspawner.js diff --git a/meson.build b/meson.build index 319f957d..483998ba 100644 --- a/meson.build +++ b/meson.build @@ -234,7 +234,9 @@ tests = [ 'empty', 'nota', 'enet', - 'wota' + 'wota', + 'portalspawner', + 'overling' ] foreach file : tests diff --git a/scripts/core/engine.js b/scripts/core/engine.js index ac9d6012..d8a42a9c 100644 --- a/scripts/core/engine.js +++ b/scripts/core/engine.js @@ -522,7 +522,6 @@ js.eval(DOCPATH, script)() var enet = use('enet') var util = use('util') var math = use('math') -var crypto = use('crypto') var $_ = {} @@ -536,24 +535,19 @@ $_.clock = function(fn) var underlings = new Set() var overling = undefined -var host = enet.create_host() -$_.host = host - -console.log(`made a host with port ${host.port()}`) +var host = enet.create_host({ + address:"127.0.0.1", // or any address like "x.x.x.x", or "broadcast" for 255.255.255.255 and "any" for o0 + port:0, + channels:0, + incoming_bandwidth:0, + outgoing_bandwidth:0 +}); globalThis.$_ = $_ var portal = undefined -var receive_fn = undefined; - -var ephemeralkeys = {} -var knownsecrets = {} - -$_.contact = function(callback, record) -{ - -} +var receive_fn $_.connection = function(callback, actor, config) { var peer = actor.peer; @@ -582,7 +576,26 @@ $_.connection = function(callback, actor, config) { var portal = undefined $_.portal = function(fn, port) { + console.log(`starting a portal on port ${port}`) + if (!port) + throw new Error("Requires a valid port."); + $_.start(e => { + switch(e.type) { + case "actor_started": + portal = e.actor + break + } + portal = e + }, undefined, { + port + }); +} + +$_.contact = function(callback, record) +{ + console.log(`connecting to ${json.encode(record)}`) + host.connect(record.address, record.port) } $_.receiver = function(fn) @@ -594,30 +607,31 @@ var greeters = {} $_.start = function(cb, prg, arg) { - var ephemeral = crypto.keypair() var guid = util.guid() greeters[guid] = cb - ephemeralkeys[guid] = ephemeral - os.createprocess([ + var argv = [ "./prosperon", "spawn", - "--program",prg, - "--overling", $_.host.port(), + "--overling", host.port, "--guid", guid, - "--parentpub", ephemeral.public - ]) + ] + + if (prg) + argv = argv.concat(['--program', prg]) + + if (arg) + argv = argv.concat(cmd.encode(arg)) + + os.createprocess(argv) guid2actor.set(guid, {peer:undefined, guid:guid}) } $_.stop = function(actor) { - if (!actor) { - os.exit(0) - } - - actor.peer.send({ - type:"stop", - }) + if (!actor) + destroyself() + + send_system(actor, {type:"stop"}) } var unneeded_fn = $_.stop @@ -644,91 +658,84 @@ $_.couple = function(actor) couplings.add(actor) } -use('cmd')(prosperon.argv) - -var child_ephemeral - -if (prosperon.overling) { - $_.host.connect("localhost", prosperon.args.overling) +function send_system(actor, message) +{ + actor.peer.send({ + type:"system", + data:message + }) } -if (prosperon.program) - actor.spawn(prosperon.program) +$_.send = function(actor, message, receive) +{ + if (typeof message !== 'object') + throw new Error('Must send an object record.') + + actor.peer.send({ // right now only peers so this works + type:"user", + data: message + }) +} -if (prosperon.args.parentpub) - child_ephemeral = crypto.keypair() -else if (prosperon.overling) - console.warn("No parentpub provided; secure handshake won't proceed!"); +var cmd = use('cmd') +cmd.process(prosperon.argv) -if (!prosperon.guid) prosperon.guid = util.guid() +if (prosperon.args.overling) + host.connect("localhost", prosperon.args.overling) -var ar = 60 // seconds before reclamation +if (prosperon.args.program) + actor.spawn(prosperon.args.program) + +if (!prosperon.args.guid) + prosperon.guid = util.guid() +else + prosperon.guid = prosperon.args.guid; + +var ar = 5 // seconds before reclamation var unneeded_timer = $_.delay($_.stop, ar) -function handle_receive(e) +function destroyself() { - var data = e.data - switch(data.type) { - case "handshake": - var ep = ephemeralkeys[data.guid] - if (!ep) - throw new Error(`No stored ephemeral keypair found for guid=${data.guid}. Cannot do secure handshake.`) - - var parent_private = ep.private - if (!data.child_public) - throw new Error("No child public key found in handshake message.") - - var shared = crypto.shared({public:data.child_public, private: ep.private}) - knownsecrets[data.guid] = shared - console.log("Sending handshake ok ..") - e.peer.send({ - type: "handshake_ok", - guid:data.guid - }) - break; - - case "handshake_ok": - if (!child_ephemeral) - throw new Error("We didn't generate a child ephemeral key. Something is off. Not deriving a shared secret!") - - if (!prosperon.args.parentpub) - throw new Error("No parent's ephemeral public key was provided. Cannot continue with shared secret.") - - if (!data.guid) - throw new Error("handshake_ok message missing guid. We won't store the shared secret.") - - console.log("got handshake ok") - knownsecrets[data.guid] = crypto.shared({public:prosperon.args.parentpub, private: child_ephemeral.private}) - - break; - - case "greet": - if (greeters[data.guid]) { - var actor = guid2actor.get(data.guid) - if (!actor) throw new Error(`No registered actor for guid ${data.guid}`) - actor.peer = e.peer - guid2actor.set(e.peer, actor) - greeters[data.guid]({ - type: "greet", - data: actor - }) - greeters[data.guid] = undefined - } - break - case "stop": - console.log("STOPPING!") - os.exit(0) - } + host.broadcast({type:"system", data:{ + type:"disconnect" + }}) + host.flush() + os.exit(0) } function handle_actor_disconnect(actor) { + if (couplings.has(actor)) + $_.stop() + guid2actor.delete(actor.guid) guid2actor.delete(actor.peer) - if (couplings.has(actor)) { - console.log(`I was connected to it, so I'm dying`) - $_.stop() +} + +function handle_system(e) +{ + var msg = e.data.data + switch(msg.type) { + case "disconnect": + handle_actor_disconnect(guid2actor.get(e.peer)) + break + + case "stop": + destroyself() + break + + case "greet": + if (greeters[msg.guid]) { + var actor = guid2actor.get(msg.guid) + if (!actor) throw new Error(`No registered actor for guid ${msg.guid}`) + actor.peer = e.peer + guid2actor.set(e.peer, actor) + greeters[msg.guid]({ + type: "actor_started", + actor + }) + } } } @@ -738,32 +745,35 @@ while (1) { unneeded_timer() unneeded_timer = $_.delay(unneeded_fn, unneeded_time) }, hang) + host.service(e => { unneeded_timer() switch(e.type) { case "connect": - if (child_ephemeral) { - console.log(`Child connected. Sending handshake ...`) - e.peer.send({ - type: "handshake", - guid: prosperon.guid, - child_public: child_ephemeral.public - }) - } else { - console.log(`connected. sending greet with guid ${prosperon.guid} to peer ${e.peer}`) - e.peer.send({ - type: "greet", + console.log(`message arrived at ${host.address}:${host.port} from somebody ... ${e.peer.address}:${e.peer.port}`) + e.peer.send({ + type:"system", + data: { + type:"greet", guid: prosperon.guid - }); - } - + } + }) break; case "receive": - handle_receive(e); + if (e.data.type === "system") + handle_system(e) + else if(receive_fn) + receive_fn(e.data.data) + else + console.log(`Got a messge but no receiver is registered.`) + break; case "disconnect": + greeters[guid2actor.get(e.peer).guid]({ + type: "actor_stopped" + }) handle_actor_disconnect(guid2actor.get(e.peer)) break } diff --git a/scripts/modules/cmd.js b/scripts/modules/cmd.js index c80d9aba..e7de3039 100644 --- a/scripts/modules/cmd.js +++ b/scripts/modules/cmd.js @@ -146,17 +146,25 @@ function parse_args(argv) return args; } +function unparse_args(args) { + var argv = []; + for (var key in args) { + if (args.hasOwnProperty(key)) { + argv.push("--" + key); // Add the flag with "--" prefix + if (args[key] !== true) { + argv.push(args[key]); // Add the value if it's not a boolean true flag + } + } + } + return argv; +} + Cmdline.register_order( "spawn", function(argv) { prosperon.args = parse_args(argv) if (!prosperon.args.program) os.exit() - - prosperon.guid = prosperon.args.guid - prosperon.overling = prosperon.args.overling - prosperon.program = prosperon.args.program - prosperon.parentpub = prosperon.args.parentpub }, "Spawn a new prosperon actor.", "TOPIC" @@ -212,4 +220,8 @@ function cmd_args(cmds) { Cmdline.orders[cmds[0]](cmds.slice(1)); } -return cmd_args; +return { + process: cmd_args, + encode: parse_args, + decode: unparse_args, +} diff --git a/source/jsffi.c b/source/jsffi.c index e71f6bf7..058ea91f 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -7667,8 +7667,6 @@ static inline void to_hex(const uint8_t *in, size_t in_len, char *out) out[2 * in_len] = '\0'; // null-terminate } -#include // for isxdigit - static inline int nibble_from_char(char c, uint8_t *nibble) { if (c >= '0' && c <= '9') { *nibble = (uint8_t)(c - '0'); return 0; } @@ -7688,9 +7686,6 @@ static inline int from_hex(const char *hex, uint8_t *out, size_t out_len) return 0; } -#include // for size_t, memcpy -#include "quickjs.h" - // Convert a JSValue containing a 64-character hex string into a 32-byte array. static inline void js2crypto(JSContext *js, JSValue v, uint8_t *crypto) { diff --git a/source/qjs_enet.c b/source/qjs_enet.c index 6f7f0f3b..d41ef02a 100644 --- a/source/qjs_enet.c +++ b/source/qjs_enet.c @@ -46,69 +46,113 @@ static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, return JS_UNDEFINED; } -/* Host creation */ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, - int argc, JSValueConst *argv) { + int argc, JSValueConst *argv) { ENetHost *host; ENetAddress address; JSValue obj; - // Default configuration matching the JavaScript object - size_t peer_count = 32; // peer_count: 32 - size_t channel_limit = 0; // channel_limit: 0 - enet_uint32 incoming_bandwidth = 0; // incoming_bandwidth: 0 - enet_uint32 outgoing_bandwidth = 0; // outgoing_bandwidth: 0 + // Default parameters (if not provided by user) + size_t peer_count = 32; + size_t channel_limit = 0; + enet_uint32 incoming_bandwidth = 0; + enet_uint32 outgoing_bandwidth = 0; - if (argc < 1) { - // Create client-like host with port 0 and "any" address + // If no arguments or first arg is not an object, create a client-like host. + if (argc < 1 || !JS_IsObject(argv[0])) { address.host = ENET_HOST_ANY; address.port = 0; - host = enet_host_create(&address, peer_count, channel_limit, - incoming_bandwidth, outgoing_bandwidth); + + host = enet_host_create(&address, + peer_count, + channel_limit, + incoming_bandwidth, + outgoing_bandwidth); if (!host) { - return JS_ThrowInternalError(ctx, "Failed to create ENet host (any address)."); + return JS_ThrowInternalError(ctx, "Failed to create ENet host with 'any:0'."); } goto RET; } - // If argument is provided, interpret as "ip:port" for server - const char *address_str = JS_ToCString(ctx, argv[0]); - if (!address_str) - return JS_EXCEPTION; // memory or conversion error - - char ip[64]; - int port; + // Now parse the object + JSValue configObj = argv[0]; - if (sscanf(address_str, "%63[^:]:%d", ip, &port) != 2) { - JS_FreeCString(ctx, address_str); - return JS_ThrowTypeError(ctx, "Invalid address format. Expected 'ip:port'."); + // 1) address + JSValue addrVal = JS_GetPropertyStr(ctx, configObj, "address"); + const char *addrStr = NULL; + if (JS_IsString(addrVal)) { + addrStr = JS_ToCString(ctx, addrVal); } - JS_FreeCString(ctx, address_str); + JS_FreeValue(ctx, addrVal); - if (strcmp(ip, "any") == 0) + // If address not given or not string, default to "any". + if (!addrStr) { + addrStr = "any"; + } + + // 2) port + JSValue portVal = JS_GetPropertyStr(ctx, configObj, "port"); + int32_t port32 = 0; + JS_ToInt32(ctx, &port32, portVal); // if invalid or undefined, remains 0 + JS_FreeValue(ctx, portVal); + + // 3) channels -> channel_limit + JSValue chanVal = JS_GetPropertyStr(ctx, configObj, "channels"); + JS_ToUint32(ctx, &channel_limit, chanVal); // default 0 if missing + JS_FreeValue(ctx, chanVal); + + // 4) incoming_bandwidth + JSValue inBWVal = JS_GetPropertyStr(ctx, configObj, "incoming_bandwidth"); + JS_ToUint32(ctx, &incoming_bandwidth, inBWVal); + JS_FreeValue(ctx, inBWVal); + + // 5) outgoing_bandwidth + JSValue outBWVal = JS_GetPropertyStr(ctx, configObj, "outgoing_bandwidth"); + JS_ToUint32(ctx, &outgoing_bandwidth, outBWVal); + JS_FreeValue(ctx, outBWVal); + + // Populate ENetAddress + if (strcmp(addrStr, "any") == 0) { address.host = ENET_HOST_ANY; - else if (strcmp(ip, "broadcast")) - address.host = ENET_HOST_BROADCAST; - else { - int err = enet_address_set_host_ip(&address, ip); + } else if (strcmp(addrStr, "broadcast") == 0) { + address.host = ENET_HOST_BROADCAST; + } else { + int err = enet_address_set_host_ip(&address, addrStr); if (err != 0) { - return JS_ThrowInternalError(ctx, "Failed to set host IP from %s. Error %d.", ip, err); + // Free addrStr only if it came from JS_ToCString + if (addrStr && addrStr != "any" && addrStr != "broadcast") { + JS_FreeCString(ctx, addrStr); + } + return JS_ThrowInternalError( + ctx, "Failed to set host IP from '%s'. Error code: %d", addrStr, err + ); } } - address.port = port; + address.port = (enet_uint16) port32; - // Create host with specified configuration - host = enet_host_create(&address, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth); + // Now that we're done using addrStr, free it if it was allocated. + if (addrStr && addrStr != "any" && addrStr != "broadcast") { + JS_FreeCString(ctx, addrStr); + } + + // Finally, create the host + host = enet_host_create(&address, + peer_count, + channel_limit, + incoming_bandwidth, + outgoing_bandwidth); if (!host) { return JS_ThrowInternalError(ctx, "Failed to create ENet host."); } RET: + // Wrap up in a QuickJS object obj = JS_NewObjectClass(ctx, enet_host_id); if (JS_IsException(obj)) { enet_host_destroy(host); return obj; } + // Associate our C pointer with this JS object JS_SetOpaque(obj, host); return obj; } @@ -464,8 +508,8 @@ static const JSCFunctionListEntry js_enet_host_funcs[] = { JS_CFUNC_DEF("connect", 2, js_enet_host_connect), JS_CFUNC_DEF("flush", 0, js_enet_host_flush), JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast), - JS_CFUNC_DEF("port", 0, js_enet_host_get_port), - JS_CFUNC_DEF("address", 0, js_enet_host_get_address), + JS_CGETSET_DEF("port", js_enet_host_get_port, NULL), + JS_CGETSET_DEF("address", js_enet_host_get_address, NULL), }; /* Getter for roundTripTime (rtt) */ @@ -584,6 +628,30 @@ static JSValue js_enet_peer_get_reliable_data_in_transit(JSContext *ctx, JSValue return JS_NewInt32(ctx, peer->reliableDataInTransit); } +static JSValue js_enet_peer_get_port(JSContext *js, JSValueConst self) +{ + ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id); + return JS_NewUint32(js, peer->address.port); +} + +static JSValue js_enet_peer_get_address(JSContext *js, JSValueConst self) +{ + ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id); + uint32_t host = ntohl(peer->address.host); + + if (host == 0x7F000001) + return JS_NewString(js, "localhost"); + + char ip_str[16]; // Enough space for "255.255.255.255" + null terminator + snprintf(ip_str, sizeof(ip_str), "%u.%u.%u.%u", + (host >> 24) & 0xFF, + (host >> 16) & 0xFF, + (host >> 8) & 0xFF, + host & 0xFF); + + return JS_NewString(js, ip_str); +} + static const JSCFunctionListEntry js_enet_peer_funcs[] = { JS_CFUNC_DEF("send", 1, js_enet_peer_send), JS_CFUNC_DEF("disconnect", 0, js_enet_peer_disconnect), @@ -605,6 +673,8 @@ static const JSCFunctionListEntry js_enet_peer_funcs[] = { JS_CGETSET_DEF("packet_loss", js_enet_peer_get_packet_loss, NULL), JS_CGETSET_DEF("state", js_enet_peer_get_state, NULL), JS_CGETSET_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit, NULL), + JS_CGETSET_DEF("port", js_enet_peer_get_port, NULL), + JS_CGETSET_DEF("address", js_enet_peer_get_address, NULL), }; /* Module entry point */ diff --git a/tests/contact.js b/tests/contact.js new file mode 100644 index 00000000..51d376b1 --- /dev/null +++ b/tests/contact.js @@ -0,0 +1,8 @@ +// Test to connect to a portal + +$_.contact((actor, reason) => { +}, { + address: "localhost", + port: 5678, + password: "abc123" +}); diff --git a/tests/overling.js b/tests/overling.js index 57c57692..25aa2b28 100644 --- a/tests/overling.js +++ b/tests/overling.js @@ -2,18 +2,19 @@ var os = use('os') $_.start(e => { switch(e.type) { - case "greet": - console.log(`parent got message from child with greet.`) - $_.connection(e => console.log(json.encode(e)), e.data) + case "actor_started": + console.log(`parent got system level actor_started msg`) + $_.connection(e => console.log(json.encode(e)), e.actor) // get connection info + + $_.send(e.actor, {message: "Hello!"}) + $_.delay(_ => { - console.log(`sending stop message to ${json.encode(e.data)}`) - $_.stop(e.data) + console.log(`sending stop message to ${json.encode(e.actor)}`) + $_.stop(e.actor) }, 1); - $_.couple(e.data) + + $_.couple(e.actor) } }, "tests/underling.js"); -$_.contact((actor, reason) => { -}, { - address: "localhost", -}); + diff --git a/tests/portal.js b/tests/portal.js new file mode 100644 index 00000000..4d6a63cd --- /dev/null +++ b/tests/portal.js @@ -0,0 +1,9 @@ +// Test to create a portal + +var password = "abc123" + +$_.portal(e => { + console.log(`received a message for contact: ${json.encode(e)}`) + if (e.password !== password) + throw new Error("Password does not match."); +}, 5678); diff --git a/tests/portalspawner.js b/tests/portalspawner.js new file mode 100644 index 00000000..714675e4 --- /dev/null +++ b/tests/portalspawner.js @@ -0,0 +1,19 @@ +// Creates a portal and a separate actor to contact + +var os = use('os') + +var children = [] + +$_.start(e => { + console.log('Portal actor finished starting.') + children.push(e.actor) + $_.start(e => { + children.push(e.actor) + console.log('Contact actor finished starting.') + }, "tests/contact.js") +}, "tests/portal.js") + +$_.delay(_ => { + for (var c of children) $_.stop(c) + $_.stop() +}, 3) diff --git a/tests/underling.js b/tests/underling.js index abe2231d..a3dc93ce 100644 --- a/tests/underling.js +++ b/tests/underling.js @@ -1,3 +1,5 @@ var os = use('os') -console.log(`started underling`) \ No newline at end of file +$_.receiver(e => { + console.log(`got message: ${json.encode(e)}`) +})