1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 08:09:08 -05:00

feat(ext/net): ALPN support in Deno.connectTls() (#12786)

This commit is contained in:
Yury Selivanov 2021-11-26 10:59:53 -08:00 committed by GitHub
parent d763633781
commit 1d3f734e18
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 185 additions and 16 deletions

View file

@ -935,6 +935,29 @@ declare namespace Deno {
certChain?: string; certChain?: string;
/** PEM formatted (RSA or PKCS8) private key of client certificate. */ /** PEM formatted (RSA or PKCS8) private key of client certificate. */
privateKey?: string; 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. /** **UNSTABLE** New API, yet to be vetted.
@ -964,6 +987,16 @@ declare namespace Deno {
alpnProtocols?: string[]; 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. /** **UNSTABLE**: New API should be tested first.
* *
* Acquire an advisory file-system lock for the provided file. `exclusive` * Acquire an advisory file-system lock for the provided file. `exclusive`

View file

@ -244,6 +244,49 @@ async function tlsPair(): Promise<[Deno.Conn, Deno.Conn]> {
return endpoints; 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( async function sendThenCloseWriteThenReceive(
conn: Deno.Conn, conn: Deno.Conn,
chunkCount: number, chunkCount: number,
@ -305,6 +348,38 @@ async function receiveThenSend(
conn.close(); 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( Deno.test(
{ permissions: { read: true, net: true } }, { permissions: { read: true, net: true } },
async function tlsServerStreamHalfCloseSendOneByte() { async function tlsServerStreamHalfCloseSendOneByte() {

View file

@ -41,6 +41,7 @@
caCerts = [], caCerts = [],
certChain = undefined, certChain = undefined,
privateKey = undefined, privateKey = undefined,
alpnProtocols = undefined,
}) { }) {
const res = await opConnectTls({ const res = await opConnectTls({
port, port,
@ -50,6 +51,7 @@
caCerts, caCerts,
certChain, certChain,
privateKey, privateKey,
alpnProtocols,
}); });
return new TlsConn(res.rid, res.remoteAddr, res.localAddr); return new TlsConn(res.rid, res.remoteAddr, res.localAddr);
} }
@ -67,7 +69,7 @@
keyFile, keyFile,
hostname = "0.0.0.0", hostname = "0.0.0.0",
transport = "tcp", transport = "tcp",
alpnProtocols, alpnProtocols = undefined,
}) { }) {
const res = opListenTls({ const res = opListenTls({
port, port,
@ -82,13 +84,19 @@
async function startTls( async function startTls(
conn, conn,
{ hostname = "127.0.0.1", certFile = undefined, caCerts = [] } = {}, {
hostname = "127.0.0.1",
certFile = undefined,
caCerts = [],
alpnProtocols = undefined,
} = {},
) { ) {
const res = await opStartTls({ const res = await opStartTls({
rid: conn.rid, rid: conn.rid,
hostname, hostname,
certFile, certFile,
caCerts, caCerts,
alpnProtocols,
}); });
return new TlsConn(res.rid, res.remoteAddr, res.localAddr); return new TlsConn(res.rid, res.remoteAddr, res.localAddr);
} }

View file

@ -52,11 +52,14 @@ declare namespace Deno {
closeWrite(): Promise<void>; closeWrite(): Promise<void>;
} }
// deno-lint-ignore no-empty-interface
export interface TlsHandshakeInfo {}
export interface TlsConn extends Conn { export interface TlsConn extends Conn {
/** Runs the client or server handshake protocol to completion if that has /** Runs the client or server handshake protocol to completion if that has
* not happened yet. Calling this method is optional; the TLS handshake * not happened yet. Calling this method is optional; the TLS handshake
* will be completed automatically as soon as data is sent or received. */ * will be completed automatically as soon as data is sent or received. */
handshake(): Promise<void>; handshake(): Promise<TlsHandshakeInfo>;
} }
export interface ListenOptions { export interface ListenOptions {

View file

@ -12,6 +12,7 @@ use deno_core::error::AnyError;
use deno_core::op_async; use deno_core::op_async;
use deno_core::op_sync; use deno_core::op_sync;
use deno_core::AsyncRefCell; use deno_core::AsyncRefCell;
use deno_core::ByteString;
use deno_core::CancelHandle; use deno_core::CancelHandle;
use deno_core::CancelTryFuture; use deno_core::CancelTryFuture;
use deno_core::OpPair; use deno_core::OpPair;
@ -84,6 +85,12 @@ pub struct OpPacket {
pub remote_addr: OpAddr, pub remote_addr: OpAddr,
} }
#[derive(Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TlsHandshakeInfo {
pub alpn_protocol: Option<ByteString>,
}
#[derive(Serialize)] #[derive(Serialize)]
pub struct IpAddr { pub struct IpAddr {
pub hostname: String, pub hostname: String,

View file

@ -4,6 +4,7 @@ use crate::io::TcpStreamResource;
use crate::ops::IpAddr; use crate::ops::IpAddr;
use crate::ops::OpAddr; use crate::ops::OpAddr;
use crate::ops::OpConn; use crate::ops::OpConn;
use crate::ops::TlsHandshakeInfo;
use crate::resolve_addr::resolve_addr; use crate::resolve_addr::resolve_addr;
use crate::resolve_addr::resolve_addr_sync; use crate::resolve_addr::resolve_addr_sync;
use crate::DefaultTlsOptions; use crate::DefaultTlsOptions;
@ -29,6 +30,7 @@ use deno_core::op_sync;
use deno_core::parking_lot::Mutex; use deno_core::parking_lot::Mutex;
use deno_core::AsyncRefCell; use deno_core::AsyncRefCell;
use deno_core::AsyncResult; use deno_core::AsyncResult;
use deno_core::ByteString;
use deno_core::CancelHandle; use deno_core::CancelHandle;
use deno_core::CancelTryFuture; use deno_core::CancelTryFuture;
use deno_core::OpPair; use deno_core::OpPair;
@ -54,7 +56,6 @@ use io::Read;
use io::Write; use io::Write;
use serde::Deserialize; use serde::Deserialize;
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::Cell;
use std::cell::RefCell; use std::cell::RefCell;
use std::convert::From; use std::convert::From;
use std::fs::File; use std::fs::File;
@ -190,6 +191,14 @@ impl TlsStream {
fn poll_handshake(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> { fn poll_handshake(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.inner_mut().poll_handshake(cx) 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 { impl AsyncRead for TlsStream {
@ -549,6 +558,10 @@ impl WriteHalf {
}) })
.await .await
} }
fn get_alpn_protocol(&mut self) -> Option<ByteString> {
self.shared.get_alpn_protocol()
}
} }
impl AsyncWrite for WriteHalf { impl AsyncWrite for WriteHalf {
@ -658,6 +671,11 @@ impl Shared {
fn drop_shared_waker(self_ptr: *const ()) { fn drop_shared_waker(self_ptr: *const ()) {
let _ = unsafe { Weak::from_raw(self_ptr as *const Self) }; 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); struct ImplementReadTrait<'a, T>(&'a mut T);
@ -698,7 +716,8 @@ pub fn init<P: NetPermissions + 'static>() -> Vec<OpPair> {
pub struct TlsStreamResource { pub struct TlsStreamResource {
rd: AsyncRefCell<ReadHalf>, rd: AsyncRefCell<ReadHalf>,
wr: AsyncRefCell<WriteHalf>, 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. cancel_handle: CancelHandle, // Only read and handshake ops get canceled.
} }
@ -707,7 +726,7 @@ impl TlsStreamResource {
Self { Self {
rd: rd.into(), rd: rd.into(),
wr: wr.into(), wr: wr.into(),
handshake_done: Cell::new(false), handshake_info: RefCell::new(None),
cancel_handle: Default::default(), cancel_handle: Default::default(),
} }
} }
@ -744,14 +763,21 @@ impl TlsStreamResource {
Ok(()) Ok(())
} }
pub async fn handshake(self: &Rc<Self>) -> Result<(), AnyError> { pub async fn handshake(
if !self.handshake_done.get() { self: &Rc<Self>,
let mut wr = RcRef::map(self, |r| &r.wr).borrow_mut().await; ) -> Result<TlsHandshakeInfo, AnyError> {
let cancel_handle = RcRef::map(self, |r| &r.cancel_handle); if let Some(tls_info) = &*self.handshake_info.borrow() {
wr.handshake().try_or_cancel(cancel_handle).await?; return Ok(tls_info.clone());
self.handshake_done.set(true);
} }
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>, ca_certs: Vec<String>,
cert_chain: Option<String>, cert_chain: Option<String>,
private_key: Option<String>, private_key: Option<String>,
alpn_protocols: Option<Vec<String>>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -795,6 +822,7 @@ pub struct StartTlsArgs {
rid: ResourceId, rid: ResourceId,
ca_certs: Vec<String>, ca_certs: Vec<String>,
hostname: String, hostname: String,
alpn_protocols: Option<Vec<String>>,
} }
pub async fn op_tls_start<NP>( pub async fn op_tls_start<NP>(
@ -851,11 +879,20 @@ where
let local_addr = tcp_stream.local_addr()?; let local_addr = tcp_stream.local_addr()?;
let remote_addr = tcp_stream.peer_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, root_cert_store,
ca_certs, ca_certs,
unsafely_ignore_certificate_errors, 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 = let tls_stream =
TlsStream::new_client_side(tcp_stream, &tls_config, hostname_dns); TlsStream::new_client_side(tcp_stream, &tls_config, hostname_dns);
@ -948,6 +985,12 @@ where
unsafely_ignore_certificate_errors, 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() { if args.cert_chain.is_some() || args.private_key.is_some() {
let cert_chain = args let cert_chain = args
.cert_chain .cert_chain
@ -1144,7 +1187,7 @@ pub async fn op_tls_handshake(
state: Rc<RefCell<OpState>>, state: Rc<RefCell<OpState>>,
rid: ResourceId, rid: ResourceId,
_: (), _: (),
) -> Result<(), AnyError> { ) -> Result<TlsHandshakeInfo, AnyError> {
let resource = state let resource = state
.borrow() .borrow()
.resource_table .resource_table