Split client/server code, add config

This commit is contained in:
reykfloeter 2019-11-30 11:21:09 +00:00
parent 88bab6d5b9
commit b656094c68
5 changed files with 297 additions and 24 deletions

View File

@ -12,4 +12,4 @@ futures = "0.3"
getopts = "0.2"
log = "0.4"
tokio = { version = "0.2", features = ["full"] }
tokio-libtls = "1.1.0-alpha.2"
tokio-libtls = "1.1.0-alpha.3"

154
src/cert.rs Normal file
View File

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

37
src/client.rs Normal file
View File

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

View File

@ -16,45 +16,80 @@
extern crate log;
mod cert;
mod client;
mod server;
use cert::ServerKeyPair;
use cert::KeyPair;
use log::LevelFilter;
use std::io;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::{
io::{Error, ErrorKind, Result},
net::SocketAddr,
path::{Path, PathBuf},
time::Duration,
};
use tokio_libtls::prelude::*;
async fn async_https_connect(servername: String) -> io::Result<()> {
let request = format!(
"GET / HTTP/1.1\r\n\
Host: {}\r\n\
Connection: close\r\n\r\n",
servername
);
#[derive(Clone, Debug, Default)]
pub(crate) struct Config {
keypair: Option<KeyPair>,
ca: Option<PathBuf>,
timeout: Option<Duration>,
servername: Option<String>,
address: Option<SocketAddr>,
}
let config = TlsConfigBuilder::new().build()?;
let mut tls = AsyncTls::connect(&(servername + ":443"), &config, None).await?;
tls.write_all(request.as_bytes()).await?;
impl Config {
pub fn new() -> Self {
Self {
address: "[::1]:8023".parse().ok(),
..Default::default()
}
}
let mut buf = vec![0u8; 1024];
tls.read_exact(&mut buf).await?;
pub fn load_keys(&self) -> Result<(&Path, &Path, &Path)> {
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";
assert_eq!(&buf[..ok.len()], ok);
pub fn load_server_options(&self) -> AsyncTlsOptions {
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]
async fn main() {
let mut config = Config::new();
env_logger::builder()
.filter_level(LevelFilter::Debug)
.init();
async_https_connect("www.example.com".to_owned())
.await
.unwrap();
let keypair = KeyPair::new();
config.ca = Some(keypair.cert.clone());
config.keypair = Some(keypair);
let server_key = ServerKeyPair::new();
info!("{:?}", config);
info!("{:?}", server_key);
server::run(config).await.expect("server");
}

47
src/server.rs Normal file
View File

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