diff --git a/builtins/node/node.cpp b/builtins/node/node.cpp new file mode 100644 index 00000000..95735784 --- /dev/null +++ b/builtins/node/node.cpp @@ -0,0 +1,21 @@ +#include "extension-api.h" + +namespace builtins::node { + +bool install(api::Engine* engine) { + // Create the process object in the global scope + JS::RootedObject process(engine->cx(), JS_NewPlainObject(engine->cx())); + if (!process) { + return false; + } + + // Add process to global + if (!JS_DefineProperty(engine->cx(), engine->global(), "process", process, + JSPROP_ENUMERATE)) { + return false; + } + + return true; +} + +} // namespace builtins::node \ No newline at end of file diff --git a/builtins/node/node.h b/builtins/node/node.h new file mode 100644 index 00000000..003c6f1f --- /dev/null +++ b/builtins/node/node.h @@ -0,0 +1,12 @@ +#ifndef BUILTINS_NODE_H +#define BUILTINS_NODE_H + +#include "extension-api.h" + +namespace builtins::node { + +bool install(api::Engine* engine); + +} // namespace builtins::node + +#endif // BUILTINS_NODE_H \ No newline at end of file diff --git a/builtins/node/process_env.cpp b/builtins/node/process_env.cpp new file mode 100644 index 00000000..5f96ea75 --- /dev/null +++ b/builtins/node/process_env.cpp @@ -0,0 +1,192 @@ +#include "process_env.h" +#include "extension-api.h" +#include "host_api.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include + +namespace builtins::node::process_env { + +// Custom proxy handler for environment variables +class EnvProxyHandler : public js::BaseProxyHandler { +public: + EnvProxyHandler() : BaseProxyHandler(nullptr) {} + + bool get(JSContext* cx, JS::HandleObject proxy, JS::HandleValue receiver, + JS::HandleId id, JS::MutableHandleValue vp) const override { + // Convert id to string + JS::RootedValue idVal(cx); + if (!JS_IdToValue(cx, id, &idVal) || !idVal.isString()) { + return false; + } + + // Convert to UTF8 + JS::RootedString idStr(cx, idVal.toString()); + JS::UniqueChars propNameUtf8 = JS_EncodeStringToUTF8(cx, idStr); + if (!propNameUtf8) { + return false; + } + + // Get all environment variables + auto env_vars = host_api::environment_get_environment(); + + // Look for the requested environment variable + for (const auto& [key, value] : env_vars) { + if (key == propNameUtf8.get()) { + // Found it! Convert the value to a JS string + JS::RootedString value_str(cx, JS_NewStringCopyZ(cx, value.c_str())); + if (!value_str) { + return false; + } + vp.setString(value_str); + return true; + } + } + + // Not found, return undefined + vp.setUndefined(); + return true; + } + + bool getOwnPropertyDescriptor(JSContext* cx, JS::HandleObject proxy, + JS::HandleId id, + JS::MutableHandle> desc) const override { + // Convert id to string + JS::RootedValue idVal(cx); + if (!JS_IdToValue(cx, id, &idVal) || !idVal.isString()) { + return false; + } + + // Convert to UTF8 + JS::RootedString idStr(cx, idVal.toString()); + JS::UniqueChars propNameUtf8 = JS_EncodeStringToUTF8(cx, idStr); + if (!propNameUtf8) { + return false; + } + + // Get all environment variables + auto env_vars = host_api::environment_get_environment(); + + // Look for the requested environment variable + for (const auto& [key, value] : env_vars) { + if (key == propNameUtf8.get()) { + // Found it! Create a property descriptor + JS::RootedString value_str(cx, JS_NewStringCopyZ(cx, value.c_str())); + if (!value_str) { + return false; + } + desc.set(mozilla::Some(JS::PropertyDescriptor::Data( + JS::StringValue(value_str), + JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT + ))); + return true; + } + } + + // Not found + desc.set(mozilla::Nothing()); + return true; + } + + bool defineProperty(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, + JS::Handle desc, + JS::ObjectOpResult& result) const override { + // Environment variables are read-only + return result.failReadOnly(); + } + + bool ownPropertyKeys(JSContext* cx, JS::HandleObject proxy, + JS::MutableHandleIdVector props) const override { + // Get all environment variables + auto env_vars = host_api::environment_get_environment(); + + // Add each environment variable name to the property list + for (const auto& [key, value] : env_vars) { + JS::RootedString key_str(cx, JS_NewStringCopyZ(cx, key.c_str())); + if (!key_str) { + return false; + } + JS::RootedId id(cx); + if (!JS_StringToId(cx, key_str, &id)) { + return false; + } + if (!props.append(id)) { + return false; + } + } + + return true; + } + + bool delete_(JSContext* cx, JS::HandleObject proxy, JS::HandleId id, + JS::ObjectOpResult& result) const override { + // Environment variables are read-only + return result.failReadOnly(); + } + + bool preventExtensions(JSContext* cx, JS::HandleObject proxy, + JS::ObjectOpResult& result) const override { + // Environment variables are already non-extensible + return result.succeed(); + } + + bool isExtensible(JSContext* cx, JS::HandleObject proxy, + bool* extensible) const override { + *extensible = false; + return true; + } + + bool getPrototypeIfOrdinary(JSContext* cx, JS::HandleObject proxy, + bool* isOrdinary, + JS::MutableHandleObject protop) const override { + *isOrdinary = true; + protop.set(nullptr); + return true; + } +}; + +static EnvProxyHandler envProxyHandler; + +bool install(api::Engine* engine) { + auto cx = engine->cx(); + auto global = engine->global(); + + // Create process object if it doesn't exist + JS::RootedObject process(cx); + JS::RootedValue process_val(cx); + if (!JS_GetProperty(cx, global, "process", &process_val) || process_val.isUndefined()) { + process = JS_NewPlainObject(cx); + if (!process) { + return false; + } + if (!JS_DefineProperty(cx, global, "process", process, JSPROP_ENUMERATE)) { + return false; + } + } else { + process = &process_val.toObject(); + } + + // Create the target object (empty object) + JS::RootedObject target(cx, JS_NewPlainObject(cx)); + if (!target) { + return false; + } + + // Create the proxy with the target object + JS::RootedValue proxyVal(cx); + JS::RootedValue targetVal(cx, JS::ObjectValue(*target)); + JS::RootedObject proxy(cx, NewProxyObject(cx, &envProxyHandler, targetVal, nullptr, js::ProxyOptions())); + if (!proxy) { + return false; + } + proxyVal.setObject(*proxy); + + // Add env to process + if (!JS_DefineProperty(cx, process, "env", proxyVal, JSPROP_ENUMERATE)) { + return false; + } + + return true; +} + +} // namespace builtins::node::process_env diff --git a/builtins/node/process_env.h b/builtins/node/process_env.h new file mode 100644 index 00000000..04e928c0 --- /dev/null +++ b/builtins/node/process_env.h @@ -0,0 +1,12 @@ +#ifndef BUILTINS_NODE_PROCESS_ENV_H +#define BUILTINS_NODE_PROCESS_ENV_H + +#include "extension-api.h" + +namespace builtins::node::process_env { + +bool install(api::Engine* engine); + +} // namespace builtins::node::process_env + +#endif \ No newline at end of file diff --git a/cmake/builtins.cmake b/cmake/builtins.cmake index 36c38303..57a0c1df 100644 --- a/cmake/builtins.cmake +++ b/cmake/builtins.cmake @@ -113,3 +113,19 @@ add_builtin( fmt INCLUDE_DIRS runtime) + +add_builtin( + builtins::node + SRC + builtins/node/node.cpp + INCLUDE_DIRS + runtime) + +add_builtin( + builtins::node::process_env + SRC + builtins/node/process_env.cpp + DEPENDENCIES + host_api + INCLUDE_DIRS + runtime) diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index e87bd9e1..befa5119 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -2,6 +2,8 @@ #include "bindings/bindings.h" #include "handles.h" +#include +#include #include static std::optional immediately_ready; @@ -128,12 +130,33 @@ vector environment_get_arguments() { return args; } +vector> environment_get_environment() { + bindings_list_tuple2_string_string_t env_vars; + wasi_cli_environment_get_environment(&env_vars); + vector> result; + for (size_t i = 0; i < env_vars.len; i++) { + // Convert each string to std::string, trimming any trailing newlines + std::string key(reinterpret_cast(env_vars.ptr[i].f0.ptr), env_vars.ptr[i].f0.len); + std::string value(reinterpret_cast(env_vars.ptr[i].f1.ptr), env_vars.ptr[i].f1.len); + + // Trim trailing newlines + while (!key.empty() && key.back() == '\n') { + key.pop_back(); + } + while (!value.empty() && value.back() == '\n') { + value.pop_back(); + } + + result.emplace_back(std::move(key), std::move(value)); + } + return result; +} + HttpHeaders::HttpHeaders(std::unique_ptr state) : HttpHeadersReadOnly(std::move(state)) {} HttpHeaders::HttpHeaders() { - handle_state_ = - std::make_unique>(wasi_http_types_constructor_fields()); + handle_state_ = std::make_unique>(wasi_http_types_constructor_fields()); } Result HttpHeaders::FromEntries(vector> &entries) { @@ -166,16 +189,11 @@ HttpHeaders::HttpHeaders(const HttpHeadersReadOnly &headers) : HttpHeadersReadOn // We guard against the list of forbidden headers Wasmtime uses: // https://github.com/bytecodealliance/wasmtime/blob/9afc64b4728d6e2067aa52331ff7b1d6f5275b5e/crates/wasi-http/src/types.rs#L273-L284 static const std::vector forbidden_request_headers = { - "connection", - "host", - "http2-settings", - "keep-alive", - "proxy-authenticate", - "proxy-authorization", - "proxy-connection", - "te", - "transfer-encoding", - "upgrade", + "connection", "host", + "http2-settings", "keep-alive", + "proxy-authenticate", "proxy-authorization", + "proxy-connection", "te", + "transfer-encoding", "upgrade", }; // WASI hosts don't currently make a difference between request and response headers @@ -395,10 +413,10 @@ class BodyWriteAllTask final : public api::AsyncTask { public: explicit BodyWriteAllTask(HttpOutgoingBody *outgoing_body, HostBytes bytes, - api::TaskCompletionCallback completion_callback, - HandleObject callback_receiver) - : outgoing_body_(outgoing_body), cb_(completion_callback), - cb_receiver_(callback_receiver), bytes_(std::move(bytes)) { + api::TaskCompletionCallback completion_callback, + HandleObject callback_receiver) + : outgoing_body_(outgoing_body), cb_(completion_callback), cb_receiver_(callback_receiver), + bytes_(std::move(bytes)) { outgoing_pollable_ = outgoing_body_->subscribe().unwrap(); } @@ -435,9 +453,7 @@ class BodyWriteAllTask final : public api::AsyncTask { return true; } - [[nodiscard]] int32_t id() override { - return outgoing_pollable_; - } + [[nodiscard]] int32_t id() override { return outgoing_pollable_; } void trace(JSTracer *trc) override { JS::TraceEdge(trc, &cb_receiver_, "BodyWriteAllTask completion callback receiver"); @@ -445,7 +461,8 @@ class BodyWriteAllTask final : public api::AsyncTask { }; Result HttpOutgoingBody::write_all(api::Engine *engine, HostBytes bytes, - api::TaskCompletionCallback callback, HandleObject cb_receiver) { + api::TaskCompletionCallback callback, + HandleObject cb_receiver) { if (!valid()) { // TODO: proper error handling for all 154 error codes. return Result::err(154); @@ -709,8 +726,7 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< wasi_http_types_method_outgoing_request_set_authority(borrow, maybe_authority); // TODO: error handling on result - wasi_http_types_method_outgoing_request_set_path_with_query(borrow, - maybe_path_with_query); + wasi_http_types_method_outgoing_request_set_path_with_query(borrow, maybe_path_with_query); } auto *state = new WASIHandle(handle); diff --git a/include/host_api.h b/include/host_api.h index 5b9c3152..ae637d34 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -534,6 +534,7 @@ class MonotonicClock final { }; vector environment_get_arguments(); +vector> environment_get_environment(); } // namespace host_api diff --git a/tests/integration/env/env.js b/tests/integration/env/env.js new file mode 100644 index 00000000..a23cac61 --- /dev/null +++ b/tests/integration/env/env.js @@ -0,0 +1,31 @@ +import { serveTest } from '../test-server.js'; +import { assert } from '../../assert.js'; + +export const handler = serveTest(({ test }) => { + test('process.env exists', () => { + assert(typeof process.env === 'object', 'process.env should be an object'); + }); + + test('process.env has environment variables', () => { + assert(typeof process.env.TEST_VAR === 'string', 'process.env.TEST_VAR should be a string'); + assert(process.env.TEST_VAR === 'test_value', 'process.env.TEST_VAR should have correct value'); + assert(typeof process.env.ANOTHER_VAR === 'string', 'process.env.ANOTHER_VAR should be a string'); + assert(process.env.ANOTHER_VAR === 'another_value', 'process.env.ANOTHER_VAR should have correct value'); + }); + + test('process.env handles empty values', () => { + assert(typeof process.env.EMPTY_VAR === 'string', 'process.env.EMPTY_VAR should be a string'); + assert(process.env.EMPTY_VAR === '', 'process.env.EMPTY_VAR should be empty string'); + }); + + test('process.env is read-only', () => { + const originalValue = process.env.TEST_VAR; + try { + process.env.TEST_VAR = 'new-value'; + assert(false, 'Should not be able to modify process.env'); + } catch (e) { + assert(e instanceof TypeError, 'Should throw TypeError when modifying process.env'); + } + assert(process.env.TEST_VAR === originalValue, 'process.env should not be modified'); + }); +}); \ No newline at end of file diff --git a/tests/integration/handlers.js b/tests/integration/handlers.js index c1ee5cbf..76b612b7 100644 --- a/tests/integration/handlers.js +++ b/tests/integration/handlers.js @@ -4,3 +4,4 @@ export { handler as performance } from './performance/performance.js'; export { handler as crypto } from './crypto/crypto.js'; export { handler as timers } from './timers/timers.js'; export { handler as fetch } from './fetch/fetch.js'; +export { handler as env } from './env/env.js'; diff --git a/tests/test.sh b/tests/test.sh index 82175be9..bd771866 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -90,7 +90,7 @@ if [ -z "$test_component" ]; then fi fi -$wasmtime serve -S common --addr 0.0.0.0:0 "$test_component" 1> "$stdout_log" 2> "$stderr_log" & +$wasmtime serve -S common --addr 0.0.0.0:0 --env TEST_VAR=test_value --env ANOTHER_VAR=another_value --env EMPTY_VAR= "$test_component" 1> "$stdout_log" 2> "$stderr_log" & wasmtime_pid="$!" function cleanup { diff --git a/tests/tests.cmake b/tests/tests.cmake index 810d2738..270de98b 100644 --- a/tests/tests.cmake +++ b/tests/tests.cmake @@ -50,3 +50,4 @@ test_integration(crypto) test_integration(fetch) test_integration(performance) test_integration(timers) +test_integration(env)