mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Where unix-dgram is installed beside hot-shots, the shim's entry sends through runtime AF_UNIX datagram calls with the addon's semantics; without it, uds reports the addon missing as hot-shots does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2231 lines
81 KiB
C
2231 lines
81 KiB
C
/* node:dgram + node:dns — UDP sockets over the event loop's readiness
|
|
* poller (the scr_platform.h contract — kqueue on macOS/BSD, epoll on
|
|
* Linux; scr_net.c has the seam's full story), and the dns.lookup slice
|
|
* that rides the same unit.
|
|
*
|
|
* ── Design note ──────────────────────────────────────────────────────
|
|
*
|
|
* Object model. One refcounted handle kind, a LEAN allocation (no cycle
|
|
* header — the ScrNetSocket precedent): ScrDgramSocket (fd, the
|
|
* bound/connected/closing state machine, message/listening/connect/
|
|
* close/error listener lists, the unref flag). Listeners MOVE in (+1)
|
|
* and are released when the handle settles ('close' fired, or the
|
|
* exit-time cleanup) — the scr_net.c ownership story verbatim, so a
|
|
* listener capturing its own socket cannot cycle past settlement.
|
|
*
|
|
* Event dispatch. One poller owned by this unit (lazily created), fds
|
|
* non-blocking. The loop (scr_async.c) calls scr_dgram_dispatch() at
|
|
* every turn top — the net hook's exact shape — alternating a SWEEP of
|
|
* deferred emits ('listening'/'connect' callbacks, bind/send failures,
|
|
* 'close', pending dns.lookup deliveries) with a zero-timeout poller
|
|
* drain (arrived datagrams, delivered macrotask-style on the main
|
|
* stack), stopping early when a callback enqueued microtasks or threw.
|
|
* Between turns the loop's idle poll(2) watches this unit's poller fd.
|
|
*
|
|
* Read model. Consumer-driven like net sockets: the read filter is armed
|
|
* only while a 'message' listener exists — unheard datagrams wait in
|
|
* the kernel's socket buffer (and drop when it fills, UDP's contract
|
|
* in Node too). One recvfrom(2) per datagram, one 'message' emit per
|
|
* datagram, rinfo parts delivered borrowed to the per-shape adapters.
|
|
*
|
|
* Send model. sendto(2) immediately — a UDP datagram either goes out or
|
|
* it doesn't; there is no partial-write buffering. An unbound sender
|
|
* implicit-binds (the kernel's ephemeral port), like Node. Send
|
|
* failures defer to the 'error' sweep.
|
|
*
|
|
* State errors, Node-matched: bind on a bound socket throws "Socket is
|
|
* already bound"; connect on a connected one "Already connected";
|
|
* send/close/address on a closed (or address on a never-bound) one
|
|
* "Not running" — all synchronous catchable Errors with Node's
|
|
* MESSAGES (the code/errno properties don't exist — the fs error
|
|
* stance, SEMANTICS.md divergence 13). Runtime failures (EADDRINUSE,
|
|
* unreachable sends, failed host lookups) are the async 'error' event;
|
|
* an 'error' with no listener prints and exits 1, the unhandled
|
|
* EventEmitter behavior. bind()/connect() emit 'listening' (connect
|
|
* implicit-binds first, so BOTH fire, Node's order) and 'connect' on
|
|
* the next dispatch pass — Node's next-tick emits.
|
|
*
|
|
* Loop liveness. An open (bound or connected) socket holds the loop
|
|
* until 'close' delivers or unref() waives it — Node's unref
|
|
* semantics; ref() re-arms. A created-but-never-bound socket holds
|
|
* nothing. Handles abandoned at exit are released by the atexit
|
|
* cleanup, so the RC audit stays clean.
|
|
*
|
|
* dns.lookup. getaddrinfo(AF_INET) runs AT CALL TIME — synchronously,
|
|
* where Node uses its threadpool (SEMANTICS.md documents the blocking
|
|
* divergence) — and the callback defers to the next dispatch pass, so
|
|
* delivery is async like Node's. Failures deliver Node's message shape
|
|
* ("getaddrinfo ENOTFOUND <hostname>") through the per-union adapters;
|
|
* the address argument is "" on failure where Node passes undefined
|
|
* (the string-typed slot cannot hold undefined). A pending lookup
|
|
* holds the loop like an in-flight request. */
|
|
#include "scr_platform.h"
|
|
#include "scr_runtime.h"
|
|
#include "scr_system_errors.h"
|
|
#include "scr_dgram_private.h"
|
|
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#ifdef _WIN32
|
|
#include <winsock2.h>
|
|
#include <ws2tcpip.h> /* getaddrinfo/EAI_*, inet_pton/ntop, socklen_t */
|
|
#include <process.h> /* _getpid */
|
|
#else
|
|
#include <netdb.h>
|
|
#include <net/if.h> /* if_nametoindex */
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <sys/socket.h>
|
|
#endif
|
|
|
|
static void scr_dgram_oom(void) {
|
|
fputs("scriptc: out of memory\n", stderr);
|
|
abort();
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
/* ── the winsock arm (scr_net.c's respelling, restated so this unit links
|
|
* without scr_net.c — the listener-list precedent below) ───────────────
|
|
* The BSD socket calls this file speaks, respelled once so every call
|
|
* site stays byte-identical: SOCKETs narrow through the int fd contract,
|
|
* failures land their WSA code in errno TRANSLATED to the POSIX constant
|
|
* scr_dgram_errname and the retry tests read (libuv performs the same
|
|
* translation for Node, so the spellings agree with the Windows oracle),
|
|
* and close is closesocket. WSAStartup rides scrp_poller_new
|
|
* (scr_loop_wsapoll.c): every socket() site is preceded by
|
|
* scr_dgram_poller_init(), and scr_dns_lookup initializes the poller on
|
|
* this arm before getaddrinfo (which needs winsock started too). The
|
|
* netdb surface (getaddrinfo/freeaddrinfo, the EAI_* codes) is native
|
|
* winsock — ws2tcpip.h spells Node's exact codes, WSAHOST_NOT_FOUND
|
|
* being EAI_NONAME. */
|
|
static int scr_dgram_w32_map_err(int e) {
|
|
switch (e) {
|
|
case WSAEWOULDBLOCK: return EAGAIN;
|
|
case WSAEINTR: return EINTR;
|
|
case WSAECONNREFUSED: return ECONNREFUSED;
|
|
case WSAECONNRESET: return ECONNRESET;
|
|
case WSAEADDRINUSE: return EADDRINUSE;
|
|
case WSAEACCES: return EACCES;
|
|
case WSAEADDRNOTAVAIL: return EADDRNOTAVAIL;
|
|
case WSAEHOSTUNREACH: return EHOSTUNREACH;
|
|
case WSAENETUNREACH: return ENETUNREACH;
|
|
case WSAEMSGSIZE: return EMSGSIZE;
|
|
case WSAEINVAL: return EINVAL;
|
|
case WSAENOPROTOOPT: return ENOPROTOOPT;
|
|
case WSAEAFNOSUPPORT: return EAFNOSUPPORT;
|
|
case WSAENOBUFS: return ENOBUFS;
|
|
default: return e;
|
|
}
|
|
}
|
|
|
|
static void scr_dgram_w32_seterr(void) { errno = scr_dgram_w32_map_err((int)WSAGetLastError()); }
|
|
|
|
static int scr_dgram_w32_socket(int af, int type, int proto) {
|
|
SOCKET s = socket(af, type, proto);
|
|
if (s == INVALID_SOCKET) {
|
|
scr_dgram_w32_seterr();
|
|
return -1;
|
|
}
|
|
return (int)s;
|
|
}
|
|
|
|
static int scr_dgram_w32_bind(int fd, const struct sockaddr *sa, socklen_t len) {
|
|
if (bind((SOCKET)fd, sa, (int)len) != 0) {
|
|
scr_dgram_w32_seterr();
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* UDP connect(2) is local and immediate on winsock too — no EINPROGRESS
|
|
* arm, unlike scr_net.c's stream wrapper. */
|
|
static int scr_dgram_w32_connect(int fd, const struct sockaddr *sa, socklen_t len) {
|
|
if (connect((SOCKET)fd, sa, (int)len) != 0) {
|
|
scr_dgram_w32_seterr();
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int scr_dgram_w32_getsockname(int fd, struct sockaddr *sa, socklen_t *len) {
|
|
int ilen = (int)*len;
|
|
int rc = getsockname((SOCKET)fd, sa, &ilen);
|
|
*len = (socklen_t)ilen;
|
|
if (rc != 0) {
|
|
scr_dgram_w32_seterr();
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int scr_dgram_w32_setsockopt(int fd, int level, int name, const void *val, socklen_t len) {
|
|
if (setsockopt((SOCKET)fd, level, name, (const char *)val, (int)len) != 0) {
|
|
scr_dgram_w32_seterr();
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int scr_dgram_w32_getsockopt(int fd, int level, int name, void *val, socklen_t *len) {
|
|
int ilen = (int)*len;
|
|
int rc = getsockopt((SOCKET)fd, level, name, (char *)val, &ilen);
|
|
*len = (socklen_t)ilen;
|
|
if (rc != 0) {
|
|
scr_dgram_w32_seterr();
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t scr_dgram_w32_sendto(int fd, const void *buf, size_t len, int flags,
|
|
const struct sockaddr *sa, socklen_t salen) {
|
|
int n = sendto((SOCKET)fd, (const char *)buf, len > 0x40000000 ? 0x40000000 : (int)len, flags,
|
|
sa, (int)salen);
|
|
if (n < 0) scr_dgram_w32_seterr();
|
|
return n;
|
|
}
|
|
|
|
static ssize_t scr_dgram_w32_recvfrom(int fd, void *buf, size_t len, int flags,
|
|
struct sockaddr *sa, socklen_t *salen) {
|
|
int ilen = salen != NULL ? (int)*salen : 0;
|
|
int n = recvfrom((SOCKET)fd, (char *)buf, len > 0x40000000 ? 0x40000000 : (int)len, flags, sa,
|
|
salen != NULL ? &ilen : NULL);
|
|
if (salen != NULL) *salen = (socklen_t)ilen;
|
|
if (n < 0) scr_dgram_w32_seterr();
|
|
return n;
|
|
}
|
|
|
|
static int scr_dgram_w32_close(int fd) {
|
|
if (closesocket((SOCKET)fd) != 0) {
|
|
scr_dgram_w32_seterr();
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
#define socket(af, type, proto) scr_dgram_w32_socket((af), (type), (proto))
|
|
#define bind(fd, sa, len) scr_dgram_w32_bind((fd), (sa), (len))
|
|
#define connect(fd, sa, len) scr_dgram_w32_connect((fd), (sa), (len))
|
|
#define getsockname(fd, sa, len) scr_dgram_w32_getsockname((fd), (sa), (len))
|
|
#define setsockopt(fd, l, n, v, len) scr_dgram_w32_setsockopt((fd), (l), (n), (v), (len))
|
|
#define getsockopt(fd, l, n, v, len) scr_dgram_w32_getsockopt((fd), (l), (n), (v), (len))
|
|
#define sendto(fd, buf, len, fl, sa, salen) scr_dgram_w32_sendto((fd), (buf), (len), (fl), (sa), (salen))
|
|
#define recvfrom(fd, buf, len, fl, sa, salen) scr_dgram_w32_recvfrom((fd), (buf), (len), (fl), (sa), (salen))
|
|
#define close(fd) scr_dgram_w32_close(fd)
|
|
#endif /* _WIN32 */
|
|
|
|
/* Nonblocking + no-inherit, the scr_net.c spelling. The win32 arm also
|
|
* turns OFF winsock's UDP connection-reset relay (an ICMP port-
|
|
* unreachable from a PAST send failing a FUTURE recvfrom with
|
|
* WSAECONNRESET) via SIO_UDP_CONNRESET — libuv does exactly this for
|
|
* every Node UDP socket, so leaving it on would diverge from the
|
|
* Windows oracle. */
|
|
static void scr_dgram_nonblock(int fd) {
|
|
#ifdef _WIN32
|
|
u_long one = 1;
|
|
ioctlsocket((SOCKET)fd, FIONBIO, &one);
|
|
SetHandleInformation((HANDLE)(SOCKET)fd, HANDLE_FLAG_INHERIT, 0);
|
|
#ifndef SIO_UDP_CONNRESET
|
|
#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
|
|
#endif
|
|
BOOL off = FALSE;
|
|
DWORD bytes = 0;
|
|
WSAIoctl((SOCKET)fd, SIO_UDP_CONNRESET, &off, sizeof off, NULL, 0, &bytes, NULL, NULL);
|
|
#else
|
|
fcntl(fd, F_SETFL, O_NONBLOCK);
|
|
fcntl(fd, F_SETFD, FD_CLOEXEC);
|
|
#endif
|
|
}
|
|
|
|
/* Node's error-code names for the socket errors this slice can meet. */
|
|
static const char *scr_dgram_errname(int err) {
|
|
switch (err) {
|
|
case EADDRINUSE: return "EADDRINUSE";
|
|
case EACCES: return "EACCES";
|
|
case EADDRNOTAVAIL: return "EADDRNOTAVAIL";
|
|
case ECONNREFUSED: return "ECONNREFUSED";
|
|
case EHOSTUNREACH: return "EHOSTUNREACH";
|
|
case ENETUNREACH: return "ENETUNREACH";
|
|
case EMSGSIZE: return "EMSGSIZE";
|
|
case EINVAL: return "EINVAL";
|
|
case EBADF: return "EBADF";
|
|
case ENOPROTOOPT: return "ENOPROTOOPT";
|
|
case EAFNOSUPPORT: return "EAFNOSUPPORT";
|
|
case ENOBUFS: return "ENOBUFS";
|
|
case ENOMEM: return "ENOMEM";
|
|
case EPERM: return "EPERM";
|
|
case ENODEV: return "ENODEV";
|
|
default: return "EUNKNOWN";
|
|
}
|
|
}
|
|
|
|
/* ── listener lists (the scr_net.c snapshot discipline, restated so this
|
|
* unit links without scr_net.c) ─────────────────────────────────────── */
|
|
|
|
|
|
static void scr_dgram_ls_add(ScrDgramLs *l, ScrClosure *cb, void *fn, bool once) {
|
|
if (l->n == l->cap) {
|
|
l->cap = l->cap ? l->cap * 2 : 2;
|
|
l->ls = realloc(l->ls, l->cap * sizeof *l->ls);
|
|
if (!l->ls) scr_dgram_oom();
|
|
}
|
|
l->ls[l->n].cb = cb;
|
|
l->ls[l->n].fn = fn;
|
|
l->ls[l->n].once = once;
|
|
l->n++;
|
|
}
|
|
|
|
static void scr_dgram_ls_drop(ScrDgramLs *l) {
|
|
for (size_t i = 0; i < l->n; i++) scr_closure_release(l->ls[i].cb);
|
|
free(l->ls);
|
|
l->ls = NULL;
|
|
l->n = l->cap = 0;
|
|
}
|
|
|
|
/* Snapshot for a firing pass: entries retained; `once` entries leave the
|
|
* LIVE list before their callback runs. */
|
|
static size_t scr_dgram_ls_snapshot(ScrDgramLs *l, ScrDgramL **out) {
|
|
size_t n = l->n;
|
|
if (n == 0) {
|
|
*out = NULL;
|
|
return 0;
|
|
}
|
|
ScrDgramL *snap = malloc(n * sizeof *snap);
|
|
if (!snap) scr_dgram_oom();
|
|
for (size_t i = 0; i < n; i++) {
|
|
snap[i] = l->ls[i];
|
|
scr_closure_retain(snap[i].cb);
|
|
}
|
|
size_t w = 0;
|
|
for (size_t i = 0; i < l->n; i++) {
|
|
if (l->ls[i].once) {
|
|
scr_closure_release(l->ls[i].cb);
|
|
} else {
|
|
l->ls[w++] = l->ls[i];
|
|
}
|
|
}
|
|
l->n = w;
|
|
*out = snap;
|
|
return n;
|
|
}
|
|
|
|
static void scr_dgram_fire0(ScrDgramLs *l) {
|
|
ScrDgramL *snap;
|
|
size_t n = scr_dgram_ls_snapshot(l, &snap);
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (!scr_exc_pending()) ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb);
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
free(snap);
|
|
}
|
|
|
|
/* 'error' with NO listener is fatal, like Node's unhandled EventEmitter
|
|
* 'error' (the scr_net.c stance — _Exit skips atexit on purpose). */
|
|
static void scr_dgram_fire_err(ScrDgramLs *l, ScrStr *msg) {
|
|
if (l->n == 0) {
|
|
fflush(stdout);
|
|
fprintf(stderr, "Unhandled 'error' event: Error: %s\n", msg->data);
|
|
_Exit(1);
|
|
}
|
|
ScrDgramL *snap;
|
|
size_t n = scr_dgram_ls_snapshot(l, &snap);
|
|
for (size_t i = 0; i < n; i++) {
|
|
if (!scr_exc_pending()) ((ScrChildErrFn)snap[i].fn)(snap[i].cb, msg);
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
free(snap);
|
|
}
|
|
|
|
/* ── the handle ──────────────────────────────────────────────────────── */
|
|
|
|
|
|
#ifdef SCR_RC_AUDIT
|
|
static SCR_TL long scr_dgram_live = 0;
|
|
long scr_dgram_live_count(void) { return scr_dgram_live; }
|
|
#endif
|
|
|
|
static SCR_TL ScrDgramSocket *scr_dgram_socks = NULL; /* registry: +1 each, tail-appended */
|
|
static SCR_TL ScrPoller *scr_dgram_poller = NULL;
|
|
|
|
/* Pending dns.lookup deliveries (already resolved — getaddrinfo ran at
|
|
* the call; the sweep fires them FIFO). */
|
|
typedef struct ScrDnsPending {
|
|
ScrClosure *cb; /* +1, or NULL for a dyn call */
|
|
ScrDyn *call; /* a dyn [callback, ...args] array (+1), or NULL */
|
|
ScrDnsLookupFn fn;
|
|
ScrStr *errmsg; /* +1 or NULL (success) */
|
|
ScrStr *addr; /* +1 ("" on failure) */
|
|
double family;
|
|
struct ScrDnsPending *next;
|
|
} ScrDnsPending;
|
|
|
|
static SCR_TL ScrDnsPending *scr_dns_pending = NULL;
|
|
|
|
/* ── RC ──────────────────────────────────────────────────────────────── */
|
|
|
|
static void scr_dgram_close_fd_raw(int fd); /* forget-then-close, defined below */
|
|
|
|
ScrDgramSocket *scr_dgram_retain(ScrDgramSocket *s) {
|
|
if (s->rc != SIZE_MAX) s->rc++;
|
|
return s;
|
|
}
|
|
|
|
void scr_dgram_release(ScrDgramSocket *s) {
|
|
if (!s || s->rc == SIZE_MAX) return;
|
|
if (--s->rc == 0) {
|
|
scr_dgram_ls_drop(&s->msg_ls);
|
|
scr_dgram_ls_drop(&s->err_ls);
|
|
scr_dgram_ls_drop(&s->listening_ls);
|
|
scr_dgram_ls_drop(&s->close_ls);
|
|
scr_dgram_ls_drop(&s->conn_ls);
|
|
scr_str_release(s->pending_err);
|
|
if (s->fd >= 0) scr_dgram_close_fd_raw(s->fd);
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_dgram_live--;
|
|
#endif
|
|
free(s);
|
|
}
|
|
}
|
|
|
|
void *scr_dgram_retain_v(void *p) { return scr_dgram_retain((ScrDgramSocket *)p); }
|
|
void scr_dgram_release_v(void *p) { scr_dgram_release((ScrDgramSocket *)p); }
|
|
|
|
/* ── poller plumbing (the scr_platform.h seam; read-only subset) ─────── */
|
|
|
|
static bool scr_dgram_poller_init(void) {
|
|
if (scr_dgram_poller != NULL) return true;
|
|
scr_dgram_poller = scrp_poller_new();
|
|
return scr_dgram_poller != NULL;
|
|
}
|
|
|
|
/* Registration failures ignored exactly as kevent's always were. */
|
|
static void scr_dgram_watch_read(int fd, void *udata, bool on) {
|
|
if (scr_dgram_poller == NULL || fd < 0) return;
|
|
(void)scrp_watch_read(scr_dgram_poller, fd, udata, on);
|
|
}
|
|
|
|
/* Forget-then-close — the epoll obligation (scr_platform.h); the kqueue
|
|
* backend's forget is a no-op, so macOS keeps its historical sequence. */
|
|
static void scr_dgram_close_fd_raw(int fd) {
|
|
if (fd < 0) return;
|
|
if (scr_dgram_poller != NULL) scrp_forget(scr_dgram_poller, fd);
|
|
close(fd);
|
|
}
|
|
|
|
/* Consumer-driven read arming (the net-socket discipline). */
|
|
static void scr_dgram_update_read(ScrDgramSocket *s) {
|
|
bool want = s->fd >= 0 && s->msg_ls.n > 0;
|
|
if (want && !s->read_armed) {
|
|
scr_dgram_watch_read(s->fd, s, true);
|
|
s->read_armed = true;
|
|
} else if (!want && s->read_armed) {
|
|
scr_dgram_watch_read(s->fd, s, false);
|
|
s->read_armed = false;
|
|
}
|
|
}
|
|
|
|
/* ── registry ────────────────────────────────────────────────────────── */
|
|
|
|
static void scr_dgram_register(ScrDgramSocket *s) {
|
|
if (s->in_registry) return;
|
|
s->in_registry = true;
|
|
s->next = NULL;
|
|
ScrDgramSocket **link = &scr_dgram_socks;
|
|
while (*link) link = &(*link)->next;
|
|
*link = scr_dgram_retain(s);
|
|
}
|
|
|
|
static void scr_dgram_unregister(ScrDgramSocket *s) {
|
|
if (!s->in_registry) return;
|
|
ScrDgramSocket **link = &scr_dgram_socks;
|
|
while (*link && *link != s) link = &(*link)->next;
|
|
if (*link) {
|
|
*link = s->next;
|
|
s->next = NULL;
|
|
s->in_registry = false;
|
|
scr_dgram_release(s);
|
|
}
|
|
}
|
|
|
|
/* ── the surface ─────────────────────────────────────────────────────── */
|
|
|
|
ScrDgramSocket *scr_dgram_create(bool reuse_addr) {
|
|
ScrDgramSocket *s = calloc(1, sizeof *s);
|
|
if (!s) scr_dgram_oom();
|
|
s->rc = 1;
|
|
s->fd = -1;
|
|
s->reuse_addr = reuse_addr;
|
|
#ifdef SCR_RC_AUDIT
|
|
scr_dgram_live++;
|
|
#endif
|
|
return s;
|
|
}
|
|
|
|
static void scr_dgram_throw(const char *msg) {
|
|
/* Node's state errors carry their ERR_SOCKET_* codes; the message IS
|
|
* the discriminant (each arm throws exactly one text). */
|
|
const char *code =
|
|
strcmp(msg, "Already connected") == 0 ? "ERR_SOCKET_DGRAM_IS_CONNECTED"
|
|
: strcmp(msg, "Not running") == 0 ? "ERR_SOCKET_DGRAM_NOT_RUNNING"
|
|
: strcmp(msg, "Socket is already bound") == 0 ? "ERR_SOCKET_ALREADY_BOUND"
|
|
: NULL;
|
|
if (code != NULL) {
|
|
scr_throw_error_msg_code(0 /* Error */, msg, strlen(msg), code);
|
|
} else {
|
|
scr_throw_error_msg(0 /* Error */, msg, strlen(msg));
|
|
}
|
|
}
|
|
|
|
/* Resolve a numeric host into a sockaddr_in; "" means any (bind's
|
|
* default), "localhost" pins to 127.0.0.1 (the net stance). False for
|
|
* non-numeric hosts — the caller defers Node's ENOTFOUND. */
|
|
static bool scr_dgram_host4(const char *host, uint16_t port, struct sockaddr_in *out) {
|
|
memset(out, 0, sizeof *out);
|
|
out->sin_family = AF_INET;
|
|
out->sin_port = htons(port);
|
|
if (host[0] == '\0') {
|
|
out->sin_addr.s_addr = INADDR_ANY;
|
|
return true;
|
|
}
|
|
if (strcmp(host, "localhost") == 0) host = "127.0.0.1";
|
|
return inet_pton(AF_INET, host, &out->sin_addr) == 1;
|
|
}
|
|
|
|
/* Create the fd (idempotent). Aborts only on resource exhaustion, the
|
|
* scr_net.c socket() stance. */
|
|
static void scr_dgram_ensure_fd(ScrDgramSocket *s) {
|
|
if (s->fd >= 0) return;
|
|
if (!scr_dgram_poller_init()) {
|
|
fputs("scriptc: event poller init failed\n", stderr);
|
|
abort();
|
|
}
|
|
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
|
if (fd < 0) {
|
|
fputs("scriptc: socket() failed\n", stderr);
|
|
abort();
|
|
}
|
|
scr_dgram_nonblock(fd);
|
|
if (s->reuse_addr) {
|
|
int one = 1;
|
|
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
|
|
}
|
|
s->fd = fd;
|
|
}
|
|
|
|
static void scr_dgram_defer_err(ScrDgramSocket *s, const char *msg) {
|
|
if (!s->pending_err) s->pending_err = scr_str_new(msg, strlen(msg));
|
|
scr_dgram_register(s); /* the sweep delivers it */
|
|
}
|
|
|
|
/* bind(port, host): binds NOW; 'listening' (and the bind callback) defer
|
|
* to the next dispatch pass. State errors throw synchronously, runtime
|
|
* failures defer to 'error' — both Node's split. */
|
|
void scr_dgram_bind(ScrDgramSocket *s, double port, ScrStr *host, ScrClosure *cb) {
|
|
if (s->closing || s->close_emitted) {
|
|
scr_closure_release(cb);
|
|
scr_dgram_throw("Not running");
|
|
return;
|
|
}
|
|
if (s->bound) {
|
|
scr_closure_release(cb);
|
|
scr_dgram_throw("Socket is already bound");
|
|
return;
|
|
}
|
|
if (cb) scr_dgram_ls_add(&s->listening_ls, cb, NULL, true);
|
|
struct sockaddr_in a4;
|
|
if (!scr_dgram_host4(host->data, (uint16_t)(int)port, &a4)) {
|
|
char msg[160];
|
|
snprintf(msg, sizeof msg, "getaddrinfo ENOTFOUND %s", host->data);
|
|
scr_dgram_defer_err(s, msg);
|
|
return;
|
|
}
|
|
scr_dgram_ensure_fd(s);
|
|
if (bind(s->fd, (struct sockaddr *)&a4, sizeof a4) != 0) {
|
|
int err = errno;
|
|
char ip[64];
|
|
inet_ntop(AF_INET, &a4.sin_addr, ip, sizeof ip);
|
|
char msg[128];
|
|
/* Node: "bind EADDRINUSE 0.0.0.0:4000" */
|
|
snprintf(msg, sizeof msg, "bind %s %s:%d", scr_dgram_errname(err), ip, (int)port);
|
|
close(s->fd); /* never watched: bind precedes any read arm */
|
|
s->fd = -1;
|
|
scr_dgram_defer_err(s, msg);
|
|
return;
|
|
}
|
|
s->bound = true;
|
|
s->emit_listening = true;
|
|
scr_dgram_register(s);
|
|
scr_dgram_update_read(s);
|
|
}
|
|
|
|
/* connect(port, host): implicit-binds (Node emits 'listening' for that,
|
|
* then 'connect'); UDP connect(2) is local and immediate, so failures
|
|
* here are deferred 'error's only for bad hosts. */
|
|
void scr_dgram_connect(ScrDgramSocket *s, double port, ScrStr *host, ScrClosure *cb) {
|
|
if (s->closing || s->close_emitted) {
|
|
scr_closure_release(cb);
|
|
scr_dgram_throw("Not running");
|
|
return;
|
|
}
|
|
if (s->connected) {
|
|
scr_closure_release(cb);
|
|
scr_dgram_throw("Already connected");
|
|
return;
|
|
}
|
|
if (cb) scr_dgram_ls_add(&s->conn_ls, cb, NULL, true);
|
|
struct sockaddr_in a4;
|
|
if (!scr_dgram_host4(host->data, (uint16_t)(int)port, &a4) || host->len == 0) {
|
|
char msg[160];
|
|
snprintf(msg, sizeof msg, "getaddrinfo ENOTFOUND %s", host->data);
|
|
scr_dgram_defer_err(s, msg);
|
|
return;
|
|
}
|
|
scr_dgram_ensure_fd(s);
|
|
if (!s->bound) {
|
|
s->bound = true;
|
|
s->emit_listening = true; /* Node's implicit bind emits 'listening' */
|
|
}
|
|
if (connect(s->fd, (struct sockaddr *)&a4, sizeof a4) != 0) {
|
|
char msg[128];
|
|
char ip[64];
|
|
inet_ntop(AF_INET, &a4.sin_addr, ip, sizeof ip);
|
|
snprintf(msg, sizeof msg, "connect %s %s:%d", scr_dgram_errname(errno), ip, (int)port);
|
|
scr_dgram_defer_err(s, msg);
|
|
return;
|
|
}
|
|
s->connected = true;
|
|
s->emit_connect = true;
|
|
scr_dgram_register(s);
|
|
scr_dgram_update_read(s);
|
|
}
|
|
|
|
static void scr_dgram_send_raw(ScrDgramSocket *s, const char *data, size_t len, double port,
|
|
ScrStr *host) {
|
|
if (s->closing || s->close_emitted) {
|
|
scr_dgram_throw("Not running");
|
|
return;
|
|
}
|
|
if (s->connected) {
|
|
scr_dgram_throw("Already connected");
|
|
return;
|
|
}
|
|
struct sockaddr_in a4;
|
|
if (!scr_dgram_host4(host->data, (uint16_t)(int)port, &a4) || host->len == 0) {
|
|
char msg[160];
|
|
snprintf(msg, sizeof msg, "getaddrinfo ENOTFOUND %s", host->data);
|
|
scr_dgram_defer_err(s, msg);
|
|
return;
|
|
}
|
|
scr_dgram_ensure_fd(s);
|
|
if (!s->bound) {
|
|
/* Implicit ephemeral bind — Node's send-before-bind, 'listening'
|
|
* emits for it there too. */
|
|
s->bound = true;
|
|
s->emit_listening = true;
|
|
}
|
|
scr_dgram_register(s); /* an open sender holds the loop, like Node */
|
|
if (sendto(s->fd, data, len, 0, (struct sockaddr *)&a4, sizeof a4) < 0 &&
|
|
errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
|
|
char ip[64];
|
|
inet_ntop(AF_INET, &a4.sin_addr, ip, sizeof ip);
|
|
char msg[128];
|
|
snprintf(msg, sizeof msg, "send %s %s:%d", scr_dgram_errname(errno), ip, (int)port);
|
|
scr_dgram_defer_err(s, msg);
|
|
}
|
|
scr_dgram_update_read(s);
|
|
}
|
|
|
|
void scr_dgram_send_str(ScrDgramSocket *s, ScrStr *data, double port, ScrStr *host) {
|
|
char *owned;
|
|
const char *bytes = scr_str_utf8(data, &owned);
|
|
scr_dgram_send_raw(s, bytes, data->len, port, host);
|
|
scr_str_utf8_end(owned);
|
|
}
|
|
|
|
void scr_dgram_send_bytes(ScrDgramSocket *s, ScrBytes *data, double port, ScrStr *host) {
|
|
SCR_BYTES_SNAPSHOT(data);
|
|
scr_dgram_send_raw(s, (const char *)data->data, data->len, port, host);
|
|
}
|
|
|
|
/* address() parts. The ip read carries the "Not running" throw; family
|
|
* and port only run after it succeeded. */
|
|
ScrStr *scr_dgram_addr_ip(ScrDgramSocket *s) {
|
|
if (s->fd < 0 || !s->bound) {
|
|
scr_dgram_throw("Not running");
|
|
return NULL;
|
|
}
|
|
struct sockaddr_storage sa;
|
|
socklen_t salen = sizeof sa;
|
|
char ip[64] = "0.0.0.0";
|
|
if (getsockname(s->fd, (struct sockaddr *)&sa, &salen) == 0 && sa.ss_family == AF_INET) {
|
|
inet_ntop(AF_INET, &((struct sockaddr_in *)&sa)->sin_addr, ip, sizeof ip);
|
|
}
|
|
return scr_str_new(ip, strlen(ip));
|
|
}
|
|
|
|
ScrStr *scr_dgram_addr_family(ScrDgramSocket *s) {
|
|
(void)s; /* udp4 is the one lowered type */
|
|
return scr_str_new("IPv4", 4);
|
|
}
|
|
|
|
double scr_dgram_addr_port(ScrDgramSocket *s) {
|
|
struct sockaddr_storage sa;
|
|
socklen_t salen = sizeof sa;
|
|
if (s->fd >= 0 && getsockname(s->fd, (struct sockaddr *)&sa, &salen) == 0 &&
|
|
sa.ss_family == AF_INET) {
|
|
return (double)ntohs(((struct sockaddr_in *)&sa)->sin_port);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* close([cb]): the fd closes NOW; 'close' (and the callback, registered
|
|
* as once('close')) fires at the next sweep. Node throws "Not running"
|
|
* on a second close. */
|
|
void scr_dgram_close(ScrDgramSocket *s, ScrClosure *cb) {
|
|
if (s->closing || s->close_emitted) {
|
|
scr_closure_release(cb);
|
|
scr_dgram_throw("Not running");
|
|
return;
|
|
}
|
|
if (cb) scr_dgram_ls_add(&s->close_ls, cb, NULL, true);
|
|
if (s->fd >= 0) {
|
|
scr_dgram_close_fd_raw(s->fd); /* registrations dropped, then the fd */
|
|
s->fd = -1;
|
|
s->read_armed = false;
|
|
}
|
|
s->closing = true;
|
|
/* Move to the registry TAIL so 'close' emits in close() CALL order
|
|
* (Node's per-close scheduled emit), not socket-creation order. The
|
|
* caller's borrowed reference keeps the handle alive across the
|
|
* unregister/register pair. */
|
|
scr_dgram_unregister(s);
|
|
scr_dgram_register(s); /* the sweep fires 'close' and settles */
|
|
}
|
|
|
|
void scr_dgram_unref(ScrDgramSocket *s) { s->unrefed = true; }
|
|
void scr_dgram_ref(ScrDgramSocket *s) { s->unrefed = false; }
|
|
|
|
/* ── socket options (dgram.Socket's setters and getters) ───────────────
|
|
* Each follows Node's dgram.js plus the libuv call under it: the value
|
|
* checks run first, then the option goes to the descriptor, so a socket
|
|
* that has no descriptor yet (never bound) answers EBADF. After close()
|
|
* Node's handle is null, so most members throw the TypeError reading a
|
|
* property of null; the ones that call healthCheck() throw "Not running". */
|
|
|
|
typedef struct { int value; const char *name; const char *message; } ScrDgramUvError;
|
|
#define SCR_DGRAM_UV_ERROR(name, message) { SCR_NODE_UV__##name, #name, message },
|
|
static const ScrDgramUvError scr_dgram_uv_errors[] = { SCR_NODE_UV_ERRNO_MAP(SCR_DGRAM_UV_ERROR) };
|
|
#undef SCR_DGRAM_UV_ERROR
|
|
|
|
/* The libuv name, number and message for an errno value (by name, so
|
|
* the win32 arm's translated errno values find the same entries). */
|
|
static const ScrDgramUvError *scr_dgram_uv_error(int err) {
|
|
static const ScrDgramUvError unknown = { SCR_NODE_UV__UNKNOWN, "UNKNOWN", "unknown error" };
|
|
const char *name = scr_dgram_errname(err);
|
|
for (size_t i = 0; i < sizeof scr_dgram_uv_errors / sizeof scr_dgram_uv_errors[0]; i++) {
|
|
if (strcmp(scr_dgram_uv_errors[i].name, name) == 0) return &scr_dgram_uv_errors[i];
|
|
}
|
|
return &unknown;
|
|
}
|
|
|
|
/* Node's ErrnoException: "<syscall> <CODE>" with code, errno and syscall. */
|
|
static void scr_dgram_throw_errno(int err, const char *syscall) {
|
|
const ScrDgramUvError *uv = scr_dgram_uv_error(err);
|
|
char text[96];
|
|
int n = snprintf(text, sizeof text, "%s %s", syscall, uv->name);
|
|
ScrStr *msg = scr_str_new(text, n < 0 ? 0 : (size_t)n);
|
|
ScrError *e = scr_error_new(SCR_ERR_ERROR, msg);
|
|
scr_str_release(msg);
|
|
scr_error_set_code(e, uv->name);
|
|
e->system_errno = uv->value;
|
|
e->system_call = scr_str_new(syscall, strlen(syscall));
|
|
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
|
}
|
|
|
|
/* Node's ERR_SOCKET_BUFFER_SIZE SystemError for a failed libuv buffer
|
|
* size call. */
|
|
static void scr_dgram_throw_buffer_size(int err, const char *syscall) {
|
|
const ScrDgramUvError *uv = scr_dgram_uv_error(err);
|
|
char text[192];
|
|
int n = snprintf(text, sizeof text, "Could not get or set buffer size: %s returned %s (%s)", syscall,
|
|
uv->name, uv->message);
|
|
ScrStr *msg = scr_str_new(text, n < 0 ? 0 : (size_t)n);
|
|
ScrError *e = scr_error_new(SCR_ERR_ERROR, msg);
|
|
scr_str_release(msg);
|
|
scr_str_release(e->name);
|
|
e->name = scr_str_new("SystemError", 11);
|
|
scr_error_set_code(e, "ERR_SOCKET_BUFFER_SIZE");
|
|
e->system_errno = uv->value;
|
|
e->system_call = scr_str_new(syscall, strlen(syscall));
|
|
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
|
}
|
|
|
|
/* After close() Node's handle is null: the member read throws. */
|
|
static bool scr_dgram_handle_gone(const ScrDgramSocket *s, const char *member) {
|
|
if (!s->closing && !s->close_emitted) return false;
|
|
char text[96];
|
|
int n = snprintf(text, sizeof text, "Cannot read properties of null (reading '%s')", member);
|
|
scr_throw_error_msg(SCR_ERR_TYPE, text, n < 0 ? 0 : (size_t)n);
|
|
return true;
|
|
}
|
|
|
|
/* healthCheck(): "Not running" once close() was called. */
|
|
static bool scr_dgram_not_running(const ScrDgramSocket *s) {
|
|
if (!s->closing && !s->close_emitted) return false;
|
|
scr_dgram_throw("Not running");
|
|
return true;
|
|
}
|
|
|
|
/* V8's ToInt32, the conversion Node's binding applies to the value. */
|
|
static int scr_dgram_to_int32(double v) {
|
|
if (!isfinite(v)) return 0;
|
|
double t = fmod(trunc(v), 4294967296.0);
|
|
if (t < 0) t += 4294967296.0;
|
|
return (int)(int32_t)(uint32_t)t;
|
|
}
|
|
|
|
/* An int option on the descriptor; false (errno set) on failure. */
|
|
static bool scr_dgram_setopt_int(const ScrDgramSocket *s, int level, int name, int value) {
|
|
if (s->fd < 0) {
|
|
errno = EBADF;
|
|
return false;
|
|
}
|
|
return setsockopt(s->fd, level, name, (const void *)&value, sizeof value) == 0;
|
|
}
|
|
|
|
void scr_dgram_set_broadcast(ScrDgramSocket *s, bool flag) {
|
|
if (scr_dgram_handle_gone(s, "setBroadcast")) return;
|
|
if (!scr_dgram_setopt_int(s, SOL_SOCKET, SO_BROADCAST, flag ? 1 : 0))
|
|
scr_dgram_throw_errno(errno, "setBroadcast");
|
|
}
|
|
|
|
double scr_dgram_set_ttl(ScrDgramSocket *s, double ttl) {
|
|
if (scr_dgram_handle_gone(s, "setTTL")) return ttl;
|
|
int value = scr_dgram_to_int32(ttl);
|
|
if (value < 1 || value > 255) {
|
|
scr_dgram_throw_errno(EINVAL, "setTTL");
|
|
} else if (!scr_dgram_setopt_int(s, IPPROTO_IP, IP_TTL, value)) {
|
|
scr_dgram_throw_errno(errno, "setTTL");
|
|
}
|
|
return ttl;
|
|
}
|
|
|
|
double scr_dgram_set_multicast_ttl(ScrDgramSocket *s, double ttl) {
|
|
if (scr_dgram_handle_gone(s, "setMulticastTTL")) return ttl;
|
|
int value = scr_dgram_to_int32(ttl);
|
|
if (value < 0 || value > 255) {
|
|
scr_dgram_throw_errno(EINVAL, "setMulticastTTL");
|
|
} else if (!scr_dgram_setopt_int(s, IPPROTO_IP, IP_MULTICAST_TTL, value)) {
|
|
scr_dgram_throw_errno(errno, "setMulticastTTL");
|
|
}
|
|
return ttl;
|
|
}
|
|
|
|
bool scr_dgram_set_multicast_loopback(ScrDgramSocket *s, bool flag) {
|
|
if (scr_dgram_handle_gone(s, "setMulticastLoopback")) return flag;
|
|
if (!scr_dgram_setopt_int(s, IPPROTO_IP, IP_MULTICAST_LOOP, flag ? 1 : 0))
|
|
scr_dgram_throw_errno(errno, "setMulticastLoopback");
|
|
return flag;
|
|
}
|
|
|
|
/* A NUL-terminated copy of a borrowed string, NULL when it is too long
|
|
* to be an address (the caller answers EINVAL). */
|
|
static const char *scr_dgram_cstr(const ScrStr *v, char *buf, size_t cap) {
|
|
if (v->len >= cap || memchr(v->data, '\0', v->len) != NULL) return NULL;
|
|
memcpy(buf, v->data, v->len);
|
|
buf[v->len] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
/* uv_ip6_addr: an IPv6 literal with an optional %scope (an interface
|
|
* name or number). */
|
|
static bool scr_dgram_ip6(const char *text, struct in6_addr *addr, unsigned *scope) {
|
|
char host[64];
|
|
const char *pct = strchr(text, '%');
|
|
size_t len = pct != NULL ? (size_t)(pct - text) : strlen(text);
|
|
if (len >= sizeof host) return false;
|
|
memcpy(host, text, len);
|
|
host[len] = '\0';
|
|
if (inet_pton(AF_INET6, host, addr) != 1) return false;
|
|
*scope = 0;
|
|
if (pct != NULL) {
|
|
#ifndef _WIN32
|
|
*scope = if_nametoindex(pct + 1);
|
|
#endif
|
|
if (*scope == 0) *scope = (unsigned)atoi(pct + 1);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void scr_dgram_set_multicast_interface(ScrDgramSocket *s, ScrStr *iface /*borrowed*/) {
|
|
if (scr_dgram_not_running(s)) return;
|
|
char buf[96];
|
|
const char *text = scr_dgram_cstr(iface, buf, sizeof buf);
|
|
struct in_addr a4;
|
|
struct in6_addr a6;
|
|
unsigned scope = 0;
|
|
bool ok;
|
|
if (text != NULL && inet_pton(AF_INET, text, &a4) == 1) {
|
|
ok = s->fd >= 0 && setsockopt(s->fd, IPPROTO_IP, IP_MULTICAST_IF, (const void *)&a4, sizeof a4) == 0;
|
|
} else if (text != NULL && scr_dgram_ip6(text, &a6, &scope)) {
|
|
ok = s->fd >= 0 &&
|
|
setsockopt(s->fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, (const void *)&scope, sizeof scope) == 0;
|
|
} else {
|
|
scr_dgram_throw_errno(EINVAL, "setMulticastInterface");
|
|
return;
|
|
}
|
|
if (s->fd < 0) errno = EBADF;
|
|
if (!ok) scr_dgram_throw_errno(errno, "setMulticastInterface");
|
|
}
|
|
|
|
/* addMembership / dropMembership. Node's libuv binds a socket that has
|
|
* no descriptor yet to an ephemeral port first; that implicit bind has
|
|
* no lowering. iface is NULL when the argument is omitted. */
|
|
static void scr_dgram_membership(ScrDgramSocket *s, ScrStr *group, ScrStr *iface, bool join) {
|
|
const char *syscall = join ? "addMembership" : "dropMembership";
|
|
if (scr_dgram_not_running(s)) return;
|
|
if (group->len == 0) {
|
|
static const char msg[] = "The \"multicastAddress\" argument must be specified";
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_MISSING_ARGS");
|
|
return;
|
|
}
|
|
char gbuf[96], ibuf[96];
|
|
const char *gtext = scr_dgram_cstr(group, gbuf, sizeof gbuf);
|
|
const char *itext = iface != NULL ? scr_dgram_cstr(iface, ibuf, sizeof ibuf) : NULL;
|
|
struct in_addr g4;
|
|
struct in6_addr g6;
|
|
unsigned gscope = 0;
|
|
bool v4 = gtext != NULL && inet_pton(AF_INET, gtext, &g4) == 1;
|
|
if (!v4 && (gtext == NULL || !scr_dgram_ip6(gtext, &g6, &gscope))) {
|
|
scr_dgram_throw_errno(EINVAL, syscall);
|
|
return;
|
|
}
|
|
if (s->fd < 0) {
|
|
char text[96];
|
|
int n = snprintf(text, sizeof text, "dgram.Socket.%s before bind() is not supported yet", syscall);
|
|
scr_throw_error_msg_code(SCR_ERR_ERROR, text, n < 0 ? 0 : (size_t)n, "SC2020");
|
|
return;
|
|
}
|
|
int rc;
|
|
if (v4) {
|
|
struct ip_mreq mreq;
|
|
memset(&mreq, 0, sizeof mreq);
|
|
mreq.imr_multiaddr = g4;
|
|
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
|
|
if (iface != NULL && (itext == NULL || inet_pton(AF_INET, itext, &mreq.imr_interface) != 1)) {
|
|
scr_dgram_throw_errno(EINVAL, syscall);
|
|
return;
|
|
}
|
|
rc = setsockopt(s->fd, IPPROTO_IP, join ? IP_ADD_MEMBERSHIP : IP_DROP_MEMBERSHIP,
|
|
(const void *)&mreq, sizeof mreq);
|
|
} else {
|
|
struct ipv6_mreq mreq;
|
|
memset(&mreq, 0, sizeof mreq);
|
|
mreq.ipv6mr_multiaddr = g6;
|
|
if (iface != NULL) {
|
|
struct in6_addr i6;
|
|
unsigned iscope = 0;
|
|
if (itext == NULL || !scr_dgram_ip6(itext, &i6, &iscope)) {
|
|
scr_dgram_throw_errno(EINVAL, syscall);
|
|
return;
|
|
}
|
|
mreq.ipv6mr_interface = iscope;
|
|
}
|
|
rc = setsockopt(s->fd, IPPROTO_IPV6, join ? IPV6_JOIN_GROUP : IPV6_LEAVE_GROUP,
|
|
(const void *)&mreq, sizeof mreq);
|
|
}
|
|
if (rc != 0) scr_dgram_throw_errno(errno, syscall);
|
|
}
|
|
|
|
void scr_dgram_add_membership(ScrDgramSocket *s, ScrStr *group, ScrStr *iface, bool has_iface) {
|
|
scr_dgram_membership(s, group, has_iface ? iface : NULL, true);
|
|
}
|
|
|
|
void scr_dgram_drop_membership(ScrDgramSocket *s, ScrStr *group, ScrStr *iface, bool has_iface) {
|
|
scr_dgram_membership(s, group, has_iface ? iface : NULL, false);
|
|
}
|
|
|
|
/* Node's bufferSize(): the size must be a uint32 (TypeError otherwise,
|
|
* checked before the handle), 0 reads the current size, and a size past
|
|
* the int32 range fails as EINVAL. */
|
|
static double scr_dgram_buffer_size(ScrDgramSocket *s, double size, bool recv) {
|
|
if (!(size >= 0 && size <= 4294967295.0 && trunc(size) == size)) {
|
|
static const char msg[] = "Buffer size must be a positive integer";
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg, sizeof msg - 1, "ERR_SOCKET_BAD_BUFFER_SIZE");
|
|
return 0;
|
|
}
|
|
if (scr_dgram_handle_gone(s, "bufferSize")) return 0;
|
|
const char *syscall = recv ? "uv_recv_buffer_size" : "uv_send_buffer_size";
|
|
if (size > 2147483647.0) {
|
|
scr_dgram_throw_buffer_size(EINVAL, syscall);
|
|
return 0;
|
|
}
|
|
int value = (int)size;
|
|
int name = recv ? SO_RCVBUF : SO_SNDBUF;
|
|
bool ok;
|
|
if (s->fd < 0) {
|
|
errno = EBADF;
|
|
ok = false;
|
|
} else if (value == 0) {
|
|
socklen_t len = sizeof value;
|
|
ok = getsockopt(s->fd, SOL_SOCKET, name, (void *)&value, &len) == 0;
|
|
} else {
|
|
ok = setsockopt(s->fd, SOL_SOCKET, name, (const void *)&value, sizeof value) == 0;
|
|
}
|
|
if (!ok) {
|
|
scr_dgram_throw_buffer_size(errno, syscall);
|
|
return 0;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
double scr_dgram_get_recv_buffer_size(ScrDgramSocket *s) { return scr_dgram_buffer_size(s, 0, true); }
|
|
double scr_dgram_get_send_buffer_size(ScrDgramSocket *s) { return scr_dgram_buffer_size(s, 0, false); }
|
|
void scr_dgram_set_recv_buffer_size(ScrDgramSocket *s, double size) { (void)scr_dgram_buffer_size(s, size, true); }
|
|
void scr_dgram_set_send_buffer_size(ScrDgramSocket *s, double size) { (void)scr_dgram_buffer_size(s, size, false); }
|
|
|
|
/* Sends go out synchronously, so nothing waits in a send queue. */
|
|
double scr_dgram_get_send_queue_size(ScrDgramSocket *s) {
|
|
(void)scr_dgram_handle_gone(s, "getSendQueueSize");
|
|
return 0;
|
|
}
|
|
|
|
double scr_dgram_get_send_queue_count(ScrDgramSocket *s) {
|
|
(void)scr_dgram_handle_gone(s, "getSendQueueCount");
|
|
return 0;
|
|
}
|
|
|
|
/* disconnect(): only a socket whose connect completed ('connect' was
|
|
* emitted) is connected, and that check comes before the handle's. An
|
|
* AF_UNSPEC connect dissolves the association; EAFNOSUPPORT (macOS) and
|
|
* EINVAL count as success, as in libuv. */
|
|
void scr_dgram_disconnect(ScrDgramSocket *s) {
|
|
if (!s->connected || s->emit_connect) {
|
|
static const char msg[] = "Not connected";
|
|
scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "ERR_SOCKET_DGRAM_NOT_CONNECTED");
|
|
return;
|
|
}
|
|
if (scr_dgram_handle_gone(s, "disconnect")) return;
|
|
struct sockaddr_storage addr;
|
|
memset(&addr, 0, sizeof addr);
|
|
addr.ss_family = AF_UNSPEC;
|
|
if (connect(s->fd, (const struct sockaddr *)&addr, sizeof addr) != 0 && errno != EAFNOSUPPORT &&
|
|
errno != EINVAL) {
|
|
scr_dgram_throw_errno(errno, "connect");
|
|
return;
|
|
}
|
|
s->connected = false;
|
|
}
|
|
|
|
void scr_dgram_on_message(ScrDgramSocket *s, ScrClosure *cb, ScrDgramMsgFn fn, bool once) {
|
|
if (s->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_dgram_ls_add(&s->msg_ls, cb, (void *)fn, once);
|
|
scr_dgram_update_read(s);
|
|
}
|
|
|
|
void scr_dgram_on_error(ScrDgramSocket *s, ScrClosure *cb, ScrChildErrFn fn, bool once) {
|
|
if (s->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_dgram_ls_add(&s->err_ls, cb, (void *)fn, once);
|
|
}
|
|
|
|
void scr_dgram_on_listening(ScrDgramSocket *s, ScrClosure *cb, bool once) {
|
|
if (s->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_dgram_ls_add(&s->listening_ls, cb, NULL, once);
|
|
}
|
|
|
|
void scr_dgram_on_close(ScrDgramSocket *s, ScrClosure *cb, bool once) {
|
|
if (s->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_dgram_ls_add(&s->close_ls, cb, NULL, once);
|
|
}
|
|
|
|
void scr_dgram_on_connect(ScrDgramSocket *s, ScrClosure *cb, bool once) {
|
|
if (s->close_emitted) {
|
|
scr_closure_release(cb);
|
|
return;
|
|
}
|
|
scr_dgram_ls_add(&s->conn_ls, cb, NULL, once);
|
|
}
|
|
|
|
/* The runtime-provided message adapters (msg/rinfo parts borrowed; the
|
|
* listener owns its +1 params per the universal convention). */
|
|
void scr_dgram_msg_thunk0(ScrClosure *cb, ScrBytes *msg, ScrStr *addr, ScrStr *family,
|
|
double port, double size) {
|
|
(void)msg; (void)addr; (void)family; (void)port; (void)size;
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
}
|
|
|
|
void scr_dgram_msg_thunk1(ScrClosure *cb, ScrBytes *msg, ScrStr *addr, ScrStr *family,
|
|
double port, double size) {
|
|
(void)addr; (void)family; (void)port; (void)size;
|
|
((void (*)(ScrClosure *, ScrBytes *))cb->fn)(cb, scr_bytes_retain(msg));
|
|
}
|
|
|
|
/* One readable wake: recvfrom until EAGAIN, one 'message' emit per
|
|
* datagram (arrival-driven, like net's read chunks). */
|
|
static void scr_dgram_read(ScrDgramSocket *s) {
|
|
while (s->fd >= 0 && s->msg_ls.n > 0) {
|
|
char buf[65536];
|
|
struct sockaddr_storage from;
|
|
socklen_t fromlen = sizeof from;
|
|
ssize_t n = recvfrom(s->fd, buf, sizeof buf, 0, (struct sockaddr *)&from, &fromlen);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
return; /* EAGAIN or a transient failure: the next wake retries */
|
|
}
|
|
char ip[64] = "0.0.0.0";
|
|
int rport = 0;
|
|
if (from.ss_family == AF_INET) {
|
|
inet_ntop(AF_INET, &((struct sockaddr_in *)&from)->sin_addr, ip, sizeof ip);
|
|
rport = ntohs(((struct sockaddr_in *)&from)->sin_port);
|
|
}
|
|
ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
|
|
chunk->is_buffer = true; /* the message is a Buffer */
|
|
memcpy(chunk->data, buf, (size_t)n);
|
|
ScrStr *addr = scr_str_new(ip, strlen(ip));
|
|
ScrStr *family = scr_str_new("IPv4", 4);
|
|
ScrDgramL *snap;
|
|
size_t nl = scr_dgram_ls_snapshot(&s->msg_ls, &snap);
|
|
for (size_t i = 0; i < nl; i++) {
|
|
if (!scr_exc_pending()) {
|
|
((ScrDgramMsgFn)snap[i].fn)(snap[i].cb, chunk, addr, family, (double)rport, (double)n);
|
|
}
|
|
scr_closure_release(snap[i].cb);
|
|
}
|
|
free(snap);
|
|
scr_bytes_release(chunk);
|
|
scr_str_release(addr);
|
|
scr_str_release(family);
|
|
if (scr_exc_pending()) return;
|
|
scr_dgram_update_read(s); /* a once-listener may have been the last consumer */
|
|
}
|
|
}
|
|
|
|
/* ── dns.lookup ──────────────────────────────────────────────────────── */
|
|
|
|
void scr_dns_lookup(ScrStr *hostname, double family, ScrClosure *cb, ScrDnsLookupFn fn) {
|
|
(void)family; /* the frontend pinned 4; AF_INET below IS that pin */
|
|
if (memchr(hostname->data, '\0', hostname->len) != NULL) {
|
|
/* validateStringWithoutNullBytes: a synchronous ERR_INVALID_ARG_VALUE
|
|
* TypeError before any lookup runs; the callback never fires. */
|
|
scr_closure_release(cb);
|
|
ScrStr *shown = scr_str_inspect_quoted(hostname, true);
|
|
ScrJsonBuf msg;
|
|
scr_jb_init(&msg);
|
|
scr_jb_puts(&msg, "The argument 'hostname' must be a string without null bytes. Received ");
|
|
scr_jb_put_str(&msg, shown);
|
|
scr_str_release(shown);
|
|
ScrStr *text = scr_jb_finish(&msg);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, text->data, text->len, "ERR_INVALID_ARG_VALUE");
|
|
scr_str_release(text);
|
|
return;
|
|
}
|
|
#ifdef _WIN32
|
|
/* getaddrinfo needs winsock started (WSANOTINITIALISED otherwise); the
|
|
* poller owns WSAStartup on this arm, so a lookup-only program rides
|
|
* the same init the socket paths do. */
|
|
(void)scr_dgram_poller_init();
|
|
#endif
|
|
ScrDnsPending *p = calloc(1, sizeof *p);
|
|
if (!p) scr_dgram_oom();
|
|
p->cb = cb; /* moves */
|
|
p->fn = fn;
|
|
p->family = 4;
|
|
struct addrinfo hints;
|
|
memset(&hints, 0, sizeof hints);
|
|
hints.ai_family = AF_INET;
|
|
hints.ai_socktype = SOCK_DGRAM;
|
|
struct addrinfo *res = NULL;
|
|
int rc = getaddrinfo(hostname->data, NULL, &hints, &res);
|
|
if (rc == 0 && res) {
|
|
char ip[64] = "0.0.0.0";
|
|
inet_ntop(AF_INET, &((struct sockaddr_in *)res->ai_addr)->sin_addr, ip, sizeof ip);
|
|
p->addr = scr_str_new(ip, strlen(ip));
|
|
} else {
|
|
/* Node maps name-not-found (and macOS's EAI_NODATA) to ENOTFOUND;
|
|
* everything else surfaces its EAI_* name. Message shape:
|
|
* "getaddrinfo ENOTFOUND <hostname>". */
|
|
const char *code =
|
|
(rc == EAI_NONAME
|
|
#ifdef EAI_NODATA
|
|
|| rc == EAI_NODATA
|
|
#endif
|
|
)
|
|
? "ENOTFOUND"
|
|
: rc == EAI_AGAIN ? "EAI_AGAIN"
|
|
: "EAI_FAIL";
|
|
char msg[192];
|
|
snprintf(msg, sizeof msg, "getaddrinfo %s %s", code, hostname->data);
|
|
p->errmsg = scr_str_new(msg, strlen(msg));
|
|
p->addr = scr_str_new("", 0);
|
|
}
|
|
if (res) freeaddrinfo(res);
|
|
/* FIFO append: deliveries fire in call order at the next sweep. */
|
|
ScrDnsPending **link = &scr_dns_pending;
|
|
while (*link) link = &(*link)->next;
|
|
*link = p;
|
|
}
|
|
|
|
/* ── dns with Node's argument forms (dyn callbacks) ──────────────────── */
|
|
|
|
/* Calls a dyn [callback, ...args] array. */
|
|
static void scr_dns_dyn_call(const ScrDyn *call) {
|
|
size_t argc = call->v.arr.len - 1;
|
|
scr_dyn_release(scr_dyn_call(call->v.arr.items[0], call->v.arr.items + 1, argc, "callback"));
|
|
}
|
|
|
|
static void scr_dns_tick_fire(ScrClosure *self) {
|
|
ScrDyn *call = (ScrDyn *)scr_box_get_ref(self->caps[0]);
|
|
scr_dns_dyn_call(call);
|
|
scr_dyn_release(call);
|
|
}
|
|
|
|
/* Queues callback(args...) (the args move): on the next tick, as Node's
|
|
* process.nextTick answers for literal addresses, or with the lookup
|
|
* deliveries. */
|
|
static void scr_dns_call_later(const ScrDyn *callback, ScrDyn **args, size_t argc, bool tick) {
|
|
ScrDyn *call = scr_dyn_new_arr();
|
|
scr_dyn_arr_push(call, scr_dyn_retain((ScrDyn *)callback));
|
|
for (size_t i = 0; i < argc; i++) scr_dyn_arr_push(call, args[i]);
|
|
if (tick) {
|
|
ScrClosure *clo = scr_closure_new((void *)&scr_dns_tick_fire, 1);
|
|
ScrBox *box = scr_box_new_obj(&scr_dyn_retain_v, &scr_dyn_release_v, NULL);
|
|
scr_box_set_ref(box, call);
|
|
clo->caps[0] = box;
|
|
scr_next_tick(clo);
|
|
return;
|
|
}
|
|
ScrDnsPending *p = calloc(1, sizeof *p);
|
|
if (!p) scr_dgram_oom();
|
|
p->call = call;
|
|
ScrDnsPending **link = &scr_dns_pending;
|
|
while (*link) link = &(*link)->next;
|
|
*link = p;
|
|
}
|
|
|
|
/* The default result order (dns.setDefaultResultOrder): 0 verbatim,
|
|
* 1 ipv4first, 2 ipv6first. */
|
|
static SCR_TL int scr_dns_default_order = 0;
|
|
static const char *const scr_dns_orders[] = { "verbatim", "ipv4first", "ipv6first" };
|
|
|
|
static int scr_dns_order_of(const ScrDyn *v) {
|
|
if (v->kind != SCR_DYN_STR) return -1;
|
|
for (int i = 0; i < 3; i++) {
|
|
size_t n = strlen(scr_dns_orders[i]);
|
|
if (v->v.str->len == n && memcmp(v->v.str->data, scr_dns_orders[i], n) == 0) return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
ScrStr *scr_dns_get_default_result_order(void) {
|
|
const char *o = scr_dns_orders[scr_dns_default_order];
|
|
return scr_str_new(o, strlen(o));
|
|
}
|
|
|
|
void scr_dns_set_default_result_order(const ScrDyn *order) {
|
|
int o = scr_dns_order_of(order);
|
|
if (o < 0) {
|
|
scr_dyn_arg_value_fail("dnsOrder", "must be one of: 'verbatim', 'ipv4first', 'ipv6first'", order);
|
|
return;
|
|
}
|
|
scr_dns_default_order = o;
|
|
}
|
|
|
|
/* Node's DNSException: "<syscall> <code> <host>" with errno, code,
|
|
* syscall and hostname. +1 dyn. */
|
|
static ScrDyn *scr_dns_exception(int rc, const char *syscall, const ScrStr *host) {
|
|
bool notfound = rc == EAI_NONAME
|
|
#ifdef EAI_NODATA
|
|
|| rc == EAI_NODATA
|
|
#endif
|
|
;
|
|
const char *code = notfound ? "ENOTFOUND" : rc == EAI_AGAIN ? "EAI_AGAIN" : "EAI_FAIL";
|
|
double errnum = rc == EAI_NONAME ? -3008
|
|
#ifdef EAI_NODATA
|
|
: rc == EAI_NODATA ? -3007
|
|
#endif
|
|
: rc == EAI_AGAIN ? -3001
|
|
: -3002;
|
|
char msg[320];
|
|
int len = snprintf(msg, sizeof msg, "%s %s %.*s", syscall, code, (int)(host->len < 255 ? host->len : 255),
|
|
host->data);
|
|
ScrStr *m = scr_str_new(msg, (size_t)len);
|
|
ScrError *e = scr_error_new(SCR_ERR_ERROR, m);
|
|
scr_str_release(m);
|
|
scr_error_set_code(e, code);
|
|
e->system_call = scr_str_new(syscall, strlen(syscall));
|
|
e->system_errno = errnum;
|
|
scr_error_set_prop(e, "hostname", 8, scr_dyn_new_str((ScrStr *)host));
|
|
ScrDyn *d = scr_dyn_from_error(e);
|
|
scr_error_release(e);
|
|
return d;
|
|
}
|
|
|
|
static double scr_dns_literal_family(const ScrStr *h) {
|
|
unsigned char buf[16];
|
|
char tmp[64];
|
|
if (h->len == 0 || h->len >= sizeof tmp) return 0;
|
|
memcpy(tmp, h->data, h->len);
|
|
tmp[h->len] = '\0';
|
|
if (inet_pton(AF_INET, tmp, buf) == 1) return 4;
|
|
if (strchr(tmp, ':') != NULL) {
|
|
char *pct = strchr(tmp, '%');
|
|
if (pct) *pct = '\0';
|
|
if (inet_pton(AF_INET6, tmp, buf) == 1) return 6;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static SCR_TL bool scr_dns_hostname_warned = false;
|
|
|
|
/* DEP0118 on the next tick, as process.emitWarning reports it: through
|
|
* the events unit's warning machinery when it is linked, otherwise the
|
|
* default stderr report. */
|
|
static void scr_dns_warn_tick(ScrClosure *self) {
|
|
ScrStr *message = (ScrStr *)scr_box_get_ref(self->caps[0]);
|
|
if (scr_warning_emit_hook) {
|
|
scr_warning_emit_hook("DeprecationWarning", "DEP0118", message);
|
|
} else {
|
|
fflush(stdout);
|
|
#ifdef _WIN32
|
|
long pid = (long)_getpid();
|
|
#else
|
|
long pid = (long)getpid();
|
|
#endif
|
|
fprintf(stderr, "(node:%ld) [DEP0118] DeprecationWarning: %.*s\n", pid, (int)message->len,
|
|
message->data);
|
|
if (!scr_warning_trace_hint_shown) {
|
|
scr_warning_trace_hint_shown = true;
|
|
fputs("(Use `node --trace-deprecation ...` to show where the warning was created)\n", stderr);
|
|
}
|
|
}
|
|
scr_str_release(message);
|
|
}
|
|
|
|
/* dns.lookup(hostname[, options], callback) as Node's lib/dns.js parses
|
|
* it: options is a family number or { family, all, order, verbatim,
|
|
* hints }, validated in Node's order. Results come back with the other
|
|
* lookups; literal addresses and an empty hostname answer on the next
|
|
* tick. */
|
|
static void scr_dns_lookup_plain(const ScrDyn *hostname, const ScrDyn *options, const ScrDyn *callback,
|
|
bool typed);
|
|
|
|
/* `typed`: the callback declares a string address (or a LookupAddress
|
|
* array), which cannot hold the undefined Node passes after an error;
|
|
* it receives "" and NaN (or []) there instead. */
|
|
void scr_dns_lookup_dyn(const ScrDyn *hostname, const ScrDyn *options, const ScrDyn *callback,
|
|
bool typed) {
|
|
/* a typed options record arrives by reference */
|
|
if (options->kind == SCR_DYN_TYPED_REF) {
|
|
ScrDyn *view = scr_dyn_typed_ref_materialize(options);
|
|
if (!scr_exc_pending()) scr_dns_lookup_plain(hostname, view, callback, typed);
|
|
scr_dyn_release(view);
|
|
return;
|
|
}
|
|
scr_dns_lookup_plain(hostname, options, callback, typed);
|
|
}
|
|
|
|
/* A lookup failure's callback arguments. */
|
|
static void scr_dns_fail(const ScrDyn *callback, int rc, const ScrStr *host, bool all, bool typed) {
|
|
ScrDyn *err = scr_dns_exception(rc, "getaddrinfo", host);
|
|
if (!typed) {
|
|
ScrDyn *args[1] = { err };
|
|
scr_dns_call_later(callback, args, 1, false);
|
|
} else if (all) {
|
|
ScrDyn *args[2] = { err, scr_dyn_new_arr() };
|
|
scr_dns_call_later(callback, args, 2, false);
|
|
} else {
|
|
ScrStr *empty = scr_str_new("", 0);
|
|
ScrDyn *args[3] = { err, scr_dyn_new_str(empty), scr_dyn_new_num(NAN) };
|
|
scr_str_release(empty);
|
|
scr_dns_call_later(callback, args, 3, false);
|
|
}
|
|
}
|
|
|
|
static void scr_dns_lookup_plain(const ScrDyn *hostname, const ScrDyn *options, const ScrDyn *callback,
|
|
bool typed) {
|
|
bool host_given = scr_dyn_truthy(hostname);
|
|
if (host_given && hostname->kind != SCR_DYN_STR) {
|
|
scr_dyn_arg_type_fail("hostname", "of type string", hostname);
|
|
return;
|
|
}
|
|
double family = 0;
|
|
bool all = false;
|
|
int order = scr_dns_default_order;
|
|
int ai_flags = 0;
|
|
if (options->kind == SCR_DYN_NUM) {
|
|
double f = options->v.num;
|
|
if (f != 0 && f != 4 && f != 6) {
|
|
scr_dyn_arg_value_fail("family", "must be one of: 0, 4, 6", options);
|
|
return;
|
|
}
|
|
family = f;
|
|
} else if (options->kind != SCR_DYN_UNDEF && options->kind != SCR_DYN_OBJ &&
|
|
options->kind != SCR_DYN_NULL) {
|
|
scr_dyn_arg_type_fail("options", "of type integer or an instance of Object", options);
|
|
return;
|
|
} else if (options->kind == SCR_DYN_OBJ) {
|
|
const ScrDyn *v = scr_dyn_obj_get(options, "hints", 5);
|
|
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) {
|
|
if (v->kind != SCR_DYN_NUM) {
|
|
scr_dyn_prop_type_fail("options.hints", "of type number", v);
|
|
return;
|
|
}
|
|
/* Node's validateHints: only dns.ADDRCONFIG, dns.V4MAPPED and
|
|
* dns.ALL (getaddrinfo's own flags), which then reach getaddrinfo */
|
|
double h = v->v.num;
|
|
int allowed = AI_ADDRCONFIG | AI_V4MAPPED | AI_ALL;
|
|
if (!(h == h && trunc(h) == h && h >= 0 && h <= 0x7fffffff) || ((int)h & ~allowed) != 0) {
|
|
scr_dyn_arg_value_fail("hints", "is invalid", v);
|
|
return;
|
|
}
|
|
ai_flags = (int)h;
|
|
}
|
|
v = scr_dyn_obj_get(options, "family", 6);
|
|
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) {
|
|
if (v->kind == SCR_DYN_STR && v->v.str->len == 4 && memcmp(v->v.str->data, "IPv4", 4) == 0) family = 4;
|
|
else if (v->kind == SCR_DYN_STR && v->v.str->len == 4 && memcmp(v->v.str->data, "IPv6", 4) == 0) family = 6;
|
|
else if (v->kind == SCR_DYN_NUM && (v->v.num == 0 || v->v.num == 4 || v->v.num == 6)) family = v->v.num;
|
|
else {
|
|
scr_dyn_arg_value_fail("options.family", "must be one of: 0, 4, 6", v);
|
|
return;
|
|
}
|
|
}
|
|
v = scr_dyn_obj_get(options, "all", 3);
|
|
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) {
|
|
if (v->kind != SCR_DYN_BOOL) {
|
|
scr_dyn_prop_type_fail("options.all", "of type boolean", v);
|
|
return;
|
|
}
|
|
all = v->v.b;
|
|
}
|
|
v = scr_dyn_obj_get(options, "verbatim", 8);
|
|
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) {
|
|
if (v->kind != SCR_DYN_BOOL) {
|
|
scr_dyn_prop_type_fail("options.verbatim", "of type boolean", v);
|
|
return;
|
|
}
|
|
order = v->v.b ? 0 : 1;
|
|
}
|
|
v = scr_dyn_obj_get(options, "order", 5);
|
|
if (v != NULL && v->kind != SCR_DYN_UNDEF && v->kind != SCR_DYN_NULL) {
|
|
int o = scr_dns_order_of(v);
|
|
if (o < 0) {
|
|
scr_dyn_arg_value_fail("options.order", "must be one of: 'verbatim', 'ipv4first', 'ipv6first'", v);
|
|
return;
|
|
}
|
|
order = o;
|
|
}
|
|
}
|
|
if (callback->kind != SCR_DYN_FUNC) {
|
|
scr_dyn_arg_type_fail("callback", "of type function", callback);
|
|
return;
|
|
}
|
|
if (!host_given) {
|
|
if (!scr_dns_hostname_warned) {
|
|
scr_dns_hostname_warned = true;
|
|
ScrStr *shown = scr_dyn_string_coerce_js(hostname);
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "The provided hostname \"");
|
|
if (shown) scr_jb_write(&b, shown->data, shown->len);
|
|
scr_jb_puts(&b, "\" is not a valid hostname, and is supported in the dns module solely for compatibility.");
|
|
scr_str_release(shown);
|
|
ScrStr *message = scr_jb_finish(&b);
|
|
ScrClosure *clo = scr_closure_new((void *)&scr_dns_warn_tick, 1);
|
|
ScrBox *box = scr_box_new(SCR_BOX_STR);
|
|
scr_box_set_ref(box, message);
|
|
clo->caps[0] = box;
|
|
scr_next_tick(clo);
|
|
}
|
|
if (all) {
|
|
ScrDyn *args[2] = { scr_dyn_new_null(), scr_dyn_new_arr() };
|
|
scr_dns_call_later(callback, args, 2, true);
|
|
} else {
|
|
ScrDyn *args[3] = { scr_dyn_new_null(), scr_dyn_new_null(), scr_dyn_new_num(family == 6 ? 6 : 4) };
|
|
scr_dns_call_later(callback, args, 3, true);
|
|
}
|
|
return;
|
|
}
|
|
const ScrStr *host = hostname->v.str;
|
|
double literal = scr_dns_literal_family(host);
|
|
if (literal != 0) {
|
|
if (all) {
|
|
ScrDyn *entry = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(entry, "address", 7, scr_dyn_new_str((ScrStr *)host));
|
|
scr_dyn_obj_set(entry, "family", 6, scr_dyn_new_num(literal));
|
|
ScrDyn *list = scr_dyn_new_arr();
|
|
scr_dyn_arr_push(list, entry);
|
|
ScrDyn *args[2] = { scr_dyn_new_null(), list };
|
|
scr_dns_call_later(callback, args, 2, true);
|
|
} else {
|
|
ScrDyn *args[3] = { scr_dyn_new_null(), scr_dyn_new_str((ScrStr *)host), scr_dyn_new_num(literal) };
|
|
scr_dns_call_later(callback, args, 3, true);
|
|
}
|
|
return;
|
|
}
|
|
if (memchr(host->data, '\0', host->len) != NULL) {
|
|
ScrStr *shown = scr_str_inspect_quoted((ScrStr *)host, true);
|
|
ScrJsonBuf msg;
|
|
scr_jb_init(&msg);
|
|
scr_jb_puts(&msg, "The argument 'hostname' must be a string without null bytes. Received ");
|
|
scr_jb_put_str(&msg, shown);
|
|
scr_str_release(shown);
|
|
ScrStr *text = scr_jb_finish(&msg);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, text->data, text->len, "ERR_INVALID_ARG_VALUE");
|
|
scr_str_release(text);
|
|
return;
|
|
}
|
|
#ifdef _WIN32
|
|
(void)scr_dgram_poller_init();
|
|
#endif
|
|
struct addrinfo hints;
|
|
memset(&hints, 0, sizeof hints);
|
|
hints.ai_family = family == 4 ? AF_INET : family == 6 ? AF_INET6 : AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
hints.ai_flags = ai_flags;
|
|
struct addrinfo *res = NULL;
|
|
int rc = getaddrinfo(host->data, NULL, &hints, &res);
|
|
if (rc != 0 || res == NULL) {
|
|
if (res) freeaddrinfo(res);
|
|
scr_dns_fail(callback, rc != 0 ? rc : EAI_NONAME, host, all, typed);
|
|
return;
|
|
}
|
|
/* the answers in order: verbatim, or one family first (stable) */
|
|
ScrDyn *list = scr_dyn_new_arr();
|
|
for (int pass = 0; pass < 2; pass++) {
|
|
for (struct addrinfo *ai = res; ai; ai = ai->ai_next) {
|
|
int fam = ai->ai_family == AF_INET6 ? 6 : ai->ai_family == AF_INET ? 4 : 0;
|
|
if (fam == 0) continue;
|
|
bool first = order == 0 || (order == 1 ? fam == 4 : fam == 6);
|
|
if (order == 0 ? pass == 1 : first != (pass == 0)) continue;
|
|
char ip[INET6_ADDRSTRLEN];
|
|
const void *src = fam == 4 ? (const void *)&((struct sockaddr_in *)ai->ai_addr)->sin_addr
|
|
: (const void *)&((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr;
|
|
if (!inet_ntop(fam == 4 ? AF_INET : AF_INET6, src, ip, sizeof ip)) continue;
|
|
ScrDyn *entry = scr_dyn_new_obj();
|
|
ScrStr *a = scr_str_new(ip, strlen(ip));
|
|
scr_dyn_obj_set(entry, "address", 7, scr_dyn_new_str(a));
|
|
scr_str_release(a);
|
|
scr_dyn_obj_set(entry, "family", 6, scr_dyn_new_num(fam));
|
|
scr_dyn_arr_push(list, entry);
|
|
}
|
|
}
|
|
freeaddrinfo(res);
|
|
if (all) {
|
|
ScrDyn *args[2] = { scr_dyn_new_null(), list };
|
|
scr_dns_call_later(callback, args, 2, false);
|
|
return;
|
|
}
|
|
if (list->v.arr.len == 0) {
|
|
scr_dyn_release(list);
|
|
scr_dns_fail(callback, EAI_NONAME, host, false, typed);
|
|
return;
|
|
}
|
|
const ScrDyn *first = list->v.arr.items[0];
|
|
ScrDyn *args[3] = { scr_dyn_new_null(), scr_dyn_retain((ScrDyn *)scr_dyn_obj_get(first, "address", 7)),
|
|
scr_dyn_retain((ScrDyn *)scr_dyn_obj_get(first, "family", 6)) };
|
|
scr_dyn_release(list);
|
|
scr_dns_call_later(callback, args, 3, false);
|
|
}
|
|
|
|
/* dns.lookupService(address, port, callback): Node's checks (an IP
|
|
* address, a valid port), then getnameinfo with NI_NAMEREQD. */
|
|
void scr_dns_lookup_service(const ScrDyn *address, const ScrDyn *port, const ScrDyn *callback,
|
|
bool typed) {
|
|
if (address->kind != SCR_DYN_STR) {
|
|
scr_dyn_arg_type_fail("address", "of type string", address);
|
|
return;
|
|
}
|
|
double fam = scr_dns_literal_family(address->v.str);
|
|
if (fam == 0) {
|
|
scr_dyn_arg_value_fail("address", "is invalid", address);
|
|
return;
|
|
}
|
|
double p = NAN;
|
|
bool numeric = (port->kind == SCR_DYN_NUM || port->kind == SCR_DYN_STR) && scr_dyn_number_coerce_js(port, &p);
|
|
if (port->kind == SCR_DYN_STR && port->v.str->len == 0) numeric = false;
|
|
if (!numeric || !(p >= 0 && p < 65536) || trunc(p) != p) {
|
|
char detail[256];
|
|
const char *d = scr_dyn_specific_type(port, detail, sizeof detail);
|
|
char msg[320];
|
|
int len = snprintf(msg, sizeof msg, "Port should be >= 0 and < 65536. Received %s.", d);
|
|
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_SOCKET_BAD_PORT");
|
|
return;
|
|
}
|
|
if (callback->kind != SCR_DYN_FUNC) {
|
|
scr_dyn_arg_type_fail("callback", "of type function", callback);
|
|
return;
|
|
}
|
|
struct sockaddr_storage ss;
|
|
memset(&ss, 0, sizeof ss);
|
|
socklen_t len;
|
|
char tmp[64];
|
|
memcpy(tmp, address->v.str->data, address->v.str->len);
|
|
tmp[address->v.str->len] = '\0';
|
|
if (fam == 4) {
|
|
struct sockaddr_in *in = (struct sockaddr_in *)&ss;
|
|
in->sin_family = AF_INET;
|
|
in->sin_port = htons((unsigned short)p);
|
|
inet_pton(AF_INET, tmp, &in->sin_addr);
|
|
len = sizeof *in;
|
|
} else {
|
|
char *pct = strchr(tmp, '%');
|
|
if (pct) *pct = '\0';
|
|
struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)&ss;
|
|
in6->sin6_family = AF_INET6;
|
|
in6->sin6_port = htons((unsigned short)p);
|
|
inet_pton(AF_INET6, tmp, &in6->sin6_addr);
|
|
len = sizeof *in6;
|
|
}
|
|
char host[1025], serv[64];
|
|
int rc = getnameinfo((struct sockaddr *)&ss, len, host, sizeof host, serv, sizeof serv, NI_NAMEREQD);
|
|
if (rc != 0) {
|
|
ScrDyn *err = scr_dns_exception(rc, "getnameinfo", address->v.str);
|
|
if (typed) {
|
|
ScrStr *empty = scr_str_new("", 0);
|
|
ScrDyn *args[3] = { err, scr_dyn_new_str(empty), scr_dyn_new_str(empty) };
|
|
scr_str_release(empty);
|
|
scr_dns_call_later(callback, args, 3, false);
|
|
} else {
|
|
ScrDyn *args[1] = { err };
|
|
scr_dns_call_later(callback, args, 1, false);
|
|
}
|
|
return;
|
|
}
|
|
ScrStr *h = scr_str_new(host, strlen(host));
|
|
ScrStr *sv = scr_str_new(serv, strlen(serv));
|
|
ScrDyn *args[3] = { scr_dyn_new_null(), scr_dyn_new_str(h), scr_dyn_new_str(sv) };
|
|
scr_str_release(h);
|
|
scr_str_release(sv);
|
|
scr_dns_call_later(callback, args, 3, false);
|
|
}
|
|
|
|
/* util.promisify(dns.lookup) and util.promisify(dns.lookupService): the
|
|
* callback forms with a callback that settles the returned promise. An
|
|
* error rejects it; otherwise it fulfills with { address, family } (the
|
|
* address array with options.all) or { hostname, service }, the objects
|
|
* Node's custom promisified forms build. An argument error rejects the
|
|
* promise instead of throwing. */
|
|
static ScrDyn *scr_dns_promise_settle(ScrClosure *clo, ScrDyn *const *args, size_t argc,
|
|
const char *first, const char *second) {
|
|
ScrPromise *p;
|
|
memcpy(&p, &clo->caps[0]->slot, sizeof p);
|
|
if (argc >= 1 && !scr_dyn_is_nullish(args[0])) {
|
|
scr_dyn_throw(scr_dyn_retain(args[0]));
|
|
scr_promise_reject_pending(p);
|
|
return scr_dyn_undefined();
|
|
}
|
|
ScrDyn *value;
|
|
if (argc == 2) {
|
|
value = scr_dyn_retain(args[1]);
|
|
} else {
|
|
value = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(value, first, strlen(first), argc >= 2 ? scr_dyn_retain(args[1]) : scr_dyn_undefined());
|
|
scr_dyn_obj_set(value, second, strlen(second), argc >= 3 ? scr_dyn_retain(args[2]) : scr_dyn_undefined());
|
|
}
|
|
scr_promise_fulfill_ref(p, value, scr_dyn_retain_v, scr_dyn_release_v, NULL);
|
|
return scr_dyn_undefined();
|
|
}
|
|
|
|
static ScrDyn *scr_dns_lookup_settle(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
|
return scr_dns_promise_settle(clo, args, argc, "address", "family");
|
|
}
|
|
|
|
static ScrDyn *scr_dns_service_settle(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
|
|
return scr_dns_promise_settle(clo, args, argc, "hostname", "service");
|
|
}
|
|
|
|
static ScrDyn *scr_dns_promise_callback(ScrPromise *p, ScrDynThunk thunk) {
|
|
ScrClosure *clo = scr_closure_new((void *)thunk, 1);
|
|
clo->caps[0] = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, NULL);
|
|
scr_box_set_ref(clo->caps[0], scr_promise_retain(p));
|
|
return scr_dyn_new_func(clo, thunk, 3, "(err,a,b)", "callback");
|
|
}
|
|
|
|
ScrPromise *scr_dns_lookup_promise(const ScrDyn *hostname, const ScrDyn *options) {
|
|
ScrPromise *p = scr_promise_new();
|
|
ScrDyn *callback = scr_dns_promise_callback(p, scr_dns_lookup_settle);
|
|
scr_dns_lookup_dyn(hostname, options, callback, false);
|
|
scr_dyn_release(callback);
|
|
if (scr_exc_pending()) scr_promise_reject_pending(p);
|
|
return p;
|
|
}
|
|
|
|
ScrPromise *scr_dns_lookup_service_promise(const ScrDyn *address, const ScrDyn *port) {
|
|
ScrPromise *p = scr_promise_new();
|
|
ScrDyn *callback = scr_dns_promise_callback(p, scr_dns_service_settle);
|
|
scr_dns_lookup_service(address, port, callback, false);
|
|
scr_dyn_release(callback);
|
|
if (scr_exc_pending()) scr_promise_reject_pending(p);
|
|
return p;
|
|
}
|
|
|
|
/* dns.getServers(): the nameserver lines of /etc/resolv.conf, as c-ares
|
|
* reads them ("[addr]:port" only for a port other than 53). +1. */
|
|
ScrDyn *scr_dns_get_servers(void) {
|
|
ScrDyn *out = scr_dyn_new_arr();
|
|
#ifndef _WIN32
|
|
FILE *f = fopen("/etc/resolv.conf", "r");
|
|
if (!f) return out;
|
|
char line[512];
|
|
while (fgets(line, sizeof line, f)) {
|
|
char *p = line;
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
if (strncmp(p, "nameserver", 10) != 0 || (p[10] != ' ' && p[10] != '\t')) continue;
|
|
p += 10;
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
char *end = p;
|
|
while (*end && *end != ' ' && *end != '\t' && *end != '\n' && *end != '\r' && *end != '#' && *end != ';') end++;
|
|
if (end == p) continue;
|
|
ScrStr *s = scr_str_new(p, (size_t)(end - p));
|
|
scr_dyn_arr_push(out, scr_dyn_new_str(s));
|
|
scr_str_release(s);
|
|
}
|
|
fclose(f);
|
|
#endif
|
|
return out;
|
|
}
|
|
|
|
void scr_dns_thunk0(ScrClosure *cb, ScrStr *errmsg, ScrStr *addr, double family) {
|
|
(void)errmsg; (void)addr; (void)family;
|
|
((void (*)(ScrClosure *))cb->fn)(cb);
|
|
}
|
|
|
|
/* ── the sweep: deferred emits ───────────────────────────────────────── */
|
|
|
|
static bool scr_dgram_flags_pending(void) {
|
|
if (scr_dns_pending) return true;
|
|
for (ScrDgramSocket *s = scr_dgram_socks; s; s = s->next) {
|
|
if (s->pending_err || s->emit_listening || s->emit_connect) return true;
|
|
if (s->closing && !s->close_emitted) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void scr_dgram_sweep(void) {
|
|
ScrDgramSocket *s = scr_dgram_socks;
|
|
while (s) {
|
|
ScrDgramSocket *next = s->next; /* may unregister below */
|
|
scr_dgram_retain(s); /* callbacks may drop every other ref */
|
|
if (s->emit_listening) {
|
|
s->emit_listening = false;
|
|
scr_dgram_fire0(&s->listening_ls);
|
|
if (scr_exc_pending()) {
|
|
scr_dgram_release(s);
|
|
return;
|
|
}
|
|
}
|
|
if (s->emit_connect) {
|
|
s->emit_connect = false;
|
|
scr_dgram_fire0(&s->conn_ls);
|
|
if (scr_exc_pending()) {
|
|
scr_dgram_release(s);
|
|
return;
|
|
}
|
|
}
|
|
if (s->pending_err) {
|
|
ScrStr *msg = s->pending_err;
|
|
s->pending_err = NULL;
|
|
scr_dgram_fire_err(&s->err_ls, msg);
|
|
scr_str_release(msg);
|
|
if (scr_exc_pending()) {
|
|
scr_dgram_release(s);
|
|
return;
|
|
}
|
|
/* A failed bind/connect/send on a never-opened socket leaves it
|
|
* inert: if nothing keeps it live (not bound, not closing), settle
|
|
* it off the registry so the loop can drain — pre-failure
|
|
* listeners drop, the net failed-listen stance. */
|
|
if (s->fd < 0 && !s->closing && !s->close_emitted) {
|
|
s->close_emitted = true;
|
|
scr_dgram_ls_drop(&s->msg_ls);
|
|
scr_dgram_ls_drop(&s->err_ls);
|
|
scr_dgram_ls_drop(&s->listening_ls);
|
|
scr_dgram_ls_drop(&s->close_ls);
|
|
scr_dgram_ls_drop(&s->conn_ls);
|
|
scr_dgram_unregister(s);
|
|
if (scr_exc_pending()) {
|
|
scr_dgram_release(s);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (s->closing && !s->close_emitted) {
|
|
s->close_emitted = true;
|
|
scr_dgram_fire0(&s->close_ls);
|
|
/* settle: listeners drop (the cycle story), off the registry */
|
|
scr_dgram_ls_drop(&s->msg_ls);
|
|
scr_dgram_ls_drop(&s->err_ls);
|
|
scr_dgram_ls_drop(&s->listening_ls);
|
|
scr_dgram_ls_drop(&s->close_ls);
|
|
scr_dgram_ls_drop(&s->conn_ls);
|
|
scr_dgram_unregister(s);
|
|
if (scr_exc_pending()) {
|
|
scr_dgram_release(s);
|
|
return;
|
|
}
|
|
}
|
|
scr_dgram_release(s);
|
|
s = next;
|
|
}
|
|
/* dns deliveries AFTER the socket emits: Node's numeric/nextTick-ish
|
|
* bind emits land before a threadpool lookup's completion — the
|
|
* observed interleave the differential fixtures pin. */
|
|
while (scr_dns_pending) {
|
|
ScrDnsPending *p = scr_dns_pending;
|
|
scr_dns_pending = p->next;
|
|
if (!scr_exc_pending()) {
|
|
if (p->call != NULL) scr_dns_dyn_call(p->call);
|
|
else p->fn(p->cb, p->errmsg, p->addr, p->family);
|
|
}
|
|
scr_dyn_release(p->call);
|
|
scr_closure_release(p->cb);
|
|
scr_str_release(p->errmsg);
|
|
scr_str_release(p->addr);
|
|
free(p);
|
|
if (scr_exc_pending()) return;
|
|
}
|
|
}
|
|
|
|
/* ── the loop hooks (scr_async.c) ────────────────────────────────────── */
|
|
|
|
static bool scr_dgram_pending(void) {
|
|
if (scr_dns_pending) return true;
|
|
for (ScrDgramSocket *s = scr_dgram_socks; s; s = s->next) {
|
|
/* Undelivered emits hold the loop even on an unref'd socket (they
|
|
* are due NOW); otherwise an open registered socket holds it unless
|
|
* unref() waived that — Node's unref semantics. */
|
|
if (s->pending_err || s->emit_listening || s->emit_connect ||
|
|
(s->closing && !s->close_emitted)) {
|
|
return true;
|
|
}
|
|
if (!s->unrefed) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int scr_dgram_pollfd(void) {
|
|
return scr_dgram_poller != NULL ? scrp_poller_fd(scr_dgram_poller) : -1;
|
|
}
|
|
|
|
static void scr_dgram_dispatch(void) {
|
|
if (!scr_dgram_socks && !scr_dns_pending) return;
|
|
for (;;) {
|
|
scr_dgram_sweep();
|
|
if (scr_exc_pending()) return;
|
|
if (scr_dgram_poller == NULL) return;
|
|
ScrPollerEvent evs[64];
|
|
int n = scrp_drain(scr_dgram_poller, evs, 64);
|
|
for (int i = 0; i < n; i++) {
|
|
ScrDgramSocket *s = (ScrDgramSocket *)evs[i].udata;
|
|
if (!s || s->fd < 0) continue; /* closed earlier in this batch */
|
|
scr_dgram_read(s);
|
|
if (scr_exc_pending()) return;
|
|
}
|
|
if (!scr_dgram_flags_pending()) return;
|
|
if (scr_loop_has_ready()) return; /* microtasks interleave first */
|
|
}
|
|
}
|
|
|
|
/* Exit-time registry cleanup (the scr_net.c precedent): sockets a
|
|
* program legitimately leaves live at exit release their listeners and
|
|
* registry references so the RC audit sees a clean heap. */
|
|
static void scr_dgram_cleanup_atexit(void) {
|
|
while (scr_dgram_socks) {
|
|
ScrDgramSocket *s = scr_dgram_socks;
|
|
scr_dgram_ls_drop(&s->msg_ls);
|
|
scr_dgram_ls_drop(&s->err_ls);
|
|
scr_dgram_ls_drop(&s->listening_ls);
|
|
scr_dgram_ls_drop(&s->close_ls);
|
|
scr_dgram_ls_drop(&s->conn_ls);
|
|
scr_dgram_unregister(s);
|
|
}
|
|
while (scr_dns_pending) {
|
|
ScrDnsPending *p = scr_dns_pending;
|
|
scr_dns_pending = p->next;
|
|
scr_dyn_release(p->call);
|
|
scr_closure_release(p->cb);
|
|
scr_str_release(p->errmsg);
|
|
scr_str_release(p->addr);
|
|
free(p);
|
|
}
|
|
}
|
|
|
|
void scr_dgram_install(void) {
|
|
static SCR_TL bool installed = false;
|
|
if (installed) return;
|
|
installed = true;
|
|
scr_atexit(scr_dgram_cleanup_atexit);
|
|
scr_loop_set_dgram(&scr_dgram_pending, &scr_dgram_dispatch, &scr_dgram_pollfd);
|
|
}
|
|
|
|
/* ── the send argument-validation ladder (checked-dynamic lane) ─────────
|
|
* Node's Socket.prototype.send signature shuffle and validation order
|
|
* over dyn arguments, ported exactly: the connected/unconnected split
|
|
* decides whether (a1, a2) are an offset/length slice or the port/address
|
|
* pair; sliceBuffer validates the buffer's type and bounds
|
|
* (ERR_BUFFER_OUT_OF_BOUNDS); list payloads validate per element with the
|
|
* LIST as the Received tail; unconnected sends validate the port
|
|
* (ERR_SOCKET_BAD_PORT, > 0 and < 65536 with the specific-type tail and
|
|
* Node's trailing period) and the address's string contract; a send with
|
|
* a port or address on a connected socket answers ERR_SOCKET_DGRAM_IS_
|
|
* CONNECTED. A validated send then runs: a list goes out as one
|
|
* datagram, a connected socket sends to its peer, and a completion
|
|
* callback receives (null, bytes) or the send failure after this turn
|
|
* (scr_dgram_send_out). All dyn arguments borrowed. */
|
|
|
|
static bool scr_dgram_dyn_truthy(const ScrDyn *v) {
|
|
switch (v->kind) {
|
|
case SCR_DYN_UNDEF:
|
|
case SCR_DYN_NULL: return false;
|
|
case SCR_DYN_BOOL: return v->v.b;
|
|
case SCR_DYN_NUM: return v->v.num == v->v.num && v->v.num != 0;
|
|
case SCR_DYN_STR: return v->v.str->len > 0;
|
|
default: return true;
|
|
}
|
|
}
|
|
|
|
static uint32_t scr_dgram_to_u32(const ScrDyn *v) {
|
|
if (v->kind != SCR_DYN_NUM) return 0; /* >>> 0 over the ladder's shapes */
|
|
double t = v->v.num;
|
|
if (t != t || isinf(t)) return 0;
|
|
t = trunc(t);
|
|
t = fmod(t, 4294967296.0);
|
|
if (t < 0) t += 4294967296.0;
|
|
return (uint32_t)t;
|
|
}
|
|
|
|
static const char SCR_DGRAM_BUF_EXPECTED[] =
|
|
"of type string or an instance of Buffer, TypedArray, or DataView";
|
|
|
|
/* A send failure as the Error Node hands a send callback: the message
|
|
* ("send ECONNREFUSED 127.0.0.1:41234", "getaddrinfo ENOTFOUND nohost")
|
|
* with the code its second word names. +1. */
|
|
static ScrDyn *scr_dgram_send_error(const char *msg) {
|
|
ScrStr *text = scr_str_new(msg, strlen(msg));
|
|
ScrError *e = scr_error_new(0 /* Error */, text);
|
|
scr_str_release(text);
|
|
const char *sp = strchr(msg, ' ');
|
|
if (sp != NULL) {
|
|
char code[48];
|
|
size_t n = 0;
|
|
while (sp[1 + n] != '\0' && sp[1 + n] != ' ') n++;
|
|
if (n > 0 && n < sizeof code) {
|
|
memcpy(code, sp + 1, n);
|
|
code[n] = '\0';
|
|
scr_error_set_code(e, code);
|
|
}
|
|
}
|
|
ScrDyn *d = scr_dyn_from_error(e);
|
|
scr_error_release(e);
|
|
return d;
|
|
}
|
|
|
|
/* The ladder's send: Node's healthCheck, then one datagram — to the
|
|
* connected peer when host is NULL, else to host:port (an empty host is
|
|
* Node's 127.0.0.1). With a completion callback the result defers to the
|
|
* pending queue the dns deliveries share: (null, bytes) on success, the
|
|
* failure (which has no 'error' event then) otherwise; without one a
|
|
* failure is the deferred 'error', as scr_dgram_send_raw has it. */
|
|
static void scr_dgram_send_out(ScrDgramSocket *s, const char *data, size_t len, double port,
|
|
const ScrStr *host, const ScrDyn *callback) {
|
|
if (s->closing || s->close_emitted) {
|
|
scr_dgram_throw("Not running");
|
|
return;
|
|
}
|
|
bool has_cb = callback->kind == SCR_DYN_FUNC;
|
|
char msg[192];
|
|
msg[0] = '\0';
|
|
struct sockaddr_in a4;
|
|
const char *h = host == NULL ? NULL : host->len > 0 ? host->data : "127.0.0.1";
|
|
if (h != NULL && !scr_dgram_host4(h, (uint16_t)(int)port, &a4)) {
|
|
snprintf(msg, sizeof msg, "getaddrinfo ENOTFOUND %s", h);
|
|
} else {
|
|
scr_dgram_ensure_fd(s);
|
|
if (!s->bound) {
|
|
/* Implicit ephemeral bind — Node's send-before-bind emits
|
|
* 'listening' for it. */
|
|
s->bound = true;
|
|
s->emit_listening = true;
|
|
}
|
|
scr_dgram_register(s); /* an open sender holds the loop, like Node */
|
|
ssize_t sent = h != NULL ? sendto(s->fd, data, len, 0, (struct sockaddr *)&a4, sizeof a4)
|
|
: send(s->fd, data, len, 0);
|
|
if (sent < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
|
|
const char *code = scr_dgram_errname(errno);
|
|
char ip[64] = "";
|
|
if (h != NULL) inet_ntop(AF_INET, &a4.sin_addr, ip, sizeof ip);
|
|
snprintf(msg, sizeof msg, "send %s %s:%d", code, ip, (int)port);
|
|
/* A connected send names no address ("send ECONNREFUSED"): cut the
|
|
* message after the code (one format string keeps the unit's
|
|
* constant data unchanged). */
|
|
if (h == NULL) msg[5 + strlen(code)] = '\0';
|
|
}
|
|
scr_dgram_update_read(s);
|
|
}
|
|
if (!has_cb) {
|
|
if (msg[0] != '\0') scr_dgram_defer_err(s, msg);
|
|
return;
|
|
}
|
|
if (msg[0] != '\0') {
|
|
ScrDyn *args[1] = { scr_dgram_send_error(msg) };
|
|
scr_dns_call_later(callback, args, 1, false);
|
|
} else {
|
|
ScrDyn *args[2] = { scr_dyn_new_null(), scr_dyn_new_num((double)len) };
|
|
scr_dns_call_later(callback, args, 2, false);
|
|
}
|
|
}
|
|
|
|
void scr_dgram_send_chk(ScrDgramSocket *s, const ScrDyn *buffer, const ScrDyn *a1,
|
|
const ScrDyn *a2, const ScrDyn *a3, const ScrDyn *a4,
|
|
const ScrDyn *a5) {
|
|
const ScrDyn *offset = a1, *length = a2, *port = a3, *address = a4;
|
|
const ScrDyn *callback = a5;
|
|
bool connected = s->connected;
|
|
bool sliced = false;
|
|
if (!connected) {
|
|
if (scr_dgram_dyn_truthy(address) ||
|
|
(scr_dgram_dyn_truthy(port) && port->kind != SCR_DYN_FUNC)) {
|
|
sliced = true;
|
|
} else {
|
|
callback = port;
|
|
port = offset;
|
|
address = length;
|
|
}
|
|
} else {
|
|
if (length->kind == SCR_DYN_NUM) {
|
|
sliced = true;
|
|
if (port->kind == SCR_DYN_FUNC) {
|
|
callback = port;
|
|
port = scr_dyn_undefined();
|
|
}
|
|
} else {
|
|
callback = offset;
|
|
}
|
|
}
|
|
size_t slice_off = 0, slice_len = 0;
|
|
if (sliced) {
|
|
double bytelen;
|
|
if (buffer->kind == SCR_DYN_STR) bytelen = (double)buffer->v.str->len;
|
|
else if (buffer->kind == SCR_DYN_BYTES) bytelen = scr_bytes_byte_len(buffer->v.bytes);
|
|
else {
|
|
scr_dyn_arg_type_fail("buffer", SCR_DGRAM_BUF_EXPECTED, buffer);
|
|
return;
|
|
}
|
|
uint32_t off = scr_dgram_to_u32(offset);
|
|
uint32_t len = scr_dgram_to_u32(length);
|
|
if ((double)off > bytelen) {
|
|
static const char msg[] = "\"offset\" is outside of buffer bounds";
|
|
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS");
|
|
return;
|
|
}
|
|
if ((double)off + (double)len > bytelen) {
|
|
static const char msg[] = "\"length\" is outside of buffer bounds";
|
|
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS");
|
|
return;
|
|
}
|
|
slice_off = off;
|
|
slice_len = len;
|
|
} else if (buffer->kind == SCR_DYN_ARR) {
|
|
for (size_t i = 0; i < buffer->v.arr.len; i++) {
|
|
const ScrDyn *e = buffer->v.arr.items[i];
|
|
if (e->kind != SCR_DYN_STR && e->kind != SCR_DYN_BYTES) {
|
|
scr_dyn_arg_type_fail("buffer list arguments", SCR_DGRAM_BUF_EXPECTED, buffer);
|
|
return;
|
|
}
|
|
}
|
|
} else if (buffer->kind != SCR_DYN_STR && buffer->kind != SCR_DYN_BYTES) {
|
|
scr_dyn_arg_type_fail("buffer", SCR_DGRAM_BUF_EXPECTED, buffer);
|
|
return;
|
|
}
|
|
double portnum = 0;
|
|
if (connected) {
|
|
if (scr_dgram_dyn_truthy(port) || scr_dgram_dyn_truthy(address)) {
|
|
scr_dgram_throw("Already connected");
|
|
return;
|
|
}
|
|
} else {
|
|
/* validatePort(port, 'Port', false): integers (or numeric strings)
|
|
* strictly between 0 and 65536; everything else renders the specific
|
|
* type with Node's trailing period. */
|
|
bool ok = false;
|
|
if (port->kind == SCR_DYN_NUM && trunc(port->v.num) == port->v.num &&
|
|
port->v.num > 0 && port->v.num < 65536) {
|
|
ok = true;
|
|
portnum = port->v.num;
|
|
} else if (port->kind == SCR_DYN_STR && port->v.str->len > 0) {
|
|
ScrStr *ps = scr_str_retain(port->v.str);
|
|
double n = scr_string_to_number(ps);
|
|
scr_str_release(ps);
|
|
if (n == n && trunc(n) == n && n > 0 && n < 65536) {
|
|
ok = true;
|
|
portnum = n;
|
|
}
|
|
}
|
|
if (!ok) {
|
|
char detail[64], msg[160];
|
|
const char *d = scr_dyn_specific_type(port, detail, sizeof detail);
|
|
int len = snprintf(msg, sizeof msg, "Port should be > 0 and < 65536. Received %s.", d);
|
|
scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_SOCKET_BAD_PORT");
|
|
return;
|
|
}
|
|
}
|
|
if (callback->kind != SCR_DYN_FUNC) callback = scr_dyn_undefined();
|
|
if (address->kind == SCR_DYN_FUNC) {
|
|
callback = address;
|
|
address = scr_dyn_undefined();
|
|
} else if (address->kind != SCR_DYN_UNDEF && address->kind != SCR_DYN_NULL &&
|
|
address->kind != SCR_DYN_STR) {
|
|
/* Node's gate is null/undefined-only: a falsy 0 still throws. */
|
|
scr_dyn_arg_type_fail("address", "of type string", address);
|
|
return;
|
|
}
|
|
/* The payload: a list concatenates into one datagram (sendmsg's
|
|
* gather), a slice narrows the single payload. */
|
|
char *joined = NULL;
|
|
char *owned = NULL;
|
|
const char *data;
|
|
size_t datalen;
|
|
if (buffer->kind == SCR_DYN_ARR) {
|
|
size_t total = 0;
|
|
for (size_t i = 0; i < buffer->v.arr.len; i++) {
|
|
const ScrDyn *e = buffer->v.arr.items[i];
|
|
total += e->kind == SCR_DYN_STR ? e->v.str->len : (size_t)scr_bytes_byte_len(e->v.bytes);
|
|
}
|
|
joined = malloc(total ? total : 1);
|
|
if (!joined) scr_dgram_oom();
|
|
size_t at = 0;
|
|
for (size_t i = 0; i < buffer->v.arr.len; i++) {
|
|
const ScrDyn *e = buffer->v.arr.items[i];
|
|
if (e->kind == SCR_DYN_STR) {
|
|
char *part_owned;
|
|
const char *part = scr_str_utf8(e->v.str, &part_owned);
|
|
memcpy(joined + at, part, e->v.str->len);
|
|
at += e->v.str->len;
|
|
scr_str_utf8_end(part_owned);
|
|
} else {
|
|
const ScrBytes *b = e->v.bytes;
|
|
SCR_BYTES_SNAPSHOT(b);
|
|
size_t n = (size_t)scr_bytes_byte_len(b);
|
|
if (n) memcpy(joined + at, b->data, n);
|
|
at += n;
|
|
}
|
|
}
|
|
data = joined;
|
|
datalen = total;
|
|
} else {
|
|
const ScrBytes *byte_input = buffer->kind == SCR_DYN_BYTES ? buffer->v.bytes : NULL;
|
|
SCR_BYTES_SNAPSHOT(byte_input);
|
|
data = buffer->kind == SCR_DYN_STR ? scr_str_utf8(buffer->v.str, &owned)
|
|
: (const char *)byte_input->data;
|
|
datalen = buffer->kind == SCR_DYN_STR ? buffer->v.str->len
|
|
: (size_t)scr_bytes_byte_len(buffer->v.bytes);
|
|
if (sliced) {
|
|
data += slice_off;
|
|
datalen = slice_len;
|
|
}
|
|
}
|
|
if (connected) {
|
|
scr_dgram_send_out(s, data, datalen, 0, NULL, callback);
|
|
} else {
|
|
ScrStr *host = address->kind == SCR_DYN_STR ? scr_str_retain(address->v.str)
|
|
: scr_str_new("127.0.0.1", 9);
|
|
scr_dgram_send_out(s, data, datalen, portnum, host, callback);
|
|
scr_str_release(host);
|
|
}
|
|
scr_str_utf8_end(owned);
|
|
free(joined);
|
|
}
|
|
|
|
/* The read arming for the checked-dynamic unit (scr_dgram_dyn.c), after
|
|
* it removes a 'message' listener. Defined last so the functions above
|
|
* keep their emission order. */
|
|
void scr_dgram_rearm(ScrDgramSocket *s) { scr_dgram_update_read(s); }
|
|
|
|
/* ── Unix datagram sockets for the hot-shots package shim ──────────────
|
|
* The unix-dgram addon's surface, which hot-shots' uds transport uses: a
|
|
* non-blocking AF_UNIX datagram socket connected to a path. Each call
|
|
* answers its result (fd, 0) or a negative errno, as the addon's binding
|
|
* does; send answers 1 when the socket would block (the addon's
|
|
* "congestion"). Defined after the functions above so their emission
|
|
* order is unchanged. */
|
|
#ifndef _WIN32
|
|
#include <sys/un.h>
|
|
#endif
|
|
|
|
double scr_dgram_unix_socket(void) {
|
|
#ifdef _WIN32
|
|
return -(double)ENOSYS;
|
|
#else
|
|
int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
|
|
if (fd < 0) return -(double)errno;
|
|
int flags = fcntl(fd, F_GETFL, 0);
|
|
if (flags >= 0) (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
|
|
(void)fcntl(fd, F_SETFD, FD_CLOEXEC);
|
|
return (double)fd;
|
|
#endif
|
|
}
|
|
|
|
double scr_dgram_unix_connect(double fd, ScrStr *path) {
|
|
#ifdef _WIN32
|
|
(void)fd;
|
|
(void)path;
|
|
return -(double)ENOSYS;
|
|
#else
|
|
struct sockaddr_un addr;
|
|
memset(&addr, 0, sizeof addr);
|
|
addr.sun_family = AF_UNIX;
|
|
char *owned;
|
|
const char *bytes = scr_str_utf8(path, &owned);
|
|
size_t n = strlen(bytes);
|
|
if (n > sizeof addr.sun_path - 1) n = sizeof addr.sun_path - 1;
|
|
memcpy(addr.sun_path, bytes, n);
|
|
scr_str_utf8_end(owned);
|
|
if (connect((int)fd, (struct sockaddr *)&addr, sizeof addr) != 0) return -(double)errno;
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
double scr_dgram_unix_send(double fd, ScrBytes *data) {
|
|
#ifdef _WIN32
|
|
(void)fd;
|
|
(void)data;
|
|
return -(double)ENOSYS;
|
|
#else
|
|
SCR_BYTES_SNAPSHOT(data);
|
|
ssize_t sent;
|
|
do {
|
|
sent = send((int)fd, data->data, data->len, 0);
|
|
} while (sent < 0 && errno == EINTR);
|
|
if (sent >= 0) return 0;
|
|
if (errno == EAGAIN || errno == EWOULDBLOCK) return 1;
|
|
return -(double)errno;
|
|
#endif
|
|
}
|
|
|
|
double scr_dgram_unix_close(double fd) {
|
|
#ifdef _WIN32
|
|
(void)fd;
|
|
return -(double)ENOSYS;
|
|
#else
|
|
return close((int)fd) != 0 ? -(double)errno : 0;
|
|
#endif
|
|
}
|