From 3e8180c793f1dd7437a497ffdb0cf7e919a9a5c3 Mon Sep 17 00:00:00 2001 From: Yoshiya Hinosawa Date: Thu, 24 Feb 2022 13:16:56 +0900 Subject: [PATCH] feat(ext/net): support cert, key options in listenTls (#13740) --- cli/tests/unit/tls_test.ts | 62 +++++++++++++++++++++++++++++++++++--- ext/net/02_tls.js | 4 +++ ext/net/lib.deno_net.d.ts | 18 ++++++++--- ext/net/ops_tls.rs | 48 +++++++++++++++++++++++------ 4 files changed, 113 insertions(+), 19 deletions(-) diff --git a/cli/tests/unit/tls_test.ts b/cli/tests/unit/tls_test.ts index 4b3dba0827..26a1668013 100644 --- a/cli/tests/unit/tls_test.ts +++ b/cli/tests/unit/tls_test.ts @@ -15,6 +15,9 @@ import { TextProtoReader } from "../../../test_util/std/textproto/mod.ts"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); +const cert = await Deno.readTextFile("cli/tests/testdata/tls/localhost.crt"); +const key = await Deno.readTextFile("cli/tests/testdata/tls/localhost.key"); +const caCerts = [await Deno.readTextFile("cli/tests/testdata/tls/RootCA.pem")]; async function sleep(msec: number) { await new Promise((res, _rej) => setTimeout(res, msec)); @@ -184,11 +187,60 @@ Deno.test( }, ); - const conn = await Deno.connectTls({ - hostname, - port, - caCerts: [Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem")], - }); + const conn = await Deno.connectTls({ hostname, port, caCerts }); + assert(conn.rid > 0); + const w = new BufWriter(conn); + const r = new BufReader(conn); + const body = `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`; + const writeResult = await w.write(encoder.encode(body)); + assertEquals(body.length, writeResult); + await w.flush(); + const tpr = new TextProtoReader(r); + const statusLine = await tpr.readLine(); + assert(statusLine !== null, `line must be read: ${String(statusLine)}`); + const m = statusLine.match(/^(.+?) (.+?) (.+?)$/); + assert(m !== null, "must be matched"); + const [_, proto, status, ok] = m; + assertEquals(proto, "HTTP/1.1"); + assertEquals(status, "200"); + assertEquals(ok, "OK"); + const headers = await tpr.readMIMEHeader(); + assert(headers !== null); + const contentLength = parseInt(headers.get("content-length")!); + const bodyBuf = new Uint8Array(contentLength); + await r.readFull(bodyBuf); + assertEquals(decoder.decode(bodyBuf), "Hello World\n"); + conn.close(); + listener.close(); + await resolvable; + }, +); +Deno.test( + { permissions: { read: false, net: true } }, + async function listenTlsWithCertAndKey() { + const resolvable = deferred(); + const hostname = "localhost"; + const port = 3500; + + const listener = Deno.listenTls({ hostname, port, cert, key }); + + const response = encoder.encode( + "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n", + ); + + listener.accept().then( + async (conn) => { + assert(conn.remoteAddr != null); + assert(conn.localAddr != null); + await conn.write(response); + setTimeout(() => { + conn.close(); + resolvable.resolve(); + }, 0); + }, + ); + + const conn = await Deno.connectTls({ hostname, port, caCerts }); assert(conn.rid > 0); const w = new BufWriter(conn); const r = new BufReader(conn); diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 90f395193c..86f6515211 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -65,7 +65,9 @@ function listenTls({ port, + cert, certFile, + key, keyFile, hostname = "0.0.0.0", transport = "tcp", @@ -73,7 +75,9 @@ }) { const res = opListenTls({ port, + cert, certFile, + key, keyFile, hostname, transport, diff --git a/ext/net/lib.deno_net.d.ts b/ext/net/lib.deno_net.d.ts index 048037d02a..0384520555 100644 --- a/ext/net/lib.deno_net.d.ts +++ b/ext/net/lib.deno_net.d.ts @@ -93,11 +93,21 @@ declare namespace Deno { ): Listener; export interface ListenTlsOptions extends ListenOptions { + /** Server private key in PEM format */ + key?: string; + /** Cert chain in PEM format */ + cert?: string; /** Path to a file containing a PEM formatted CA certificate. Requires - * `--allow-read`. */ - certFile: string; - /** Server public key file. Requires `--allow-read`.*/ - keyFile: string; + * `--allow-read`. + * + * @deprecated This option is deprecated and will be removed in Deno 2.0. + */ + certFile?: string; + /** Server private key file. Requires `--allow-read`. + * + * @deprecated This option is deprecated and will be removed in Deno 2.0. + */ + keyFile?: string; transport?: "tcp"; } diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index 64f1d458ba..7fce34820f 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -1004,8 +1004,12 @@ pub struct ListenTlsArgs { transport: String, hostname: String, port: u16, - cert_file: String, - key_file: String, + cert: Option, + // TODO(kt3k): Remove this option at v2.0. + cert_file: Option, + key: Option, + // TODO(kt3k): Remove this option at v2.0. + key_file: Option, alpn_protocols: Option>, } @@ -1020,23 +1024,47 @@ where assert_eq!(args.transport, "tcp"); let hostname = &*args.hostname; let port = args.port; - let cert_file = &*args.cert_file; - let key_file = &*args.key_file; + let cert_file = args.cert_file.as_deref(); + let key_file = args.key_file.as_deref(); + let cert = args.cert.as_deref(); + let key = args.key.as_deref(); { let permissions = state.borrow_mut::(); permissions.check_net(&(hostname, Some(port)))?; - permissions.check_read(Path::new(cert_file))?; - permissions.check_read(Path::new(key_file))?; + if let Some(path) = cert_file { + permissions.check_read(Path::new(path))?; + } + if let Some(path) = key_file { + permissions.check_read(Path::new(path))?; + } } + let cert_chain = if cert_file.is_some() && cert.is_some() { + return Err(generic_error("Both cert and certFile is specified. You can specify either one of them.")); + } else if let Some(path) = cert_file { + load_certs_from_file(path)? + } else if let Some(cert) = cert { + load_certs(&mut BufReader::new(cert.as_bytes()))? + } else { + return Err(generic_error("`cert` is not specified.")); + }; + let key_der = if key_file.is_some() && key.is_some() { + return Err(generic_error( + "Both key and keyFile is specified. You can specify either one of them.", + )); + } else if let Some(path) = key_file { + load_private_keys_from_file(path)?.remove(0) + } else if let Some(key) = key { + load_private_keys(key.as_bytes())?.remove(0) + } else { + return Err(generic_error("`key` is not specified.")); + }; + let mut tls_config = ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() - .with_single_cert( - load_certs_from_file(cert_file)?, - load_private_keys_from_file(key_file)?.remove(0), - ) + .with_single_cert(cert_chain, key_der) .expect("invalid key or certificate"); if let Some(alpn_protocols) = args.alpn_protocols { super::check_unstable(state, "Deno.listenTls#alpn_protocols");