Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Deno.listenTLS #3152

Merged
merged 14 commits into from Oct 21, 2019
2 changes: 1 addition & 1 deletion cli/js/deno.ts
Expand Up @@ -75,7 +75,7 @@ export {
export { truncateSync, truncate } from "./truncate.ts";
export { FileInfo } from "./file_info.ts";
export { connect, dial, listen, Listener, Conn } from "./net.ts";
export { dialTLS } from "./tls.ts";
export { dialTLS, listenTLS } from "./tls.ts";
export { metrics, Metrics } from "./metrics.ts";
export { resources } from "./resources.ts";
export {
Expand Down
3 changes: 3 additions & 0 deletions cli/js/dispatch.ts
Expand Up @@ -26,9 +26,11 @@ export let OP_METRICS: number;
export let OP_REPL_START: number;
export let OP_REPL_READLINE: number;
export let OP_ACCEPT: number;
export let OP_ACCEPT_TLS: number;
export let OP_DIAL: number;
export let OP_SHUTDOWN: number;
export let OP_LISTEN: number;
export let OP_LISTEN_TLS: number;
export let OP_RESOURCES: number;
export let OP_GET_RANDOM_VALUES: number;
export let OP_GLOBAL_TIMER_STOP: number;
Expand Down Expand Up @@ -81,6 +83,7 @@ export function asyncMsgFromRust(opId: number, ui8: Uint8Array): void {
case OP_REPL_START:
case OP_REPL_READLINE:
case OP_ACCEPT:
case OP_ACCEPT_TLS:
case OP_DIAL:
case OP_GLOBAL_TIMER:
case OP_HOST_GET_WORKER_CLOSED:
Expand Down
24 changes: 24 additions & 0 deletions cli/js/lib.deno_runtime.d.ts
Expand Up @@ -990,6 +990,29 @@ declare namespace Deno {
*/
export function listen(options: ListenOptions): Listener;

export interface ListenTLSOptions {
port: number;
hostname?: string;
transport?: Transport;
certFile: string;
keyFile: string;
}

/** Listen announces on the local transport address over TLS (transport layer security).
*
* @param options
* @param options.port The port to connect to. (Required.)
* @param options.hostname A literal IP address or host name that can be
* resolved to an IP address. If not specified, defaults to 0.0.0.0
* @param options.certFile Server certificate file
* @param options.keyFile Server public key file
*
* Examples:
*
* listen({ port: 443, certFile: "./my_server.crt", keyFile: "./my_server.key" })
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved
*/
export function listenTLS(options: ListenTLSOptions): Listener;

export interface DialOptions {
port: number;
hostname?: string;
Expand Down Expand Up @@ -1018,6 +1041,7 @@ declare namespace Deno {
export interface DialTLSOptions {
port: number;
hostname?: string;
certFile?: string;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion cli/js/net.ts
Expand Up @@ -78,7 +78,7 @@ export class ConnImpl implements Conn {
}
}

class ListenerImpl implements Listener {
export class ListenerImpl implements Listener {
constructor(
readonly rid: number,
private transport: Transport,
Expand Down
46 changes: 44 additions & 2 deletions cli/js/tls.ts
@@ -1,13 +1,14 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { sendAsync } from "./dispatch_json.ts";
import { sendAsync, sendSync } from "./dispatch_json.ts";
import * as dispatch from "./dispatch.ts";
import { Conn, ConnImpl } from "./net.ts";
import { Listener, Transport, Conn, ConnImpl, ListenerImpl } from "./net.ts";

// TODO(ry) There are many configuration options to add...
// https://docs.rs/rustls/0.16.0/rustls/struct.ClientConfig.html
interface DialTLSOptions {
port: number;
hostname?: string;
certFile?: string;
}
const dialTLSDefaults = { hostname: "127.0.0.1", transport: "tcp" };

Expand All @@ -19,3 +20,44 @@ export async function dialTLS(options: DialTLSOptions): Promise<Conn> {
const res = await sendAsync(dispatch.OP_DIAL_TLS, options);
return new ConnImpl(res.rid, res.remoteAddr!, res.localAddr!);
}

class TLSListenerImpl extends ListenerImpl {
async accept(): Promise<Conn> {
const res = await sendAsync(dispatch.OP_ACCEPT_TLS, { rid: this.rid });
return new ConnImpl(res.rid, res.remoteAddr, res.localAddr);
}
}

export interface ListenTLSOptions {
port: number;
hostname?: string;
transport?: Transport;
certFile: string;
keyFile: string;
}

/** Listen announces on the local transport address over TLS (transport layer security).
*
* @param options
* @param options.port The port to connect to. (Required.)
* @param options.hostname A literal IP address or host name that can be
* resolved to an IP address. If not specified, defaults to 0.0.0.0
* @param options.certFile Server certificate file
* @param options.keyFile Server public key file
*
* Examples:
*
* listen({ port: 443, certFile: "./my_server.crt", keyFile: "./my_server.key" })
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved
*/
export function listenTLS(options: ListenTLSOptions): Listener {
const hostname = options.hostname || "0.0.0.0";
const transport = options.transport || "tcp";
const res = sendSync(dispatch.OP_LISTEN_TLS, {
hostname,
port: options.port,
transport,
certFile: options.certFile,
keyFile: options.keyFile
});
return new TLSListenerImpl(res.rid, transport, res.localAddr);
}
43 changes: 35 additions & 8 deletions cli/js/tls_test.ts
Expand Up @@ -3,8 +3,9 @@ import { test, testPerm, assert, assertEquals } from "./test_util.ts";
import { BufWriter, BufReader } from "../../std/io/bufio.ts";
import { TextProtoReader } from "../../std/textproto/mod.ts";
import { runIfMain } from "../../std/testing/mod.ts";
// TODO(ry) The tests in this file use github.com:443, but it would be better to
// not rely on an internet connection and rather use a localhost TLS server.

const encoder = new TextEncoder();
const decoder = new TextDecoder();

test(async function dialTLSNoPerm(): Promise<void> {
let err;
Expand All @@ -17,15 +18,40 @@ test(async function dialTLSNoPerm(): Promise<void> {
assertEquals(err.name, "PermissionDenied");
});

testPerm({ net: true }, async function dialTLSBasic(): Promise<void> {
const conn = await Deno.dialTLS({ hostname: "github.com", port: 443 });
testPerm({ net: true }, async function dialAndListenTLS(): Promise<void> {
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved
const hostname = "localhost";
const port = 4500;

const listener = Deno.listenTLS({
hostname,
port,
certFile: "cli/tests/tls/localhost.crt",
keyFile: "cli/tests/tls/localhost.key"
});

const response = encoder.encode(
"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n"
);

listener.accept().then(
async (conn): Promise<void> => {
assert(conn.remoteAddr != null);
assert(conn.localAddr != null);
await conn.write(response);
conn.close();
}
);

const conn = await Deno.dialTLS({
hostname,
port,
certFile: "cli/tests/tls/RootCA.pem"
});
assert(conn.rid > 0);
const w = new BufWriter(conn);
const r = new BufReader(conn);
let body = "GET / HTTP/1.1\r\n";
body += "Host: github.com\r\n";
body += "\r\n";
const writeResult = await w.write(new TextEncoder().encode(body));
const body = `GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`;
const writeResult = await w.write(encoder.encode(body));
assertEquals(body.length, writeResult);
await w.flush();
const tpr = new TextProtoReader(r);
Expand All @@ -41,6 +67,7 @@ testPerm({ net: true }, async function dialTLSBasic(): Promise<void> {
const contentLength = parseInt(headers.get("content-length"));
const bodyBuf = new Uint8Array(contentLength);
await r.readFull(bodyBuf);
assertEquals(decoder.decode(bodyBuf), "Hello World\n");
conn.close();
});

Expand Down
2 changes: 2 additions & 0 deletions cli/lib.rs
Expand Up @@ -13,8 +13,10 @@ extern crate indexmap;
#[cfg(unix)]
extern crate nix;
extern crate rand;
extern crate reqwest;
extern crate serde;
extern crate serde_derive;
extern crate tokio_rustls;
extern crate url;

pub mod colors;
Expand Down
133 changes: 130 additions & 3 deletions cli/ops/tls.rs
Expand Up @@ -4,25 +4,47 @@ use crate::ops::json_op;
use crate::resolve_addr::resolve_addr;
use crate::resources;
use crate::state::ThreadSafeState;
use crate::tokio_util;
use deno::*;
use futures::Future;
use std;
use std::convert::From;
use std::fs::File;
use std::io::BufReader;
use std::sync::Arc;
use tokio;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
use tokio_rustls::{rustls::ClientConfig, TlsConnector};
use tokio_rustls::{
rustls::{
internal::pemfile::{certs, rsa_private_keys},
Certificate, NoClientAuth, PrivateKey, ServerConfig,
},
TlsAcceptor,
};
use webpki;
use webpki::DNSNameRef;
use webpki_roots;

pub fn init(i: &mut Isolate, s: &ThreadSafeState) {
i.register_op("dial_tls", s.core_op(json_op(s.stateful_op(op_dial_tls))));
i.register_op(
"listen_tls",
s.core_op(json_op(s.stateful_op(op_listen_tls))),
);
i.register_op(
"accept_tls",
s.core_op(json_op(s.stateful_op(op_accept_tls))),
);
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DialTLSArgs {
hostname: String,
port: u16,
}
pub fn init(i: &mut Isolate, s: &ThreadSafeState) {
i.register_op("dial_tls", s.core_op(json_op(s.stateful_op(op_dial_tls))));
cert_file: Option<String>,
}

pub fn op_dial_tls(
Expand All @@ -38,6 +60,7 @@ pub fn op_dial_tls(

state.check_net(&address)?;

let cert_file = args.cert_file;
let mut domain = args.hostname;
if domain.is_empty() {
domain.push_str("localhost");
Expand All @@ -53,6 +76,12 @@ pub fn op_dial_tls(
.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));
Ok((tls_connector, tcp_stream, local_addr, remote_addr))
})
Expand All @@ -78,3 +107,101 @@ pub fn op_dial_tls(

Ok(JsonOp::Async(Box::new(op)))
}

fn load_certs(path: &str) -> Result<Vec<Certificate>, ErrBox> {
let cert_file = File::open(path)?;
let reader = &mut BufReader::new(cert_file);
Ok(certs(reader).unwrap())
}

fn load_keys(path: &str) -> Result<Vec<PrivateKey>, ErrBox> {
let key_file = File::open(path)?;
let reader = &mut BufReader::new(key_file);
let keys = rsa_private_keys(reader).unwrap();
Ok(keys)
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ListenTlsArgs {
transport: String,
hostname: String,
port: u16,
cert_file: String,
key_file: String,
}

fn op_listen_tls(
state: &ThreadSafeState,
args: Value,
_zero_copy: Option<PinnedBuf>,
) -> Result<JsonOp, ErrBox> {
let args: ListenTlsArgs = serde_json::from_value(args)?;
assert_eq!(args.transport, "tcp");

// TODO(ry) Using format! is suboptimal here. Better would be if
// state.check_net and resolve_addr() took hostname and port directly.
let address = format!("{}:{}", args.hostname, args.port);

state.check_net(&address)?;

let mut config = ServerConfig::new(NoClientAuth::new());
config
.set_single_cert(
load_certs(&args.cert_file)?,
load_keys(&args.key_file)?.remove(0),
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved
)
.expect("invalid key or certificate");
let acceptor = TlsAcceptor::from(Arc::new(config));
let addr = resolve_addr(&address).wait()?;
let listener = TcpListener::bind(&addr)?;
let local_addr = listener.local_addr()?;
let resource = resources::add_tls_listener(listener, acceptor);

Ok(JsonOp::Sync(json!({
"rid": resource.rid,
"localAddr": local_addr.to_string()
})))
}

#[derive(Deserialize)]
struct AcceptTlsArgs {
rid: i32,
}

fn op_accept_tls(
_state: &ThreadSafeState,
args: Value,
_zero_copy: Option<PinnedBuf>,
) -> Result<JsonOp, ErrBox> {
let args: AcceptTlsArgs = serde_json::from_value(args)?;
let server_rid = args.rid as u32;

let server_resource = resources::lookup(server_rid)?;
let op = tokio_util::accept(server_resource)
.and_then(move |(tcp_stream, _socket_addr)| {
let local_addr = tcp_stream.local_addr()?;
let remote_addr = tcp_stream.peer_addr()?;
Ok((tcp_stream, local_addr, remote_addr))
})
.and_then(move |(tcp_stream, local_addr, remote_addr)| {
let mut server_resource = resources::lookup(server_rid).unwrap();
server_resource
.poll_accept_tls(tcp_stream)
.and_then(move |tls_stream| {
let tls_stream_resource =
resources::add_server_tls_stream(tls_stream);
Ok((tls_stream_resource, local_addr, remote_addr))
})
})
.map_err(ErrBox::from)
.and_then(move |(tls_stream_resource, local_addr, remote_addr)| {
futures::future::ok(json!({
"rid": tls_stream_resource.rid,
"localAddr": local_addr.to_string(),
"remoteAddr": remote_addr.to_string(),
}))
});

Ok(JsonOp::Async(Box::new(op)))
}