// calogNet.c -- calog network library (see calogNet.h). TCP + UDP for v1 over the shared // typed handle table; every blocking call is an inline native, so it stalls only the // calling script's context thread. #define _GNU_SOURCE #include "calogNet.h" #include "calogHandle.h" #include "calogInternal.h" #include "calogPlatform.h" #include #include #include #include #include #include #include #include #include // Handle type tags, distinct across the whole registry so a stray handle of the wrong kind // fails to resolve (e.g. a listener passed to tcpSend). #define NET_TYPE_TCP 1u #define NET_TYPE_TCP_LISTEN 2u #define NET_TYPE_UDP 3u #define NET_TYPE_ENET_HOST 4u #define NET_TYPE_ENET_PEER 5u // Upper bound on a single recv/recvFrom allocation, so a script cannot request an arbitrary // buffer size. #define NET_MAX_RECV (64 * 1024 * 1024) #define NET_PORT_MAX 65535 // Upper bound (ms) on a TLS server handshake, so a client that opens the socket but never sends a // ClientHello cannot pin the accepting thread forever. #define NET_TLS_HANDSHAKE_MS 10000 typedef struct NetSocketT { CalogSocketT fd; SSL *ssl; // non-NULL: a TLS connection (accepted from a TLS listener) SSL_CTX *tlsCtx; // non-NULL: a TLS listener, owning the server context } NetSocketT; // Process-wide network library state shared by every runtime that registers the natives. typedef struct NetLibT { CalogHandleTableT *handles; int32_t refCount; } NetLibT; // One row of the registration table below: native name paired with its implementation. typedef struct NetNativeT { const char *name; CalogNativeFnT fn; } NetNativeT; static pthread_mutex_t gNetLibMutex = PTHREAD_MUTEX_INITIALIZER; static NetLibT *gNetLib = NULL; static int32_t enetClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetConnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetDisconnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetHost(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetSend(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t enetService(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void netCloser(uint32_t type, void *resource); static int32_t netOpenBound(uint16_t port, int socktype, bool doListen, CalogValueT *result, CalogSocketT *fdOut); static bool netPortOk(int64_t port); static int netResolve(const char *host, uint16_t port, int socktype, bool passive, struct addrinfo **out); static const CalogValueT *netOptField(const CalogValueT *map, const char *name); static int32_t netSocketClose(NetLibT *lib, int64_t handleId, uint32_t type1, uint32_t type2, CalogValueT *result, const char *message); static void netSocketFree(NetSocketT *sock); static int32_t netStore(NetLibT *lib, CalogSocketT fd, uint32_t type, CalogValueT *result); static bool netTlsAccept(SSL *ssl, CalogSocketT fd, int timeoutMs); static SSL_CTX *netTlsServerContext(const CalogValueT *opts, const char **errOut); static int32_t tcpAccept(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t tcpClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t tcpConnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t tcpListen(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t tcpRecv(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t tcpSend(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t udpClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t udpOpen(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t udpRecvFrom(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t udpSendTo(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); // Table-driven registration so a failed calogRegisterInline call can be detected (and the // whole batch unwound) instead of the return value being silently discarded per call. static const NetNativeT gNetNatives[] = { { "tcpConnect", tcpConnect }, { "tcpListen", tcpListen }, { "tcpAccept", tcpAccept }, { "tcpSend", tcpSend }, { "tcpRecv", tcpRecv }, { "tcpClose", tcpClose }, { "udpOpen", udpOpen }, { "udpSendTo", udpSendTo }, { "udpRecvFrom", udpRecvFrom }, { "udpClose", udpClose }, { "enetHost", enetHost }, { "enetConnect", enetConnect }, { "enetService", enetService }, { "enetSend", enetSend }, { "enetDisconnect", enetDisconnect }, { "enetClose", enetClose }, }; int32_t calogNetRegister(CalogT *calog) { NetLibT *lib; int32_t status; size_t nativeIndex; // A TLS listener writes with SSL_write, whose OpenSSL socket BIO issues a bare write() with no // MSG_NOSIGNAL; on Linux (where SO_NOSIGPIPE does not exist) a peer reset would raise SIGPIPE and // kill the process. Ignore it process-wide (idempotent; the plain send() path uses MSG_NOSIGNAL). #ifndef _WIN32 signal(SIGPIPE, SIG_IGN); #endif pthread_mutex_lock(&gNetLibMutex); if (gNetLib == NULL) { NetLibT *newLib; // Windows requires WSAStartup before any socket use (no-op on POSIX). if (calogPlatformNetInit() != 0) { pthread_mutex_unlock(&gNetLibMutex); return calogErrOomE; } newLib = (NetLibT *)calloc(1, sizeof(*newLib)); if (newLib == NULL) { calogPlatformNetShutdown(); pthread_mutex_unlock(&gNetLibMutex); return calogErrOomE; } newLib->handles = calogHandleTableCreate(); if (newLib->handles == NULL) { free(newLib); calogPlatformNetShutdown(); pthread_mutex_unlock(&gNetLibMutex); return calogErrOomE; } if (enet_initialize() != 0) { calogHandleTableDestroy(newLib->handles, NULL); free(newLib); calogPlatformNetShutdown(); pthread_mutex_unlock(&gNetLibMutex); return calogErrOomE; } gNetLib = newLib; } gNetLib->refCount++; lib = gNetLib; pthread_mutex_unlock(&gNetLibMutex); status = calogOkE; for (nativeIndex = 0; nativeIndex < sizeof(gNetNatives) / sizeof(gNetNatives[0]); nativeIndex++) { status = calogRegisterInline(calog, gNetNatives[nativeIndex].name, gNetNatives[nativeIndex].fn, lib); if (status != calogOkE) { break; } } if (status != calogOkE) { // Roll back the refcount bump (and, if we were the sole holder, the whole registry) // so a partially-registered runtime does not leave a phantom reference behind. calogNetShutdown(); return status; } return calogAtDestroy(calog, calogNetShutdown, calogDestroyAfterContextsE); } void calogNetShutdown(void) { pthread_mutex_lock(&gNetLibMutex); if (gNetLib == NULL) { pthread_mutex_unlock(&gNetLibMutex); return; } gNetLib->refCount--; if (gNetLib->refCount <= 0) { calogHandleTableDestroy(gNetLib->handles, netCloser); enet_deinitialize(); calogPlatformNetShutdown(); free(gNetLib); gNetLib = NULL; } pthread_mutex_unlock(&gNetLibMutex); } static void netCloser(uint32_t type, void *resource) { switch (type) { case NET_TYPE_TCP: case NET_TYPE_TCP_LISTEN: case NET_TYPE_UDP: netSocketFree((NetSocketT *)resource); break; case NET_TYPE_ENET_HOST: enet_host_destroy((ENetHost *)resource); break; case NET_TYPE_ENET_PEER: // Peers are owned by their host; enet_host_destroy frees them. break; default: break; } } // Create a socket bound to the given local port (0 = ephemeral), optionally listening. // Returns calogOkE with *fdOut set, or an error with result populated. static int32_t netOpenBound(uint16_t port, int socktype, bool doListen, CalogValueT *result, CalogSocketT *fdOut) { struct addrinfo *res; struct addrinfo *rp; CalogSocketT fd; int rc; int yes; *fdOut = CALOG_INVALID_SOCKET; yes = 1; rc = netResolve(NULL, port, socktype, true, &res); if (rc != 0) { return calogFail(result, calogErrArgE, gai_strerror(rc)); } fd = CALOG_INVALID_SOCKET; for (rp = res; rp != NULL; rp = rp->ai_next) { fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (fd == CALOG_INVALID_SOCKET) { continue; } #if defined(_WIN32) // On Windows SO_REUSEADDR lets an unrelated process bind (and hijack) a port already in // use; SO_EXCLUSIVEADDRUSE is the correct hardening for a server listener. A client or UDP // bind (doListen == false) needs neither, so it is left at the default. if (doListen) { setsockopt(fd, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (const char *)&yes, sizeof(yes)); } #else // POSIX SO_REUSEADDR only relaxes the TIME_WAIT restriction (fast listener restart) and // carries no hijack hazard, so it is applied to every bind. setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes)); #endif if (bind(fd, rp->ai_addr, (socklen_t)rp->ai_addrlen) == 0) { break; } calogSockClose(fd); fd = CALOG_INVALID_SOCKET; } freeaddrinfo(res); if (fd == CALOG_INVALID_SOCKET) { return calogFail(result, calogErrArgE, "could not bind the requested port"); } if (doListen && listen(fd, SOMAXCONN) != 0) { int32_t status; status = calogFail(result, calogErrArgE, calogSockErrStr()); calogSockClose(fd); return status; } *fdOut = fd; return calogOkE; } // True if port is a valid IPv4/IPv6 port number (0 = ephemeral is allowed by callers that // permit it; this only checks the range). static bool netPortOk(int64_t port) { return port >= 0 && port <= NET_PORT_MAX; } static int netResolve(const char *host, uint16_t port, int socktype, bool passive, struct addrinfo **out) { struct addrinfo hints; char portBuffer[8]; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = socktype; if (passive) { hints.ai_flags = AI_PASSIVE; } snprintf(portBuffer, sizeof(portBuffer), "%u", (unsigned int)port); return getaddrinfo(host, portBuffer, &hints, out); } // Remove and close a socket handle, trying type1 then (if non-zero) type2. Used by tcpClose // (TCP + TCP_LISTEN) and udpClose (UDP alone). static int32_t netSocketClose(NetLibT *lib, int64_t handleId, uint32_t type1, uint32_t type2, CalogValueT *result, const char *message) { NetSocketT *sock; sock = (NetSocketT *)calogHandleRemove(lib->handles, handleId, type1); if (sock == NULL && type2 != 0) { sock = (NetSocketT *)calogHandleRemove(lib->handles, handleId, type2); } if (sock == NULL) { return calogFail(result, calogErrArgE, message); } netSocketFree(sock); return calogOkE; } // Drive the TLS server handshake to completion under a TOTAL wall-clock deadline (timeoutMs), on a // non-blocking socket gated by calogPoll. This bounds every stall vector -- a silent client, a // slow-drip client that dribbles bytes to keep resetting a per-read timeout, AND a client that stalls // the server's own writes (a ServerHello/Certificate that never drains) -- none of which a per-recv // SO_RCVTIMEO would catch. Restores blocking mode on success so tcpRecv/tcpSend behave normally. // Returns false on timeout or a hard handshake error (the caller then closes the socket). static bool netTlsAccept(SSL *ssl, CalogSocketT fd, int timeoutMs) { int64_t deadline; deadline = (int64_t)calogMonotonicMillis() + timeoutMs; calogSockSetNonblock(fd, 1); for (;;) { struct pollfd pfd; int64_t remaining; int rc; int err; rc = SSL_accept(ssl); if (rc == 1) { calogSockSetNonblock(fd, 0); return true; } err = SSL_get_error(ssl, rc); if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { return false; } remaining = deadline - (int64_t)calogMonotonicMillis(); if (remaining <= 0) { return false; } pfd.fd = fd; pfd.events = (short)((err == SSL_ERROR_WANT_WRITE) ? POLLOUT : POLLIN); pfd.revents = 0; if (calogPoll(&pfd, 1, (int)(remaining > INT32_MAX ? INT32_MAX : remaining)) <= 0) { return false; } } } // Close the fd and release the NetSocketT. Shared by netSocketClose and the handle-table // teardown path (netCloser). static void netSocketFree(NetSocketT *sock) { // The fd has a single owner: the socket BIO is set BIO_NOCLOSE in tcpAccept, so SSL_free never // touches it and calogSockClose below is the one and only close. We deliberately do NOT call // SSL_shutdown here -- writing the close_notify alert can block on an unresponsive peer with a // full send buffer, stalling teardown; a truncating (dirty) close is acceptable for a server. if (sock->ssl != NULL) { SSL_free(sock->ssl); } if (sock->tlsCtx != NULL) { SSL_CTX_free(sock->tlsCtx); } calogSockClose(sock->fd); free(sock); } // Look up a string-keyed field in an opts map, or NULL (option names are ASCII). static const CalogValueT *netOptField(const CalogValueT *map, const char *name) { CalogValueT key; CalogValueT *field; if (calogValueString(&key, name, (int64_t)strlen(name)) != calogOkE) { return NULL; } field = calogAggGet(map->as.agg, &key); calogValueFree(&key); return field; } // Build a server-side SSL_CTX from opts { cert = "chain.pem", key = "key.pem" }. On failure returns // NULL and points *errOut at a static message. static SSL_CTX *netTlsServerContext(const CalogValueT *opts, const char **errOut) { SSL_CTX *ctx; const CalogValueT *cert; const CalogValueT *key; cert = netOptField(opts, "cert"); key = netOptField(opts, "key"); if (cert == NULL || cert->type != calogStringE || key == NULL || key->type != calogStringE) { *errOut = "tcpListen: tls requires cert and key file paths"; return NULL; } ctx = SSL_CTX_new(TLS_server_method()); if (ctx == NULL) { *errOut = "tcpListen: could not create the TLS context"; return NULL; } SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); if (SSL_CTX_use_certificate_chain_file(ctx, cert->as.s.bytes) != 1) { SSL_CTX_free(ctx); *errOut = "tcpListen: could not load the TLS certificate"; return NULL; } if (SSL_CTX_use_PrivateKey_file(ctx, key->as.s.bytes, SSL_FILETYPE_PEM) != 1) { SSL_CTX_free(ctx); *errOut = "tcpListen: could not load the TLS private key"; return NULL; } if (SSL_CTX_check_private_key(ctx) != 1) { SSL_CTX_free(ctx); *errOut = "tcpListen: certificate and key do not match"; return NULL; } return ctx; } // Wrap an open fd in a handle-table entry, transferring ownership. On failure the fd is // closed. Sets result to the new integer handle on success. static int32_t netStore(NetLibT *lib, CalogSocketT fd, uint32_t type, CalogValueT *result) { NetSocketT *sock; int64_t handle; sock = (NetSocketT *)malloc(sizeof(*sock)); if (sock == NULL) { calogSockClose(fd); return calogFail(result, calogErrOomE, "out of memory"); } sock->fd = fd; sock->ssl = NULL; sock->tlsCtx = NULL; calogSockNoSigpipe(fd); // macOS: no MSG_NOSIGNAL, so guard broken-pipe writes per-socket handle = calogHandleAdd(lib->handles, type, sock); if (handle == 0) { calogSockClose(fd); free(sock); return calogFail(result, calogErrOomE, "out of memory"); } calogValueInt(result, handle); return calogOkE; } static int32_t tcpAccept(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; NetSocketT *listener; CalogSocketT fd; lib = (NetLibT *)userData; calogValueNil(result); if (argCount < 1 || argCount > 2 || args[0].type != calogIntE || (argCount == 2 && args[1].type != calogIntE)) { return calogFail(result, calogErrArgE, "tcpAccept expects (listenerHandle [, timeoutMs])"); } listener = (NetSocketT *)calogHandleGet(lib->handles, args[0].as.i, NET_TYPE_TCP_LISTEN); if (listener == NULL) { return calogFail(result, calogErrArgE, "tcpAccept: invalid listener handle"); } // Optional timeout: poll-gate the accept so a script's accept loop wakes periodically to re-check // its own stop condition (a blocking accept would pin the context thread until a connection lands). if (argCount == 2) { struct pollfd pfd; int timeout; int ready; timeout = args[1].as.i < 0 ? 0 : (args[1].as.i > INT32_MAX ? INT32_MAX : (int)args[1].as.i); pfd.fd = listener->fd; pfd.events = POLLIN; pfd.revents = 0; ready = calogPoll(&pfd, 1, timeout); if (ready <= 0) { return calogOkE; // timeout (nil result) -- the script loop decides whether to keep going } } fd = accept(listener->fd, NULL, NULL); if (fd == CALOG_INVALID_SOCKET) { return calogFail(result, calogErrArgE, calogSockErrStr()); } if (listener->tlsCtx == NULL) { return netStore(lib, fd, NET_TYPE_TCP, result); } // TLS listener: complete the server handshake, then attach the session to the stored connection. // The socket BIO is BIO_NOCLOSE so the NetSocketT stays the single fd owner (see netSocketFree). { SSL *ssl; NetSocketT *sock; int32_t status; ssl = SSL_new(listener->tlsCtx); if (ssl == NULL) { calogSockClose(fd); return calogFail(result, calogErrOomE, "tcpAccept: could not create the TLS session"); } SSL_set_fd(ssl, (int)fd); (void)BIO_set_close(SSL_get_rbio(ssl), BIO_NOCLOSE); // Bound the handshake by a TOTAL deadline on a non-blocking socket, so a stalled/slow/malicious // client cannot pin this accepting thread (see netTlsAccept). if (!netTlsAccept(ssl, fd, NET_TLS_HANDSHAKE_MS)) { SSL_free(ssl); calogSockClose(fd); return calogFail(result, calogErrArgE, "tcpAccept: TLS handshake failed or timed out"); } status = netStore(lib, fd, NET_TYPE_TCP, result); if (status != calogOkE) { SSL_free(ssl); // netStore closed fd; the NOCLOSE BIO leaves it untouched return status; } sock = (NetSocketT *)calogHandleGet(lib->handles, result->as.i, NET_TYPE_TCP); sock->ssl = ssl; return calogOkE; } } static int32_t tcpClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "tcpClose expects (handle)"); } return netSocketClose(lib, args[0].as.i, NET_TYPE_TCP, NET_TYPE_TCP_LISTEN, result, "tcpClose: invalid handle"); } static int32_t tcpConnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; struct addrinfo *res; struct addrinfo *rp; CalogSocketT fd; int rc; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogIntE) { return calogFail(result, calogErrArgE, "tcpConnect expects (host, port)"); } if (!netPortOk(args[1].as.i)) { return calogFail(result, calogErrArgE, "tcpConnect: port out of range"); } rc = netResolve(args[0].as.s.bytes, (uint16_t)args[1].as.i, SOCK_STREAM, false, &res); if (rc != 0) { return calogFail(result, calogErrArgE, gai_strerror(rc)); } fd = CALOG_INVALID_SOCKET; for (rp = res; rp != NULL; rp = rp->ai_next) { fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (fd == CALOG_INVALID_SOCKET) { continue; } if (connect(fd, rp->ai_addr, (socklen_t)rp->ai_addrlen) == 0) { break; } calogSockClose(fd); fd = CALOG_INVALID_SOCKET; } freeaddrinfo(res); if (fd == CALOG_INVALID_SOCKET) { return calogFail(result, calogErrArgE, "tcpConnect: could not connect"); } return netStore(lib, fd, NET_TYPE_TCP, result); } static int32_t tcpListen(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; const CalogValueT *opts; SSL_CTX *ctx; CalogSocketT fd; int32_t status; lib = (NetLibT *)userData; calogValueNil(result); if (argCount < 1 || argCount > 2 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "tcpListen expects (port [, opts])"); } if (argCount == 2 && (args[1].type != calogAggE || !calogAggIsKeyed(args[1].as.agg))) { return calogFail(result, calogErrArgE, "tcpListen: opts must be a map"); } if (!netPortOk(args[0].as.i)) { return calogFail(result, calogErrArgE, "tcpListen: port out of range"); } // TLS is opt-in: { tls = true, cert = "...pem", key = "...pem" } builds a server SSL_CTX that the // listener owns; tcpAccept then wraps each accepted connection in a TLS session. opts = argCount == 2 ? &args[1] : NULL; ctx = NULL; if (opts != NULL) { const CalogValueT *tls; const CalogValueT *cert; tls = netOptField(opts, "tls"); cert = netOptField(opts, "cert"); if ((tls != NULL && tls->type == calogBoolE && tls->as.b) || cert != NULL) { const char *err; err = NULL; ctx = netTlsServerContext(opts, &err); if (ctx == NULL) { return calogFail(result, calogErrArgE, err != NULL ? err : "tcpListen: TLS setup failed"); } } } status = netOpenBound((uint16_t)args[0].as.i, SOCK_STREAM, true, result, &fd); if (status != calogOkE) { if (ctx != NULL) { SSL_CTX_free(ctx); } return status; } status = netStore(lib, fd, NET_TYPE_TCP_LISTEN, result); if (status != calogOkE) { if (ctx != NULL) { SSL_CTX_free(ctx); // netStore already closed fd } return status; } if (ctx != NULL) { NetSocketT *listener; listener = (NetSocketT *)calogHandleGet(lib->handles, result->as.i, NET_TYPE_TCP_LISTEN); listener->tlsCtx = ctx; } return calogOkE; } static int32_t tcpRecv(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; NetSocketT *sock; char *buffer; ssize_t received; int32_t status; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) { return calogFail(result, calogErrArgE, "tcpRecv expects (handle, maxBytes)"); } if (args[1].as.i < 1 || args[1].as.i > NET_MAX_RECV) { return calogFail(result, calogErrArgE, "tcpRecv: maxBytes out of range"); } sock = (NetSocketT *)calogHandleGet(lib->handles, args[0].as.i, NET_TYPE_TCP); if (sock == NULL) { return calogFail(result, calogErrArgE, "tcpRecv: invalid handle"); } buffer = (char *)malloc((size_t)args[1].as.i); if (buffer == NULL) { return calogFail(result, calogErrOomE, "tcpRecv: out of memory"); } if (sock->ssl != NULL) { int got; got = SSL_read(sock->ssl, buffer, (int)args[1].as.i); received = got > 0 ? (ssize_t)got : (got == 0 ? 0 : -1); // 0 = clean TLS shutdown -> EOF } else { received = recv(sock->fd, buffer, (size_t)args[1].as.i, 0); } if (received < 0) { status = calogFail(result, calogErrArgE, calogSockErrStr()); free(buffer); return status; } if (received == 0) { // Peer closed the connection: nil signals end of stream. free(buffer); return calogOkE; } status = calogValueString(result, buffer, (int64_t)received); free(buffer); return status; } static int32_t tcpSend(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; NetSocketT *sock; int64_t total; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogStringE) { return calogFail(result, calogErrArgE, "tcpSend expects (handle, data)"); } sock = (NetSocketT *)calogHandleGet(lib->handles, args[0].as.i, NET_TYPE_TCP); if (sock == NULL) { return calogFail(result, calogErrArgE, "tcpSend: invalid handle"); } total = 0; while (total < args[1].as.s.length) { ssize_t sent; if (sock->ssl != NULL) { int chunk; int wrote; chunk = (args[1].as.s.length - total) > INT32_MAX ? INT32_MAX : (int)(args[1].as.s.length - total); wrote = SSL_write(sock->ssl, args[1].as.s.bytes + total, chunk); sent = wrote > 0 ? (ssize_t)wrote : -1; } else { sent = send(sock->fd, args[1].as.s.bytes + total, (size_t)(args[1].as.s.length - total), CALOG_MSG_NOSIGNAL); } if (sent < 0) { return calogFail(result, calogErrArgE, calogSockErrStr()); } total += sent; } calogValueInt(result, total); return calogOkE; } static int32_t udpClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "udpClose expects (handle)"); } return netSocketClose(lib, args[0].as.i, NET_TYPE_UDP, 0, result, "udpClose: invalid handle"); } static int32_t udpOpen(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; CalogSocketT fd; int32_t status; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "udpOpen expects (port)"); } if (!netPortOk(args[0].as.i)) { return calogFail(result, calogErrArgE, "udpOpen: port out of range"); } status = netOpenBound((uint16_t)args[0].as.i, SOCK_DGRAM, false, result, &fd); if (status != calogOkE) { return status; } return netStore(lib, fd, NET_TYPE_UDP, result); } static int32_t udpRecvFrom(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; NetSocketT *sock; CalogAggT *map; char *buffer; struct sockaddr_in from; socklen_t fromLength; ssize_t received; char hostBuffer[INET_ADDRSTRLEN]; int32_t status; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) { return calogFail(result, calogErrArgE, "udpRecvFrom expects (handle, maxBytes)"); } if (args[1].as.i < 1 || args[1].as.i > NET_MAX_RECV) { return calogFail(result, calogErrArgE, "udpRecvFrom: maxBytes out of range"); } sock = (NetSocketT *)calogHandleGet(lib->handles, args[0].as.i, NET_TYPE_UDP); if (sock == NULL) { return calogFail(result, calogErrArgE, "udpRecvFrom: invalid handle"); } buffer = (char *)malloc((size_t)args[1].as.i); if (buffer == NULL) { return calogFail(result, calogErrOomE, "udpRecvFrom: out of memory"); } fromLength = sizeof(from); received = recvfrom(sock->fd, buffer, (size_t)args[1].as.i, 0, (struct sockaddr *)&from, &fromLength); if (received < 0) { status = calogFail(result, calogErrArgE, calogSockErrStr()); free(buffer); return status; } if (inet_ntop(AF_INET, &from.sin_addr, hostBuffer, sizeof(hostBuffer)) == NULL) { hostBuffer[0] = '\0'; } status = calogAggCreate(&map, calogMapE); if (status != calogOkE) { free(buffer); return calogFail(result, status, "udpRecvFrom: out of memory"); } status = calogMapSetStr(map, "data", buffer, (int64_t)received); free(buffer); if (status == calogOkE) { status = calogMapSetStr(map, "host", hostBuffer, (int64_t)strlen(hostBuffer)); } if (status == calogOkE) { status = calogMapSetInt(map, "port", (int64_t)ntohs(from.sin_port)); } if (status != calogOkE) { calogAggFree(map); return calogFail(result, status, "udpRecvFrom: failed to build the result"); } calogValueAgg(result, map); return calogOkE; } static int32_t udpSendTo(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; NetSocketT *sock; struct addrinfo *res; ssize_t sent; int rc; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 4 || args[0].type != calogIntE || args[1].type != calogStringE || args[2].type != calogIntE || args[3].type != calogStringE) { return calogFail(result, calogErrArgE, "udpSendTo expects (handle, host, port, data)"); } if (!netPortOk(args[2].as.i)) { return calogFail(result, calogErrArgE, "udpSendTo: port out of range"); } sock = (NetSocketT *)calogHandleGet(lib->handles, args[0].as.i, NET_TYPE_UDP); if (sock == NULL) { return calogFail(result, calogErrArgE, "udpSendTo: invalid handle"); } rc = netResolve(args[1].as.s.bytes, (uint16_t)args[2].as.i, SOCK_DGRAM, false, &res); if (rc != 0) { return calogFail(result, calogErrArgE, gai_strerror(rc)); } sent = sendto(sock->fd, args[3].as.s.bytes, (size_t)args[3].as.s.length, 0, res->ai_addr, res->ai_addrlen); freeaddrinfo(res); if (sent < 0) { return calogFail(result, calogErrArgE, calogSockErrStr()); } calogValueInt(result, (int64_t)sent); return calogOkE; } static int32_t enetClose(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; ENetHost *host; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "enetClose expects (hostHandle)"); } host = (ENetHost *)calogHandleRemove(lib->handles, args[0].as.i, NET_TYPE_ENET_HOST); if (host == NULL) { return calogFail(result, calogErrArgE, "enetClose: invalid host handle"); } // enet_host_destroy frees the peer array, so drop every outstanding peer handle for this // host first -- otherwise those handles would resolve to freed memory (use-after-free). { size_t peerIndex; for (peerIndex = 0; peerIndex < host->peerCount; peerIndex++) { ENetPeer *peer; int64_t peerHandle; peer = &host->peers[peerIndex]; peerHandle = (int64_t)(intptr_t)peer->data; if (peerHandle != 0) { calogHandleRemove(lib->handles, peerHandle, NET_TYPE_ENET_PEER); peer->data = NULL; } } } enet_host_destroy(host); return calogOkE; } static int32_t enetConnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; ENetHost *host; ENetPeer *peer; ENetAddress address; int64_t handle; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 4 || args[0].type != calogIntE || args[1].type != calogStringE || args[2].type != calogIntE || args[3].type != calogIntE) { return calogFail(result, calogErrArgE, "enetConnect expects (hostHandle, host, port, channels)"); } if (!netPortOk(args[2].as.i)) { return calogFail(result, calogErrArgE, "enetConnect: port out of range"); } if (args[3].as.i < 1) { return calogFail(result, calogErrArgE, "enetConnect: channels must be positive"); } host = (ENetHost *)calogHandleGet(lib->handles, args[0].as.i, NET_TYPE_ENET_HOST); if (host == NULL) { return calogFail(result, calogErrArgE, "enetConnect: invalid host handle"); } if (enet_address_set_host(&address, args[1].as.s.bytes) != 0) { return calogFail(result, calogErrArgE, "enetConnect: could not resolve host"); } address.port = (enet_uint16)args[2].as.i; peer = enet_host_connect(host, &address, (size_t)args[3].as.i, 0); if (peer == NULL) { return calogFail(result, calogErrArgE, "enetConnect: no available peer slots"); } handle = calogHandleAdd(lib->handles, NET_TYPE_ENET_PEER, peer); if (handle == 0) { enet_peer_reset(peer); return calogFail(result, calogErrOomE, "enetConnect: out of memory"); } peer->data = (void *)(intptr_t)handle; calogValueInt(result, handle); return calogOkE; } static int32_t enetDisconnect(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; ENetPeer *peer; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogIntE) { return calogFail(result, calogErrArgE, "enetDisconnect expects (peerHandle)"); } peer = (ENetPeer *)calogHandleGet(lib->handles, args[0].as.i, NET_TYPE_ENET_PEER); if (peer == NULL) { return calogFail(result, calogErrArgE, "enetDisconnect: invalid peer handle"); } // Graceful: the actual removal happens when the disconnect event is serviced. enet_peer_disconnect(peer, 0); return calogOkE; } static int32_t enetHost(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; ENetHost *host; ENetAddress address; ENetAddress *addressPtr; int64_t handle; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) { return calogFail(result, calogErrArgE, "enetHost expects (port, maxPeers)"); } if (!netPortOk(args[0].as.i)) { return calogFail(result, calogErrArgE, "enetHost: port out of range"); } if (args[1].as.i < 1) { return calogFail(result, calogErrArgE, "enetHost: maxPeers must be positive"); } // port 0 -> a client host (no bind); port > 0 -> a server host bound to that port. addressPtr = NULL; if (args[0].as.i > 0) { address.host = ENET_HOST_ANY; address.port = (enet_uint16)args[0].as.i; addressPtr = &address; } host = enet_host_create(addressPtr, (size_t)args[1].as.i, 0, 0, 0); if (host == NULL) { return calogFail(result, calogErrArgE, "enetHost: could not create host"); } handle = calogHandleAdd(lib->handles, NET_TYPE_ENET_HOST, host); if (handle == 0) { enet_host_destroy(host); return calogFail(result, calogErrOomE, "enetHost: out of memory"); } calogValueInt(result, handle); return calogOkE; } static int32_t enetSend(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; ENetPeer *peer; ENetPacket *packet; enet_uint32 flags; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 4 || args[0].type != calogIntE || args[1].type != calogIntE || args[2].type != calogStringE || args[3].type != calogBoolE) { return calogFail(result, calogErrArgE, "enetSend expects (peerHandle, channel, data, reliable)"); } if (args[1].as.i < 0 || args[1].as.i > ENET_PROTOCOL_MAXIMUM_CHANNEL_COUNT) { return calogFail(result, calogErrArgE, "enetSend: channel out of range"); } peer = (ENetPeer *)calogHandleGet(lib->handles, args[0].as.i, NET_TYPE_ENET_PEER); if (peer == NULL) { return calogFail(result, calogErrArgE, "enetSend: invalid peer handle"); } flags = 0; if (args[3].as.b) { flags = (enet_uint32)ENET_PACKET_FLAG_RELIABLE; } packet = enet_packet_create(args[2].as.s.bytes, (size_t)args[2].as.s.length, flags); if (packet == NULL) { return calogFail(result, calogErrOomE, "enetSend: out of memory"); } if (enet_peer_send(peer, (enet_uint8)args[1].as.i, packet) != 0) { enet_packet_destroy(packet); return calogFail(result, calogErrArgE, "enetSend: could not queue the packet"); } return calogOkE; } static int32_t enetService(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { NetLibT *lib; ENetHost *host; ENetEvent event; CalogAggT *map; int64_t peerHandle; int32_t status; int serviced; lib = (NetLibT *)userData; calogValueNil(result); if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) { return calogFail(result, calogErrArgE, "enetService expects (hostHandle, timeoutMs)"); } // enet_host_service takes the timeout as enet_uint32 milliseconds; reject anything that // would silently wrap (a value >= 2^32 would otherwise become a near-zero busy-poll). if (args[1].as.i < 0 || args[1].as.i > (int64_t)UINT32_MAX) { return calogFail(result, calogErrArgE, "enetService: timeout out of range"); } host = (ENetHost *)calogHandleGet(lib->handles, args[0].as.i, NET_TYPE_ENET_HOST); if (host == NULL) { return calogFail(result, calogErrArgE, "enetService: invalid host handle"); } // Build the result map before consuming the event: on OOM here there is nothing yet to // leak or leave in a stale handle-table state (see the failure paths below, which used to // run after the event was already dequeued). status = calogAggCreate(&map, calogMapE); if (status != calogOkE) { return calogFail(result, status, "enetService: out of memory"); } serviced = enet_host_service(host, &event, (enet_uint32)args[1].as.i); if (serviced < 0) { calogAggFree(map); return calogFail(result, calogErrArgE, "enetService: service failed"); } if (serviced == 0 || event.type == ENET_EVENT_TYPE_NONE) { status = calogMapSetStr(map, "type", "none", (int64_t)strlen("none")); if (status != calogOkE) { calogAggFree(map); return calogFail(result, status, "enetService: out of memory"); } calogValueAgg(result, map); return calogOkE; } // Every peer carries its stable handle in peer->data (0 = not yet assigned, e.g. a fresh // incoming connection on a server host). peerHandle = (int64_t)(intptr_t)event.peer->data; if (peerHandle == 0) { peerHandle = calogHandleAdd(lib->handles, NET_TYPE_ENET_PEER, event.peer); if (peerHandle == 0) { if (event.type == ENET_EVENT_TYPE_RECEIVE) { enet_packet_destroy(event.packet); } calogAggFree(map); return calogFail(result, calogErrOomE, "enetService: out of memory"); } event.peer->data = (void *)(intptr_t)peerHandle; } switch (event.type) { case ENET_EVENT_TYPE_CONNECT: status = calogMapSetStr(map, "type", "connect", (int64_t)strlen("connect")); break; case ENET_EVENT_TYPE_RECEIVE: status = calogMapSetStr(map, "type", "receive", (int64_t)strlen("receive")); if (status == calogOkE) { status = calogMapSetInt(map, "channel", (int64_t)event.channelID); } if (status == calogOkE) { status = calogMapSetStr(map, "data", (const char *)event.packet->data, (int64_t)event.packet->dataLength); } enet_packet_destroy(event.packet); break; case ENET_EVENT_TYPE_DISCONNECT: status = calogMapSetStr(map, "type", "disconnect", (int64_t)strlen("disconnect")); // The peer is now invalid; drop its handle but still report it in this event. calogHandleRemove(lib->handles, peerHandle, NET_TYPE_ENET_PEER); event.peer->data = NULL; break; default: // Unreachable: ENET_EVENT_TYPE_NONE is handled above and ENetEventType has no // other values. Kept only to satisfy -Wswitch; a peer handle was possibly just // allocated above for this event, so this is not a safe fallback to "none". calogAggFree(map); return calogFail(result, calogErrArgE, "enetService: unexpected event type"); } if (status == calogOkE) { status = calogMapSetInt(map, "peer", peerHandle); } if (status != calogOkE) { calogAggFree(map); return calogFail(result, status, "enetService: failed to build the event"); } calogValueAgg(result, map); return calogOkE; }