239 lines
6.5 KiB
C
239 lines
6.5 KiB
C
#include "cell.h"
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
|
|
#if defined(__APPLE__)
|
|
/* SecureTransport — deprecated but functional, no external deps */
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
#include <Security/Security.h>
|
|
#include <Security/SecureTransport.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#include <poll.h>
|
|
|
|
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
|