mirror of
https://github.com/denoland/deno.git
synced 2024-10-29 08:58:01 -04:00
feat: support client certificates for connectTls (#11598)
Co-authored-by: Daniel Lamando <dan@danopia.net> Co-authored-by: Erik Price <github@erikprice.net>
This commit is contained in:
parent
f402904e6e
commit
3ab50b3551
6 changed files with 262 additions and 36 deletions
|
@ -11,6 +11,7 @@ import {
|
||||||
unitTest,
|
unitTest,
|
||||||
} from "./test_util.ts";
|
} from "./test_util.ts";
|
||||||
import { BufReader, BufWriter } from "../../../test_util/std/io/bufio.ts";
|
import { BufReader, BufWriter } from "../../../test_util/std/io/bufio.ts";
|
||||||
|
import { readAll } from "../../../test_util/std/io/util.ts";
|
||||||
import { TextProtoReader } from "../../../test_util/std/textproto/mod.ts";
|
import { TextProtoReader } from "../../../test_util/std/textproto/mod.ts";
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
|
@ -26,7 +27,7 @@ function unreachable(): never {
|
||||||
|
|
||||||
unitTest(async function connectTLSNoPerm() {
|
unitTest(async function connectTLSNoPerm() {
|
||||||
await assertThrowsAsync(async () => {
|
await assertThrowsAsync(async () => {
|
||||||
await Deno.connectTls({ hostname: "github.com", port: 443 });
|
await Deno.connectTls({ hostname: "deno.land", port: 443 });
|
||||||
}, Deno.errors.PermissionDenied);
|
}, Deno.errors.PermissionDenied);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ unitTest(
|
||||||
unitTest(async function connectTLSCertFileNoReadPerm() {
|
unitTest(async function connectTLSCertFileNoReadPerm() {
|
||||||
await assertThrowsAsync(async () => {
|
await assertThrowsAsync(async () => {
|
||||||
await Deno.connectTls({
|
await Deno.connectTls({
|
||||||
hostname: "github.com",
|
hostname: "deno.land",
|
||||||
port: 443,
|
port: 443,
|
||||||
certFile: "cli/tests/tls/RootCA.crt",
|
certFile: "cli/tests/tls/RootCA.crt",
|
||||||
});
|
});
|
||||||
|
@ -985,3 +986,66 @@ unitTest(
|
||||||
conn.close();
|
conn.close();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
unitTest(
|
||||||
|
{ perms: { read: true, net: true } },
|
||||||
|
async function connectTLSBadClientCertPrivateKey(): Promise<void> {
|
||||||
|
await assertThrowsAsync(async () => {
|
||||||
|
await Deno.connectTls({
|
||||||
|
hostname: "deno.land",
|
||||||
|
port: 443,
|
||||||
|
certChain: "bad data",
|
||||||
|
privateKey: await Deno.readTextFile("cli/tests/tls/localhost.key"),
|
||||||
|
});
|
||||||
|
}, Deno.errors.InvalidData);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
unitTest(
|
||||||
|
{ perms: { read: true, net: true } },
|
||||||
|
async function connectTLSBadPrivateKey(): Promise<void> {
|
||||||
|
await assertThrowsAsync(async () => {
|
||||||
|
await Deno.connectTls({
|
||||||
|
hostname: "deno.land",
|
||||||
|
port: 443,
|
||||||
|
certChain: await Deno.readTextFile("cli/tests/tls/localhost.crt"),
|
||||||
|
privateKey: "bad data",
|
||||||
|
});
|
||||||
|
}, Deno.errors.InvalidData);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
unitTest(
|
||||||
|
{ perms: { read: true, net: true } },
|
||||||
|
async function connectTLSNotPrivateKey(): Promise<void> {
|
||||||
|
await assertThrowsAsync(async () => {
|
||||||
|
await Deno.connectTls({
|
||||||
|
hostname: "deno.land",
|
||||||
|
port: 443,
|
||||||
|
certChain: await Deno.readTextFile("cli/tests/tls/localhost.crt"),
|
||||||
|
privateKey: "",
|
||||||
|
});
|
||||||
|
}, Deno.errors.InvalidData);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
unitTest(
|
||||||
|
{ perms: { 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 cli/tests/tls/localhost.key \
|
||||||
|
// --cert cli/tests/tls/localhost.crt \
|
||||||
|
// --cacert cli/tests/tls/RootCA.crt https://localhost:4552/
|
||||||
|
const conn = await Deno.connectTls({
|
||||||
|
hostname: "localhost",
|
||||||
|
port: 4552,
|
||||||
|
certChain: await Deno.readTextFile("cli/tests/tls/localhost.crt"),
|
||||||
|
privateKey: await Deno.readTextFile("cli/tests/tls/localhost.key"),
|
||||||
|
certFile: "cli/tests/tls/RootCA.crt",
|
||||||
|
});
|
||||||
|
const result = decoder.decode(await readAll(conn));
|
||||||
|
assertEquals(result, "PASS");
|
||||||
|
conn.close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -28,12 +28,16 @@
|
||||||
hostname = "127.0.0.1",
|
hostname = "127.0.0.1",
|
||||||
transport = "tcp",
|
transport = "tcp",
|
||||||
certFile = undefined,
|
certFile = undefined,
|
||||||
|
certChain = undefined,
|
||||||
|
privateKey = undefined,
|
||||||
}) {
|
}) {
|
||||||
const res = await opConnectTls({
|
const res = await opConnectTls({
|
||||||
port,
|
port,
|
||||||
hostname,
|
hostname,
|
||||||
transport,
|
transport,
|
||||||
certFile,
|
certFile,
|
||||||
|
certChain,
|
||||||
|
privateKey,
|
||||||
});
|
});
|
||||||
return new Conn(res.rid, res.remoteAddr, res.localAddr);
|
return new Conn(res.rid, res.remoteAddr, res.localAddr);
|
||||||
}
|
}
|
||||||
|
|
5
extensions/net/lib.deno_net.d.ts
vendored
5
extensions/net/lib.deno_net.d.ts
vendored
|
@ -68,9 +68,10 @@ declare namespace Deno {
|
||||||
): Listener;
|
): Listener;
|
||||||
|
|
||||||
export interface ListenTlsOptions extends ListenOptions {
|
export interface ListenTlsOptions extends ListenOptions {
|
||||||
/** Server certificate file. */
|
/** Path to a file containing a PEM formatted CA certificate. Requires
|
||||||
|
* `--allow-read`. */
|
||||||
certFile: string;
|
certFile: string;
|
||||||
/** Server public key file. */
|
/** Server public key file. Requires `--allow-read`.*/
|
||||||
keyFile: string;
|
keyFile: string;
|
||||||
|
|
||||||
transport?: "tcp";
|
transport?: "tcp";
|
||||||
|
|
26
extensions/net/lib.deno_net.unstable.d.ts
vendored
26
extensions/net/lib.deno_net.unstable.d.ts
vendored
|
@ -191,6 +191,32 @@ declare namespace Deno {
|
||||||
options: ConnectOptions | UnixConnectOptions,
|
options: ConnectOptions | UnixConnectOptions,
|
||||||
): Promise<Conn>;
|
): Promise<Conn>;
|
||||||
|
|
||||||
|
export interface ConnectTlsClientCertOptions {
|
||||||
|
/** PEM formatted client certificate chain. */
|
||||||
|
certChain: string;
|
||||||
|
/** PEM formatted (RSA or PKCS8) private key of client certificate. */
|
||||||
|
privateKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** **UNSTABLE** New API, yet to be vetted.
|
||||||
|
*
|
||||||
|
* Create a TLS connection with an attached client certificate.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const conn = await Deno.connectTls({
|
||||||
|
* hostname: "deno.land",
|
||||||
|
* port: 443,
|
||||||
|
* certChain: "---- BEGIN CERTIFICATE ----\n ...",
|
||||||
|
* privateKey: "---- BEGIN PRIVATE KEY ----\n ...",
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Requires `allow-net` permission.
|
||||||
|
*/
|
||||||
|
export function connectTls(
|
||||||
|
options: ConnectTlsOptions & ConnectTlsClientCertOptions,
|
||||||
|
): Promise<Conn>;
|
||||||
|
|
||||||
export interface StartTlsOptions {
|
export interface StartTlsOptions {
|
||||||
/** A literal IP address or host name that can be resolved to an IP address.
|
/** A literal IP address or host name that can be resolved to an IP address.
|
||||||
* If not specified, defaults to `127.0.0.1`. */
|
* If not specified, defaults to `127.0.0.1`. */
|
||||||
|
|
|
@ -14,6 +14,7 @@ use deno_core::error::bad_resource_id;
|
||||||
use deno_core::error::custom_error;
|
use deno_core::error::custom_error;
|
||||||
use deno_core::error::generic_error;
|
use deno_core::error::generic_error;
|
||||||
use deno_core::error::invalid_hostname;
|
use deno_core::error::invalid_hostname;
|
||||||
|
use deno_core::error::type_error;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::futures::future::poll_fn;
|
use deno_core::futures::future::poll_fn;
|
||||||
use deno_core::futures::ready;
|
use deno_core::futures::ready;
|
||||||
|
@ -57,6 +58,7 @@ use std::cell::RefCell;
|
||||||
use std::convert::From;
|
use std::convert::From;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::io::BufRead;
|
||||||
use std::io::BufReader;
|
use std::io::BufReader;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
@ -649,6 +651,8 @@ pub struct ConnectTlsArgs {
|
||||||
hostname: String,
|
hostname: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
cert_file: Option<String>,
|
cert_file: Option<String>,
|
||||||
|
cert_chain: Option<String>,
|
||||||
|
private_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -717,6 +721,7 @@ where
|
||||||
let remote_addr = tcp_stream.peer_addr()?;
|
let remote_addr = tcp_stream.peer_addr()?;
|
||||||
|
|
||||||
let tls_config = Arc::new(create_client_config(root_cert_store, ca_data)?);
|
let tls_config = Arc::new(create_client_config(root_cert_store, ca_data)?);
|
||||||
|
|
||||||
let tls_stream =
|
let tls_stream =
|
||||||
TlsStream::new_client_side(tcp_stream, &tls_config, hostname_dns);
|
TlsStream::new_client_side(tcp_stream, &tls_config, hostname_dns);
|
||||||
|
|
||||||
|
@ -755,6 +760,14 @@ where
|
||||||
};
|
};
|
||||||
let port = args.port;
|
let port = args.port;
|
||||||
let cert_file = args.cert_file.as_deref();
|
let cert_file = args.cert_file.as_deref();
|
||||||
|
|
||||||
|
if args.cert_chain.is_some() {
|
||||||
|
super::check_unstable2(&state, "ConnectTlsOptions.certChain");
|
||||||
|
}
|
||||||
|
if args.private_key.is_some() {
|
||||||
|
super::check_unstable2(&state, "ConnectTlsOptions.privateKey");
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let mut s = state.borrow_mut();
|
let mut s = state.borrow_mut();
|
||||||
let permissions = s.borrow_mut::<NP>();
|
let permissions = s.borrow_mut::<NP>();
|
||||||
|
@ -788,7 +801,28 @@ where
|
||||||
let tcp_stream = TcpStream::connect(connect_addr).await?;
|
let tcp_stream = TcpStream::connect(connect_addr).await?;
|
||||||
let local_addr = tcp_stream.local_addr()?;
|
let local_addr = tcp_stream.local_addr()?;
|
||||||
let remote_addr = tcp_stream.peer_addr()?;
|
let remote_addr = tcp_stream.peer_addr()?;
|
||||||
let tls_config = Arc::new(create_client_config(root_cert_store, ca_data)?);
|
|
||||||
|
let mut tls_config = create_client_config(root_cert_store, ca_data)?;
|
||||||
|
|
||||||
|
if args.cert_chain.is_some() || args.private_key.is_some() {
|
||||||
|
let cert_chain = args
|
||||||
|
.cert_chain
|
||||||
|
.ok_or_else(|| type_error("No certificate chain provided"))?;
|
||||||
|
let private_key = args
|
||||||
|
.private_key
|
||||||
|
.ok_or_else(|| type_error("No private key provided"))?;
|
||||||
|
|
||||||
|
// The `remove` is safe because load_private_keys checks that there is at least one key.
|
||||||
|
let private_key = load_private_keys(private_key.as_bytes())?.remove(0);
|
||||||
|
|
||||||
|
tls_config.set_single_client_cert(
|
||||||
|
load_certs(&mut cert_chain.as_bytes())?,
|
||||||
|
private_key,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tls_config = Arc::new(tls_config);
|
||||||
|
|
||||||
let tls_stream =
|
let tls_stream =
|
||||||
TlsStream::new_client_side(tcp_stream, &tls_config, hostname_dns);
|
TlsStream::new_client_side(tcp_stream, &tls_config, hostname_dns);
|
||||||
|
|
||||||
|
@ -812,10 +846,7 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_certs(path: &str) -> Result<Vec<Certificate>, AnyError> {
|
fn load_certs(reader: &mut dyn BufRead) -> Result<Vec<Certificate>, AnyError> {
|
||||||
let cert_file = File::open(path)?;
|
|
||||||
let reader = &mut BufReader::new(cert_file);
|
|
||||||
|
|
||||||
let certs = certs(reader)
|
let certs = certs(reader)
|
||||||
.map_err(|_| custom_error("InvalidData", "Unable to decode certificate"))?;
|
.map_err(|_| custom_error("InvalidData", "Unable to decode certificate"))?;
|
||||||
|
|
||||||
|
@ -827,6 +858,12 @@ fn load_certs(path: &str) -> Result<Vec<Certificate>, AnyError> {
|
||||||
Ok(certs)
|
Ok(certs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_certs_from_file(path: &str) -> Result<Vec<Certificate>, AnyError> {
|
||||||
|
let cert_file = File::open(path)?;
|
||||||
|
let reader = &mut BufReader::new(cert_file);
|
||||||
|
load_certs(reader)
|
||||||
|
}
|
||||||
|
|
||||||
fn key_decode_err() -> AnyError {
|
fn key_decode_err() -> AnyError {
|
||||||
custom_error("InvalidData", "Unable to decode key")
|
custom_error("InvalidData", "Unable to decode key")
|
||||||
}
|
}
|
||||||
|
@ -836,27 +873,22 @@ fn key_not_found_err() -> AnyError {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts with -----BEGIN RSA PRIVATE KEY-----
|
/// Starts with -----BEGIN RSA PRIVATE KEY-----
|
||||||
fn load_rsa_keys(path: &str) -> Result<Vec<PrivateKey>, AnyError> {
|
fn load_rsa_keys(mut bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> {
|
||||||
let key_file = File::open(path)?;
|
let keys = rsa_private_keys(&mut bytes).map_err(|_| key_decode_err())?;
|
||||||
let reader = &mut BufReader::new(key_file);
|
|
||||||
let keys = rsa_private_keys(reader).map_err(|_| key_decode_err())?;
|
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts with -----BEGIN PRIVATE KEY-----
|
/// Starts with -----BEGIN PRIVATE KEY-----
|
||||||
fn load_pkcs8_keys(path: &str) -> Result<Vec<PrivateKey>, AnyError> {
|
fn load_pkcs8_keys(mut bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> {
|
||||||
let key_file = File::open(path)?;
|
let keys = pkcs8_private_keys(&mut bytes).map_err(|_| key_decode_err())?;
|
||||||
let reader = &mut BufReader::new(key_file);
|
|
||||||
let keys = pkcs8_private_keys(reader).map_err(|_| key_decode_err())?;
|
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_keys(path: &str) -> Result<Vec<PrivateKey>, AnyError> {
|
fn load_private_keys(bytes: &[u8]) -> Result<Vec<PrivateKey>, AnyError> {
|
||||||
let path = path.to_string();
|
let mut keys = load_rsa_keys(bytes)?;
|
||||||
let mut keys = load_rsa_keys(&path)?;
|
|
||||||
|
|
||||||
if keys.is_empty() {
|
if keys.is_empty() {
|
||||||
keys = load_pkcs8_keys(&path)?;
|
keys = load_pkcs8_keys(bytes)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if keys.is_empty() {
|
if keys.is_empty() {
|
||||||
|
@ -866,6 +898,13 @@ fn load_keys(path: &str) -> Result<Vec<PrivateKey>, AnyError> {
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_private_keys_from_file(
|
||||||
|
path: &str,
|
||||||
|
) -> Result<Vec<PrivateKey>, AnyError> {
|
||||||
|
let key_bytes = std::fs::read(path)?;
|
||||||
|
load_private_keys(&key_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
pub struct TlsListenerResource {
|
pub struct TlsListenerResource {
|
||||||
tcp_listener: AsyncRefCell<TcpListener>,
|
tcp_listener: AsyncRefCell<TcpListener>,
|
||||||
tls_config: Arc<ServerConfig>,
|
tls_config: Arc<ServerConfig>,
|
||||||
|
@ -921,7 +960,10 @@ where
|
||||||
alpn_protocols.into_iter().map(|s| s.into_bytes()).collect();
|
alpn_protocols.into_iter().map(|s| s.into_bytes()).collect();
|
||||||
}
|
}
|
||||||
tls_config
|
tls_config
|
||||||
.set_single_cert(load_certs(cert_file)?, load_keys(key_file)?.remove(0))
|
.set_single_cert(
|
||||||
|
load_certs_from_file(cert_file)?,
|
||||||
|
load_private_keys_from_file(key_file)?.remove(0),
|
||||||
|
)
|
||||||
.expect("invalid key or certificate");
|
.expect("invalid key or certificate");
|
||||||
|
|
||||||
let bind_addr = resolve_addr_sync(hostname, port)?
|
let bind_addr = resolve_addr_sync(hostname, port)?
|
||||||
|
|
|
@ -37,9 +37,10 @@ use std::sync::MutexGuard;
|
||||||
use std::task::Context;
|
use std::task::Context;
|
||||||
use std::task::Poll;
|
use std::task::Poll;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio_rustls::rustls;
|
use tokio_rustls::rustls::{self, Session};
|
||||||
use tokio_rustls::TlsAcceptor;
|
use tokio_rustls::TlsAcceptor;
|
||||||
use tokio_tungstenite::accept_async;
|
use tokio_tungstenite::accept_async;
|
||||||
|
|
||||||
|
@ -56,6 +57,7 @@ const DOUBLE_REDIRECTS_PORT: u16 = 4548;
|
||||||
const INF_REDIRECTS_PORT: u16 = 4549;
|
const INF_REDIRECTS_PORT: u16 = 4549;
|
||||||
const REDIRECT_ABSOLUTE_PORT: u16 = 4550;
|
const REDIRECT_ABSOLUTE_PORT: u16 = 4550;
|
||||||
const AUTH_REDIRECT_PORT: u16 = 4551;
|
const AUTH_REDIRECT_PORT: u16 = 4551;
|
||||||
|
const TLS_CLIENT_AUTH_PORT: u16 = 4552;
|
||||||
const HTTPS_PORT: u16 = 5545;
|
const HTTPS_PORT: u16 = 5545;
|
||||||
const WS_PORT: u16 = 4242;
|
const WS_PORT: u16 = 4242;
|
||||||
const WSS_PORT: u16 = 4243;
|
const WSS_PORT: u16 = 4243;
|
||||||
|
@ -224,6 +226,7 @@ async fn auth_redirect(req: Request<Body>) -> hyper::Result<Response<Body>> {
|
||||||
|
|
||||||
async fn run_ws_server(addr: &SocketAddr) {
|
async fn run_ws_server(addr: &SocketAddr) {
|
||||||
let listener = TcpListener::bind(addr).await.unwrap();
|
let listener = TcpListener::bind(addr).await.unwrap();
|
||||||
|
println!("ready: ws"); // Eye catcher for HttpServerCount
|
||||||
while let Ok((stream, _addr)) = listener.accept().await {
|
while let Ok((stream, _addr)) = listener.accept().await {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let ws_stream_fut = accept_async(stream);
|
let ws_stream_fut = accept_async(stream);
|
||||||
|
@ -260,18 +263,28 @@ async fn run_ws_close_server(addr: &SocketAddr) {
|
||||||
async fn get_tls_config(
|
async fn get_tls_config(
|
||||||
cert: &str,
|
cert: &str,
|
||||||
key: &str,
|
key: &str,
|
||||||
|
ca: &str,
|
||||||
) -> io::Result<Arc<rustls::ServerConfig>> {
|
) -> io::Result<Arc<rustls::ServerConfig>> {
|
||||||
let mut cert_path = root_path();
|
let mut cert_path = root_path();
|
||||||
let mut key_path = root_path();
|
let mut key_path = root_path();
|
||||||
|
let mut ca_path = root_path();
|
||||||
cert_path.push(cert);
|
cert_path.push(cert);
|
||||||
key_path.push(key);
|
key_path.push(key);
|
||||||
|
ca_path.push(ca);
|
||||||
|
|
||||||
let cert_file = std::fs::File::open(cert_path)?;
|
let cert_file = std::fs::File::open(cert_path)?;
|
||||||
let key_file = std::fs::File::open(key_path)?;
|
let key_file = std::fs::File::open(key_path)?;
|
||||||
|
let ca_file = std::fs::File::open(ca_path)?;
|
||||||
|
|
||||||
let mut cert_reader = io::BufReader::new(cert_file);
|
let mut cert_reader = io::BufReader::new(cert_file);
|
||||||
let cert = rustls::internal::pemfile::certs(&mut cert_reader)
|
let cert = rustls::internal::pemfile::certs(&mut cert_reader)
|
||||||
.expect("Cannot load certificate");
|
.expect("Cannot load certificate");
|
||||||
|
|
||||||
|
let mut ca_cert_reader = io::BufReader::new(ca_file);
|
||||||
|
let ca_cert = rustls::internal::pemfile::certs(&mut ca_cert_reader)
|
||||||
|
.expect("Cannot load CA certificate")
|
||||||
|
.remove(0);
|
||||||
|
|
||||||
let mut key_reader = io::BufReader::new(key_file);
|
let mut key_reader = io::BufReader::new(key_file);
|
||||||
let key = {
|
let key = {
|
||||||
let pkcs8_key =
|
let pkcs8_key =
|
||||||
|
@ -290,7 +303,12 @@ async fn get_tls_config(
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
Some(key) => {
|
Some(key) => {
|
||||||
let mut config = rustls::ServerConfig::new(rustls::NoClientAuth::new());
|
let mut root_cert_store = rustls::RootCertStore::empty();
|
||||||
|
root_cert_store.add(&ca_cert).unwrap();
|
||||||
|
// Allow (but do not require) client authentication.
|
||||||
|
let allow_client_auth =
|
||||||
|
rustls::AllowAnyAnonymousOrAuthenticatedClient::new(root_cert_store);
|
||||||
|
let mut config = rustls::ServerConfig::new(allow_client_auth);
|
||||||
config
|
config
|
||||||
.set_single_cert(cert, key)
|
.set_single_cert(cert, key)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
@ -307,10 +325,14 @@ async fn get_tls_config(
|
||||||
async fn run_wss_server(addr: &SocketAddr) {
|
async fn run_wss_server(addr: &SocketAddr) {
|
||||||
let cert_file = "cli/tests/tls/localhost.crt";
|
let cert_file = "cli/tests/tls/localhost.crt";
|
||||||
let key_file = "cli/tests/tls/localhost.key";
|
let key_file = "cli/tests/tls/localhost.key";
|
||||||
|
let ca_cert_file = "cli/tests/tls/RootCA.pem";
|
||||||
|
|
||||||
let tls_config = get_tls_config(cert_file, key_file).await.unwrap();
|
let tls_config = get_tls_config(cert_file, key_file, ca_cert_file)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let tls_acceptor = TlsAcceptor::from(tls_config);
|
let tls_acceptor = TlsAcceptor::from(tls_config);
|
||||||
let listener = TcpListener::bind(addr).await.unwrap();
|
let listener = TcpListener::bind(addr).await.unwrap();
|
||||||
|
println!("ready: wss"); // Eye catcher for HttpServerCount
|
||||||
|
|
||||||
while let Ok((stream, _addr)) = listener.accept().await {
|
while let Ok((stream, _addr)) = listener.accept().await {
|
||||||
let acceptor = tls_acceptor.clone();
|
let acceptor = tls_acceptor.clone();
|
||||||
|
@ -338,6 +360,71 @@ async fn run_wss_server(addr: &SocketAddr) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This server responds with 'PASS' if client authentication was successful. Try it by running
|
||||||
|
/// test_server and
|
||||||
|
/// curl --key cli/tests/tls/localhost.key \
|
||||||
|
/// --cert cli/tests/tls/localhost.crt \
|
||||||
|
/// --cacert cli/tests/tls/RootCA.crt https://localhost:4552/
|
||||||
|
async fn run_tls_client_auth_server() {
|
||||||
|
let cert_file = "cli/tests/tls/localhost.crt";
|
||||||
|
let key_file = "cli/tests/tls/localhost.key";
|
||||||
|
let ca_cert_file = "cli/tests/tls/RootCA.pem";
|
||||||
|
let tls_config = get_tls_config(cert_file, key_file, ca_cert_file)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let tls_acceptor = TlsAcceptor::from(tls_config);
|
||||||
|
|
||||||
|
// Listen on ALL addresses that localhost can resolves to.
|
||||||
|
let accept = |listener: tokio::net::TcpListener| {
|
||||||
|
async {
|
||||||
|
let result = listener.accept().await;
|
||||||
|
Some((result, listener))
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
};
|
||||||
|
|
||||||
|
let host_and_port = &format!("localhost:{}", TLS_CLIENT_AUTH_PORT);
|
||||||
|
|
||||||
|
let listeners = tokio::net::lookup_host(host_and_port)
|
||||||
|
.await
|
||||||
|
.expect(host_and_port)
|
||||||
|
.inspect(|address| println!("{} -> {}", host_and_port, address))
|
||||||
|
.map(tokio::net::TcpListener::bind)
|
||||||
|
.collect::<futures::stream::FuturesUnordered<_>>()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.unwrap())
|
||||||
|
.map(|listener| futures::stream::unfold(listener, accept))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
println!("ready: tls client auth"); // Eye catcher for HttpServerCount
|
||||||
|
|
||||||
|
let mut listeners = futures::stream::select_all(listeners);
|
||||||
|
|
||||||
|
while let Some(Ok((stream, _addr))) = listeners.next().await {
|
||||||
|
let acceptor = tls_acceptor.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match acceptor.accept(stream).await {
|
||||||
|
Ok(mut tls_stream) => {
|
||||||
|
let (_, tls_session) = tls_stream.get_mut();
|
||||||
|
// We only need to check for the presence of client certificates
|
||||||
|
// here. Rusttls ensures that they are valid and signed by the CA.
|
||||||
|
let response = match tls_session.get_peer_certificates() {
|
||||||
|
Some(_certs) => b"PASS",
|
||||||
|
None => b"FAIL",
|
||||||
|
};
|
||||||
|
tls_stream.write_all(response).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("TLS accept error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn absolute_redirect(
|
async fn absolute_redirect(
|
||||||
req: Request<Body>,
|
req: Request<Body>,
|
||||||
) -> hyper::Result<Response<Body>> {
|
) -> hyper::Result<Response<Body>> {
|
||||||
|
@ -775,14 +862,15 @@ async fn wrap_main_https_server() {
|
||||||
let main_server_https_addr = SocketAddr::from(([127, 0, 0, 1], HTTPS_PORT));
|
let main_server_https_addr = SocketAddr::from(([127, 0, 0, 1], HTTPS_PORT));
|
||||||
let cert_file = "cli/tests/tls/localhost.crt";
|
let cert_file = "cli/tests/tls/localhost.crt";
|
||||||
let key_file = "cli/tests/tls/localhost.key";
|
let key_file = "cli/tests/tls/localhost.key";
|
||||||
let tls_config = get_tls_config(cert_file, key_file)
|
let ca_cert_file = "cli/tests/tls/RootCA.pem";
|
||||||
|
let tls_config = get_tls_config(cert_file, key_file, ca_cert_file)
|
||||||
.await
|
.await
|
||||||
.expect("Cannot get TLS config");
|
.expect("Cannot get TLS config");
|
||||||
loop {
|
loop {
|
||||||
let tcp = TcpListener::bind(&main_server_https_addr)
|
let tcp = TcpListener::bind(&main_server_https_addr)
|
||||||
.await
|
.await
|
||||||
.expect("Cannot bind TCP");
|
.expect("Cannot bind TCP");
|
||||||
println!("tls ready");
|
println!("ready: https"); // Eye catcher for HttpServerCount
|
||||||
let tls_acceptor = TlsAcceptor::from(tls_config.clone());
|
let tls_acceptor = TlsAcceptor::from(tls_config.clone());
|
||||||
// Prepare a long-running future stream to accept and serve cients.
|
// Prepare a long-running future stream to accept and serve cients.
|
||||||
let incoming_tls_stream = async_stream::stream! {
|
let incoming_tls_stream = async_stream::stream! {
|
||||||
|
@ -832,6 +920,8 @@ pub async fn run_all_servers() {
|
||||||
let ws_close_addr = SocketAddr::from(([127, 0, 0, 1], WS_CLOSE_PORT));
|
let ws_close_addr = SocketAddr::from(([127, 0, 0, 1], WS_CLOSE_PORT));
|
||||||
let ws_close_server_fut = run_ws_close_server(&ws_close_addr);
|
let ws_close_server_fut = run_ws_close_server(&ws_close_addr);
|
||||||
|
|
||||||
|
let tls_client_auth_server_fut = run_tls_client_auth_server();
|
||||||
|
|
||||||
let main_server_fut = wrap_main_server();
|
let main_server_fut = wrap_main_server();
|
||||||
let main_server_https_fut = wrap_main_https_server();
|
let main_server_https_fut = wrap_main_https_server();
|
||||||
|
|
||||||
|
@ -840,6 +930,7 @@ pub async fn run_all_servers() {
|
||||||
redirect_server_fut,
|
redirect_server_fut,
|
||||||
ws_server_fut,
|
ws_server_fut,
|
||||||
wss_server_fut,
|
wss_server_fut,
|
||||||
|
tls_client_auth_server_fut,
|
||||||
ws_close_server_fut,
|
ws_close_server_fut,
|
||||||
another_redirect_server_fut,
|
another_redirect_server_fut,
|
||||||
auth_redirect_server_fut,
|
auth_redirect_server_fut,
|
||||||
|
@ -856,7 +947,7 @@ pub async fn run_all_servers() {
|
||||||
futures::future::poll_fn(move |cx| {
|
futures::future::poll_fn(move |cx| {
|
||||||
let poll_result = server_fut.poll_unpin(cx);
|
let poll_result = server_fut.poll_unpin(cx);
|
||||||
if !replace(&mut did_print_ready, true) {
|
if !replace(&mut did_print_ready, true) {
|
||||||
println!("ready");
|
println!("ready: server_fut"); // Eye catcher for HttpServerCount
|
||||||
}
|
}
|
||||||
poll_result
|
poll_result
|
||||||
})
|
})
|
||||||
|
@ -985,17 +1076,15 @@ impl HttpServerCount {
|
||||||
let stdout = test_server.stdout.as_mut().unwrap();
|
let stdout = test_server.stdout.as_mut().unwrap();
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
let lines = BufReader::new(stdout).lines();
|
let lines = BufReader::new(stdout).lines();
|
||||||
let mut ready = false;
|
|
||||||
let mut tls_ready = false;
|
// Wait for all the servers to report being ready.
|
||||||
|
let mut ready_count = 0;
|
||||||
for maybe_line in lines {
|
for maybe_line in lines {
|
||||||
if let Ok(line) = maybe_line {
|
if let Ok(line) = maybe_line {
|
||||||
if line.starts_with("ready") {
|
if line.starts_with("ready:") {
|
||||||
ready = true;
|
ready_count += 1;
|
||||||
}
|
}
|
||||||
if line.starts_with("tls ready") {
|
if ready_count == 5 {
|
||||||
tls_ready = true;
|
|
||||||
}
|
|
||||||
if ready && tls_ready {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue