Split client/server code, add config
This commit is contained in:
parent
88bab6d5b9
commit
b656094c68
5 changed files with 297 additions and 24 deletions
|
@ -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
154
src/cert.rs
Normal 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
37
src/client.rs
Normal 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(())
|
||||
}
|
81
src/main.rs
81
src/main.rs
|
@ -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
47
src/server.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue