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;
|
||||
/** 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`
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
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>;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue