1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-30 03:05:27 -05:00
denoland-deno/tests/unit/tls_test.ts
Matt Mastracci e190acbfa8
refactor(ext/net): extract TLS key and certificate from interfaces (#23296)
Removes the certificate options from all the interfaces and replaces
them with a new `TlsCertifiedKeyOptions`. This allows us to centralize
the documentation for TLS key management for both client and server, and
will allow us to add key object support in the future.

Also adds an option `keyFormat` field to the cert/key that must be
omitted or set to `pem`. This will allow us to load other format keys in
the future `der`, `pfx`, etc.

In a future PR, we will add a way to load a certified key object, and we
will add another option to `TlsCertifiedKeyOptions` like so:

```ts
export interface TlsCertifiedKeyOptions =
    | TlsCertifiedKeyPem
    | TlsCertifiedKeyFromFile
    | TlsCertifiedKeyConnectTls
    | { key: Deno.CertifiedKey }
```
2024-04-09 16:23:22 -06:00

1648 lines
44 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import {
assert,
assertEquals,
assertNotEquals,
assertRejects,
assertStrictEquals,
assertThrows,
} from "./test_util.ts";
import { BufReader, BufWriter } from "@std/io/mod.ts";
import { readAll } from "@std/io/read_all.ts";
import { writeAll } from "@std/io/write_all.ts";
import { TextProtoReader } from "../testdata/run/textproto.ts";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const cert = Deno.readTextFileSync("tests/testdata/tls/localhost.crt");
const key = Deno.readTextFileSync("tests/testdata/tls/localhost.key");
const caCerts = [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")];
async function sleep(msec: number) {
await new Promise((res, _rej) => setTimeout(res, msec));
}
function listenTls(
options?: { alpnProtocols?: string[]; reusePort?: boolean },
): { listener: Deno.TlsListener; port: number; hostname: string } {
const tlsOptions = { port: 0, hostname: "localhost", cert, key, ...options };
const listener = Deno.listenTls(tlsOptions);
return {
listener,
port: (<Deno.NetAddr> listener.addr).port,
hostname: "localhost",
};
}
function listenTcp(): {
listener: Deno.Listener;
port: number;
hostname: string;
} {
const listener = Deno.listen({ port: 0, hostname: "localhost" });
return {
listener,
port: (<Deno.NetAddr> listener.addr).port,
hostname: "localhost",
};
}
function unreachable(): never {
throw new Error("Unreachable code reached");
}
Deno.test({ permissions: { net: false } }, async function connectTLSNoPerm() {
await assertRejects(async () => {
await Deno.connectTls({ hostname: "deno.land", port: 443 });
}, Deno.errors.PermissionDenied);
});
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSInvalidHost() {
await assertRejects(async () => {
await Deno.connectTls({ hostname: "256.0.0.0", port: 3567 });
}, TypeError);
},
);
Deno.test(
{ permissions: { net: true, read: false } },
async function connectTLSCertFileNoReadPerm() {
await assertRejects(async () => {
await Deno.connectTls({
hostname: "deno.land",
port: 443,
certFile: "tests/testdata/tls/RootCA.crt",
});
}, Deno.errors.PermissionDenied);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
function listenTLSNonExistentCertKeyFiles() {
const options = {
hostname: "localhost",
port: 0,
certFile: "tests/testdata/tls/localhost.crt",
keyFile: "tests/testdata/tls/localhost.key",
};
assertThrows(() => {
Deno.listenTls({
...options,
certFile: "./non/existent/file",
});
}, Deno.errors.NotFound);
assertThrows(() => {
Deno.listenTls({
...options,
keyFile: "./non/existent/file",
});
}, Deno.errors.NotFound);
},
);
Deno.test(
{ permissions: { net: true, read: false } },
function listenTLSNoReadPerm() {
assertThrows(() => {
Deno.listenTls({
hostname: "localhost",
port: 0,
certFile: "tests/testdata/tls/localhost.crt",
keyFile: "tests/testdata/tls/localhost.key",
});
}, Deno.errors.PermissionDenied);
},
);
Deno.test(
{
permissions: { read: true, write: true, net: true },
},
function listenTLSEmptyKeyFile() {
const options = {
hostname: "localhost",
port: 0,
certFile: "tests/testdata/tls/localhost.crt",
keyFile: "tests/testdata/tls/localhost.key",
};
const testDir = Deno.makeTempDirSync();
const keyFilename = testDir + "/key.pem";
Deno.writeFileSync(keyFilename, new Uint8Array([]), {
mode: 0o666,
});
assertThrows(() => {
Deno.listenTls({
...options,
keyFile: keyFilename,
});
}, Error);
},
);
Deno.test(
{ permissions: { read: true, write: true, net: true } },
function listenTLSEmptyCertFile() {
const options = {
hostname: "localhost",
port: 0,
certFile: "tests/testdata/tls/localhost.crt",
keyFile: "tests/testdata/tls/localhost.key",
};
const testDir = Deno.makeTempDirSync();
const certFilename = testDir + "/cert.crt";
Deno.writeFileSync(certFilename, new Uint8Array([]), {
mode: 0o666,
});
assertThrows(() => {
Deno.listenTls({
...options,
certFile: certFilename,
});
}, Error);
},
);
Deno.test(
{ permissions: { net: true } },
async function startTlsWithoutExclusiveAccessToTcpConn() {
const { listener, hostname, port } = listenTcp();
const [serverConn, clientConn] = await Promise.all([
listener.accept(),
Deno.connect({ hostname, port }),
]);
const buf = new Uint8Array(128);
const readPromise = clientConn.read(buf);
// `clientConn` is being used by a pending promise (`readPromise`) so
// `Deno.startTls` cannot consume the connection.
await assertRejects(
() => Deno.startTls(clientConn, { hostname }),
Deno.errors.BadResource,
);
serverConn.close();
listener.close();
await readPromise;
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function dialAndListenTLS() {
const { promise, resolve } = Promise.withResolvers<void>();
const { listener, port, hostname } = listenTls();
const response = encoder.encode(
"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n",
);
listener.accept().then(
async (conn) => {
assert(conn.remoteAddr != null);
assert(conn.localAddr != null);
await conn.write(response);
// TODO(bartlomieju): this might be a bug
setTimeout(() => {
conn.close();
resolve();
}, 0);
},
);
const conn = await Deno.connectTls({ hostname, port, caCerts });
assert(conn.rid > 0);
const w = new BufWriter(conn);
const r = new BufReader(conn);
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);
const statusLine = await tpr.readLine();
assert(statusLine !== null, `line must be read: ${String(statusLine)}`);
const m = statusLine.match(/^(.+?) (.+?) (.+?)$/);
assert(m !== null, "must be matched");
const [_, proto, status, ok] = m;
assertEquals(proto, "HTTP/1.1");
assertEquals(status, "200");
assertEquals(ok, "OK");
const headers = await tpr.readMimeHeader();
assert(headers !== null);
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();
listener.close();
await promise;
},
);
Deno.test(
{ permissions: { read: false, net: true } },
async function listenTlsWithCertAndKey() {
const { promise, resolve } = Promise.withResolvers<void>();
const { listener, hostname, port } = listenTls();
const response = encoder.encode(
"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World\n",
);
listener.accept().then(
async (conn) => {
assert(conn.remoteAddr != null);
assert(conn.localAddr != null);
await conn.write(response);
setTimeout(() => {
conn.close();
resolve();
}, 0);
},
);
const conn = await Deno.connectTls({ hostname, port, caCerts });
assert(conn.rid > 0);
const w = new BufWriter(conn);
const r = new BufReader(conn);
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);
const statusLine = await tpr.readLine();
assert(statusLine !== null, `line must be read: ${String(statusLine)}`);
const m = statusLine.match(/^(.+?) (.+?) (.+?)$/);
assert(m !== null, "must be matched");
const [_, proto, status, ok] = m;
assertEquals(proto, "HTTP/1.1");
assertEquals(status, "200");
assertEquals(ok, "OK");
const headers = await tpr.readMimeHeader();
assert(headers !== null);
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();
listener.close();
await promise;
},
);
async function tlsPair(): Promise<[Deno.Conn, Deno.Conn]> {
const { listener, hostname, port } = listenTls();
const acceptPromise = listener.accept();
const connectPromise = Deno.connectTls({
hostname,
port,
caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
});
const endpoints = await Promise.all([acceptPromise, connectPromise]);
listener.close();
return endpoints;
}
async function tlsAlpn(
useStartTls: boolean,
): Promise<[Deno.TlsConn, Deno.TlsConn]> {
const { listener, port } = listenTls({
alpnProtocols: ["deno", "rocks"],
});
const acceptPromise = listener.accept();
const caCerts = [Deno.readTextFileSync("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,
chunkSize: number,
) {
const byteCount = chunkCount * chunkSize;
const buf = new Uint8Array(chunkSize); // Note: buf is size of _chunk_.
let n: number;
// Slowly send 42s.
buf.fill(42);
for (let remaining = byteCount; remaining > 0; remaining -= n) {
n = await conn.write(buf.subarray(0, remaining));
assert(n >= 1);
await sleep(10);
}
// Send EOF.
await conn.closeWrite();
// Receive 69s.
for (let remaining = byteCount; remaining > 0; remaining -= n) {
buf.fill(0);
n = await conn.read(buf) as number;
assert(n >= 1);
assertStrictEquals(buf[0], 69);
assertStrictEquals(buf[n - 1], 69);
}
conn.close();
}
async function receiveThenSend(
conn: Deno.Conn,
chunkCount: number,
chunkSize: number,
) {
const byteCount = chunkCount * chunkSize;
const buf = new Uint8Array(byteCount); // Note: buf size equals `byteCount`.
let n: number;
// Receive 42s.
for (let remaining = byteCount; remaining > 0; remaining -= n) {
buf.fill(0);
n = await conn.read(buf) as number;
assert(n >= 1);
assertStrictEquals(buf[0], 42);
assertStrictEquals(buf[n - 1], 42);
}
// Slowly send 69s.
buf.fill(69);
for (let remaining = byteCount; remaining > 0; remaining -= n) {
n = await conn.write(buf.subarray(0, remaining));
assert(n >= 1);
await sleep(10);
}
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() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendThenCloseWriteThenReceive(serverConn, 1, 1),
receiveThenSend(clientConn, 1, 1),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsClientStreamHalfCloseSendOneByte() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendThenCloseWriteThenReceive(clientConn, 1, 1),
receiveThenSend(serverConn, 1, 1),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsServerStreamHalfCloseSendOneChunk() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendThenCloseWriteThenReceive(serverConn, 1, 1 << 20 /* 1 MB */),
receiveThenSend(clientConn, 1, 1 << 20 /* 1 MB */),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsClientStreamHalfCloseSendOneChunk() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendThenCloseWriteThenReceive(clientConn, 1, 1 << 20 /* 1 MB */),
receiveThenSend(serverConn, 1, 1 << 20 /* 1 MB */),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsServerStreamHalfCloseSendManyBytes() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendThenCloseWriteThenReceive(serverConn, 100, 1),
receiveThenSend(clientConn, 100, 1),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsClientStreamHalfCloseSendManyBytes() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendThenCloseWriteThenReceive(clientConn, 100, 1),
receiveThenSend(serverConn, 100, 1),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsServerStreamHalfCloseSendManyChunks() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendThenCloseWriteThenReceive(serverConn, 100, 1 << 16 /* 64 kB */),
receiveThenSend(clientConn, 100, 1 << 16 /* 64 kB */),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsClientStreamHalfCloseSendManyChunks() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendThenCloseWriteThenReceive(clientConn, 100, 1 << 16 /* 64 kB */),
receiveThenSend(serverConn, 100, 1 << 16 /* 64 kB */),
]);
},
);
const largeAmount = 1 << 20 /* 1 MB */;
async function sendAlotReceiveNothing(conn: Deno.Conn) {
// Start receive op.
const readBuf = new Uint8Array(1024);
const readPromise = conn.read(readBuf);
const timeout = setTimeout(() => {
throw new Error("Failed to send buffer in a reasonable amount of time");
}, 10_000);
// Send 1 MB of data.
const writeBuf = new Uint8Array(largeAmount);
writeBuf.fill(42);
await writeAll(conn, writeBuf);
clearTimeout(timeout);
// Send EOF.
await conn.closeWrite();
// Close the connection.
conn.close();
// Read op should be canceled.
await assertRejects(
async () => await readPromise,
Deno.errors.Interrupted,
);
}
async function receiveAlotSendNothing(conn: Deno.Conn) {
const readBuf = new Uint8Array(1024);
let n: number | null;
let nread = 0;
const timeout = setTimeout(() => {
throw new Error(
`Failed to read buffer in a reasonable amount of time (got ${nread}/${largeAmount})`,
);
}, 10_000);
// Receive 1 MB of data.
try {
for (; nread < largeAmount; nread += n!) {
n = await conn.read(readBuf);
assertStrictEquals(typeof n, "number");
assert(n! > 0);
assertStrictEquals(readBuf[0], 42);
}
} catch (e) {
throw new Error(
`Got an error (${e.message}) after reading ${nread}/${largeAmount} bytes`,
{ cause: e },
);
}
clearTimeout(timeout);
// Close the connection, without sending anything at all.
conn.close();
}
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsServerStreamCancelRead() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendAlotReceiveNothing(serverConn),
receiveAlotSendNothing(clientConn),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsClientStreamCancelRead() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendAlotReceiveNothing(clientConn),
receiveAlotSendNothing(serverConn),
]);
},
);
async function sendReceiveEmptyBuf(conn: Deno.Conn) {
const byteBuf = new Uint8Array([1]);
const emptyBuf = new Uint8Array(0);
let n: number | null;
n = await conn.write(emptyBuf);
assertStrictEquals(n, 0);
n = await conn.read(emptyBuf);
assertStrictEquals(n, 0);
n = await conn.write(byteBuf);
assertStrictEquals(n, 1);
n = await conn.read(byteBuf);
assertStrictEquals(n, 1);
await conn.closeWrite();
n = await conn.write(emptyBuf);
assertStrictEquals(n, 0);
await assertRejects(async () => {
await conn.write(byteBuf);
}, Deno.errors.NotConnected);
n = await conn.write(emptyBuf);
assertStrictEquals(n, 0);
n = await conn.read(byteBuf);
assertStrictEquals(n, null);
conn.close();
}
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsStreamSendReceiveEmptyBuf() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
sendReceiveEmptyBuf(serverConn),
sendReceiveEmptyBuf(clientConn),
]);
},
);
function immediateClose(conn: Deno.Conn) {
conn.close();
return Promise.resolve();
}
async function closeWriteAndClose(conn: Deno.Conn) {
await conn.closeWrite();
if (await conn.read(new Uint8Array(1)) !== null) {
throw new Error("did not expect to receive data on TLS stream");
}
conn.close();
}
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsServerStreamImmediateClose() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
immediateClose(serverConn),
closeWriteAndClose(clientConn),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsClientStreamImmediateClose() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
closeWriteAndClose(serverConn),
immediateClose(clientConn),
]);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsClientAndServerStreamImmediateClose() {
const [serverConn, clientConn] = await tlsPair();
await Promise.all([
immediateClose(serverConn),
immediateClose(clientConn),
]);
},
);
async function tlsWithTcpFailureTestImpl(
phase: "handshake" | "traffic",
cipherByteCount: number,
failureMode: "corruption" | "shutdown",
reverse: boolean,
) {
const tls = listenTls();
const tcp = listenTcp();
const [tlsServerConn, tcpServerConn] = await Promise.all([
tls.listener.accept(),
Deno.connect({ hostname: tls.hostname, port: tls.port }),
]);
const [tcpClientConn, tlsClientConn] = await Promise.all([
tcp.listener.accept(),
Deno.connectTls({
hostname: tcp.hostname,
port: tcp.port,
caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
}),
]);
tls.listener.close();
tcp.listener.close();
const {
tlsConn1,
tlsConn2,
tcpConn1,
tcpConn2,
} = reverse
? {
tlsConn1: tlsClientConn,
tlsConn2: tlsServerConn,
tcpConn1: tcpClientConn,
tcpConn2: tcpServerConn,
}
: {
tlsConn1: tlsServerConn,
tlsConn2: tlsClientConn,
tcpConn1: tcpServerConn,
tcpConn2: tcpClientConn,
};
const tcpForwardingInterruptDeferred1 = Promise.withResolvers<void>();
const tcpForwardingPromise1 = forwardBytes(
tcpConn2,
tcpConn1,
cipherByteCount,
tcpForwardingInterruptDeferred1,
);
const tcpForwardingInterruptDeferred2 = Promise.withResolvers<void>();
const tcpForwardingPromise2 = forwardBytes(
tcpConn1,
tcpConn2,
Infinity,
tcpForwardingInterruptDeferred2,
);
switch (phase) {
case "handshake": {
let expectedError;
switch (failureMode) {
case "corruption":
expectedError = Deno.errors.InvalidData;
break;
case "shutdown":
expectedError = Deno.errors.UnexpectedEof;
break;
default:
unreachable();
}
const tlsTrafficPromise1 = Promise.all([
assertRejects(
() => sendBytes(tlsConn1, 0x01, 1),
expectedError,
),
assertRejects(
() => receiveBytes(tlsConn1, 0x02, 1),
expectedError,
),
]);
const tlsTrafficPromise2 = Promise.all([
assertRejects(
() => sendBytes(tlsConn2, 0x02, 1),
Deno.errors.UnexpectedEof,
),
assertRejects(
() => receiveBytes(tlsConn2, 0x01, 1),
Deno.errors.UnexpectedEof,
),
]);
await tcpForwardingPromise1;
switch (failureMode) {
case "corruption":
await sendBytes(tcpConn1, 0xff, 1 << 14 /* 16 kB */);
break;
case "shutdown":
await tcpConn1.closeWrite();
break;
default:
unreachable();
}
await tlsTrafficPromise1;
tcpForwardingInterruptDeferred2.resolve();
await tcpForwardingPromise2;
await tcpConn2.closeWrite();
await tlsTrafficPromise2;
break;
}
case "traffic": {
await Promise.all([
sendBytes(tlsConn2, 0x88, 8888),
receiveBytes(tlsConn1, 0x88, 8888),
sendBytes(tlsConn1, 0x99, 99999),
receiveBytes(tlsConn2, 0x99, 99999),
]);
tcpForwardingInterruptDeferred1.resolve();
await tcpForwardingInterruptDeferred1.promise;
switch (failureMode) {
case "corruption":
await sendBytes(tcpConn1, 0xff, 1 << 14 /* 16 kB */);
await assertRejects(
() => receiveEof(tlsConn1),
Deno.errors.InvalidData,
);
tcpForwardingInterruptDeferred2.resolve();
break;
case "shutdown":
await Promise.all([
tcpConn1.closeWrite(),
await assertRejects(
() => receiveEof(tlsConn1),
Deno.errors.UnexpectedEof,
),
await tlsConn1.closeWrite(),
await receiveEof(tlsConn2),
]);
break;
default:
unreachable();
}
await tcpForwardingPromise2;
break;
}
default:
unreachable();
}
tlsServerConn.close();
tlsClientConn.close();
tcpServerConn.close();
tcpClientConn.close();
async function sendBytes(
conn: Deno.Conn,
byte: number,
count: number,
) {
let buf = new Uint8Array(1 << 12 /* 4 kB */);
buf.fill(byte);
while (count > 0) {
buf = buf.subarray(0, Math.min(buf.length, count));
const nwritten = await conn.write(buf);
assertStrictEquals(nwritten, buf.length);
count -= nwritten;
}
}
async function receiveBytes(
conn: Deno.Conn,
byte: number,
count: number,
) {
let buf = new Uint8Array(1 << 12 /* 4 kB */);
while (count > 0) {
buf = buf.subarray(0, Math.min(buf.length, count));
const r = await conn.read(buf);
assertNotEquals(r, null);
assert(buf.subarray(0, r!).every((b) => b === byte));
count -= r!;
}
}
async function receiveEof(conn: Deno.Conn) {
const buf = new Uint8Array(1);
const r = await conn.read(buf);
assertStrictEquals(r, null);
}
async function forwardBytes(
source: Deno.Conn,
sink: Deno.Conn,
count: number,
interruptPromise: ReturnType<typeof Promise.withResolvers<void>>,
) {
let buf = new Uint8Array(1 << 12 /* 4 kB */);
while (count > 0) {
buf = buf.subarray(0, Math.min(buf.length, count));
const nread = await Promise.race([
source.read(buf),
interruptPromise.promise,
]);
if (nread == null) break; // Either EOF or interrupted.
const nwritten = await sink.write(buf.subarray(0, nread));
assertStrictEquals(nread, nwritten);
count -= nwritten;
}
}
}
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsHandshakeWithTcpCorruptionImmediately() {
await tlsWithTcpFailureTestImpl("handshake", 0, "corruption", false);
await tlsWithTcpFailureTestImpl("handshake", 0, "corruption", true);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsHandshakeWithTcpShutdownImmediately() {
await tlsWithTcpFailureTestImpl("handshake", 0, "shutdown", false);
await tlsWithTcpFailureTestImpl("handshake", 0, "shutdown", true);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsHandshakeWithTcpCorruptionAfter70Bytes() {
await tlsWithTcpFailureTestImpl("handshake", 76, "corruption", false);
await tlsWithTcpFailureTestImpl("handshake", 78, "corruption", true);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsHandshakeWithTcpShutdownAfter70bytes() {
await tlsWithTcpFailureTestImpl("handshake", 77, "shutdown", false);
await tlsWithTcpFailureTestImpl("handshake", 79, "shutdown", true);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsHandshakeWithTcpCorruptionAfter200Bytes() {
await tlsWithTcpFailureTestImpl("handshake", 200, "corruption", false);
await tlsWithTcpFailureTestImpl("handshake", 202, "corruption", true);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsHandshakeWithTcpShutdownAfter200bytes() {
await tlsWithTcpFailureTestImpl("handshake", 201, "shutdown", false);
await tlsWithTcpFailureTestImpl("handshake", 203, "shutdown", true);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsTrafficWithTcpCorruption() {
await tlsWithTcpFailureTestImpl("traffic", Infinity, "corruption", false);
await tlsWithTcpFailureTestImpl("traffic", Infinity, "corruption", true);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsTrafficWithTcpShutdown() {
await tlsWithTcpFailureTestImpl("traffic", Infinity, "shutdown", false);
await tlsWithTcpFailureTestImpl("traffic", Infinity, "shutdown", true);
},
);
function createHttpsListener(): {
listener: Deno.TlsListener;
hostname: string;
port: number;
} {
// Query format: `curl --insecure https://localhost:8443/z/12345`
// The server returns a response consisting of 12345 times the letter 'z'.
const { listener, hostname, port } = listenTls();
serve(listener);
return { listener, hostname, port };
async function serve(listener: Deno.Listener) {
for await (const conn of listener) {
const EOL = "\r\n";
// Read GET request plus headers.
const buf = new Uint8Array(1 << 12 /* 4 kB */);
const decoder = new TextDecoder();
let req = "";
while (!req.endsWith(EOL + EOL)) {
const n = await conn.read(buf);
if (n === null) throw new Error("Unexpected EOF");
req += decoder.decode(buf.subarray(0, n));
}
// Parse GET request.
const { filler, count, version } =
/^GET \/(?<filler>[^\/]+)\/(?<count>\d+) HTTP\/(?<version>1\.\d)\r\n/
.exec(req)!.groups as {
filler: string;
count: string;
version: string;
};
// Generate response.
const resBody = new TextEncoder().encode(filler.repeat(+count));
const resHead = new TextEncoder().encode(
[
`HTTP/${version} 200 OK`,
`Content-Length: ${resBody.length}`,
"Content-Type: text/plain",
].join(EOL) + EOL + EOL,
);
// Send response.
await writeAll(conn, resHead);
await writeAll(conn, resBody);
// Close TCP connection.
conn.close();
}
}
}
async function curl(url: string): Promise<string> {
const { success, code, stdout, stderr } = await new Deno.Command("curl", {
args: ["--insecure", url],
}).output();
if (!success) {
throw new Error(
`curl ${url} failed: ${code}:\n${new TextDecoder().decode(stderr)}`,
);
}
return new TextDecoder().decode(stdout);
}
Deno.test(
{ permissions: { read: true, net: true, run: true } },
async function curlFakeHttpsServer() {
const { listener, port } = createHttpsListener();
const res1 = await curl(`https://localhost:${port}/d/1`);
assertStrictEquals(res1, "d");
const res2 = await curl(`https://localhost:${port}/e/12345`);
assertStrictEquals(res2, "e".repeat(12345));
const count3 = 1 << 17; // 128 kB.
const res3 = await curl(`https://localhost:${port}/n/${count3}`);
assertStrictEquals(res3, "n".repeat(count3));
const count4 = 12345678;
const res4 = await curl(`https://localhost:${port}/o/${count4}`);
assertStrictEquals(res4, "o".repeat(count4));
listener.close();
},
);
Deno.test(
// Ignored because gmail appears to reject us on CI sometimes
{ ignore: true, permissions: { read: true, net: true } },
async function startTls() {
const hostname = "smtp.gmail.com";
const port = 587;
const encoder = new TextEncoder();
const conn = await Deno.connect({
hostname,
port,
});
let writer = new BufWriter(conn);
let reader = new TextProtoReader(new BufReader(conn));
let line: string | null = (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");
const tlsConn = await Deno.startTls(conn, { hostname });
writer = new BufWriter(tlsConn);
reader = new TextProtoReader(new BufReader(tlsConn));
// 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;
}
tlsConn.close();
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSBadClientCertPrivateKey(): Promise<void> {
await assertRejects(async () => {
await Deno.connectTls({
hostname: "deno.land",
port: 443,
certChain: "bad data",
privateKey: Deno.readTextFileSync(
"tests/testdata/tls/localhost.key",
),
});
}, Deno.errors.InvalidData);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSBadCertKey(): Promise<void> {
await assertRejects(async () => {
await Deno.connectTls({
hostname: "deno.land",
port: 443,
cert: "bad data",
key: Deno.readTextFileSync(
"tests/testdata/tls/localhost.key",
),
});
}, Deno.errors.InvalidData);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSBadPrivateKey(): Promise<void> {
await assertRejects(async () => {
await Deno.connectTls({
hostname: "deno.land",
port: 443,
certChain: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
privateKey: "bad data",
});
}, Deno.errors.InvalidData);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSBadKey(): Promise<void> {
await assertRejects(async () => {
await Deno.connectTls({
hostname: "deno.land",
port: 443,
cert: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
key: "bad data",
});
}, Deno.errors.InvalidData);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSNotPrivateKey(): Promise<void> {
await assertRejects(async () => {
await Deno.connectTls({
hostname: "deno.land",
port: 443,
certChain: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
privateKey: "",
});
}, Deno.errors.InvalidData);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSNotKey(): Promise<void> {
await assertRejects(async () => {
await Deno.connectTls({
hostname: "deno.land",
port: 443,
cert: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
key: "",
});
}, Deno.errors.InvalidData);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectWithClientCert() {
// The test_server running on port 4552 responds with 'PASS' if client
// authentication was successful. Try it by running test_server and
// curl --key tests/testdata/tls/localhost.key \
// --cert tests/testdata/tls/localhost.crt \
// --cacert tests/testdata/tls/RootCA.crt https://localhost:4552/
const conn = await Deno.connectTls({
hostname: "localhost",
port: 4552,
certChain: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
privateKey: Deno.readTextFileSync(
"tests/testdata/tls/localhost.key",
),
caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
});
const result = decoder.decode(await readAll(conn));
assertEquals(result, "PASS");
conn.close();
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectWithCert() {
// The test_server running on port 4552 responds with 'PASS' if client
// authentication was successful. Try it by running test_server and
// curl --key cli/tests/testdata/tls/localhost.key \
// --cert cli/tests/testdata/tls/localhost.crt \
// --cacert cli/tests/testdata/tls/RootCA.crt https://localhost:4552/
const conn = await Deno.connectTls({
hostname: "localhost",
port: 4552,
cert: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
key: Deno.readTextFileSync(
"tests/testdata/tls/localhost.key",
),
caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
});
const result = decoder.decode(await readAll(conn));
assertEquals(result, "PASS");
conn.close();
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTlsConflictingCertOptions(): Promise<void> {
await assertRejects(
async () => {
await Deno.connectTls({
hostname: "deno.land",
port: 443,
cert: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
certChain: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
key: Deno.readTextFileSync(
"tests/testdata/tls/localhost.key",
),
});
},
TypeError,
"Cannot specify both `certChain` and `cert`",
);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTlsConflictingKeyOptions(): Promise<void> {
await assertRejects(
async () => {
await Deno.connectTls({
hostname: "deno.land",
port: 443,
cert: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
privateKey: Deno.readTextFileSync(
"tests/testdata/tls/localhost.crt",
),
key: Deno.readTextFileSync(
"tests/testdata/tls/localhost.key",
),
});
},
TypeError,
"Cannot specify both `key` and `privateKey` for `Deno.connectTls`.",
);
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSCaCerts() {
const conn = await Deno.connectTls({
hostname: "localhost",
port: 4557,
caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
});
const result = decoder.decode(await readAll(conn));
assertEquals(result, "PASS");
conn.close();
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function connectTLSCertFile() {
const conn = await Deno.connectTls({
hostname: "localhost",
port: 4557,
certFile: "tests/testdata/tls/RootCA.pem",
});
const result = decoder.decode(await readAll(conn));
assertEquals(result, "PASS");
conn.close();
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function startTLSCaCerts() {
const plainConn = await Deno.connect({
hostname: "localhost",
port: 4557,
});
const conn = await Deno.startTls(plainConn, {
hostname: "localhost",
caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
});
const result = decoder.decode(await readAll(conn));
assertEquals(result, "PASS");
conn.close();
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsHandshakeSuccess() {
const { listener, hostname, port } = listenTls();
const acceptPromise = listener.accept();
const connectPromise = Deno.connectTls({
hostname,
port,
certFile: "tests/testdata/tls/RootCA.crt",
});
const [conn1, conn2] = await Promise.all([acceptPromise, connectPromise]);
listener.close();
await Promise.all([conn1.handshake(), conn2.handshake()]);
// Begin sending a 10mb blob over the TLS connection.
const whole = new Uint8Array(10 << 20); // 10mb.
whole.fill(42);
const sendPromise = writeAll(conn1, whole);
// Set up the other end to receive half of the large blob.
const half = new Uint8Array(whole.byteLength / 2);
const receivePromise = readFull(conn2, half);
await conn1.handshake();
await conn2.handshake();
// Finish receiving the first 5mb.
assertEquals(await receivePromise, half.length);
// See that we can call `handshake()` in the middle of large reads and writes.
await conn1.handshake();
await conn2.handshake();
// Receive second half of large blob. Wait for the send promise and check it.
assertEquals(await readFull(conn2, half), half.length);
await sendPromise;
await conn1.handshake();
await conn2.handshake();
await conn1.closeWrite();
await conn2.closeWrite();
await conn1.handshake();
await conn2.handshake();
conn1.close();
conn2.close();
async function readFull(conn: Deno.Conn, buf: Uint8Array) {
let offset, n;
for (offset = 0; offset < buf.length; offset += n) {
n = await conn.read(buf.subarray(offset, buf.length));
assert(n != null && n > 0);
}
return offset;
}
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function tlsHandshakeFailure() {
let tls: { listener: Deno.TlsListener; port: number; hostname: string };
async function server() {
for await (const conn of tls.listener) {
for (let i = 0; i < 10; i++) {
// Handshake fails because the client rejects the server certificate.
await assertRejects(
() => conn.handshake(),
Deno.errors.InvalidData,
"received fatal alert",
);
}
conn.close();
break;
}
}
async function connectTlsClient() {
const conn = await Deno.connectTls({
hostname: tls.hostname,
port: tls.port,
});
// Handshake fails because the server presents a self-signed certificate.
await assertRejects(
() => conn.handshake(),
Deno.errors.InvalidData,
"invalid peer certificate: UnknownIssuer",
);
conn.close();
}
tls = listenTls();
await Promise.all([server(), connectTlsClient()]);
async function startTlsClient() {
const tcpConn = await Deno.connect({
hostname: tls.hostname,
port: tls.port,
});
const tlsConn = await Deno.startTls(tcpConn, {
hostname: "foo.land",
caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
});
// Handshake fails because hostname doesn't match the certificate.
await assertRejects(
() => tlsConn.handshake(),
Deno.errors.InvalidData,
"NotValidForName",
);
tlsConn.close();
}
tls = listenTls();
await Promise.all([server(), startTlsClient()]);
},
);
Deno.test(
{ permissions: { net: true } },
async function listenTlsWithReuseAddr() {
const deferred1 = Promise.withResolvers<void>();
const { listener: listener1, port, hostname } = listenTls();
listener1.accept().then((conn) => {
conn.close();
deferred1.resolve();
});
const conn1 = await Deno.connectTls({ hostname, port, caCerts });
conn1.close();
await deferred1.promise;
listener1.close();
const deferred2 = Promise.withResolvers<void>();
const listener2 = Deno.listenTls({ hostname, port, cert, key });
listener2.accept().then((conn) => {
conn.close();
deferred2.resolve();
});
const conn2 = await Deno.connectTls({ hostname, port, caCerts });
conn2.close();
await deferred2.promise;
listener2.close();
},
);
Deno.test({
ignore: Deno.build.os !== "linux",
permissions: { net: true },
}, async function listenTlsReusePort() {
const { listener: listener1, port, hostname } = listenTls({
reusePort: true,
});
const listener2 = Deno.listenTls({
hostname,
port,
cert,
key,
reusePort: true,
});
let p1;
let p2;
let listener1Recv = false;
let listener2Recv = false;
while (!listener1Recv || !listener2Recv) {
if (!p1) {
p1 = listener1.accept().then((conn) => {
conn.close();
listener1Recv = true;
p1 = undefined;
listener1.close();
}).catch(() => {});
}
if (!p2) {
p2 = listener2.accept().then((conn) => {
conn.close();
listener2Recv = true;
p2 = undefined;
listener2.close();
}).catch(() => {});
}
const conn = await Deno.connectTls({ hostname, port, caCerts });
conn.close();
await Promise.race([p1, p2]);
}
});
Deno.test({
ignore: Deno.build.os === "linux",
permissions: { net: true },
}, function listenTlsReusePortDoesNothing() {
const { listener: listener1, hostname, port } = listenTls({
reusePort: true,
});
assertThrows(() => {
Deno.listenTls({ hostname, port, cert, key, reusePort: true });
}, Deno.errors.AddrInUse);
listener1.close();
});
Deno.test({
permissions: { net: true },
}, function listenTlsDoesNotThrowOnStringPort() {
const listener = Deno.listenTls({
hostname: "localhost",
// @ts-ignore String port is not allowed by typing, but it shouldn't throw
// for backwards compatibility.
port: "0",
cert,
key,
});
listener.close();
});
Deno.test(
{ permissions: { net: true, read: true } },
function listenTLSInvalidCert() {
assertThrows(() => {
Deno.listenTls({
hostname: "localhost",
port: 0,
certFile: "tests/testdata/tls/invalid.crt",
keyFile: "tests/testdata/tls/localhost.key",
});
}, Deno.errors.InvalidData);
},
);
Deno.test(
{ permissions: { net: true, read: true } },
function listenTLSInvalidKey() {
assertThrows(() => {
Deno.listenTls({
hostname: "localhost",
port: 0,
certFile: "tests/testdata/tls/localhost.crt",
keyFile: "tests/testdata/tls/invalid.key",
});
}, Deno.errors.InvalidData);
},
);
Deno.test(
{ permissions: { net: true, read: true } },
function listenTLSEcKey() {
const listener = Deno.listenTls({
hostname: "localhost",
port: 0,
certFile: "tests/testdata/tls/localhost_ecc.crt",
keyFile: "tests/testdata/tls/localhost_ecc.key",
});
listener.close();
},
);