From 82c97d3fd498d7fd4358dbf4eb382a1063c7d1b2 Mon Sep 17 00:00:00 2001 From: losfair Date: Tue, 2 Jul 2024 00:06:24 +0800 Subject: [PATCH] feat(serve): env var `DENO_SERVE_ADDRESS` for configuring default listen address --- Cargo.lock | 1 + cli/args/flags.rs | 2 + ext/http/00_serve.ts | 42 +++++++++++++++++ tests/Cargo.toml | 1 + tests/integration/serve_tests.rs | 75 +++++++++++++++++++++++++++++++ tests/testdata/serve/run_serve.ts | 3 ++ 6 files changed, 124 insertions(+) create mode 100644 tests/testdata/serve/run_serve.ts diff --git a/Cargo.lock b/Cargo.lock index 4bf79e1fa5..09949cffa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -744,6 +744,7 @@ dependencies = [ "pretty_assertions", "regex", "serde", + "tempfile", "test_server", "tokio", "tower-lsp", diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 3298d6f83e..b4045579e1 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -1100,6 +1100,8 @@ static ENV_VARIABLES_HELP: &str = color_print::cstr!( DENO_NO_UPDATE_CHECK Set to disable checking if a newer Deno version is available + DENO_SERVE_ADDRESS Default listening address for `Deno.serve()` + DENO_TLS_CA_STORE Comma-separated list of order dependent certificate stores. Possible values: "system", "mozilla". Defaults to "mozilla". diff --git a/ext/http/00_serve.ts b/ext/http/00_serve.ts index 670b64676e..b1816c2b9d 100644 --- a/ext/http/00_serve.ts +++ b/ext/http/00_serve.ts @@ -595,6 +595,48 @@ function serve(arg1, arg2) { options = { __proto__: null }; } + const canOverrideOptions = !ObjectHasOwn(options, "path") + && !ObjectHasOwn(options, "hostname") + && !ObjectHasOwn(options, "port"); + const env = Deno.permissions.querySync({ name: "env", variable: "DENO_SERVE_ADDRESS" }).state === "granted" + && Deno.env.get("DENO_SERVE_ADDRESS"); + + if (canOverrideOptions && env) { + const delim = env.indexOf("/"); + if (delim >= 0) { + const network = env.slice(0, delim); + const address = env.slice(delim + 1); + + switch (network) { + case "tcp": { + const ipv6Delim = address.indexOf("]"); + let hostname: string; + let port: number; + if (ipv6Delim >= 0) { + hostname = address.slice(0, ipv6Delim + 1); + port = parseInt(address.slice(ipv6Delim + 2)); + } else { + const [hostname_, port_] = address.split(":"); + hostname = hostname_; + port = parseInt(port_); + } + if (!Number.isSafeInteger(port) || port < 0 || port > 65535) { + throw new TypeError(`DENO_SERVE_ADDRESS: Invalid port: ${port}`); + } + options.hostname = hostname; + options.port = port; + break; + } + case "unix": { + options.path = address; + break; + } + default: + console.error(`DENO_SERVE_ADDRESS: Invalid network type: ${network}`); + } + } + } + const wantsHttps = hasTlsKeyPairOptions(options); const wantsUnix = ObjectHasOwn(options, "path"); const signal = options.signal; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 9d513571b1..a1ad53fab8 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -54,6 +54,7 @@ os_pipe.workspace = true pretty_assertions.workspace = true regex.workspace = true serde.workspace = true +tempfile.workspace = true test_util.workspace = true tokio.workspace = true tower-lsp.workspace = true diff --git a/tests/integration/serve_tests.rs b/tests/integration/serve_tests.rs index 85de068c9c..cdb15cc065 100644 --- a/tests/integration/serve_tests.rs +++ b/tests/integration/serve_tests.rs @@ -1,5 +1,7 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::io::BufRead; +use std::io::BufReader; use std::io::Read; use deno_fetch::reqwest; @@ -92,3 +94,76 @@ async fn deno_serve_no_args() { child.kill().unwrap(); child.wait().unwrap(); } + +#[tokio::test] +async fn deno_run_serve_with_tcp_from_env() { + let mut child = util::deno_cmd() + .current_dir(util::testdata_path()) + .arg("run") + .arg("--allow-env=DENO_SERVE_ADDRESS") + .arg("--allow-net") + .arg("./serve/run_serve.ts") + .env("DENO_SERVE_ADDRESS", format!("tcp/127.0.0.1:0")) + .stdout_piped() + .spawn() + .unwrap(); + let stdout = BufReader::new(child.stdout.as_mut().unwrap()); + let msg = stdout.lines().next().unwrap().unwrap(); + + // Deno.serve() listens on 0.0.0.0 by default. This checks DENO_SERVE_ADDRESS + // is not ignored by ensuring it's listening on 127.0.0.1. + let port_regex = Regex::new(r"http:\/\/127\.0\.0\.1:(\d+)").unwrap(); + let port = port_regex.captures(&msg).unwrap().get(1).unwrap().as_str(); + + let client = reqwest::Client::builder().build().unwrap(); + + let res = client + .get(&format!("http://127.0.0.1:{port}")) + .send() + .await + .unwrap(); + assert_eq!(200, res.status()); + + let body = res.text().await.unwrap(); + assert_eq!(body, "Deno.serve() works!"); + + child.kill().unwrap(); + child.wait().unwrap(); +} + +#[tokio::test] +#[cfg(unix)] +async fn deno_run_serve_with_unix_socket_from_env() { + use tokio::io::AsyncReadExt; + use tokio::io::AsyncWriteExt; + use tokio::net::UnixStream; + + let dir = tempfile::TempDir::new().unwrap(); + let sock = dir.path().join("listen.sock"); + let mut child = util::deno_cmd() + .current_dir(util::testdata_path()) + .arg("run") + .arg("--allow-env=DENO_SERVE_ADDRESS") + .arg(format!("--allow-read={}", sock.display())) + .arg(format!("--allow-write={}", sock.display())) + .arg("./serve/run_serve.ts") + .env("DENO_SERVE_ADDRESS", format!("unix/{}", sock.display())) + .stdout_piped() + .spawn() + .unwrap(); + let stdout = BufReader::new(child.stdout.as_mut().unwrap()); + stdout.lines().next().unwrap().unwrap(); + + // reqwest does not support connecting to unix sockets yet, so here we send the http + // payload directly + let mut conn = UnixStream::connect(dir.path().join("listen.sock")) + .await + .unwrap(); + conn.write_all(b"GET / HTTP/1.0\r\n\r\n").await.unwrap(); + let mut response = String::new(); + conn.read_to_string(&mut response).await.unwrap(); + assert!(response.ends_with("\r\nDeno.serve() works!")); + + child.kill().unwrap(); + child.wait().unwrap(); +} diff --git a/tests/testdata/serve/run_serve.ts b/tests/testdata/serve/run_serve.ts new file mode 100644 index 0000000000..120e01dd53 --- /dev/null +++ b/tests/testdata/serve/run_serve.ts @@ -0,0 +1,3 @@ +Deno.serve((_req: Request) => { + return new Response("Deno.serve() works!"); +}) \ No newline at end of file