add basic question uploading

This commit is contained in:
2025-05-02 13:03:30 +02:00
parent 4af8c825bb
commit e90dfa0b0b
10 changed files with 656 additions and 43 deletions

389
Cargo.lock generated
View File

@@ -32,6 +32,48 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "askama"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
dependencies = [
"askama_derive",
"itoa",
"percent-encoding",
"serde",
"serde_json",
]
[[package]]
name = "askama_derive"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
dependencies = [
"askama_parser",
"basic-toml",
"memchr",
"proc-macro2",
"quote",
"rustc-hash",
"serde",
"serde_derive",
"syn",
]
[[package]]
name = "askama_parser"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
dependencies = [
"memchr",
"serde",
"serde_derive",
"winnow",
]
[[package]] [[package]]
name = "atomic-waker" name = "atomic-waker"
version = "1.1.2" version = "1.1.2"
@@ -59,6 +101,15 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "basic-toml"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.0" version = "2.9.0"
@@ -79,9 +130,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.20" version = "1.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@@ -112,6 +163,17 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@@ -124,6 +186,15 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@@ -190,9 +261,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.2" version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]] [[package]]
name = "http" name = "http"
@@ -305,6 +376,145 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "idna"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.9.0" version = "2.9.0"
@@ -337,6 +547,12 @@ version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "litemap"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@@ -426,6 +642,12 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@@ -471,12 +693,24 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.20" version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@@ -507,6 +741,18 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.8" version = "0.6.8"
@@ -556,6 +802,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.101" version = "2.0.101"
@@ -567,10 +819,22 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "theseus-server" name = "theseus-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"askama",
"chrono", "chrono",
"http-body-util", "http-body-util",
"hyper", "hyper",
@@ -578,6 +842,17 @@ dependencies = [
"serde", "serde",
"tokio", "tokio",
"toml", "toml",
"url",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
] ]
[[package]] [[package]]
@@ -700,6 +975,29 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "url"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "want" name = "want"
version = "0.3.1" version = "0.3.1"
@@ -907,9 +1205,88 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.7" version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" checksum = "9e27d6ad3dac991091e4d35de9ba2d2d00647c5d0fc26c5496dee55984ae111b"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "yoke"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

View File

@@ -4,6 +4,9 @@ SRCS_RUST_THESEUS_SERVER := $(shell find theseus-server -type f)
build: \ build: \
dst/release/theseus-server dst/release/theseus-server
include config/make.mk
include client/make.mk
.PHONY: run .PHONY: run
run: dst/dev.toml $(TARGETS_CLIENT) run: dst/dev.toml $(TARGETS_CLIENT)
cargo run --package theseus-server -- dst/dev.toml cargo run --package theseus-server -- dst/dev.toml
@@ -12,9 +15,6 @@ run: dst/dev.toml $(TARGETS_CLIENT)
clean: client_clean clean: client_clean
rm -rf dst rm -rf dst
include config/make.mk
include client/make.mk
dst/release/theseus-server: $(SRCS_RUST_THESEUS_SERVER) $(TARGETS_CLIENT) dst/release/theseus-server: $(SRCS_RUST_THESEUS_SERVER) $(TARGETS_CLIENT)
cargo build --package theseus-server --release cargo build --package theseus-server --release
touch $@ touch $@

View File

@@ -10,9 +10,14 @@
<header> <header>
<h1>Ask Stallman</h1> <h1>Ask Stallman</h1>
</header> </header>
{%- if message.len() != 0 -%}
<div class="notification {{ ntfy_class }}">
<p>{{ message }}</p>
</div>
{%- endif -%}
<div class="question"> <div class="question">
<form method="post"> <form method="post">
<input type="text" name="question" placeholder="Your question" autocomplete="off"> <input type="text" name="question" placeholder="Your question" autocomplete="off" maxlength="1024">
</form> </form>
</div> </div>
<div class="info"> <div class="info">

View File

@@ -1,2 +1,12 @@
[server] [server]
bind_to = "[::1]:8080" bind_to = "[::1]:8080"
max_question_body_size = 25
[performance]
memory_limit = 50
[maintenance]
interval = 60
[push]
endpoint = "[::1]:8081"

View File

@@ -1,2 +1,3 @@
dst/dev.toml: config/dev.toml dst/dev.toml: config/dev.toml
@mkdir -p $(@D)
ln -f $< $@ ln -f $< $@

View File

@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
askama = "0.14.0"
chrono = "0.4.41" chrono = "0.4.41"
http-body-util = "0.1.3" http-body-util = "0.1.3"
hyper = { version = "1.6.0", features = ["full"] } hyper = { version = "1.6.0", features = ["full"] }
@@ -11,3 +12,4 @@ hyper-util = { version = "0.1.11", features = ["full"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
tokio = { version = "1.44.2", features = ["full"] } tokio = { version = "1.44.2", features = ["full"] }
toml = "0.8.22" toml = "0.8.22"
url = "2.5.4"

View File

@@ -0,0 +1,2 @@
[general]
dirs = ["../dst/"]

View File

@@ -1,9 +1,8 @@
use std::net::SocketAddr;
use serde::Deserialize; use serde::Deserialize;
macro_rules! def_config { macro_rules! def_config {
($(config $config_id: ident ($(config $config_id: ident
{ $([$namespace: ident $struct_name: ident]$($var_id: ident = $var_type: ty),*)*$(,)? })* { $([$namespace: ident $struct_name: ident]$($var_id: ident = $var_type: ty),*$(,)?)* })*
) => { ) => {
$( $(
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -24,7 +23,17 @@ def_config! {
config Config { config Config {
[server Server] [server Server]
bind_to = SocketAddr, // where to bind the server tcp socket in format: "<host>:<port>" bind_to = std::net::SocketAddr, // recommended format: "<host>:<port>"
max_question_body_size = u64 // in bytes
[performance Performance]
memory_limit = usize,
[maintenance Maintenance]
interval = u64, // in seconds
[push Push]
endpoint = std::net::SocketAddr, // recommended format: "<host>:<port>"
} }

View File

@@ -17,4 +17,9 @@ macro_rules! log {
let now = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false); let now = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
println!(concat!("[{}] ERROR ", $text), now, $($($arg),*)?); println!(concat!("[{}] ERROR ", $text), now, $($($arg),*)?);
}; };
(fatal $text: literal$(, $($arg: expr),*$(,)?)?) => {
let now = chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false);
println!(concat!("[{}] !!FATAL ERROR!! ", $text), now, $($($arg),*)?);
};
} }

View File

@@ -1,62 +1,259 @@
use std::sync::{atomic::AtomicU32, Arc}; use std::{collections::HashSet, sync::{atomic::{AtomicUsize, Ordering}, Arc}};
use http_body_util::{combinators::BoxBody, BodyExt, Full}; use askama::Template;
use http_body_util::{combinators::BoxBody, BodyExt, Collected, Full};
use hyper::{ use hyper::{
body::{Bytes, Incoming}, body::{Body, Bytes, Incoming},
server::conn::http1, service::service_fn, Error, Method, Request, Response header::HeaderValue, server::conn::http1, service::service_fn, Error, Method, Request, Response
}; };
use hyper_util::rt::TokioIo; use hyper_util::rt::TokioIo;
use tokio::net::TcpListener; use tokio::{net::TcpListener, sync::Mutex};
mod logger; mod logger;
mod args; mod args;
mod config; mod config;
const CTYPE_FORM: HeaderValue = HeaderValue::from_static("application/x-www-form-urlencoded");
macro_rules! response { macro_rules! response {
($status: ident $body: expr) => {{ (@server) => { "zumepro/ask_stallman" };
(file $req: expr, $source: literal, $mime: literal) => {
response!(
$req, OK include_str!(concat!("../../dst/", $source)),
CONTENT_TYPE: $mime,
CACHE_CONTROL: "max-age=180, public",
)
};
(main_page $req: expr, $message_class: literal, $message: literal) => {
response!( $req, OK MainPage {
ntfy_class: $message_class, message: $message, prefill_question: None
}.render()?, CONTENT_TYPE: "text/html")
};
(main_page $req: expr, $message_class: literal, $message: literal$(, $prefill: expr)?) => {
response!($req, OK MainPage {
ntfy_class: $message_class, message: $message, prefill_question: Some($prefill)
}.render()?, CONTENT_TYPE: "text/html")
};
($req: expr, $status: ident $body: expr) => {{
log!(info "{} \"{} {:?}\"", hyper::StatusCode::$status, $req.method(), $req.uri());
let mut res = Response::new(Full::new(Bytes::from($body)).map_err(|n| match n {}).boxed()); let mut res = Response::new(Full::new(Bytes::from($body)).map_err(|n| match n {}).boxed());
res.headers_mut().append(
hyper::header::SERVER, hyper::header::HeaderValue::from_static(response!(@server))
);
*res.status_mut() = hyper::StatusCode::$status; *res.status_mut() = hyper::StatusCode::$status;
res res
}}; }};
($status: ident $body: expr, $($hkey: ident : $hval: literal),*$(,)?) => {{ ($req: expr, $status: ident $body: expr, $($hkey: ident : $hval: literal),*$(,)?) => {{
log!(info "{} \"{} {:?}\"", hyper::StatusCode::$status, $req.method(), $req.uri());
let mut res = Response::new(Full::new(Bytes::from($body)).map_err(|n| match n {}).boxed()); let mut res = Response::new(Full::new(Bytes::from($body)).map_err(|n| match n {}).boxed());
*res.status_mut() = hyper::StatusCode::$status; *res.status_mut() = hyper::StatusCode::$status;
res.headers_mut().append(
hyper::header::SERVER, hyper::header::HeaderValue::from_static(response!(@server))
);
$(res.headers_mut() $(res.headers_mut()
.append(hyper::header::$hkey, hyper::header::HeaderValue::from_static($hval));)* .append(hyper::header::$hkey, hyper::header::HeaderValue::from_static($hval));)*
res res
}}; }};
} }
#[derive(askama::Template, Default)]
#[template(path = "index.html")]
struct MainPage<'a> {
ntfy_class: &'a str,
message: &'a str,
prefill_question: Option<String>,
}
#[derive(Debug)]
enum RouterError {
Templating(askama::Error),
NotImplemented,
IO(Error),
}
impl std::fmt::Display for RouterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Templating(e) => write!(f, "templating: {}", e),
Self::IO(e) => write!(f, "io: {}", e),
Self::NotImplemented => write!(f, "not implemented"),
}
}
}
impl std::error::Error for RouterError {
fn cause(&self) -> Option<&dyn std::error::Error> {
match self {
Self::Templating(e) => Some(e),
Self::IO(e) => Some(e),
_ => None,
}
}
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Templating(e) => Some(e),
Self::IO(e) => Some(e),
_ => None,
}
}
fn description(&self) -> &str {
match self {
Self::Templating(_) => "could not construct template",
Self::IO(_) => "i/o error",
Self::NotImplemented => "reached a code block not yet implemented",
}
}
}
impl From<Error> for RouterError {
fn from(value: Error) -> Self {
Self::IO(value)
}
}
impl From<askama::Error> for RouterError {
fn from(value: askama::Error) -> Self {
Self::Templating(value)
}
}
async fn router( async fn router(
req: Request<Incoming>, req: Request<Incoming>,
_: Arc<SharedState>, state: Arc<SharedState>,
) -> Result<Response<BoxBody<Bytes, Error>>, Error> { ) -> Result<Response<BoxBody<Bytes, Error>>, RouterError> {
match (req.method(), req.uri().path()) { match (req.method(), req.uri().path()) {
(&Method::GET, "/") => Ok(response!( // pages
OK include_str!("../../dst/index.html"), (&Method::GET, "/") => Ok(response!(main_page req, "", "")),
CONTENT_TYPE: "text/html", (&Method::POST, "/") => new_question(req, state).await,
)),
_ => Ok(response!( // assets
(&Method::GET, "/script.js") => Ok(response!(file req, "script.js", "text/javascript")),
(&Method::GET, "/style.css") => Ok(response!(file req, "style.css", "text/css")),
_ => Ok(response!(req,
NOT_FOUND include_str!("../../dst/not_found.html"), NOT_FOUND include_str!("../../dst/not_found.html"),
CONTENT_TYPE: "text/html", CONTENT_TYPE: "text/html",
)), )),
} }
} }
fn load_config(path: &str) -> Result<config::Config, &'static str> { fn parse_form<'a>(bytes: &'a [u8]) -> Result<std::borrow::Cow<'a, str>, ()> {
let Ok(file_contents) = std::fs::read_to_string(path) else { let parsed = url::form_urlencoded::parse(bytes);
return Err("could not read the config file"); for field in parsed {
if field.0 == "question" {
return Ok(field.1);
}
}
Err(())
}
async fn new_question(
mut req: Request<Incoming>,
state: Arc<SharedState>,
) -> Result<Response<BoxBody<Bytes, Error>>, RouterError> {
// check size
let body_size = req.body().size_hint().upper().unwrap_or(u64::MAX);
if body_size > state.config.server.max_question_body_size {
return Ok(response!(main_page req, "error", "Question was too long to add."));
}
// check headers
if req.headers().get(hyper::header::CONTENT_TYPE) != Some(&CTYPE_FORM) {
return Ok(response!(main_page req,
"error", "Your browser sent a POST request without form data. Please try again."
));
}
// get question
let body = (&mut req).collect().await?.to_bytes();
let Ok(question) = parse_form(&body) else {
return Ok(response!(main_page req,
"error", "The question your browser sent was in invalid format. Please try again."
));
}; };
toml::from_str(&file_contents).map_err(|_| "invalid config file structure or fields") let question = question.to_string();
// insert question
match state.questions.lock().await.add_new(question) {
Ok(()) => {},
Err(()) => return Ok(response!(main_page req,
"error", "We got too many questions in total. So we are not accepting new ones \
anymore. We are so sorry. :(")
)
}
Ok(response!(main_page req, "info", "Your question was successfully added."))
}
async fn maintenance(state: Arc<SharedState>) {
let interval = std::time::Duration::from_secs(state.config.maintenance.interval);
log!(debug "started maintenance routine with {:?} interval", interval);
loop {
log!(debug "running maintenance");
tokio::time::sleep(interval).await;
}
}
fn load_config(path: &str) -> Result<config::Config, String> {
let Ok(file_contents) = std::fs::read_to_string(path) else {
return Err("could not read the config file".to_string());
};
toml::from_str(&file_contents)
.map_err(|e| format!("invalid config file structure or fields: {:?}", e))
}
#[derive(Default)]
struct Questions {
total_size: usize,
max_size: usize,
to_push: HashSet<String>,
pushed: HashSet<String>,
}
impl Questions {
fn with_capacity(max_size: usize) -> Self {
Self {
max_size,
..Default::default()
}
}
fn add_new(&mut self, question: String) -> Result<(), ()> {
if self.pushed.contains(&question) || self.to_push.contains(&question) { return Ok(()); }
if self.total_size + question.len() > self.max_size {
return Err(());
}
self.total_size += question.len();
self.to_push.insert(question);
Ok(())
}
fn consume_to_push(&mut self) -> HashSet<String> {
for question in self.to_push.iter() {
self.pushed.insert(question.clone());
}
std::mem::take(&mut self.to_push)
}
} }
struct SharedState { struct SharedState {
counter: AtomicU32, config: config::Config,
questions: Mutex<Questions>,
} }
impl Default for SharedState { impl SharedState {
fn default() -> Self { fn new(config: config::Config) -> Self {
Self { counter: 0.into() } let memory_limit = config.performance.memory_limit;
Self {
config,
questions: Mutex::new(Questions::with_capacity(memory_limit)),
}
} }
} }
@@ -73,34 +270,39 @@ async fn main() {
let config = match load_config(args.config_path()) { let config = match load_config(args.config_path()) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
eprintln!("{}", e); log!(fatal "{}", e);
return; return;
} }
}; };
// shared state // shared state
let state = Arc::new(SharedState::default()); let state = Arc::new(SharedState::new(config));
// server // server initialization
let Ok(listener) = TcpListener::bind(config.server.bind_to).await else { let Ok(listener) = TcpListener::bind(state.config.server.bind_to).await else {
eprintln!("unable to bind to: {:?}", config.server.bind_to); log!(fatal "unable to bind to {:?}", state.config.server.bind_to);
return; return;
}; };
// server runtime
let state_maintenance = state.clone();
tokio::task::spawn(async move { maintenance(state_maintenance).await; });
log!(info "listening on {:?}", state.config.server.bind_to);
loop { loop {
let Ok((stream, addr)) = listener.accept().await else { let Ok((stream, addr)) = listener.accept().await else {
eprintln!("unable to accept new connections"); log!(fatal "unable to accept new connections");
return; return;
}; };
let io = TokioIo::new(stream);
log!(debug "new connection from {:?}", addr); log!(debug "new connection from {:?}", addr);
let io = TokioIo::new(stream);
let state_clone = state.clone(); let state_clone = state.clone();
tokio::task::spawn(async move { tokio::task::spawn(async move {
if let Err(_) = http1::Builder::new().serve_connection(io, service_fn(move |req| { if let Err(_) = http1::Builder::new().serve_connection(io, service_fn(move |req| {
router(req, state_clone.clone()) router(req, state_clone.clone())
})).await { })).await {
println!("closed connection"); log!(debug "transport error to {:?}", addr);
} }
log!(debug "closing connection to {:?}", addr);
}); });
} }
} }