initial attempt at portal and contact
Some checks failed
Build and Deploy / build-linux (push) Failing after 1m41s
Build and Deploy / build-windows (CLANG64) (push) Failing after 9m19s
Build and Deploy / package-dist (push) Has been skipped
Build and Deploy / deploy-itch (push) Has been skipped
Build and Deploy / deploy-gitea (push) Has been skipped

This commit is contained in:
2025-03-06 21:18:05 -06:00
parent 01f7e715a4
commit 69df7302d5
10 changed files with 298 additions and 170 deletions

View File

@@ -234,7 +234,9 @@ tests = [
'empty',
'nota',
'enet',
'wota'
'wota',
'portalspawner',
'overling'
]
foreach file : tests

View File

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

View File

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

View File

@@ -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 <ctype.h> // 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 <string.h> // 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)
{

View File

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

8
tests/contact.js Normal file
View File

@@ -0,0 +1,8 @@
// Test to connect to a portal
$_.contact((actor, reason) => {
}, {
address: "localhost",
port: 5678,
password: "abc123"
});

View File

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

9
tests/portal.js Normal file
View File

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

19
tests/portalspawner.js Normal file
View File

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

View File

@@ -1,3 +1,5 @@
var os = use('os')
console.log(`started underling`)
$_.receiver(e => {
console.log(`got message: ${json.encode(e)}`)
})