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:
*
* Deno.listenTLS({ port: 443, certFile: "./my_server.crt", keyFile: "./my_server.key" })
*/
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:
*
* Deno.listenTLS({ port: 443, certFile: "./my_server.crt", keyFile: "./my_server.key" })
*/
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);
}
131 changes: 123 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,128 @@ 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(
{ read: true, net: true },
async function listenTLSNonExistentCertKeyFiles(): Promise<void> {
let err;
const options = {
hostname: "localhost",
port: 4500,
certFile: "cli/tests/tls/localhost.crt",
keyFile: "cli/tests/tls/localhost.key"
};

for (const field of ["certFile", "keyFile"]) {
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved
try {
Deno.listenTLS({
...options,
[field]: "./non/existent/file"
});
} catch (e) {
err = e;
}
assertEquals(err.kind, Deno.ErrorKind.NotFound);
assertEquals(err.name, "NotFound");
}
}
bartlomieju marked this conversation as resolved.
Show resolved Hide resolved
);

testPerm(
{ read: true, write: true, net: true },
async function listenTLSEmptyKeyFile(): Promise<void> {
let err;
const options = {
hostname: "localhost",
port: 4500,
certFile: "cli/tests/tls/localhost.crt",
keyFile: "cli/tests/tls/localhost.key"
};

const testDir = Deno.makeTempDirSync();
const keyFilename = testDir + "/key.pem";
Deno.writeFileSync(keyFilename, new Uint8Array([]), {
perm: 0o666
});

try {
Deno.listenTLS({
...options,
keyFile: keyFilename
});
} catch (e) {
err = e;
}
assertEquals(err.kind, Deno.ErrorKind.Other);
assertEquals(err.name, "Other");
}
);

testPerm(
{ read: true, write: true, net: true },
async function listenTLSEmptyCertFile(): Promise<void> {
let err;
const options = {
hostname: "localhost",
port: 4500,
certFile: "cli/tests/tls/localhost.crt",
keyFile: "cli/tests/tls/localhost.key"
};

const testDir = Deno.makeTempDirSync();
const certFilename = testDir + "/cert.crt";
Deno.writeFileSync(certFilename, new Uint8Array([]), {
perm: 0o666
});

try {
Deno.listenTLS({
...options,
certFile: certFilename
});
} catch (e) {
err = e;
}
assertEquals(err.kind, Deno.ErrorKind.Other);
assertEquals(err.name, "Other");
}
);

testPerm({ read: true, net: true }, async function dialAndListenTLS(): Promise<
void
> {
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 +155,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