summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYury Selivanov <yury@edgedb.com>2021-11-26 10:59:53 -0800
committerGitHub <noreply@github.com>2021-11-26 19:59:53 +0100
commit1d3f734e1815bf1649e0cac445be9eacb4cd296d (patch)
treef0178e951f3f44313def5d7ea8e2e391219f7791
parentd763633781be484bb19b458208dd7c11efb83228 (diff)
feat(ext/net): ALPN support in `Deno.connectTls()` (#12786)
-rw-r--r--cli/dts/lib.deno.unstable.d.ts33
-rw-r--r--cli/tests/unit/tls_test.ts75
-rw-r--r--ext/net/02_tls.js12
-rw-r--r--ext/net/lib.deno_net.d.ts5
-rw-r--r--ext/net/ops.rs7
-rw-r--r--ext/net/ops_tls.rs69
6 files changed, 185 insertions, 16 deletions
diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts
index ddf597a0a..fd62a9486 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<TlsHandshakeInfo>;
}
/** **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 4062ef504..7e6d68900 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,
@@ -307,6 +350,38 @@ async function receiveThenSend(
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() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
diff --git a/ext/net/02_tls.js b/ext/net/02_tls.js
index 9ae6cb055..00acd7c96 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 81c248871..accf01f96 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<void>;
}
+ // 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<void>;
+ handshake(): Promise<TlsHandshakeInfo>;
}
export interface ListenOptions {
diff --git a/ext/net/ops.rs b/ext/net/ops.rs
index d4fa2e5da..1f7005247 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<ByteString>,
+}
+
#[derive(Serialize)]
pub struct IpAddr {
pub hostname: String,
diff --git a/ext/net/ops_tls.rs b/ext/net/ops_tls.rs
index 87744ed63..fd2308ef1 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<io::Result<()>> {
self.inner_mut().poll_handshake(cx)
}
+
+ fn get_alpn_protocol(&mut self) -> Option<ByteString> {
+ 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<ByteString> {
+ 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<Self>) -> Option<ByteString> {
+ 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<P: NetPermissions + 'static>() -> Vec<OpPair> {
pub struct TlsStreamResource {
rd: AsyncRefCell<ReadHalf>,
wr: AsyncRefCell<WriteHalf>,
- handshake_done: Cell<bool>,
+ // `None` when a TLS handshake hasn't been done.
+ handshake_info: RefCell<Option<TlsHandshakeInfo>>,
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<Self>) -> 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<Self>,
+ ) -> Result<TlsHandshakeInfo, AnyError> {
+ 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<String>,
cert_chain: Option<String>,
private_key: Option<String>,
+ alpn_protocols: Option<Vec<String>>,
}
#[derive(Deserialize)]
@@ -795,6 +822,7 @@ pub struct StartTlsArgs {
rid: ResourceId,
ca_certs: Vec<String>,
hostname: String,
+ alpn_protocols: Option<Vec<String>>,
}
pub async fn op_tls_start<NP>(
@@ -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<RefCell<OpState>>,
rid: ResourceId,
_: (),
-) -> Result<(), AnyError> {
+) -> Result<TlsHandshakeInfo, AnyError> {
let resource = state
.borrow()
.resource_table