#include "cell.h" #include #include #include #include #if defined(__APPLE__) /* SecureTransport — deprecated but functional, no external deps */ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" #include #include #include #include #include typedef struct { SSLContextRef ssl; int fd; } tls_ctx; static void tls_ctx_free(JSRuntime *rt, tls_ctx *ctx) { if (!ctx) return; if (ctx->ssl) { SSLClose(ctx->ssl); CFRelease(ctx->ssl); } if (ctx->fd >= 0) close(ctx->fd); free(ctx); } QJSCLASS(tls_ctx,) static OSStatus tls_read_cb(SSLConnectionRef conn, void *data, size_t *len) { int fd = *(const int *)conn; size_t requested = *len; size_t total = 0; while (total < requested) { ssize_t n = read(fd, (char *)data + total, requested - total); if (n > 0) { total += n; } else if (n == 0) { *len = total; return errSSLClosedGraceful; } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { *len = total; return (total > 0) ? noErr : errSSLWouldBlock; } *len = total; return errSSLClosedAbort; } } *len = total; return noErr; } static OSStatus tls_write_cb(SSLConnectionRef conn, const void *data, size_t *len) { int fd = *(const int *)conn; size_t requested = *len; size_t total = 0; while (total < requested) { ssize_t n = write(fd, (const char *)data + total, requested - total); if (n > 0) { total += n; } else if (n == 0) { *len = total; return errSSLClosedGraceful; } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { *len = total; return (total > 0) ? noErr : errSSLWouldBlock; } *len = total; return errSSLClosedAbort; } } *len = total; return noErr; } /* tls.wrap(fd, hostname) -> ctx */ JSC_CCALL(tls_wrap, int fd = -1; if (JS_ToInt32(js, &fd, argv[0]) < 0) return JS_RaiseDisrupt(js, "tls.wrap: fd must be a number"); const char *hostname = JS_ToCString(js, argv[1]); if (!hostname) return JS_RaiseDisrupt(js, "tls.wrap: hostname must be a string"); tls_ctx *ctx = calloc(1, sizeof(tls_ctx)); ctx->fd = fd; ctx->ssl = SSLCreateContext(NULL, kSSLClientSide, kSSLStreamType); if (!ctx->ssl) { free(ctx); JS_FreeCString(js, hostname); return JS_RaiseDisrupt(js, "tls.wrap: SSLCreateContext failed"); } SSLSetIOFuncs(ctx->ssl, tls_read_cb, tls_write_cb); SSLSetConnection(ctx->ssl, &ctx->fd); SSLSetPeerDomainName(ctx->ssl, hostname, strlen(hostname)); JS_FreeCString(js, hostname); /* Retry handshake on non-blocking sockets (errSSLWouldBlock) */ OSStatus status; for (int attempts = 0; attempts < 200; attempts++) { status = SSLHandshake(ctx->ssl); if (status == noErr) break; if (status != errSSLWouldBlock) break; struct pollfd pfd = { .fd = ctx->fd, .events = POLLIN | POLLOUT }; poll(&pfd, 1, 50); } if (status != noErr) { CFRelease(ctx->ssl); ctx->ssl = NULL; ctx->fd = -1; /* don't close caller's fd */ free(ctx); return JS_RaiseDisrupt(js, "tls.wrap: handshake failed (status %d)", (int)status); } return tls_ctx2js(js, ctx); ) /* tls.send(ctx, data) -> bytes_sent */ JSC_CCALL(tls_send, tls_ctx *ctx = js2tls_ctx(js, argv[0]); if (!ctx || !ctx->ssl) return JS_RaiseDisrupt(js, "tls.send: invalid context"); size_t len; size_t written = 0; OSStatus status; if (JS_IsText(argv[1])) { const char *data = JS_ToCStringLen(js, &len, argv[1]); status = SSLWrite(ctx->ssl, data, len, &written); JS_FreeCString(js, data); } else { unsigned char *data = js_get_blob_data(js, &len, argv[1]); if (!data) return JS_RaiseDisrupt(js, "tls.send: invalid data"); status = SSLWrite(ctx->ssl, data, len, &written); } if (status != noErr && status != errSSLWouldBlock) return JS_RaiseDisrupt(js, "tls.send: write failed (status %d)", (int)status); return JS_NewInt64(js, (int64_t)written); ) /* tls.recv(ctx, len) -> blob */ JSC_CCALL(tls_recv, tls_ctx *ctx = js2tls_ctx(js, argv[0]); if (!ctx || !ctx->ssl) return JS_RaiseDisrupt(js, "tls.recv: invalid context"); size_t len = 4096; if (argc > 1) len = js2number(js, argv[1]); void *out; ret = js_new_blob_alloc(js, len, &out); if (JS_IsException(ret)) return ret; size_t received = 0; OSStatus status = SSLRead(ctx->ssl, out, len, &received); if (status != noErr && status != errSSLWouldBlock && status != errSSLClosedGraceful) { return JS_RaiseDisrupt(js, "tls.recv: read failed (status %d)", (int)status); } js_blob_stone(ret, received); return ret; ) /* tls.close(ctx) -> null */ JSC_CCALL(tls_close, tls_ctx *ctx = js2tls_ctx(js, argv[0]); if (!ctx) return JS_NULL; if (ctx->ssl) { SSLClose(ctx->ssl); CFRelease(ctx->ssl); ctx->ssl = NULL; } if (ctx->fd >= 0) { close(ctx->fd); ctx->fd = -1; } return JS_NULL; ) /* tls.fd(ctx) -> number — get underlying fd for on_readable */ JSC_CCALL(tls_fd, tls_ctx *ctx = js2tls_ctx(js, argv[0]); if (!ctx) return JS_RaiseDisrupt(js, "tls.fd: invalid context"); return JS_NewInt32(js, ctx->fd); ) /* tls.on_readable(ctx, callback) -> null */ JSC_CCALL(tls_on_readable, tls_ctx *ctx = js2tls_ctx(js, argv[0]); if (!ctx) return JS_RaiseDisrupt(js, "tls.on_readable: invalid context"); if (!JS_IsFunction(argv[1])) return JS_RaiseDisrupt(js, "tls.on_readable: callback must be a function"); actor_watch_readable(js, ctx->fd, argv[1]); return JS_NULL; ) static const JSCFunctionListEntry js_tls_funcs[] = { MIST_FUNC_DEF(tls, wrap, 2), MIST_FUNC_DEF(tls, send, 2), MIST_FUNC_DEF(tls, recv, 2), MIST_FUNC_DEF(tls, close, 1), MIST_FUNC_DEF(tls, fd, 1), MIST_FUNC_DEF(tls, on_readable, 2), }; JSValue js_core_net_tls_use(JSContext *js) { JS_FRAME(js); QJSCLASSPREP_NO_FUNCS(tls_ctx); JS_ROOT(mod, JS_NewObject(js)); JS_SetPropertyFunctionList(js, mod.val, js_tls_funcs, countof(js_tls_funcs)); JS_RETURN(mod.val); } #pragma clang diagnostic pop #else /* Stub for non-Apple platforms — TLS not yet implemented */ JSValue js_core_net_tls_use(JSContext *js) { return JS_RaiseDisrupt(js, "TLS not available on this platform"); } #endif