Skip to content

Commit

Permalink
src: add built-in .env file support
Browse files Browse the repository at this point in the history
PR-URL: #48890
Refs: https://github.com/orgs/nodejs/discussions/44975
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
  • Loading branch information
anonrig authored Aug 17, 2023
1 parent 6bb400f commit 769823e
Show file tree
Hide file tree
Showing 19 changed files with 508 additions and 16 deletions.
36 changes: 36 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,41 @@ surface on other platforms, but the performance impact may be severe.
This flag is inherited from V8 and is subject to change upstream. It may
disappear in a non-semver-major release.

### `--env-file=config`

> Stability: 1.1 - Active development
<!-- YAML
added: REPLACEME
-->

Loads environment variables from a file relative to the current directory,
making them available to applications on `process.env`. The [environment
variables which configure Node.js][environment_variables], such as `NODE_OPTIONS`,
are parsed and applied. If the same variable is defined in the environment and
in the file, the value from the environment takes precedence.

The format of the file should be one line per key-value pair of environment
variable name and value separated by `=`:

```text
PORT=3000
```

Any text after a `#` is treated as a comment:

```text
# This is a comment
PORT=3000 # This is also a comment
```

Values can start and end with the following quotes: `\`, `"` or `'`.
They are omitted from the values.

```text
USERNAME="nodejs" # will result in `nodejs` as the value.
```

### `--max-http-header-size=size`

<!-- YAML
Expand Down Expand Up @@ -2647,6 +2682,7 @@ done
[debugger]: debugger.md
[debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications
[emit_warning]: process.md#processemitwarningwarning-options
[environment_variables]: #environment-variables
[filtering tests by name]: test.md#filtering-tests-by-name
[jitless]: https://v8.dev/blog/jitless
[libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
'src/node_contextify.cc',
'src/node_credentials.cc',
'src/node_dir.cc',
'src/node_dotenv.cc',
'src/node_env_var.cc',
'src/node_errors.cc',
'src/node_external_reference.cc',
Expand Down Expand Up @@ -214,6 +215,7 @@
'src/node_context_data.h',
'src/node_contextify.h',
'src/node_dir.h',
'src/node_dotenv.h',
'src/node_errors.h',
'src/node_exit_code.h',
'src/node_external_reference.h',
Expand Down
9 changes: 4 additions & 5 deletions src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ void Environment::TryLoadAddon(
}
}

std::string Environment::GetCwd() {
std::string Environment::GetCwd(const std::string& exec_path) {
char cwd[PATH_MAX_BYTES];
size_t size = PATH_MAX_BYTES;
const int err = uv_cwd(cwd, &size);
Expand All @@ -696,7 +696,6 @@ std::string Environment::GetCwd() {

// This can fail if the cwd is deleted. In that case, fall back to
// exec_path.
const std::string& exec_path = exec_path_;
return exec_path.substr(0, exec_path.find_last_of(kPathSeparator));
}

Expand Down Expand Up @@ -730,7 +729,7 @@ std::unique_ptr<v8::BackingStore> Environment::release_managed_buffer(
return bs;
}

std::string GetExecPath(const std::vector<std::string>& argv) {
std::string Environment::GetExecPath(const std::vector<std::string>& argv) {
char exec_path_buf[2 * PATH_MAX];
size_t exec_path_len = sizeof(exec_path_buf);
std::string exec_path;
Expand Down Expand Up @@ -772,7 +771,7 @@ Environment::Environment(IsolateData* isolate_data,
timer_base_(uv_now(isolate_data->event_loop())),
exec_argv_(exec_args),
argv_(args),
exec_path_(GetExecPath(args)),
exec_path_(Environment::GetExecPath(args)),
exit_info_(
isolate_, kExitInfoFieldCount, MAYBE_FIELD_PTR(env_info, exit_info)),
should_abort_on_uncaught_toggle_(
Expand Down Expand Up @@ -1922,7 +1921,7 @@ size_t Environment::NearHeapLimitCallback(void* data,

std::string dir = env->options()->diagnostic_dir;
if (dir.empty()) {
dir = env->GetCwd();
dir = Environment::GetCwd(env->exec_path_);
}
DiagnosticFilename name(env, "Heap", "heapsnapshot");
std::string filename = dir + kPathSeparator + (*name);
Expand Down
5 changes: 3 additions & 2 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,9 @@ class Environment : public MemoryRetainer {

SET_MEMORY_INFO_NAME(Environment)

static std::string GetExecPath(const std::vector<std::string>& argv);
static std::string GetCwd(const std::string& exec_path);

inline size_t SelfSize() const override;
bool IsRootNode() const override { return true; }
void MemoryInfo(MemoryTracker* tracker) const override;
Expand All @@ -604,8 +607,6 @@ class Environment : public MemoryRetainer {
// Should be called before InitializeInspector()
void InitializeDiagnostics();

std::string GetCwd();

#if HAVE_INSPECTOR
// If the environment is created for a worker, pass parent_handle and
// the ownership if transferred into the Environment.
Expand Down
4 changes: 3 additions & 1 deletion src/heap_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,9 @@ void TriggerHeapSnapshot(const FunctionCallbackInfo<Value>& args) {
if (filename_v->IsUndefined()) {
DiagnosticFilename name(env, "Heap", "heapsnapshot");
THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kFileSystemWrite, env->GetCwd());
env,
permission::PermissionScope::kFileSystemWrite,
Environment::GetCwd(env->exec_path()));
if (WriteSnapshot(env, *name, options).IsNothing()) return;
if (String::NewFromUtf8(isolate, *name).ToLocal(&filename_v)) {
args.GetReturnValue().Set(filename_v);
Expand Down
6 changes: 4 additions & 2 deletions src/inspector_profiler.cc
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,8 @@ void StartProfilers(Environment* env) {
if (env->options()->cpu_prof) {
const std::string& dir = env->options()->cpu_prof_dir;
env->set_cpu_prof_interval(env->options()->cpu_prof_interval);
env->set_cpu_prof_dir(dir.empty() ? env->GetCwd() : dir);
env->set_cpu_prof_dir(dir.empty() ? Environment::GetCwd(env->exec_path())
: dir);
if (env->options()->cpu_prof_name.empty()) {
DiagnosticFilename filename(env, "CPU", "cpuprofile");
env->set_cpu_prof_name(*filename);
Expand All @@ -446,7 +447,8 @@ void StartProfilers(Environment* env) {
if (env->options()->heap_prof) {
const std::string& dir = env->options()->heap_prof_dir;
env->set_heap_prof_interval(env->options()->heap_prof_interval);
env->set_heap_prof_dir(dir.empty() ? env->GetCwd() : dir);
env->set_heap_prof_dir(dir.empty() ? Environment::GetCwd(env->exec_path())
: dir);
if (env->options()->heap_prof_name.empty()) {
DiagnosticFilename filename(env, "Heap", "heapprofile");
env->set_heap_prof_name(*filename);
Expand Down
26 changes: 23 additions & 3 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
// USE OR OTHER DEALINGS IN THE SOFTWARE.

#include "node.h"
#include "node_dotenv.h"

// ========== local headers ==========

Expand Down Expand Up @@ -140,6 +141,10 @@ using v8::Value;

namespace per_process {

// node_dotenv.h
// Instance is used to store environment variables including NODE_OPTIONS.
node::Dotenv dotenv_file = Dotenv();

// node_revert.h
// Bit flag used to track security reverts.
unsigned int reverted_cve = 0;
Expand Down Expand Up @@ -305,6 +310,10 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
}
#endif

if (env->options()->has_env_file_string) {
per_process::dotenv_file.SetEnvironment(env);
}

// TODO(joyeecheung): move these conditions into JS land and let the
// deserialize main function take precedence. For workers, we need to
// move the pre-execution part into a different file that can be
Expand Down Expand Up @@ -831,11 +840,22 @@ static ExitCode InitializeNodeWithArgsInternal(

HandleEnvOptions(per_process::cli_options->per_isolate->per_env);

std::string node_options;
auto file_path = node::Dotenv::GetPathFromArgs(*argv);

if (file_path.has_value()) {
auto cwd = Environment::GetCwd(Environment::GetExecPath(*argv));
std::string path = cwd + kPathSeparator + file_path.value();
CHECK(!per_process::v8_initialized);
per_process::dotenv_file.ParsePath(path);
per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);
}

#if !defined(NODE_WITHOUT_NODE_OPTIONS)
if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
std::string node_options;

if (credentials::SafeGetenv("NODE_OPTIONS", &node_options)) {
// NODE_OPTIONS environment variable is preferred over the file one.
if (credentials::SafeGetenv("NODE_OPTIONS", &node_options) ||
!node_options.empty()) {
std::vector<std::string> env_argv =
ParseNodeOptionsEnvVar(node_options, errors);

Expand Down
1 change: 0 additions & 1 deletion src/node_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ bool SafeGetenv(const char* key,
}

fail:
text->clear();
return false;
}

Expand Down
164 changes: 164 additions & 0 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#include "node_dotenv.h"
#include "env-inl.h"
#include "node_file.h"
#include "uv.h"

namespace node {

using v8::NewStringType;
using v8::String;

std::optional<std::string> Dotenv::GetPathFromArgs(
const std::vector<std::string>& args) {
std::string_view flag = "--env-file";
// Match the last `--env-file`
// This is required to imitate the default behavior of Node.js CLI argument
// matching.
auto path =
std::find_if(args.rbegin(), args.rend(), [&flag](const std::string& arg) {
return strncmp(arg.c_str(), flag.data(), flag.size()) == 0;
});

if (path == args.rend()) {
return std::nullopt;
}

auto equal_char = path->find('=');

if (equal_char != std::string::npos) {
return path->substr(equal_char + 1);
}

auto next_arg = std::prev(path);

if (next_arg == args.rend()) {
return std::nullopt;
}

return *next_arg;
}

void Dotenv::SetEnvironment(node::Environment* env) {
if (store_.empty()) {
return;
}

auto isolate = env->isolate();

for (const auto& entry : store_) {
auto key = entry.first;
auto value = entry.second;
env->env_vars()->Set(
isolate,
v8::String::NewFromUtf8(
isolate, key.data(), NewStringType::kNormal, key.size())
.ToLocalChecked(),
v8::String::NewFromUtf8(
isolate, value.data(), NewStringType::kNormal, value.size())
.ToLocalChecked());
}
}

void Dotenv::ParsePath(const std::string_view path) {
uv_fs_t req;
auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });

uv_file file = uv_fs_open(nullptr, &req, path.data(), 0, 438, nullptr);
if (req.result < 0) {
// req will be cleaned up by scope leave.
return;
}
uv_fs_req_cleanup(&req);

auto defer_close = OnScopeLeave([file]() {
uv_fs_t close_req;
CHECK_EQ(0, uv_fs_close(nullptr, &close_req, file, nullptr));
uv_fs_req_cleanup(&close_req);
});

std::string result{};
char buffer[8192];
uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer));

while (true) {
auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr);
if (req.result < 0) {
// req will be cleaned up by scope leave.
return;
}
uv_fs_req_cleanup(&req);
if (r <= 0) {
break;
}
result.append(buf.base, r);
}

using std::string_view_literals::operator""sv;
auto lines = SplitString(result, "\n"sv);

for (const auto& line : lines) {
ParseLine(line);
}
}

void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) {
auto match = store_.find("NODE_OPTIONS");

if (match != store_.end()) {
*node_options = match->second;
}
}

void Dotenv::ParseLine(const std::string_view line) {
auto equal_index = line.find('=');

if (equal_index == std::string_view::npos) {
return;
}

auto key = line.substr(0, equal_index);

// Remove leading and trailing space characters from key.
while (!key.empty() && std::isspace(key.front())) key.remove_prefix(1);
while (!key.empty() && std::isspace(key.back())) key.remove_suffix(1);

// Omit lines with comments
if (key.front() == '#' || key.empty()) {
return;
}

auto value = std::string(line.substr(equal_index + 1));

// Might start and end with `"' characters.
auto quotation_index = value.find_first_of("`\"'");

if (quotation_index == 0) {
auto quote_character = value[quotation_index];
value.erase(0, 1);

auto end_quotation_index = value.find_last_of(quote_character);

// We couldn't find the closing quotation character. Terminate.
if (end_quotation_index == std::string::npos) {
return;
}

value.erase(end_quotation_index);
} else {
auto hash_index = value.find('#');

// Remove any inline comments
if (hash_index != std::string::npos) {
value.erase(hash_index);
}

// Remove any leading/trailing spaces from unquoted values.
while (!value.empty() && std::isspace(value.front())) value.erase(0, 1);
while (!value.empty() && std::isspace(value.back()))
value.erase(value.size() - 1);
}

store_.emplace(key, value);
}

} // namespace node
Loading

0 comments on commit 769823e

Please sign in to comment.