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:
parent
d763633781
commit
1d3f734e18
6 changed files with 185 additions and 16 deletions
33
cli/dts/lib.deno.unstable.d.ts
vendored
33
cli/dts/lib.deno.unstable.d.ts
vendored
|
@ -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`
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
5
ext/net/lib.deno_net.d.ts
vendored
5
ext/net/lib.deno_net.d.ts
vendored
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue