Skip to content

Commit 9bf4544

Browse files
committed
feat(serve): Add --tls-cert and --tls-key to deno serve CLI (#25177)
1 parent 49c3825 commit 9bf4544

11 files changed

Lines changed: 192 additions & 25 deletions

File tree

cli/args/flags.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4137,6 +4137,20 @@ Start a server defined in server.ts, watching for changes and running on port 50
41374137
.help("Open the browser on the address that the server is running on.")
41384138
.action(ArgAction::SetTrue)
41394139
)
4140+
.arg(
4141+
Arg::new("tls-cert")
4142+
.long("tls-cert")
4143+
.value_name("FILE")
4144+
.help("Load TLS certificate from PEM encoded file")
4145+
.value_hint(ValueHint::FilePath)
4146+
)
4147+
.arg(
4148+
Arg::new("tls-key")
4149+
.long("tls-key")
4150+
.value_name("FILE")
4151+
.help("Load TLS private key from PEM encoded file")
4152+
.value_hint(ValueHint::FilePath)
4153+
)
41404154
.arg(
41414155
parallel_arg("multiple server workers")
41424156
)
@@ -7842,6 +7856,22 @@ fn serve_parse(
78427856

78437857
flags.tunnel = matches.get_flag("tunnel");
78447858

7859+
let tls_cert = matches.remove_one::<String>("tls-cert");
7860+
let tls_key = matches.remove_one::<String>("tls-key");
7861+
7862+
let tls_cert_and_key = match (tls_cert, tls_key) {
7863+
(Some(cert), Some(key)) => Some((cert, key)),
7864+
(None, None) => None,
7865+
_ => {
7866+
let mut app = app;
7867+
let subcommand = &mut app.find_subcommand_mut("serve").unwrap();
7868+
return Err(subcommand.error(
7869+
clap::error::ErrorKind::MissingRequiredArgument,
7870+
"Both --tls-cert and --tls-key must be specified, or neither",
7871+
));
7872+
}
7873+
};
7874+
78457875
let mut script_arg =
78467876
matches.remove_many::<String>("script_arg").ok_or_else(|| {
78477877
let mut app = app;
@@ -7865,6 +7895,7 @@ fn serve_parse(
78657895
host,
78667896
parallel: matches.get_flag("parallel"),
78677897
open_site,
7898+
tls_cert_and_key,
78687899
});
78697900

78707901
Ok(())
@@ -9664,6 +9695,49 @@ mod tests {
96649695
..Flags::default()
96659696
}
96669697
);
9698+
let r = flags_from_vec(svec![
9699+
"deno",
9700+
"serve",
9701+
"--tls-cert",
9702+
"cert.pem",
9703+
"--tls-key",
9704+
"cert.key",
9705+
"main.ts"
9706+
]);
9707+
assert_eq!(
9708+
r.unwrap(),
9709+
Flags {
9710+
subcommand: DenoSubcommand::Serve(ServeFlags {
9711+
tls_cert_and_key: Some((
9712+
"cert.pem".to_string(),
9713+
"cert.key".to_string()
9714+
)),
9715+
..ServeFlags::new_default("main.ts".to_string(), 8000, "0.0.0.0")
9716+
}),
9717+
permissions: PermissionFlags {
9718+
allow_net: None,
9719+
..Default::default()
9720+
},
9721+
code_cache_enabled: true,
9722+
..Flags::default()
9723+
}
9724+
);
9725+
let r = flags_from_vec(svec![
9726+
"deno",
9727+
"serve",
9728+
"--tls-cert",
9729+
"cert.pem",
9730+
"main.ts"
9731+
]);
9732+
assert!(r.is_err());
9733+
let r = flags_from_vec(svec![
9734+
"deno",
9735+
"serve",
9736+
"--tls-key",
9737+
"cert.key",
9738+
"main.ts"
9739+
]);
9740+
assert!(r.is_err());
96679741
}
96689742

96699743
#[test]

cli/args/mod.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use deno_config::workspace::WorkspaceDirLintConfig;
3434
use deno_config::workspace::WorkspaceDirectory;
3535
use deno_config::workspace::WorkspaceDirectoryRc;
3636
use deno_config::workspace::WorkspaceLintConfig;
37+
use deno_core::anyhow::Context;
3738
use deno_core::anyhow::bail;
3839
use deno_core::error::AnyError;
3940
use deno_core::url::Url;
@@ -694,6 +695,37 @@ impl CliOptions {
694695
}
695696
}
696697

698+
pub fn serve_cert(&self) -> Result<Option<String>, AnyError> {
699+
if let DenoSubcommand::Serve(flags) = self.sub_command() {
700+
flags
701+
.tls_cert_and_key
702+
.as_ref()
703+
.map(|(cert, _key)| {
704+
std::fs::read_to_string(cert).with_context(|| {
705+
format!("Failed to read TLS certificate file: {cert:?}")
706+
})
707+
})
708+
.transpose()
709+
} else {
710+
Ok(None)
711+
}
712+
}
713+
714+
pub fn serve_key(&self) -> Result<Option<String>, AnyError> {
715+
if let DenoSubcommand::Serve(flags) = self.sub_command() {
716+
flags
717+
.tls_cert_and_key
718+
.as_ref()
719+
.map(|(_cert, key)| {
720+
std::fs::read_to_string(key)
721+
.with_context(|| format!("Failed to read TLS key file: {key:?}"))
722+
})
723+
.transpose()
724+
} else {
725+
Ok(None)
726+
}
727+
}
728+
697729
pub fn eszip(&self) -> bool {
698730
self.flags.eszip
699731
}

cli/factory.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,8 @@ impl CliFactory {
12831283
node_ipc_init: cli_options.node_ipc_init(&self.sys())?,
12841284
serve_port: cli_options.serve_port(),
12851285
serve_host: cli_options.serve_host(),
1286+
serve_cert: cli_options.serve_cert()?,
1287+
serve_key: cli_options.serve_key()?,
12861288
otel_config: cli_options.otel_config(),
12871289
no_legacy_abort: cli_options.no_legacy_abort(),
12881290
startup_snapshot: deno_snapshots::CLI_SNAPSHOT,

cli/lib/worker.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,8 @@ pub struct LibMainWorkerOptions {
291291
pub residual_lazy_esm_sources: &'static [(&'static str, &'static str)],
292292
pub serve_port: Option<u16>,
293293
pub serve_host: Option<String>,
294+
pub serve_cert: Option<String>,
295+
pub serve_key: Option<String>,
294296
pub close_on_idle: bool,
295297
pub maybe_initial_cwd: Option<Url>,
296298
/// When true, the `OffscreenCanvas` global is removed at bootstrap.
@@ -493,6 +495,8 @@ impl<TSys: DenoLibSys> LibWorkerFactorySharedState<TSys> {
493495
mode: WorkerExecutionMode::Worker,
494496
serve_port: shared.options.serve_port,
495497
serve_host: shared.options.serve_host.clone(),
498+
serve_cert: shared.options.serve_cert.clone(),
499+
serve_key: shared.options.serve_key.clone(),
496500
otel_config: shared.options.otel_config.clone(),
497501
no_legacy_abort: shared.options.no_legacy_abort,
498502
close_on_idle: args.close_on_idle,
@@ -739,6 +743,8 @@ impl<TSys: DenoLibSys> LibMainWorkerFactory<TSys> {
739743
no_legacy_abort: shared.options.no_legacy_abort,
740744
serve_port: shared.options.serve_port,
741745
serve_host: shared.options.serve_host.clone(),
746+
serve_cert: shared.options.serve_cert.clone(),
747+
serve_key: shared.options.serve_key.clone(),
742748
otel_config: shared.options.otel_config.clone(),
743749
close_on_idle: shared.options.close_on_idle,
744750
disable_offscreen_canvas: shared.options.disable_offscreen_canvas,

cli/rt/run.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,8 @@ pub struct RunOptions {
12231223
pub auto_serve: bool,
12241224
pub serve_port: Option<u16>,
12251225
pub serve_host: Option<String>,
1226+
pub serve_cert: Option<String>,
1227+
pub serve_key: Option<String>,
12261228
/// Enable HMR file watching from this directory.
12271229
pub hmr_watch_dir: Option<PathBuf>,
12281230
/// Callback invoked after each successful HMR replacement.
@@ -1677,6 +1679,8 @@ pub async fn run_with_options(
16771679
node_ipc_init: deno_lib::args::node_ipc_init(&sys)?,
16781680
serve_port: options.serve_port,
16791681
serve_host: options.serve_host,
1682+
serve_cert: options.serve_cert,
1683+
serve_key: options.serve_key,
16801684
otel_config: metadata.otel_config,
16811685
no_legacy_abort: false,
16821686
startup_snapshot: deno_snapshots::CLI_SNAPSHOT,

cli/rt_desktop/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,6 +1669,8 @@ async fn run_desktop(
16691669
auto_serve: true,
16701670
serve_port: Some(desktop_serve_port),
16711671
serve_host: Some("127.0.0.1".to_string()),
1672+
serve_cert: None,
1673+
serve_key: None,
16721674
hmr_watch_dir: if is_framework_dev {
16731675
None
16741676
} else {

cli/tools/serve.rs

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ pub async fn serve(
6969
);
7070

7171
if serve_flags.open_site {
72-
let url = resolve_serve_url(serve_flags.host, serve_flags.port);
72+
let url = resolve_serve_url(
73+
serve_flags.host,
74+
serve_flags.port,
75+
serve_flags.tls_cert_and_key.is_some(),
76+
);
7377
let _ = open::that_detached(url);
7478
}
7579

@@ -232,18 +236,20 @@ async fn serve_with_watch(
232236
Ok(0)
233237
}
234238

235-
fn resolve_serve_url(host: String, port: u16) -> String {
239+
fn resolve_serve_url(host: String, port: u16, https: bool) -> String {
236240
let host = if matches!(host.as_str(), "0.0.0.0" | "::") {
237241
"127.0.0.1".to_string()
238242
} else if std::net::Ipv6Addr::from_str(&host).is_ok() {
239243
format!("[{}]", host)
240244
} else {
241245
host
242246
};
243-
if port == 80 {
244-
format!("http://{host}/")
247+
let (scheme, default_port) =
248+
if https { ("https", 443) } else { ("http", 80) };
249+
if port == default_port {
250+
format!("{scheme}://{host}/")
245251
} else {
246-
format!("http://{host}:{port}/")
252+
format!("{scheme}://{host}:{port}/")
247253
}
248254
}
249255

@@ -254,17 +260,36 @@ mod test {
254260
#[test]
255261
fn test_resolve_serve_url() {
256262
assert_eq!(
257-
resolve_serve_url("localhost".to_string(), 80),
263+
resolve_serve_url("localhost".to_string(), 80, false),
258264
"http://localhost/"
259265
);
260266
assert_eq!(
261-
resolve_serve_url("0.0.0.0".to_string(), 80),
267+
resolve_serve_url("0.0.0.0".to_string(), 80, false),
268+
"http://127.0.0.1/"
269+
);
270+
assert_eq!(
271+
resolve_serve_url("::".to_string(), 80, false),
262272
"http://127.0.0.1/"
263273
);
264-
assert_eq!(resolve_serve_url("::".to_string(), 80), "http://127.0.0.1/");
265274
assert_eq!(
266-
resolve_serve_url("::".to_string(), 90),
275+
resolve_serve_url("::".to_string(), 90, false),
267276
"http://127.0.0.1:90/"
268277
);
278+
assert_eq!(
279+
resolve_serve_url("localhost".to_string(), 443, true),
280+
"https://localhost/"
281+
);
282+
assert_eq!(
283+
resolve_serve_url("0.0.0.0".to_string(), 443, true),
284+
"https://127.0.0.1/"
285+
);
286+
assert_eq!(
287+
resolve_serve_url("::".to_string(), 443, true),
288+
"https://127.0.0.1/"
289+
);
290+
assert_eq!(
291+
resolve_serve_url("::".to_string(), 90, true),
292+
"https://127.0.0.1:90/"
293+
);
269294
}
270295
}

ext/http/00_serve.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,11 +1689,15 @@ function registerDeclarativeServer(exports) {
16891689
return ({
16901690
servePort,
16911691
serveHost,
1692+
serveCert,
1693+
serveKey,
16921694
workerCountWhenMain,
16931695
}) => {
16941696
const server = Deno.serve({
16951697
port: servePort,
16961698
hostname: serveHost,
1699+
cert: serveCert ?? undefined,
1700+
key: serveKey ?? undefined,
16971701
[kLoadBalanced]: workerCountWhenMain == null
16981702
? true
16991703
: workerCountWhenMain > 0,
@@ -1706,11 +1710,13 @@ function registerDeclarativeServer(exports) {
17061710

17071711
let target;
17081712
switch (localAddr.transport) {
1709-
case "tcp":
1710-
target = `http://${
1713+
case "tcp": {
1714+
const protocol = serveCert === null ? "http" : "https";
1715+
target = `${protocol}://${
17111716
formatHostName(localAddr.hostname)
17121717
}:${localAddr.port}/`;
17131718
break;
1719+
}
17141720
case "unix":
17151721
target = localAddr.path;
17161722
break;

libs/cli_parser/src/flags.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ pub struct ServeFlags {
576576
pub host: String,
577577
pub parallel: bool,
578578
pub open_site: bool,
579+
pub tls_cert_and_key: Option<(String, String)>,
579580
}
580581

581582
impl ServeFlags {
@@ -587,6 +588,7 @@ impl ServeFlags {
587588
host: host.to_owned(),
588589
parallel: false,
589590
open_site: false,
591+
tls_cert_and_key: None,
590592
}
591593
}
592594
}

runtime/js/99_main.js

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -883,14 +883,16 @@ function bootstrapMainRuntime(runtimeOptions, warmup = false) {
883883
8: mode,
884884
9: servePort,
885885
10: serveHost,
886-
11: serveIsMain,
887-
12: serveWorkerCountOrIndex,
888-
13: otelConfig,
889-
15: standalone,
890-
16: autoServe,
891-
17: nodeClusterUniqueId,
892-
18: nodeClusterSchedPolicy,
893-
19: disableOffscreenCanvas,
886+
11: serveCert,
887+
12: serveKey,
888+
13: serveIsMain,
889+
14: serveWorkerCountOrIndex,
890+
15: otelConfig,
891+
17: standalone,
892+
18: autoServe,
893+
19: nodeClusterUniqueId,
894+
20: nodeClusterSchedPolicy,
895+
21: disableOffscreenCanvas,
894896
} = runtimeOptions;
895897

896898
denoNs.build.standalone = standalone;
@@ -962,6 +964,8 @@ function bootstrapMainRuntime(runtimeOptions, warmup = false) {
962964
serve({
963965
servePort,
964966
serveHost,
967+
serveCert,
968+
serveKey,
965969
workerCountWhenMain: serveIsMain_
966970
? serveWorkerCountOrIndex_
967971
: undefined,
@@ -1137,16 +1141,16 @@ function bootstrapWorkerRuntime(
11371141
5: hasNodeModulesDir,
11381142
6: argv0,
11391143
7: nodeDebug,
1140-
13: otelConfig,
1141-
15: standalone,
1142-
17: nodeClusterUniqueId,
1143-
18: nodeClusterSchedPolicy,
1144-
19: disableOffscreenCanvas,
1144+
15: otelConfig,
1145+
17: standalone,
1146+
19: nodeClusterUniqueId,
1147+
20: nodeClusterSchedPolicy,
1148+
21: disableOffscreenCanvas,
11451149
} = runtimeOptions;
11461150

11471151
denoNs.build.standalone = standalone;
11481152

1149-
closeOnIdle = runtimeOptions[14];
1153+
closeOnIdle = runtimeOptions[16];
11501154

11511155
performance.setTimeOrigin();
11521156
globalThis_ = globalThis;

0 commit comments

Comments
 (0)