Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions engine/net/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build/
86 changes: 86 additions & 0 deletions engine/net/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# PocketJS network core — host build (macOS/Linux) for the conformance
# harness. ESP-IDF consumes the same sources through
# hosts/esp-idf/components/pocketjs_net_core.
#
# cmake -S engine/net -B engine/net/build && cmake --build engine/net/build
# ctest --test-dir engine/net/build --output-on-failure

cmake_minimum_required(VERSION 3.16)
project(pocketjs_net C)

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON)

set(PNET_CORE_SOURCES
src/pnet_util.c
src/pnet_json.c
src/pnet_url.c
src/pnet_policy.c
src/pnet_http1.c
src/pnet_runtime.c
src/pnet_http_client.c
src/pnet_http_server.c
src/pnet_ws.c)

add_library(pocketjs_net STATIC ${PNET_CORE_SOURCES})
target_include_directories(pocketjs_net PUBLIC include PRIVATE src)
target_compile_options(pocketjs_net PRIVATE -Wall -Wextra -Werror -pedantic -Wshadow -Wconversion -Wno-sign-conversion)

add_library(pocketjs_net_posix STATIC drivers/posix/pnet_posix_driver.c)
target_include_directories(pocketjs_net_posix PUBLIC include drivers/posix)
target_compile_options(pocketjs_net_posix PRIVATE -Wall -Wextra -Werror -Wshadow)
target_link_libraries(pocketjs_net_posix PUBLIC pocketjs_net)

# Optional OpenSSL TlsProvider + TLS conformance harness (desktop only).
find_package(OpenSSL QUIET)
if(NOT OpenSSL_FOUND AND EXISTS "/opt/homebrew/opt/openssl@3")
set(OPENSSL_ROOT_DIR "/opt/homebrew/opt/openssl@3")
find_package(OpenSSL QUIET)
endif()

option(PNET_SANITIZE "Build the tests with ASan/UBSan" ON)

enable_testing()
add_executable(pnet_unit_test test/unit_test.c)
target_include_directories(pnet_unit_test PRIVATE src)
target_link_libraries(pnet_unit_test PRIVATE pocketjs_net)
# The shared conformance vectors (TypeScript reference, C core, Rust core).
get_filename_component(PNET_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../.." ABSOLUTE)
target_compile_definitions(pnet_unit_test PRIVATE "PNET_VECTORS_DIR=\"${PNET_REPO_ROOT}/contracts/spec/vectors\"")
add_test(NAME unit COMMAND pnet_unit_test)

add_executable(pnet_host_test test/host_test.c)
target_include_directories(pnet_host_test PRIVATE src)
target_link_libraries(pnet_host_test PRIVATE pocketjs_net_posix pocketjs_net)
find_package(Threads REQUIRED)
target_link_libraries(pnet_host_test PRIVATE Threads::Threads)
add_test(NAME host COMMAND pnet_host_test)

if(OpenSSL_FOUND)
add_library(pocketjs_net_openssl STATIC drivers/openssl/pnet_openssl_tls.c)
target_include_directories(pocketjs_net_openssl PUBLIC include drivers/openssl PRIVATE src)
target_link_libraries(pocketjs_net_openssl PUBLIC pocketjs_net OpenSSL::SSL OpenSSL::Crypto)
target_compile_options(pocketjs_net_openssl PRIVATE -Wall -Wextra -Werror)

add_executable(pnet_tls_test test/tls_test.c)
target_include_directories(pnet_tls_test PRIVATE src drivers/openssl)
target_link_libraries(pnet_tls_test PRIVATE pocketjs_net_openssl pocketjs_net_posix pocketjs_net OpenSSL::SSL OpenSSL::Crypto Threads::Threads)
add_test(NAME tls COMMAND pnet_tls_test)
else()
message(STATUS "OpenSSL not found; skipping the TLS provider and tls conformance test")
endif()

if(OpenSSL_FOUND AND PNET_SANITIZE)
# OpenSSL leak reports from its one-time global init are not our bug.
target_compile_options(pnet_tls_test PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -g)
target_link_options(pnet_tls_test PRIVATE -fsanitize=address,undefined)
target_compile_options(pocketjs_net_openssl PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -g)
endif()

if(PNET_SANITIZE)
foreach(t pocketjs_net pocketjs_net_posix pnet_unit_test pnet_host_test)
target_compile_options(${t} PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer -g)
target_link_options(${t} PRIVATE -fsanitize=address,undefined)
endforeach()
endif()
229 changes: 229 additions & 0 deletions engine/net/drivers/openssl/pnet_openssl_tls.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/* OpenSSL TlsProvider (see pnet_openssl_tls.h). */
#include "pnet_openssl_tls.h"

#include <stdlib.h>
#include <string.h>

#include <openssl/err.h>
#include <openssl/ssl.h>
#include <openssl/x509v3.h>

#include "pocketjs/net/spec.h"

#define MAX_SESSIONS 16

typedef struct session {
pnet_sock s;
SSL *ssl;
bool in_use;
} session;

struct pnet_openssl_tls {
const pnet_driver_ops *driver;
void *driver_ctx;
SSL_CTX *ctx;
session sessions[MAX_SESSIONS];
};

static session *session_for(pnet_openssl_tls *tls, pnet_sock s) {
for (int i = 0; i < MAX_SESSIONS; i++)
if (tls->sessions[i].in_use && tls->sessions[i].s == s) return &tls->sessions[i];
return NULL;
}

static session *session_alloc(pnet_openssl_tls *tls, pnet_sock s) {
for (int i = 0; i < MAX_SESSIONS; i++) {
if (!tls->sessions[i].in_use) {
tls->sessions[i].in_use = true;
tls->sessions[i].s = s;
tls->sessions[i].ssl = NULL;
return &tls->sessions[i];
}
}
return NULL;
}

pnet_openssl_tls *pnet_openssl_tls_create(const pnet_driver_ops *driver, void *driver_ctx,
const pnet_openssl_tls_config *config) {
if (!driver || !driver->native_handle) return NULL;
pnet_openssl_tls *tls = calloc(1, sizeof *tls);
if (!tls) return NULL;
tls->driver = driver;
tls->driver_ctx = driver_ctx;
tls->ctx = SSL_CTX_new(TLS_client_method());
if (!tls->ctx) {
free(tls);
return NULL;
}
int min = config && config->min_version ? config->min_version : TLS1_2_VERSION;
SSL_CTX_set_min_proto_version(tls->ctx, min);
SSL_CTX_set_options(tls->ctx, SSL_OP_NO_RENEGOTIATION | SSL_OP_NO_TICKET);
SSL_CTX_set_mode(tls->ctx, SSL_MODE_AUTO_RETRY | SSL_MODE_ENABLE_PARTIAL_WRITE);
SSL_CTX_set_verify(tls->ctx, SSL_VERIFY_PEER, NULL);
if (config && config->ca_pem) {
X509_STORE *store = SSL_CTX_get_cert_store(tls->ctx);
BIO *bio = BIO_new_mem_buf(config->ca_pem, -1);
X509 *cert;
while (bio && (cert = PEM_read_bio_X509(bio, NULL, NULL, NULL)) != NULL) {
X509_STORE_add_cert(store, cert);
X509_free(cert);
}
if (bio) BIO_free(bio);
} else {
SSL_CTX_set_default_verify_paths(tls->ctx);
}
return tls;
}

void pnet_openssl_tls_destroy(pnet_openssl_tls *tls) {
if (!tls) return;
for (int i = 0; i < MAX_SESSIONS; i++) {
if (tls->sessions[i].in_use && tls->sessions[i].ssl) SSL_free(tls->sessions[i].ssl);
}
if (tls->ctx) SSL_CTX_free(tls->ctx);
free(tls);
}

void *pnet_openssl_tls_ctx(pnet_openssl_tls *tls) {
return tls;
}

static int op_start(void *ctx, pnet_sock s, const pnet_tls_policy *policy) {
pnet_openssl_tls *tls = ctx;
int fd = tls->driver->native_handle(tls->driver_ctx, s);
if (fd < 0) return PNET_IO_ERROR;
session *sess = session_alloc(tls, s);
if (!sess) return PNET_IO_NOMEM;
sess->ssl = SSL_new(tls->ctx);
if (!sess->ssl) {
sess->in_use = false;
return PNET_IO_NOMEM;
}
SSL_set_fd(sess->ssl, fd);
SSL_set_connect_state(sess->ssl);
if (policy->server_name && *policy->server_name) {
/* SNI + hostname verification against the authorized name. IP literals
* are set as IP-ID, everything else as DNS-ID. */
SSL_set_tlsext_host_name(sess->ssl, policy->server_name);
X509_VERIFY_PARAM *param = SSL_get0_param(sess->ssl);
X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
if (X509_VERIFY_PARAM_set1_ip_asc(param, policy->server_name) != 1) {
X509_VERIFY_PARAM_set1_host(param, policy->server_name, 0);
}
}
if (!policy->verify) {
SSL_set_verify(sess->ssl, SSL_VERIFY_NONE, NULL);
}
if (policy->alpn) {
unsigned char protos[64];
size_t plen = strlen(policy->alpn);
if (plen < sizeof protos - 1) {
protos[0] = (unsigned char)plen;
memcpy(protos + 1, policy->alpn, plen);
SSL_set_alpn_protos(sess->ssl, protos, (unsigned)(plen + 1));
}
}
return 0;
}

static const char *map_verify_failure(long verify_result) {
/* Any peer-certificate verification failure maps to one of two stable
* codes: a name/identity mismatch, or an invalid certificate (chain,
* validity, trust, signature). X509_V_OK means the failure was not a
* verification problem — the caller reports tls_handshake_failed. */
if (verify_result == X509_V_OK) return NULL;
if (verify_result == X509_V_ERR_HOSTNAME_MISMATCH || verify_result == X509_V_ERR_IP_ADDRESS_MISMATCH ||
verify_result == X509_V_ERR_EMAIL_MISMATCH) {
return PNET_ERROR_TLS_HOSTNAME_MISMATCH;
}
return PNET_ERROR_TLS_CERTIFICATE_INVALID;
}

static int op_step(void *ctx, pnet_sock s, pnet_tls_failure *failure) {
pnet_openssl_tls *tls = ctx;
session *sess = session_for(tls, s);
if (!sess || !sess->ssl) return -1;
ERR_clear_error();
int rc = SSL_do_handshake(sess->ssl);
if (rc == 1) return 1;
int err = SSL_get_error(sess->ssl, rc);
if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) return 0;
/* Failure: classify. */
long verify = SSL_get_verify_result(sess->ssl);
const char *code = map_verify_failure(verify);
if (!code) code = PNET_ERROR_TLS_HANDSHAKE_FAILED;
failure->code = code;
failure->cause = (int)ERR_peek_last_error();
return -1;
}

static int map_io(session *sess, int rc) {
int err = SSL_get_error(sess->ssl, rc);
switch (err) {
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
return PNET_IO_AGAIN;
case SSL_ERROR_ZERO_RETURN:
return PNET_IO_EOF;
case SSL_ERROR_SYSCALL:
return rc == 0 ? PNET_IO_EOF : PNET_IO_CLOSED;
default:
return PNET_IO_CLOSED;
}
}

static int op_read(void *ctx, pnet_sock s, uint8_t *buf, size_t len) {
pnet_openssl_tls *tls = ctx;
session *sess = session_for(tls, s);
if (!sess || !sess->ssl) return PNET_IO_ERROR;
ERR_clear_error();
int rc = SSL_read(sess->ssl, buf, (int)len);
if (rc > 0) return rc;
return map_io(sess, rc);
}

static int op_write(void *ctx, pnet_sock s, const uint8_t *buf, size_t len) {
pnet_openssl_tls *tls = ctx;
session *sess = session_for(tls, s);
if (!sess || !sess->ssl) return PNET_IO_ERROR;
ERR_clear_error();
int rc = SSL_write(sess->ssl, buf, (int)len);
if (rc > 0) return rc;
int mapped = map_io(sess, rc);
return mapped == PNET_IO_AGAIN ? PNET_IO_AGAIN : mapped;
}

static unsigned op_interest(void *ctx, pnet_sock s) {
pnet_openssl_tls *tls = ctx;
session *sess = session_for(tls, s);
if (!sess || !sess->ssl) return PNET_INTEREST_READ;
/* During the handshake OpenSSL tells us which direction it is blocked on
* through the last want; default to read. */
return SSL_want_write(sess->ssl) ? PNET_INTEREST_WRITE : PNET_INTEREST_READ;
}

static void op_close(void *ctx, pnet_sock s) {
pnet_openssl_tls *tls = ctx;
session *sess = session_for(tls, s);
if (!sess) return;
if (sess->ssl) {
/* One non-blocking close_notify attempt; do not block on the peer. */
SSL_shutdown(sess->ssl);
SSL_free(sess->ssl);
sess->ssl = NULL;
}
sess->in_use = false;
}

static const pnet_tls_ops OPS = {
.start = op_start,
.step = op_step,
.read = op_read,
.write = op_write,
.interest = op_interest,
.close = op_close,
};

const pnet_tls_ops *pnet_openssl_tls_ops(void) {
return &OPS;
}
42 changes: 42 additions & 0 deletions engine/net/drivers/openssl/pnet_openssl_tls.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* PocketJS network core — OpenSSL TlsProvider (desktop conformance).
*
* One `pnet_tls_ops` over OpenSSL, layered on the driver's plain sockets via
* `native_handle`. It owns a shared SSL_CTX (host trust or a pinned CA),
* runs non-blocking client handshakes with SNI and DNS-ID/IP-ID hostname
* verification, TLS 1.2 minimum, and maps failures onto the four stable
* tls_* codes. It is the reference `NativeTlsProvider` for POSIX hosts and
* the peer against which the portable cores are tested; ESP-IDF uses its own
* ESP-TLS provider.
*/
#ifndef POCKETJS_NET_OPENSSL_TLS_H
#define POCKETJS_NET_OPENSSL_TLS_H

#include "pocketjs/net/driver.h"

#ifdef __cplusplus
extern "C" {
#endif

typedef struct pnet_openssl_tls pnet_openssl_tls;

typedef struct pnet_openssl_tls_config {
/** PEM CA bundle to trust; NULL uses the system default paths. */
const char *ca_pem;
/** Minimum protocol: 0x0303 = TLS 1.2 (default), 0x0304 = TLS 1.3. */
int min_version;
} pnet_openssl_tls_config;

/** Create a provider. `driver`/`driver_ctx` are the same the runtime uses;
* the provider calls `native_handle` to reach the fd. NULL on failure. */
pnet_openssl_tls *pnet_openssl_tls_create(const pnet_driver_ops *driver, void *driver_ctx,
const pnet_openssl_tls_config *config);
void pnet_openssl_tls_destroy(pnet_openssl_tls *tls);
const pnet_tls_ops *pnet_openssl_tls_ops(void);
/** The ctx to pass as `tls_ctx` to `pnet_runtime_create_tls`. */
void *pnet_openssl_tls_ctx(pnet_openssl_tls *tls);

#ifdef __cplusplus
}
#endif

#endif /* POCKETJS_NET_OPENSSL_TLS_H */
Loading