+Generate PKCS (PFX)
+
+openssl genpkey -algorithm RSA -out server.pem -pkeyopt rsa_keygen_bits:2048
+openssl req -new -key server.pem -out request.csr
+openssl x509 -req -in request.csr -signkey server.pem -out server.crt -days 365
+openssl pkcs12 -export -out server.pfx -inkey server.pem -in server.crt
+
+
+## Launch
+
+``` bash
+btracker-gemini -i /path/to/server.pfx\
+ -s /path/to/btracker-fs
+```
+* prepend `RUST_LOG=trace` or `RUST_LOG=btracker_gemini=trace` to debug
+* use `-b` to bind server on specified `host:port`
+* use `-h` to print all available options
\ No newline at end of file
diff --git a/src/argument.rs b/src/argument.rs
new file mode 100644
index 0000000..9301f1e
--- /dev/null
+++ b/src/argument.rs
@@ -0,0 +1,30 @@
+use clap::Parser;
+use std::path::PathBuf;
+
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+pub struct Argument {
+ /// Bind server `host:port` to listen incoming connections on it
+ #[arg(short, long, default_value_t = String::from("127.0.0.1:1965"))]
+ pub bind: String,
+
+ /// Filepath to server identity in PKCS (PFX) format
+ #[arg(short, long)]
+ pub identity: PathBuf,
+
+ /// Passphrase to unlock encrypted identity
+ #[arg(short, long, default_value_t = String::new())]
+ pub password: String,
+
+ /// btracker-fs directory
+ #[arg(short, long)]
+ pub storage: PathBuf,
+
+ /// Listing items limit
+ #[arg(short, long, default_value_t = 20)]
+ pub limit: usize,
+
+ /// Default index capacity
+ #[arg(short, long, default_value_t = 1000)]
+ pub capacity: usize,
+}
diff --git a/src/format.rs b/src/format.rs
new file mode 100644
index 0000000..1170454
--- /dev/null
+++ b/src/format.rs
@@ -0,0 +1,17 @@
+pub fn size(value: u64) -> String {
+ const KB: f32 = 1024.0;
+ const MB: f32 = KB * KB;
+ const GB: f32 = MB * KB;
+
+ let f = value as f32;
+
+ if f < KB {
+ format!("{value} B")
+ } else if f < MB {
+ format!("{:.2} KB", f / KB)
+ } else if f < GB {
+ format!("{:.2} MB", f / MB)
+ } else {
+ format!("{:.2} GB", f / GB)
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..acb4949
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,294 @@
+mod argument;
+mod format;
+
+use anyhow::Result;
+use argument::Argument;
+use btracker_fs::public::{Order, Public, Sort};
+use chrono::Local;
+use clap::Parser;
+use librqbit_core::torrent_metainfo::{TorrentMetaV1Owned, torrent_from_bytes};
+use log::*;
+use native_tls::{HandshakeError, Identity, TlsAcceptor, TlsStream};
+use plurify::Plurify;
+use std::{
+ fs::File,
+ io::{Read, Write},
+ net::{SocketAddr, TcpListener, TcpStream},
+ sync::Arc,
+ thread,
+};
+use titanite::*;
+
+fn main() -> Result<()> {
+ if std::env::var("RUST_LOG").is_ok() {
+ use tracing_subscriber::{EnvFilter, fmt::*};
+ struct T;
+ impl time::FormatTime for T {
+ fn format_time(&self, w: &mut format::Writer<'_>) -> std::fmt::Result {
+ write!(w, "{}", Local::now())
+ }
+ }
+ fmt()
+ .with_timer(T)
+ .with_env_filter(EnvFilter::from_default_env())
+ .init()
+ }
+
+ let argument = Arc::new(Argument::parse());
+
+ let public =
+ Arc::new(Public::init(&argument.storage, argument.limit, argument.capacity).unwrap());
+
+ // https://geminiprotocol.net/docs/protocol-specification.gmi#the-use-of-tls
+ let acceptor = TlsAcceptor::new(Identity::from_pkcs12(
+ &{
+ let mut buffer = vec![];
+ File::open(&argument.identity)?.read_to_end(&mut buffer)?;
+ buffer
+ },
+ &argument.password,
+ )?)?;
+
+ let listener = TcpListener::bind(&argument.bind)?;
+
+ info!("Server started on `{}`", argument.bind);
+
+ for stream in listener.incoming() {
+ match stream {
+ Ok(stream) => {
+ thread::spawn({
+ let public = public.clone();
+ let peer = stream.peer_addr()?;
+ let connection = acceptor.accept(stream);
+ move || handle(public, peer, connection)
+ });
+ }
+ Err(e) => error!("{e}"),
+ }
+ }
+ Ok(())
+}
+
+fn handle(
+ public: Arc