diff options
Diffstat (limited to 'cli')
-rw-r--r-- | cli/js/deno.ts | 2 | ||||
-rw-r--r-- | cli/js/lib.deno.ns.d.ts | 27 | ||||
-rw-r--r-- | cli/js/ops/tls.ts | 14 | ||||
-rw-r--r-- | cli/js/tests/tls_test.ts | 51 | ||||
-rw-r--r-- | cli/js/tls.ts | 17 | ||||
-rw-r--r-- | cli/ops/io.rs | 8 | ||||
-rw-r--r-- | cli/ops/net.rs | 10 | ||||
-rw-r--r-- | cli/ops/tls.rs | 80 |
8 files changed, 197 insertions, 12 deletions
diff --git a/cli/js/deno.ts b/cli/js/deno.ts index 89795119b..0322bac27 100644 --- a/cli/js/deno.ts +++ b/cli/js/deno.ts @@ -106,7 +106,7 @@ export { resources, close } from "./ops/resources.ts"; export { signal, signals, Signal, SignalStream } from "./signals.ts"; export { FileInfo, statSync, lstatSync, stat, lstat } from "./ops/fs/stat.ts"; export { symlinkSync, symlink } from "./ops/fs/symlink.ts"; -export { connectTLS, listenTLS } from "./tls.ts"; +export { connectTLS, listenTLS, startTLS } from "./tls.ts"; export { truncateSync, truncate } from "./ops/fs/truncate.ts"; export { isatty, setRaw } from "./ops/tty.ts"; export { umask } from "./ops/fs/umask.ts"; diff --git a/cli/js/lib.deno.ns.d.ts b/cli/js/lib.deno.ns.d.ts index cdaff073b..9ab40248f 100644 --- a/cli/js/lib.deno.ns.d.ts +++ b/cli/js/lib.deno.ns.d.ts @@ -2038,6 +2038,33 @@ declare namespace Deno { */ export function connectTLS(options: ConnectTLSOptions): Promise<Conn>; + export interface StartTLSOptions { + /** A literal IP address or host name that can be resolved to an IP address. + * If not specified, defaults to `127.0.0.1`. */ + hostname?: string; + /** Server certificate file. */ + certFile?: string; + } + + /** **UNSTABLE**: new API, yet to be vetted. + * + * Start TLS handshake from an existing connection using + * an optional cert file, hostname (default is "127.0.0.1"). The + * cert file is optional and if not included Mozilla's root certificates will + * be used (see also https://github.com/ctz/webpki-roots for specifics) + * Using this function requires that the other end of the connection is + * prepared for TLS handshake. + * + * const conn = await Deno.connect({ port: 80, hostname: "127.0.0.1" }); + * const tlsConn = await Deno.startTLS(conn, { certFile: "./certs/my_custom_root_CA.pem", hostname: "127.0.0.1", port: 80 }); + * + * Requires `allow-net` permission. + */ + export function startTLS( + conn: Conn, + options?: StartTLSOptions + ): Promise<Conn>; + /** **UNSTABLE**: not sure if broken or not */ export interface Metrics { opsDispatched: number; diff --git a/cli/js/ops/tls.ts b/cli/js/ops/tls.ts index 234e569dd..3964c44bb 100644 --- a/cli/js/ops/tls.ts +++ b/cli/js/ops/tls.ts @@ -8,7 +8,7 @@ export interface ConnectTLSRequest { certFile?: string; } -interface ConnectTLSResponse { +interface EstablishTLSResponse { rid: number; localAddr: { hostname: string; @@ -24,7 +24,7 @@ interface ConnectTLSResponse { export function connectTLS( args: ConnectTLSRequest -): Promise<ConnectTLSResponse> { +): Promise<EstablishTLSResponse> { return sendAsync("op_connect_tls", args); } @@ -66,3 +66,13 @@ interface ListenTLSResponse { export function listenTLS(args: ListenTLSRequest): ListenTLSResponse { return sendSync("op_listen_tls", args); } + +export interface StartTLSRequest { + rid: number; + hostname: string; + certFile?: string; +} + +export function startTLS(args: StartTLSRequest): Promise<EstablishTLSResponse> { + return sendAsync("op_start_tls", args); +} diff --git a/cli/js/tests/tls_test.ts b/cli/js/tests/tls_test.ts index 019b81652..e42e477c3 100644 --- a/cli/js/tests/tls_test.ts +++ b/cli/js/tests/tls_test.ts @@ -209,3 +209,54 @@ unitTest( await resolvable; } ); + +unitTest( + { perms: { read: true, net: true } }, + async function startTLS(): Promise<void> { + const hostname = "smtp.gmail.com"; + const port = 587; + const encoder = new TextEncoder(); + + let conn = await Deno.connect({ + hostname, + port, + }); + + let writer = new BufWriter(conn); + let reader = new TextProtoReader(new BufReader(conn)); + + let line: string | Deno.EOF = (await reader.readLine()) as string; + assert(line.startsWith("220")); + + await writer.write(encoder.encode(`EHLO ${hostname}\r\n`)); + await writer.flush(); + + while ((line = (await reader.readLine()) as string)) { + assert(line.startsWith("250")); + if (line.startsWith("250 ")) break; + } + + await writer.write(encoder.encode("STARTTLS\r\n")); + await writer.flush(); + + line = await reader.readLine(); + + // Received the message that the server is ready to establish TLS + assertEquals(line, "220 2.0.0 Ready to start TLS"); + + conn = await Deno.startTLS(conn, { hostname }); + writer = new BufWriter(conn); + reader = new TextProtoReader(new BufReader(conn)); + + // After that use TLS communication again + await writer.write(encoder.encode(`EHLO ${hostname}\r\n`)); + await writer.flush(); + + while ((line = (await reader.readLine()) as string)) { + assert(line.startsWith("250")); + if (line.startsWith("250 ")) break; + } + + conn.close(); + } +); diff --git a/cli/js/tls.ts b/cli/js/tls.ts index ef87b5aa1..f60eb24cb 100644 --- a/cli/js/tls.ts +++ b/cli/js/tls.ts @@ -57,3 +57,20 @@ export function listenTLS({ }); return new TLSListenerImpl(res.rid, res.localAddr); } + +interface StartTLSOptions { + hostname?: string; + certFile?: string; +} + +export async function startTLS( + conn: Conn, + { hostname = "127.0.0.1", certFile = undefined }: StartTLSOptions = {} +): Promise<Conn> { + const res = await tlsOps.startTLS({ + rid: conn.rid, + hostname, + certFile, + }); + return new ConnImpl(res.rid, res.remoteAddr!, res.localAddr!); +} diff --git a/cli/ops/io.rs b/cli/ops/io.rs index e045eddfb..9c228ffad 100644 --- a/cli/ops/io.rs +++ b/cli/ops/io.rs @@ -157,7 +157,7 @@ impl StreamResourceHolder { pub enum StreamResource { Stdin(tokio::io::Stdin, TTYMetadata), FsFile(Option<(tokio::fs::File, FileMetadata)>), - TcpStream(tokio::net::TcpStream), + TcpStream(Option<tokio::net::TcpStream>), #[cfg(not(windows))] UnixStream(tokio::net::UnixStream), ServerTlsStream(Box<ServerTlsStream<TcpStream>>), @@ -195,7 +195,7 @@ impl DenoAsyncRead for StreamResource { FsFile(Some((f, _))) => f, FsFile(None) => return Poll::Ready(Err(OpError::resource_unavailable())), Stdin(f, _) => f, - TcpStream(f) => f, + TcpStream(Some(f)) => f, #[cfg(not(windows))] UnixStream(f) => f, ClientTlsStream(f) => f, @@ -297,7 +297,7 @@ impl DenoAsyncWrite for StreamResource { let f: &mut dyn UnpinAsyncWrite = match self { FsFile(Some((f, _))) => f, FsFile(None) => return Poll::Pending, - TcpStream(f) => f, + TcpStream(Some(f)) => f, #[cfg(not(windows))] UnixStream(f) => f, ClientTlsStream(f) => f, @@ -315,7 +315,7 @@ impl DenoAsyncWrite for StreamResource { let f: &mut dyn UnpinAsyncWrite = match self { FsFile(Some((f, _))) => f, FsFile(None) => return Poll::Pending, - TcpStream(f) => f, + TcpStream(Some(f)) => f, #[cfg(not(windows))] UnixStream(f) => f, ClientTlsStream(f) => f, diff --git a/cli/ops/net.rs b/cli/ops/net.rs index 2636a2c2d..101fc5130 100644 --- a/cli/ops/net.rs +++ b/cli/ops/net.rs @@ -81,9 +81,9 @@ fn accept_tcp( let mut state = state_.borrow_mut(); let rid = state.resource_table.add( "tcpStream", - Box::new(StreamResourceHolder::new(StreamResource::TcpStream( + Box::new(StreamResourceHolder::new(StreamResource::TcpStream(Some( tcp_stream, - ))), + )))), ); Ok(json!({ "rid": rid, @@ -280,9 +280,9 @@ fn op_connect( let mut state = state_.borrow_mut(); let rid = state.resource_table.add( "tcpStream", - Box::new(StreamResourceHolder::new(StreamResource::TcpStream( + Box::new(StreamResourceHolder::new(StreamResource::TcpStream(Some( tcp_stream, - ))), + )))), ); Ok(json!({ "rid": rid, @@ -367,7 +367,7 @@ fn op_shutdown( .get_mut::<StreamResourceHolder>(rid) .ok_or_else(OpError::bad_resource_id)?; match resource_holder.resource { - StreamResource::TcpStream(ref mut stream) => { + StreamResource::TcpStream(Some(ref mut stream)) => { TcpStream::shutdown(stream, shutdown_mode).map_err(OpError::from)?; } #[cfg(unix)] diff --git a/cli/ops/tls.rs b/cli/ops/tls.rs index 60338f7fc..dca2d8012 100644 --- a/cli/ops/tls.rs +++ b/cli/ops/tls.rs @@ -28,6 +28,7 @@ use tokio_rustls::{ use webpki::DNSNameRef; pub fn init(i: &mut Isolate, s: &State) { + i.register_op("op_start_tls", s.stateful_json_op(op_start_tls)); i.register_op("op_connect_tls", s.stateful_json_op(op_connect_tls)); i.register_op("op_listen_tls", s.stateful_json_op(op_listen_tls)); i.register_op("op_accept_tls", s.stateful_json_op(op_accept_tls)); @@ -42,6 +43,85 @@ struct ConnectTLSArgs { cert_file: Option<String>, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct StartTLSArgs { + rid: u32, + cert_file: Option<String>, + hostname: String, +} + +pub fn op_start_tls( + state: &State, + args: Value, + _zero_copy: Option<ZeroCopyBuf>, +) -> Result<JsonOp, OpError> { + let args: StartTLSArgs = serde_json::from_value(args)?; + let rid = args.rid as u32; + let cert_file = args.cert_file.clone(); + let state_ = state.clone(); + + let mut domain = args.hostname; + if domain.is_empty() { + domain.push_str("localhost"); + } + + let op = async move { + let mut state = state_.borrow_mut(); + + let mut resource_holder = + match state.resource_table.remove::<StreamResourceHolder>(rid) { + Some(resource) => *resource, + None => return Err(OpError::bad_resource_id()), + }; + + if let StreamResource::TcpStream(ref mut tcp_stream) = + resource_holder.resource + { + let tcp_stream = tcp_stream.take().unwrap(); + let local_addr = tcp_stream.local_addr()?; + let remote_addr = tcp_stream.peer_addr()?; + let mut config = ClientConfig::new(); + config + .root_store + .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + if let Some(path) = cert_file { + let key_file = File::open(path)?; + let reader = &mut BufReader::new(key_file); + config.root_store.add_pem_file(reader).unwrap(); + } + + let tls_connector = TlsConnector::from(Arc::new(config)); + let dnsname = + DNSNameRef::try_from_ascii_str(&domain).expect("Invalid DNS lookup"); + let tls_stream = tls_connector.connect(dnsname, tcp_stream).await?; + + let rid = state.resource_table.add( + "clientTlsStream", + Box::new(StreamResourceHolder::new(StreamResource::ClientTlsStream( + Box::new(tls_stream), + ))), + ); + Ok(json!({ + "rid": rid, + "localAddr": { + "hostname": local_addr.ip().to_string(), + "port": local_addr.port(), + "transport": "tcp", + }, + "remoteAddr": { + "hostname": remote_addr.ip().to_string(), + "port": remote_addr.port(), + "transport": "tcp", + } + })) + } else { + Err(OpError::bad_resource_id()) + } + }; + Ok(JsonOp::Async(op.boxed_local())) +} + pub fn op_connect_tls( state: &State, args: Value, |