use titanite library for backend

This commit is contained in:
yggverse 2025-02-21 22:18:07 +02:00
parent a4518ba93d
commit 08c437920c
6 changed files with 89 additions and 147 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "titanit" name = "titanit"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"
@ -14,3 +14,8 @@ anyhow = "1.0.95"
clap = { version = "4.5.30", features = ["derive"] } clap = { version = "4.5.30", features = ["derive"] }
native-tls = "0.2.13" native-tls = "0.2.13"
regex = "1.11.1" regex = "1.11.1"
titanite = "0.1.1"
# development
# [patch.crates-io]
# titanite = { path = "../titanite" }

View file

@ -34,6 +34,7 @@ openssl pkcs12 -export -out server.pfx -inkey server.pem -in server.crt
* `--bind`, `-b` required, server `host:port` to listen incoming connections * `--bind`, `-b` required, server `host:port` to listen incoming connections
* `--identity`, `-i` required, filepath to server identity in PKCS (PFX) format * `--identity`, `-i` required, filepath to server identity in PKCS (PFX) format
* `--password`, `-p` optional, unlock encrypted `identity` by passphrase * `--password`, `-p` optional, unlock encrypted `identity` by passphrase
* `--chunk`, `-c` optional, buffer chunk size (`1024` by default)
* `--size`, `-s` optional, max size limit in bytes (unlimited by default) * `--size`, `-s` optional, max size limit in bytes (unlimited by default)
* `--mime`, `-m` optional, uploads MIME type whitelist (comma separated, all by default) * `--mime`, `-m` optional, uploads MIME type whitelist (comma separated, all by default)
* `--directory`, `-d` optional, uploads target directory (`public` by default) * `--directory`, `-d` optional, uploads target directory (`public` by default)

View file

@ -23,6 +23,10 @@ pub struct Argument {
#[arg(short, long)] #[arg(short, long)]
pub size: Option<usize>, pub size: Option<usize>,
/// Buffer chunk size (`1024` by default)
#[arg(short, long, default_value_t = 1024)]
pub chunk: usize,
/// Uploads MIME type allowed (comma separated, all by default) /// Uploads MIME type allowed (comma separated, all by default)
/// * based on headers /// * based on headers
#[arg(short, long)] #[arg(short, long)]

View file

@ -1,56 +0,0 @@
use anyhow::{bail, Result};
use native_tls::TlsStream;
use std::{io::Read, net::TcpStream};
pub struct Header {
pub mime: Option<String>,
pub size: usize,
}
impl Header {
pub fn for_stream(stream: &mut TlsStream<TcpStream>) -> Result<Self> {
let mut header: Vec<u8> = Vec::new();
let mut buffer = vec![0];
loop {
match stream.read(&mut buffer) {
Ok(0) => bail!("Invalid protocol"),
Ok(_) => {
if header.len() > 1024 {
bail!("Invalid length")
}
if buffer[0] == b'\r' {
continue;
}
if buffer[0] == b'\n' {
break;
}
header.push(buffer[0])
}
Err(e) => bail!(e),
}
}
Self::from_bytes(header)
}
fn from_bytes(buffer: Vec<u8>) -> Result<Self> {
use anyhow::bail;
use regex::Regex;
let header = String::from_utf8(buffer)?;
Ok(Self {
mime: match Regex::new(r"mime=([^\/]+\/[^\s;]+)")?.captures(&header) {
Some(c) => c.get(1).map(|v| v.as_str().to_string()),
None => None,
},
size: match Regex::new(r"size=(\d+)")?.captures(&header) {
Some(c) => match c.get(1) {
Some(v) => match v.as_str().parse::<usize>() {
Ok(s) => s,
Err(e) => bail!(e),
},
None => bail!("Size required"),
},
None => bail!("Size required"),
},
})
}
}

View file

@ -1,11 +1,9 @@
mod argument; mod argument;
mod header;
mod storage; mod storage;
use anyhow::Result; use anyhow::Result;
use argument::Argument; use argument::Argument;
use clap::Parser; use clap::Parser;
use header::Header;
use native_tls::{Identity, TlsAcceptor, TlsStream}; use native_tls::{Identity, TlsAcceptor, TlsStream};
use std::{ use std::{
fs::File, fs::File,
@ -15,6 +13,7 @@ use std::{
thread, thread,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
use titanite::response::success::Default;
fn main() -> Result<()> { fn main() -> Result<()> {
let argument = Arc::new(Argument::parse()); let argument = Arc::new(Argument::parse());
@ -51,106 +50,92 @@ fn main() -> Result<()> {
} }
fn handle(argument: Arc<Argument>, peer: SocketAddr, mut stream: TlsStream<TcpStream>) { fn handle(argument: Arc<Argument>, peer: SocketAddr, mut stream: TlsStream<TcpStream>) {
use titanite::{request::Titan, response::*};
println!("[{}] [info] [{peer}] New connection", now()); println!("[{}] [info] [{peer}] New connection", now());
match Header::for_stream(&mut stream) {
Ok(header) => { // read header bytes
// do not trust header values, but check it to continue let mut input = vec![0; 1024];
if argument.size.is_some_and(|s| header.size > s) { match stream.read(&mut input) {
println!("[{}] [error] [{peer}] Max size limit reached", now()); Ok(0) => todo!("Canceled"),
return; Ok(l) => {
} match Titan::from_bytes(&input[..l]) {
// make sure mime type whitelisted Ok(titan) => {
if argument // init memory pool
.mime let mut data: Vec<u8> = Vec::with_capacity(titan.size);
.as_ref()
.is_some_and(|m| header.mime.is_some_and(|h| m.contains(&h)))
{
println!("[{}] [error] [{peer}] MIME type not allowed", now());
return;
}
// begin data handle
let mut total = 0;
let mut buffer = vec![0]; // @TODO optional chunk size
match storage::Item::create(&argument.directory) {
Ok(mut item) => {
loop { loop {
match stream.read(&mut buffer) { // read data bytes
let mut input = vec![0; argument.chunk];
match stream.read(&mut input) {
Ok(0) => { Ok(0) => {
item.delete().unwrap(); todo!("Canceled")
println!(
"[{}] [warning] [{peer}] Connection closed with rollback",
now()
);
break;
} }
Ok(n) => { Ok(l) => {
total += n; // validate real data size data.extend(&input[..l]);
if argument.size.is_some_and(|s| s > total) {
item.delete().unwrap(); let total = data.len();
println!("[{}] [error] [{peer}] Max size limit reached", now()); if argument.size.is_some_and(|limit| total > limit) {
break; todo!("Allowed max length limit reached")
} }
if total > header.size { if titan.size >= total {
item.delete().unwrap(); if titan.size > total {
println!( println!(
"[{}] [error] [{peer}] Content size larger than declared in headers", now() "[{}] [warning] [{peer}] Data size mismatch header declaration",
); now()
break; );
} return;
if let Err(e) = item.file.write(&buffer) { }
item.delete().unwrap(); // success
println!("[{}] [error] [{peer}] Failed to write file from stream: {e}", now()); match storage::Item::create(&argument.directory) {
break; Ok(mut tmp) => match tmp.file.write(titan.data) {
} Ok(_) => match &stream.write_all(
// transfer completed &Success::Default(Default {
if total == header.size { mime: "text/gemini".to_string(),
match &stream.write_all( })
// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection .into_bytes(),
format!( ) {
"31 {}\r\n", // @TODO detect/validate/cache mime based on data received
argument Ok(()) => match tmp.commit() {
.redirect Ok(path) => match stream.flush() {
.clone() // Close connection gracefully
.unwrap_or(format!("gemini://{}", argument.bind)) // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections
) Ok(()) => match stream.shutdown() {
.as_bytes(), Ok(()) => println!(
) { "[{}] [info] [{peer}] Data saved to {path}",
Ok(_) => { now()
item.commit().unwrap(); // @TODO detect/cache mime based on content type received ),
println!("[{}] [info] [{peer}] Success", now()); Err(e) => println!("[{}] [warning] [{peer}] {e}", now())
// @TODO close connection gracefully },
// https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections Err(e) => println!("[{}] [warning] [{peer}] {e}", now())
match stream.flush() { }
Ok(_) => println!("[{}] [info] [{peer}] Connection closed by server.", now()), Err(e) => println!("[{}] [warning] [{peer}] {e}", now())
Err(e) => println!("[{}] [error] [{peer}] {e}", now()) },
}; Err(e) => {
} println!("[{}] [error] [{peer}] {e}", now());
Err(e) => { if let Err(e) = tmp.delete() {
item.delete().unwrap(); println!("[{}] [error] [{peer}] {e}", now())
println!( }
"[{}] [error] [{peer}] Failed write to stream: {e}", }
now() },
) Err(e) => {
} println!("[{}] [error] [{peer}] {e}", now());
if let Err(e) = tmp.delete() {
println!("[{}] [error] [{peer}] {e}", now())
}
}
},
Err(e) => println!("[{}] [error] [{peer}] {e}", now()),
} }
break; break;
} }
} }
Err(e) => { Err(e) => todo!("{e}"),
item.delete().unwrap();
println!(
"[{}] [error] [{peer}] Failed read from stream: {e}",
now()
);
break;
}
} }
} }
} }
Err(e) => println!("[{}] [error] [{peer}] Could not create Item: {e}", now()), Err(e) => todo!("{e}"),
} }
} }
Err(e) => println!("[{}] [error] [{peer}] Header error: {e}", now()), Err(e) => todo!("{e}"),
} }
} }

View file

@ -63,10 +63,13 @@ impl Item {
// Actions // Actions
/// Take object processed, commit its changes /// Take object processed, commit its changes
pub fn commit(self) -> Result<()> { pub fn commit(self) -> Result<String> {
match self.path.to_str() { match self.path.to_str() {
Some(old) => match old.strip_suffix(TMP_SUFFIX) { Some(old) => match old.strip_suffix(TMP_SUFFIX) {
Some(new) => Ok(rename(old, new)?), Some(new) => {
rename(old, new)?;
Ok(new.to_string())
}
None => bail!("Invalid TMP suffix"), // | panic None => bail!("Invalid TMP suffix"), // | panic
}, },
None => bail!("Invalid Item path"), // | panic None => bail!("Invalid Item path"), // | panic