mirror of
https://github.com/denoland/deno.git
synced 2024-11-25 15:29:32 -05:00
feat: startTLS (#4773)
This commit is contained in:
parent
10469ec279
commit
47617e60d5
9 changed files with 222 additions and 12 deletions
|
@ -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";
|
||||
|
|
27
cli/js/lib.deno.ns.d.ts
vendored
27
cli/js/lib.deno.ns.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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!);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue