mirror of
https://github.com/YGGverse/titanit.git
synced 2026-03-31 17:15:30 +00:00
use titanite library for backend
This commit is contained in:
parent
a4518ba93d
commit
08c437920c
6 changed files with 89 additions and 147 deletions
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
161
src/main.rs
161
src/main.rs
|
|
@ -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}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue