diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index ddf597a0ab..fd62a9486f 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -935,6 +935,29 @@ declare namespace Deno { certChain?: string; /** PEM formatted (RSA or PKCS8) private key of client certificate. */ privateKey?: string; + /** **UNSTABLE**: new API, yet to be vetted. + * + * Application-Layer Protocol Negotiation (ALPN) protocols supported by + * the client. If not specified, no ALPN extension will be included in the + * TLS handshake. + */ + alpnProtocols?: string[]; + } + + export interface TlsHandshakeInfo { + /** **UNSTABLE**: new API, yet to be vetted. + * + * Contains the ALPN protocol selected during negotiation with the server. + * If no ALPN protocol selected, returns `null`. + */ + alpnProtocol: string | null; + } + + export interface TlsConn extends Conn { + /** Runs the client or server handshake protocol to completion if that has + * not happened yet. Calling this method is optional; the TLS handshake + * will be completed automatically as soon as data is sent or received. */ + handshake(): Promise; } /** **UNSTABLE** New API, yet to be vetted. @@ -964,6 +987,16 @@ declare namespace Deno { alpnProtocols?: string[]; } + export interface StartTlsOptions { + /** **UNSTABLE**: new API, yet to be vetted. + * + * Application-Layer Protocol Negotiation (ALPN) protocols to announce to + * the client. If not specified, no ALPN extension will be included in the + * TLS handshake. + */ + alpnProtocols?: string[]; + } + /** **UNSTABLE**: New API should be tested first. * * Acquire an advisory file-system lock for the provided file. `exclusive` diff --git a/cli/tests/unit/tls_test.ts b/cli/tests/unit/tls_test.ts index 4062ef5046..7e6d689005 100644 --- a/cli/tests/unit/tls_test.ts +++ b/cli/tests/unit/tls_test.ts @@ -244,6 +244,49 @@ async function tlsPair(): Promise<[Deno.Conn, Deno.Conn]> { return endpoints; } +async function tlsAlpn( + useStartTls: boolean, +): Promise<[Deno.TlsConn, Deno.TlsConn]> { + const port = getPort(); + const listener = Deno.listenTls({ + hostname: "localhost", + port, + certFile: "cli/tests/testdata/tls/localhost.crt", + keyFile: "cli/tests/testdata/tls/localhost.key", + alpnProtocols: ["deno", "rocks"], + }); + + const acceptPromise = listener.accept(); + + const caCerts = [Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem")]; + const clientAlpnProtocols = ["rocks", "rises"]; + let endpoints: [Deno.TlsConn, Deno.TlsConn]; + + if (!useStartTls) { + const connectPromise = Deno.connectTls({ + hostname: "localhost", + port, + caCerts, + alpnProtocols: clientAlpnProtocols, + }); + endpoints = await Promise.all([acceptPromise, connectPromise]); + } else { + const client = await Deno.connect({ + hostname: "localhost", + port, + }); + const connectPromise = Deno.startTls(client, { + hostname: "localhost", + caCerts, + alpnProtocols: clientAlpnProtocols, + }); + endpoints = await Promise.all([acceptPromise, connectPromise]); + } + + listener.close(); + return endpoints; +} + async function sendThenCloseWriteThenReceive( conn: Deno.Conn, chunkCount: number, @@ -305,6 +348,38 @@ async function receiveThenSend( conn.close(); } +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerAlpnListenConnect() { + const [serverConn, clientConn] = await tlsAlpn(false); + const [serverHS, clientHS] = await Promise.all([ + serverConn.handshake(), + clientConn.handshake(), + ]); + assertStrictEquals(serverHS.alpnProtocol, "rocks"); + assertStrictEquals(clientHS.alpnProtocol, "rocks"); + + serverConn.close(); + clientConn.close(); + }, +); + +Deno.test( + { permissions: { read: true, net: true } }, + async function tlsServerAlpnListenStartTls() { + const [serverConn, clientConn] = await tlsAlpn(true); + const [serverHS, clientHS] = await Promise.all([ + serverConn.handshake(), + clientConn.handshake(), + ]); + assertStrictEquals(serverHS.alpnProtocol, "rocks"); + assertStrictEquals(clientHS.alpnProtocol, "rocks"); + + serverConn.close(); + clientConn.close(); + }, +); + Deno.test( { permissions: { read: true, net: true } }, async function tlsServerStreamHalfCloseSendOneByte() { diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js index 9ae6cb055c..00acd7c969 100644 --- a/ext/net/02_tls.js +++ b/ext/net/02_tls.js @@ -41,6 +41,7 @@ caCerts = [], certChain = undefined, privateKey = undefined, + alpnProtocols = undefined, }) { const res = await opConnectTls({ port, @@ -50,6 +51,7 @@ caCerts, certChain, privateKey, + alpnProtocols, }); return new TlsConn(res.rid, res.remoteAddr, res.localAddr); } @@ -67,7 +69,7 @@ keyFile, hostname = "0.0.0.0", transport = "tcp", - alpnProtocols, + alpnProtocols = undefined, }) { const res = opListenTls({ port, @@ -82,13 +84,19 @@ async function startTls( conn, - { hostname = "127.0.0.1", certFile = undefined, caCerts = [] } = {}, + { + hostname = "127.0.0.1", + certFile = undefined, + caCerts = [], + alpnProtocols = undefined, + } = {}, ) { const res = await opStartTls({ rid: conn.rid, hostname, certFile, caCerts, + alpnProtocols, }); return new TlsConn(res.rid, res.remoteAddr, res.localAddr); } diff --git a/ext/net/lib.deno_net.d.ts b/ext/net/lib.deno_net.d.ts index 81c248871d..accf01f964 100644 --- a/ext/net/lib.deno_net.d.ts +++ b/ext/net/lib.deno_net.d.ts @@ -52,11 +52,14 @@ declare namespace Deno { closeWrite(): Promise; } + // deno-lint-ignore no-empty-interface + export interface TlsHandshakeInfo {} + export interface TlsConn extends Conn { /** Runs the client or server handshake protocol to completion if that has * not happened yet. Calling this method is optional; the TLS handshake * will be completed automatically as soon as data is sent or received. */ - handshake(): Promise; + handshake(): Promise; } export interface ListenOptions { diff --git a/ext/net/ops.rs b/ext/net/ops.rs index d4fa2e5da6..1f70052477 100644 --- a/ext/net/ops.rs +++ b/ext/net/ops.rs @@ -12,6 +12,7 @@ use deno_core::error::AnyError; use deno_core::op_async; use deno_core::op_sync; use deno_core::AsyncRefCell; +use deno_core::ByteString; use deno_core::CancelHandle; use deno_core::CancelTryFuture; use deno_core::OpPair; @@ -84,6 +85,12 @@ pub struct OpPacket { pub remote_addr: OpAddr, } +#[derive(Serialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct TlsHandshakeInfo { + pub alpn_protocol: Option, +} + #[derive(Serialize)] pub struct IpAddr { pub hostname: String, diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs index 87744ed63a..fd2308ef18 100644 --- a/ext/net/ops_tls.rs +++ b/ext/net/ops_tls.rs @@ -4,6 +4,7 @@ use crate::io::TcpStreamResource; use crate::ops::IpAddr; use crate::ops::OpAddr; use crate::ops::OpConn; +use crate::ops::TlsHandshakeInfo; use crate::resolve_addr::resolve_addr; use crate::resolve_addr::resolve_addr_sync; use crate::DefaultTlsOptions; @@ -29,6 +30,7 @@ use deno_core::op_sync; use deno_core::parking_lot::Mutex; use deno_core::AsyncRefCell; use deno_core::AsyncResult; +use deno_core::ByteString; use deno_core::CancelHandle; use deno_core::CancelTryFuture; use deno_core::OpPair; @@ -54,7 +56,6 @@ use io::Read; use io::Write; use serde::Deserialize; use std::borrow::Cow; -use std::cell::Cell; use std::cell::RefCell; use std::convert::From; use std::fs::File; @@ -190,6 +191,14 @@ impl TlsStream { fn poll_handshake(&mut self, cx: &mut Context<'_>) -> Poll> { self.inner_mut().poll_handshake(cx) } + + fn get_alpn_protocol(&mut self) -> Option { + self + .inner_mut() + .tls + .get_alpn_protocol() + .map(|s| ByteString(s.to_owned())) + } } impl AsyncRead for TlsStream { @@ -549,6 +558,10 @@ impl WriteHalf { }) .await } + + fn get_alpn_protocol(&mut self) -> Option { + self.shared.get_alpn_protocol() + } } impl AsyncWrite for WriteHalf { @@ -658,6 +671,11 @@ impl Shared { fn drop_shared_waker(self_ptr: *const ()) { let _ = unsafe { Weak::from_raw(self_ptr as *const Self) }; } + + fn get_alpn_protocol(self: &Arc) -> Option { + let mut tls_stream = self.tls_stream.lock(); + tls_stream.get_alpn_protocol() + } } struct ImplementReadTrait<'a, T>(&'a mut T); @@ -698,7 +716,8 @@ pub fn init() -> Vec { pub struct TlsStreamResource { rd: AsyncRefCell, wr: AsyncRefCell, - handshake_done: Cell, + // `None` when a TLS handshake hasn't been done. + handshake_info: RefCell>, cancel_handle: CancelHandle, // Only read and handshake ops get canceled. } @@ -707,7 +726,7 @@ impl TlsStreamResource { Self { rd: rd.into(), wr: wr.into(), - handshake_done: Cell::new(false), + handshake_info: RefCell::new(None), cancel_handle: Default::default(), } } @@ -744,14 +763,21 @@ impl TlsStreamResource { Ok(()) } - pub async fn handshake(self: &Rc) -> Result<(), AnyError> { - if !self.handshake_done.get() { - let mut wr = RcRef::map(self, |r| &r.wr).borrow_mut().await; - let cancel_handle = RcRef::map(self, |r| &r.cancel_handle); - wr.handshake().try_or_cancel(cancel_handle).await?; - self.handshake_done.set(true); + pub async fn handshake( + self: &Rc, + ) -> Result { + if let Some(tls_info) = &*self.handshake_info.borrow() { + return Ok(tls_info.clone()); } - Ok(()) + + let mut wr = RcRef::map(self, |r| &r.wr).borrow_mut().await; + let cancel_handle = RcRef::map(self, |r| &r.cancel_handle); + wr.handshake().try_or_cancel(cancel_handle).await?; + + let alpn_protocol = wr.get_alpn_protocol(); + let tls_info = TlsHandshakeInfo { alpn_protocol }; + self.handshake_info.replace(Some(tls_info.clone())); + Ok(tls_info) } } @@ -787,6 +813,7 @@ pub struct ConnectTlsArgs { ca_certs: Vec, cert_chain: Option, private_key: Option, + alpn_protocols: Option>, } #[derive(Deserialize)] @@ -795,6 +822,7 @@ pub struct StartTlsArgs { rid: ResourceId, ca_certs: Vec, hostname: String, + alpn_protocols: Option>, } pub async fn op_tls_start( @@ -851,11 +879,20 @@ where let local_addr = tcp_stream.local_addr()?; let remote_addr = tcp_stream.peer_addr()?; - let tls_config = Arc::new(create_client_config( + let mut tls_config = create_client_config( root_cert_store, ca_certs, unsafely_ignore_certificate_errors, - )?); + )?; + + if let Some(alpn_protocols) = args.alpn_protocols { + super::check_unstable2(&state, "Deno.startTls#alpnProtocols"); + tls_config.alpn_protocols = + alpn_protocols.into_iter().map(|s| s.into_bytes()).collect(); + } + + let tls_config = Arc::new(tls_config); + let tls_stream = TlsStream::new_client_side(tcp_stream, &tls_config, hostname_dns); @@ -948,6 +985,12 @@ where unsafely_ignore_certificate_errors, )?; + if let Some(alpn_protocols) = args.alpn_protocols { + super::check_unstable2(&state, "Deno.connectTls#alpnProtocols"); + tls_config.alpn_protocols = + alpn_protocols.into_iter().map(|s| s.into_bytes()).collect(); + } + if args.cert_chain.is_some() || args.private_key.is_some() { let cert_chain = args .cert_chain @@ -1144,7 +1187,7 @@ pub async fn op_tls_handshake( state: Rc>, rid: ResourceId, _: (), -) -> Result<(), AnyError> { +) -> Result { let resource = state .borrow() .resource_table