feat: startTLS (#4773)

This commit is contained in:
EnokMan 2020-04-18 10:21:20 -05:00 committed by GitHub
parent 10469ec279
commit 47617e60d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 222 additions and 12 deletions

View file

@ -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";

View file

@ -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;

View file

@ -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);
}

View file

@ -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();
}
);

View file

@ -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!);
}

View file

@ -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,

View file

@ -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)]

View file

@ -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,

View file

@ -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());
}
}