summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEnokMan <416828041@qq.com>2020-04-18 10:21:20 -0500
committerGitHub <noreply@github.com>2020-04-18 11:21:20 -0400
commit47617e60d551665ec509e013cfcae30987cb3b2b (patch)
tree5717061b7fd567b5fb8508f565993062f8014722
parent10469ec2798a7f02a6d9371207cc984502039bfa (diff)
feat: startTLS (#4773)
-rw-r--r--cli/js/deno.ts2
-rw-r--r--cli/js/lib.deno.ns.d.ts27
-rw-r--r--cli/js/ops/tls.ts14
-rw-r--r--cli/js/tests/tls_test.ts51
-rw-r--r--cli/js/tls.ts17
-rw-r--r--cli/ops/io.rs8
-rw-r--r--cli/ops/net.rs10
-rw-r--r--cli/ops/tls.rs80
-rw-r--r--core/resources.rs25
9 files changed, 222 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,
diff --git a/core/resources.rs b/core/resources.rs
index ba69e8c98..7a42a68a9 100644
--- a/core/resources.rs
+++ b/core/resources.rs
@@ -72,6 +72,17 @@ impl ResourceTable {
pub fn close(&mut self, rid: ResourceId) -> Option<()> {
self.map.remove(&rid).map(|(_name, _resource)| ())
}
+
+ pub fn remove<T: Resource>(&mut self, rid: ResourceId) -> Option<Box<T>> {
+ if let Some((_name, resource)) = self.map.remove(&rid) {
+ let res = match resource.downcast::<T>() {
+ Ok(res) => Some(res),
+ Err(_e) => None,
+ };
+ return res;
+ }
+ None
+ }
}
/// Abstract type representing resource in Deno.
@@ -138,4 +149,18 @@ mod tests {
table.close(rid2);
assert_eq!(table.map.len(), 0);
}
+
+ #[test]
+ fn test_take_from_resource_table() {
+ let mut table = ResourceTable::default();
+ let rid1 = table.add("fake1", Box::new(FakeResource::new(1)));
+ let rid2 = table.add("fake2", Box::new(FakeResource::new(2)));
+ assert_eq!(table.map.len(), 2);
+ let res1 = table.remove::<FakeResource>(rid1);
+ assert_eq!(table.map.len(), 1);
+ assert!(res1.is_some());
+ let res2 = table.remove::<FakeResource>(rid2);
+ assert_eq!(table.map.len(), 0);
+ assert!(res2.is_some());
+ }
}