Split client/server code, add config
This commit is contained in:
parent
88bab6d5b9
commit
b656094c68
|
@ -12,4 +12,4 @@ futures = "0.3"
|
||||||
getopts = "0.2"
|
getopts = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tokio = { version = "0.2", features = ["full"] }
|
tokio = { version = "0.2", features = ["full"] }
|
||||||
tokio-libtls = "1.1.0-alpha.2"
|
tokio-libtls = "1.1.0-alpha.3"
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
// Copyright (c) 2019 Reyk Floeter <contact@reykfloeter.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
use derive_more::{Deref, From, Into};
|
||||||
|
use dirs::home_dir;
|
||||||
|
use std::{
|
||||||
|
fs::{create_dir, File},
|
||||||
|
io::{Error, ErrorKind, Result, Write},
|
||||||
|
path::PathBuf,
|
||||||
|
process::{Command, Output},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const COUNTRY: &str = "XX";
|
||||||
|
pub const CONFIG_DIR: &str = ".yodle";
|
||||||
|
pub const SERVER_CERT: &str = "yodle.crt";
|
||||||
|
pub const SERVER_KEY: &str = "yodle.key";
|
||||||
|
const SERVER_CSR: &str = "yodle.csr";
|
||||||
|
|
||||||
|
/// A generic keypair representing a client or the server
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Cert {
|
||||||
|
pub name: String,
|
||||||
|
pub cert: PathBuf,
|
||||||
|
pub key: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The local server keypair
|
||||||
|
#[derive(Clone, Debug, Deref, From, Into)]
|
||||||
|
pub struct KeyPair(Cert);
|
||||||
|
|
||||||
|
impl KeyPair {
|
||||||
|
/// Get the server keys or generate them if they don't exist
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function may fail if the key cannot be generated.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let home = home_dir().unwrap();
|
||||||
|
let mut config_dir = PathBuf::from(home);
|
||||||
|
config_dir.push(CONFIG_DIR);
|
||||||
|
|
||||||
|
debug!("config_dir {}", config_dir.display());
|
||||||
|
|
||||||
|
let mut key = PathBuf::from(&config_dir);
|
||||||
|
key.push(SERVER_KEY);
|
||||||
|
let key_path = key.to_string_lossy();
|
||||||
|
|
||||||
|
let mut cert = PathBuf::from(&config_dir);
|
||||||
|
cert.push(SERVER_CERT);
|
||||||
|
let cert_path = cert.to_string_lossy();
|
||||||
|
|
||||||
|
let mut csr = PathBuf::from(&config_dir);
|
||||||
|
csr.push(SERVER_CSR);
|
||||||
|
let csr_path = csr.to_string_lossy();
|
||||||
|
|
||||||
|
if !config_dir.exists() {
|
||||||
|
create_dir(&config_dir).expect("failed to create config dir");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !key.exists() {
|
||||||
|
gen_key(&key_path).expect("failed to generate key");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !csr.exists() {
|
||||||
|
gen_csr(&key_path, &csr_path).expect("failed to generate signing request");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cert.exists() {
|
||||||
|
gen_cert(&key_path, &csr_path, &cert_path).expect("failed to generate cert");
|
||||||
|
}
|
||||||
|
|
||||||
|
Cert {
|
||||||
|
name: "localhost".into(),
|
||||||
|
cert,
|
||||||
|
key: Some(key),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_ok(output: Output) -> Result<()> {
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
String::from_utf8(output.stderr).unwrap_or("error".to_string()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_key(key_path: &str) -> Result<()> {
|
||||||
|
Command::new("openssl")
|
||||||
|
.args(&["ecparam", "-name", "secp384r1", "-genkey", "-out", key_path])
|
||||||
|
.output()?;
|
||||||
|
Command::new("chmod").args(&["0640", key_path]).output()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_csr(key_path: &str, csr_path: &str) -> Result<()> {
|
||||||
|
debug!("key {} csr {}", key_path, csr_path);
|
||||||
|
|
||||||
|
// XXX use gethostname() libc function or a crate around it
|
||||||
|
let hostname = String::from_utf8(Command::new("hostname").output()?.stdout)
|
||||||
|
.map_err(|err| Error::new(ErrorKind::Other, err))?;
|
||||||
|
let subject = format!("/C={cn}/CN={hn}/O=yodle/", cn = COUNTRY, hn = hostname);
|
||||||
|
|
||||||
|
command_ok(
|
||||||
|
Command::new("openssl")
|
||||||
|
.args(&[
|
||||||
|
"req", "-new", "-batch", "-subj", &subject, "-key", key_path, "-out", csr_path,
|
||||||
|
])
|
||||||
|
.output()?,
|
||||||
|
)?;
|
||||||
|
Command::new("chmod").args(&["0640", csr_path]).output()?;
|
||||||
|
|
||||||
|
let mut f = File::create(csr_path.to_owned() + ".ext")?;
|
||||||
|
f.write_all(format!("subjectAltName=DNS:localhost,DNS:{}", hostname).as_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_cert(key_path: &str, csr_path: &str, cert_path: &str) -> Result<()> {
|
||||||
|
command_ok(
|
||||||
|
Command::new("openssl")
|
||||||
|
.args(&[
|
||||||
|
"x509",
|
||||||
|
"-sha384",
|
||||||
|
"-req",
|
||||||
|
"-days",
|
||||||
|
"365",
|
||||||
|
"-fingerprint",
|
||||||
|
"-in",
|
||||||
|
csr_path,
|
||||||
|
"-signkey",
|
||||||
|
key_path,
|
||||||
|
"-out",
|
||||||
|
cert_path,
|
||||||
|
"-extfile",
|
||||||
|
&(csr_path.to_owned() + ".ext"),
|
||||||
|
])
|
||||||
|
.output()?,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright (c) 2019 Reyk Floeter <contact@reykfloeter.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
use crate::Config;
|
||||||
|
use std::io::Result;
|
||||||
|
use tokio::prelude::*;
|
||||||
|
use tokio_libtls::prelude::*;
|
||||||
|
|
||||||
|
pub(crate) async fn run(config: Config) -> Result<()> {
|
||||||
|
let (cert, key, ca) = config.load_keys()?;
|
||||||
|
let mut options = config.load_client_options();
|
||||||
|
|
||||||
|
let tls_config = TlsConfigBuilder::new()
|
||||||
|
.ca_file(ca)
|
||||||
|
.keypair_file(cert, key, None)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
let addr = config.address.unwrap();
|
||||||
|
let mut tls = AsyncTls::connect(&addr.to_string(), &tls_config, options.build())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let _ = tls.write_all(b"OK\r\n").await;
|
||||||
|
let mut buf = vec![0u8; 1024];
|
||||||
|
let _ = tls.read(&mut buf).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
81
src/main.rs
81
src/main.rs
|
@ -16,45 +16,80 @@
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
|
||||||
mod cert;
|
mod cert;
|
||||||
|
mod client;
|
||||||
|
mod server;
|
||||||
|
|
||||||
use cert::ServerKeyPair;
|
use cert::KeyPair;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use std::io;
|
use std::{
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
io::{Error, ErrorKind, Result},
|
||||||
|
net::SocketAddr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use tokio_libtls::prelude::*;
|
use tokio_libtls::prelude::*;
|
||||||
|
|
||||||
async fn async_https_connect(servername: String) -> io::Result<()> {
|
#[derive(Clone, Debug, Default)]
|
||||||
let request = format!(
|
pub(crate) struct Config {
|
||||||
"GET / HTTP/1.1\r\n\
|
keypair: Option<KeyPair>,
|
||||||
Host: {}\r\n\
|
ca: Option<PathBuf>,
|
||||||
Connection: close\r\n\r\n",
|
timeout: Option<Duration>,
|
||||||
servername
|
servername: Option<String>,
|
||||||
);
|
address: Option<SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
let config = TlsConfigBuilder::new().build()?;
|
impl Config {
|
||||||
let mut tls = AsyncTls::connect(&(servername + ":443"), &config, None).await?;
|
pub fn new() -> Self {
|
||||||
tls.write_all(request.as_bytes()).await?;
|
Self {
|
||||||
|
address: "[::1]:8023".parse().ok(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut buf = vec![0u8; 1024];
|
pub fn load_keys(&self) -> Result<(&Path, &Path, &Path)> {
|
||||||
tls.read_exact(&mut buf).await?;
|
let keypair = self
|
||||||
|
.keypair
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Error::new(ErrorKind::Other, "keypair"))?;
|
||||||
|
let key = keypair
|
||||||
|
.key
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Error::new(ErrorKind::Other, "key"))?;
|
||||||
|
let ca = self.ca.as_ref().ok_or(Error::new(ErrorKind::Other, "CA"))?;
|
||||||
|
Ok((&keypair.cert, key, ca))
|
||||||
|
}
|
||||||
|
|
||||||
let ok = b"HTTP/1.1 200 OK\r\n";
|
pub fn load_server_options(&self) -> AsyncTlsOptions {
|
||||||
assert_eq!(&buf[..ok.len()], ok);
|
let mut options = AsyncTlsOptions::new();
|
||||||
|
if let Some(timeout) = self.timeout {
|
||||||
|
options.timeout(timeout);
|
||||||
|
}
|
||||||
|
if let Some(ref servername) = self.servername {
|
||||||
|
options.servername(servername);
|
||||||
|
} else {
|
||||||
|
options.servername("localhost");
|
||||||
|
}
|
||||||
|
options
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
pub fn load_client_options(&self) -> AsyncTlsOptions {
|
||||||
|
self.load_server_options()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
let mut config = Config::new();
|
||||||
|
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
.filter_level(LevelFilter::Debug)
|
.filter_level(LevelFilter::Debug)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
async_https_connect("www.example.com".to_owned())
|
let keypair = KeyPair::new();
|
||||||
.await
|
config.ca = Some(keypair.cert.clone());
|
||||||
.unwrap();
|
config.keypair = Some(keypair);
|
||||||
|
|
||||||
let server_key = ServerKeyPair::new();
|
info!("{:?}", config);
|
||||||
|
|
||||||
info!("{:?}", server_key);
|
server::run(config).await.expect("server");
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright (c) 2019 Reyk Floeter <contact@reykfloeter.com>
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
use crate::Config;
|
||||||
|
use std::{io::Result};
|
||||||
|
use tokio::{net::TcpListener, prelude::*};
|
||||||
|
use tokio_libtls::prelude::*;
|
||||||
|
|
||||||
|
pub(crate) async fn run(config: Config) -> Result<()> {
|
||||||
|
let (cert, key, ca) = config.load_keys()?;
|
||||||
|
let mut options = config.load_server_options();
|
||||||
|
|
||||||
|
let tls_config = TlsConfigBuilder::new()
|
||||||
|
.ca_file(ca)
|
||||||
|
.keypair_file(cert, key, None)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let addr = config.address.unwrap();
|
||||||
|
let mut listener = TcpListener::bind(&addr).await?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (tcp, _) = listener.accept().await?;
|
||||||
|
let options = options.build();
|
||||||
|
let mut tls = AsyncTls::accept_stream(tcp, &tls_config, options).await?;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let mut buf = vec![0u8; 1024];
|
||||||
|
if !tls.read(&mut buf).await.is_ok() || !tls.write_all(&buf).await.is_ok() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue