implement server part
This commit is contained in:
parent
b656094c68
commit
965ba76a76
4 changed files with 239 additions and 15 deletions
|
@ -5,6 +5,7 @@ authors = ["Reyk Floeter <contact@reykfloeter.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bubblebabble = "0.1"
|
||||||
derive_more = "0.99"
|
derive_more = "0.99"
|
||||||
dirs = "2.0"
|
dirs = "2.0"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
|
|
43
src/cert.rs
43
src/cert.rs
|
@ -12,11 +12,14 @@
|
||||||
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
use crate::Config;
|
||||||
|
use bubblebabble::stablebabble;
|
||||||
use derive_more::{Deref, From, Into};
|
use derive_more::{Deref, From, Into};
|
||||||
use dirs::home_dir;
|
use dirs::home_dir;
|
||||||
use std::{
|
use std::{
|
||||||
fs::{create_dir, File},
|
fs::{create_dir, File},
|
||||||
io::{Error, ErrorKind, Result, Write},
|
io::{Error, ErrorKind, Result, Write},
|
||||||
|
net::SocketAddr,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::{Command, Output},
|
process::{Command, Output},
|
||||||
};
|
};
|
||||||
|
@ -37,7 +40,7 @@ pub struct Cert {
|
||||||
|
|
||||||
/// The local server keypair
|
/// The local server keypair
|
||||||
#[derive(Clone, Debug, Deref, From, Into)]
|
#[derive(Clone, Debug, Deref, From, Into)]
|
||||||
pub struct KeyPair(Cert);
|
pub(crate) struct KeyPair(Cert);
|
||||||
|
|
||||||
impl KeyPair {
|
impl KeyPair {
|
||||||
/// Get the server keys or generate them if they don't exist
|
/// Get the server keys or generate them if they don't exist
|
||||||
|
@ -45,8 +48,9 @@ impl KeyPair {
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// This function may fail if the key cannot be generated.
|
/// This function may fail if the key cannot be generated.
|
||||||
pub fn new() -> Self {
|
pub(crate) fn new(config: &Config) -> Self {
|
||||||
let home = home_dir().unwrap();
|
let home = home_dir().unwrap();
|
||||||
|
|
||||||
let mut config_dir = PathBuf::from(home);
|
let mut config_dir = PathBuf::from(home);
|
||||||
config_dir.push(CONFIG_DIR);
|
config_dir.push(CONFIG_DIR);
|
||||||
|
|
||||||
|
@ -73,7 +77,7 @@ impl KeyPair {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !csr.exists() {
|
if !csr.exists() {
|
||||||
gen_csr(&key_path, &csr_path).expect("failed to generate signing request");
|
gen_csr(&key_path, &csr_path, config).expect("failed to generate signing request");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cert.exists() {
|
if !cert.exists() {
|
||||||
|
@ -108,13 +112,31 @@ fn gen_key(key_path: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gen_csr(key_path: &str, csr_path: &str) -> Result<()> {
|
fn gen_csr(key_path: &str, csr_path: &str, config: &Config) -> Result<()> {
|
||||||
debug!("key {} csr {}", key_path, csr_path);
|
debug!("key {} csr {}", key_path, csr_path);
|
||||||
|
|
||||||
// XXX use gethostname() libc function or a crate around it
|
// XXX use gethostname() libc function or a crate around it
|
||||||
let hostname = String::from_utf8(Command::new("hostname").output()?.stdout)
|
let hostname = String::from_utf8(Command::new("hostname").output()?.stdout)
|
||||||
.map_err(|err| Error::new(ErrorKind::Other, err))?;
|
.map_err(|err| Error::new(ErrorKind::Other, err))?;
|
||||||
let subject = format!("/C={cn}/CN={hn}/O=yodle/", cn = COUNTRY, hn = hostname);
|
let hostname = hostname.trim();
|
||||||
|
|
||||||
|
let babble = if let Some(SocketAddr::V6(ref address)) = config.address {
|
||||||
|
stablebabble(&address.ip().octets())
|
||||||
|
} else {
|
||||||
|
return Err(Error::new(ErrorKind::Other, "invalid address"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let subject = format!(
|
||||||
|
"/C={cn}/CN={hn}/O={sn}/OU={bb}",
|
||||||
|
cn = COUNTRY,
|
||||||
|
hn = hostname,
|
||||||
|
sn = config
|
||||||
|
.servername
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("yodle"),
|
||||||
|
bb = babble,
|
||||||
|
);
|
||||||
|
|
||||||
command_ok(
|
command_ok(
|
||||||
Command::new("openssl")
|
Command::new("openssl")
|
||||||
|
@ -126,7 +148,16 @@ fn gen_csr(key_path: &str, csr_path: &str) -> Result<()> {
|
||||||
Command::new("chmod").args(&["0640", csr_path]).output()?;
|
Command::new("chmod").args(&["0640", csr_path]).output()?;
|
||||||
|
|
||||||
let mut f = File::create(csr_path.to_owned() + ".ext")?;
|
let mut f = File::create(csr_path.to_owned() + ".ext")?;
|
||||||
f.write_all(format!("subjectAltName=DNS:localhost,DNS:{}", hostname).as_bytes())?;
|
let mut san = format!(
|
||||||
|
"subjectAltName=DNS:localhost,DNS:{hn},DNS:{bb}",
|
||||||
|
hn = hostname,
|
||||||
|
bb = babble
|
||||||
|
);
|
||||||
|
if let Some(ref servername) = config.servername {
|
||||||
|
san.push_str(&format!(",DNS:{}", servername));
|
||||||
|
}
|
||||||
|
eprintln!("{}", san);
|
||||||
|
f.write_all(san.as_bytes())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
59
src/main.rs
59
src/main.rs
|
@ -19,12 +19,16 @@ mod cert;
|
||||||
mod client;
|
mod client;
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
|
use bubblebabble::stablebabble;
|
||||||
use cert::KeyPair;
|
use cert::KeyPair;
|
||||||
|
use getopts::Options;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use std::{
|
use std::{
|
||||||
|
env,
|
||||||
io::{Error, ErrorKind, Result},
|
io::{Error, ErrorKind, Result},
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
process,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio_libtls::prelude::*;
|
use tokio_libtls::prelude::*;
|
||||||
|
@ -36,12 +40,16 @@ pub(crate) struct Config {
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
servername: Option<String>,
|
servername: Option<String>,
|
||||||
address: Option<SocketAddr>,
|
address: Option<SocketAddr>,
|
||||||
|
size_limit: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
address: "[::1]:8023".parse().ok(),
|
address: "[::1]:8023".parse().ok(),
|
||||||
|
servername: Some(env::var("USER").unwrap_or("localhost".to_string())),
|
||||||
|
timeout: Some(Duration::from_secs(10)),
|
||||||
|
size_limit: 1_073_741_824,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,19 +85,64 @@ impl Config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn usage(program: &str, opts: Options) -> ! {
|
||||||
|
let brief = format!("Usage: {} [options]", program);
|
||||||
|
print!("{}", opts.usage(&brief));
|
||||||
|
process::exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
let program = args[0].clone();
|
||||||
let mut config = Config::new();
|
let mut config = Config::new();
|
||||||
|
|
||||||
|
let mut opts = Options::new();
|
||||||
|
opts.optopt(
|
||||||
|
"a",
|
||||||
|
"address",
|
||||||
|
"client/server address",
|
||||||
|
&config.address.unwrap().to_string(),
|
||||||
|
);
|
||||||
|
opts.optopt(
|
||||||
|
"n",
|
||||||
|
"name",
|
||||||
|
"server name",
|
||||||
|
&config.servername.as_ref().unwrap(),
|
||||||
|
);
|
||||||
|
opts.optflag("s", "server", "run server");
|
||||||
|
opts.optflag("c", "client", "connect as client");
|
||||||
|
opts.optflag("h", "help", "print this help menu");
|
||||||
|
let matches = match opts.parse(&args[1..]) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(f) => panic!(f.to_string()),
|
||||||
|
};
|
||||||
|
if matches.opt_present("h") || (matches.opt_present("c") && matches.opt_present("s")) {
|
||||||
|
usage(&program, opts);
|
||||||
|
}
|
||||||
|
|
||||||
env_logger::builder()
|
env_logger::builder()
|
||||||
.filter_level(LevelFilter::Debug)
|
.filter_level(LevelFilter::Debug)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let keypair = KeyPair::new();
|
let addr = match config.address {
|
||||||
|
Some(SocketAddr::V6(addr)) => addr.clone(),
|
||||||
|
_ => panic!("invalid address: {:?}", config.address),
|
||||||
|
};
|
||||||
|
|
||||||
|
let keypair = KeyPair::new(&config);
|
||||||
config.ca = Some(keypair.cert.clone());
|
config.ca = Some(keypair.cert.clone());
|
||||||
config.keypair = Some(keypair);
|
config.keypair = Some(keypair);
|
||||||
|
|
||||||
info!("{:?}", config);
|
debug!(
|
||||||
|
"{}:{} started",
|
||||||
|
stablebabble(&addr.ip().octets()),
|
||||||
|
addr.port()
|
||||||
|
);
|
||||||
|
|
||||||
server::run(config).await.expect("server");
|
if matches.opt_present("c") {
|
||||||
|
client::run(config).await.expect("client");
|
||||||
|
} else {
|
||||||
|
server::run(config).await.expect("server");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
151
src/server.rs
151
src/server.rs
|
@ -13,10 +13,19 @@
|
||||||
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
use std::{io::Result};
|
use std::{
|
||||||
use tokio::{net::TcpListener, prelude::*};
|
io::{Error, ErrorKind, Result},
|
||||||
|
path::{PathBuf, MAIN_SEPARATOR},
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
fs::{remove_file, File},
|
||||||
|
io::{self, AsyncReadExt},
|
||||||
|
net::TcpListener,
|
||||||
|
};
|
||||||
use tokio_libtls::prelude::*;
|
use tokio_libtls::prelude::*;
|
||||||
|
|
||||||
|
const MAX_COPIES: usize = 1024;
|
||||||
|
|
||||||
pub(crate) async fn run(config: Config) -> Result<()> {
|
pub(crate) async fn run(config: Config) -> Result<()> {
|
||||||
let (cert, key, ca) = config.load_keys()?;
|
let (cert, key, ca) = config.load_keys()?;
|
||||||
let mut options = config.load_server_options();
|
let mut options = config.load_server_options();
|
||||||
|
@ -32,16 +41,146 @@ pub(crate) async fn run(config: Config) -> Result<()> {
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (tcp, _) = listener.accept().await?;
|
let (tcp, _) = listener.accept().await?;
|
||||||
|
let size_limit = config.size_limit as u64;
|
||||||
|
let peer_addr: String = tcp.peer_addr()?.to_string();
|
||||||
let options = options.build();
|
let options = options.build();
|
||||||
let mut tls = AsyncTls::accept_stream(tcp, &tls_config, options).await?;
|
let mut tls = match AsyncTls::accept_stream(tcp, &tls_config, options).await {
|
||||||
|
Ok(tls) => {
|
||||||
|
debug!("{} status: connected", peer_addr);
|
||||||
|
tls
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
debug!("{} status: TLS connection error ({})", peer_addr, err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
let mut filename = PathBuf::from("/home/reyk/Downloads");
|
||||||
let mut buf = vec![0u8; 1024];
|
|
||||||
if !tls.read(&mut buf).await.is_ok() || !tls.write_all(&buf).await.is_ok() {
|
// Read and validate the filename
|
||||||
|
let line = match read_line(&peer_addr, &mut tls).await {
|
||||||
|
Ok(s) if !(s.contains(MAIN_SEPARATOR) || s.contains('/') || s.contains('\\')) => s,
|
||||||
|
Err(err) => {
|
||||||
|
debug!("{}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug!("{} failed: filename invalid", peer_addr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
filename.push(&line);
|
||||||
|
|
||||||
|
// If the filename exists, try to append a counter (e.g. foo_1.gz)
|
||||||
|
for i in 1..=(MAX_COPIES + 1) {
|
||||||
|
if i == MAX_COPIES {
|
||||||
|
debug!(
|
||||||
|
"{} failed: {} exists more than {} times",
|
||||||
|
peer_addr, line, i
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !filename.exists() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
let ext = line.rfind('.').unwrap_or(line.len());
|
||||||
|
filename.set_file_name(format!("{}_{}{}", &line[..ext], i, &line[ext..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the file size
|
||||||
|
let line = match read_line(&peer_addr, &mut tls).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(err) => {
|
||||||
|
debug!("{}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let file_size: u64 = match line.parse() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(err) => {
|
||||||
|
debug!("{} failed: file size ({})", peer_addr, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if file_size == 0 || (size_limit > 0 && file_size > size_limit) {
|
||||||
|
debug!(
|
||||||
|
"{} failed: file size (out of limits, {} bytes)",
|
||||||
|
peer_addr, file_size
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"{} status: receiving {} ({} bytes)",
|
||||||
|
peer_addr,
|
||||||
|
filename.display(),
|
||||||
|
file_size
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create output file
|
||||||
|
let mut file = match File::create(&filename).await {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(err) => {
|
||||||
|
debug!(
|
||||||
|
"{} failed {}: file ({})",
|
||||||
|
peer_addr,
|
||||||
|
filename.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// I/O
|
||||||
|
let mut reader = tls.take(file_size);
|
||||||
|
let copied = match io::copy(&mut reader, &mut file).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(err) => {
|
||||||
|
debug!("{} failed: I/O ({})", peer_addr, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if copied != file_size {
|
||||||
|
drop(file);
|
||||||
|
let _ = remove_file("a.txt").await.is_ok();
|
||||||
|
warn!(
|
||||||
|
"{} failed: {} ({}/{} bytes)",
|
||||||
|
peer_addr,
|
||||||
|
filename.display(),
|
||||||
|
copied,
|
||||||
|
file_size
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
info!(
|
||||||
|
"{} success: {} ({} bytes)",
|
||||||
|
peer_addr,
|
||||||
|
filename.display(),
|
||||||
|
copied
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_line<T: io::AsyncRead + Unpin>(peer: &str, reader: &mut T) -> Result<String> {
|
||||||
|
let mut buf = vec![0u8; 1024];
|
||||||
|
if let Err(err) = reader.read(&mut buf).await {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
format!("{} failed: read ({})", peer, err),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let line = match String::from_utf8(buf) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(err) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
format!("{} read failed: line ({})", peer, err),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let len = line.find(|c: char| c == '\r' || c == '\n').unwrap_or(0);
|
||||||
|
Ok((&line[0..len]).to_owned())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue