implement server logic
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -840,6 +840,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"url",
|
"url",
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["lib/search_and_replace", "theseus-server"]
|
members = ["lib/search_and_replace", "theseus-server"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
lto = true
|
||||||
|
2
Makefile
2
Makefile
@@ -15,6 +15,6 @@ run: dst/dev.toml $(TARGETS_CLIENT)
|
|||||||
clean: client_clean
|
clean: client_clean
|
||||||
rm -rf dst
|
rm -rf dst
|
||||||
|
|
||||||
dst/release/theseus-server: $(SRCS_RUST_THESEUS_SERVER) $(TARGETS_CLIENT)
|
dst/release/theseus-server: $(SRCS_RUST_THESEUS_SERVER) $(TARGETS_CLIENT) dst/prod.toml
|
||||||
cargo build --package theseus-server --release
|
cargo build --package theseus-server --release
|
||||||
touch $@
|
touch $@
|
||||||
|
@@ -6,7 +6,7 @@ max_question_body_size = 25
|
|||||||
memory_limit = 50
|
memory_limit = 50
|
||||||
|
|
||||||
[maintenance]
|
[maintenance]
|
||||||
interval = 60
|
interval = 10
|
||||||
|
|
||||||
[push]
|
[push]
|
||||||
endpoint = "[::1]:8081"
|
endpoint = "http://[::1]:8081/push"
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
dst/dev.toml: config/dev.toml
|
dst/dev.toml: config/dev.toml
|
||||||
@mkdir -p $(@D)
|
@mkdir -p $(@D)
|
||||||
ln -f $< $@
|
ln -f $< $@
|
||||||
|
|
||||||
|
dst/prod.toml: config/prod.toml
|
||||||
|
@mkdir -p $(@D)
|
||||||
|
ln -f $< $@
|
||||||
|
12
config/prod.toml
Normal file
12
config/prod.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[server]
|
||||||
|
bind_to = "[::1]:8080"
|
||||||
|
max_question_body_size = 2048
|
||||||
|
|
||||||
|
[performance]
|
||||||
|
memory_limit = 536870912
|
||||||
|
|
||||||
|
[maintenance]
|
||||||
|
interval = 60
|
||||||
|
|
||||||
|
[push]
|
||||||
|
endpoint = "http://[::1]:9091/api.php?cmd=newmodmessages"
|
@@ -10,6 +10,7 @@ http-body-util = "0.1.3"
|
|||||||
hyper = { version = "1.6.0", features = ["full"] }
|
hyper = { version = "1.6.0", features = ["full"] }
|
||||||
hyper-util = { version = "0.1.11", features = ["full"] }
|
hyper-util = { version = "0.1.11", features = ["full"] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = "1.0.140"
|
||||||
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"
|
url = "2.5.4"
|
||||||
|
@@ -33,7 +33,7 @@ def_config! {
|
|||||||
interval = u64, // in seconds
|
interval = u64, // in seconds
|
||||||
|
|
||||||
[push Push]
|
[push Push]
|
||||||
endpoint = std::net::SocketAddr, // recommended format: "<host>:<port>"
|
endpoint = String, // recommended format: "<proto>://<host>:<port>/<path>"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
use std::{collections::HashSet, sync::{atomic::{AtomicUsize, Ordering}, Arc}};
|
use std::{collections::HashSet, sync::Arc};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use http_body_util::{combinators::BoxBody, BodyExt, Collected, Full};
|
use http_body_util::{combinators::BoxBody, BodyExt, Full};
|
||||||
use hyper::{
|
use hyper::{
|
||||||
body::{Body, Bytes, Incoming},
|
body::{Body, Bytes, Incoming},
|
||||||
header::HeaderValue, 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, sync::Mutex};
|
use tokio::{net::{TcpListener, TcpStream}, sync::Mutex};
|
||||||
|
|
||||||
mod logger;
|
mod logger;
|
||||||
mod args;
|
mod args;
|
||||||
@@ -191,15 +191,102 @@ async fn new_question(
|
|||||||
Ok(response!(main_page req, "info", "Your question was successfully added."))
|
Ok(response!(main_page req, "info", "Your question was successfully added."))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PushEndpoint<'a> {
|
||||||
|
uri: &'a hyper::Uri,
|
||||||
|
host: &'a str,
|
||||||
|
port: u16,
|
||||||
|
addr: String,
|
||||||
|
authority: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PushEndpoint<'a> {
|
||||||
|
fn new(uri: &'a hyper::Uri) -> Result<PushEndpoint<'a>, &'static str> {
|
||||||
|
let host = uri.host().ok_or("no host provided")?;
|
||||||
|
let port = uri.port_u16().ok_or("no port provided")?;
|
||||||
|
let addr = format!("{}:{}", host, port);
|
||||||
|
let authority = uri.authority().ok_or("no authority provided")?.to_string();
|
||||||
|
Ok(Self { uri, host, port, addr, authority })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn maintenance(state: Arc<SharedState>) {
|
async fn maintenance(state: Arc<SharedState>) {
|
||||||
|
let Ok(uri): Result<hyper::Uri, _> = state.config.push.endpoint.parse() else {
|
||||||
|
log!(fatal "could not parse uri: {:?}", state.config.push.endpoint);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let uri = match PushEndpoint::new(&uri) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
log!(fatal "could not parse endpoint address: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
let interval = std::time::Duration::from_secs(state.config.maintenance.interval);
|
let interval = std::time::Duration::from_secs(state.config.maintenance.interval);
|
||||||
|
let mut questions: HashSet<String> = HashSet::default();
|
||||||
log!(debug "started maintenance routine with {:?} interval", interval);
|
log!(debug "started maintenance routine with {:?} interval", interval);
|
||||||
loop {
|
loop {
|
||||||
log!(debug "running maintenance");
|
log!(debug "----- MAINTENANCE -----");
|
||||||
|
let questions_new = state.questions.lock().await.consume_to_push();
|
||||||
|
questions.extend(questions_new);
|
||||||
|
log!(debug "pushing {} questions", questions.len());
|
||||||
|
match push_questions(&questions, &uri).await {
|
||||||
|
Ok(()) => questions = HashSet::default(),
|
||||||
|
Err(()) => { log!(debug "push failed - will try again"); },
|
||||||
|
};
|
||||||
|
log!(debug "----- /MAINTENANCE -----");
|
||||||
tokio::time::sleep(interval).await;
|
tokio::time::sleep(interval).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_push_request<'a>(
|
||||||
|
questions: &HashSet<String>, uri: &PushEndpoint<'a>
|
||||||
|
) -> Result<hyper::Request<Full<Bytes>>, ()> {
|
||||||
|
let Ok(body) = serde_json::to_string(questions) else { return Err(()); };
|
||||||
|
hyper::Request::builder()
|
||||||
|
.uri(uri.uri)
|
||||||
|
.header(hyper::header::HOST, &uri.authority)
|
||||||
|
.header(hyper::header::CONTENT_TYPE, "application/json")
|
||||||
|
.body(Full::new(Bytes::from(body))).map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_to_push_endpoint(
|
||||||
|
addr: &String,
|
||||||
|
) -> Result<hyper::client::conn::http1::SendRequest<Full<Bytes>>, ()> {
|
||||||
|
let stream = TcpStream::connect(addr).await.map_err(|_| ())?;
|
||||||
|
let (sender, conn) = hyper::client::conn::http1::handshake(
|
||||||
|
TokioIo::new(stream)
|
||||||
|
).await.map_err(|_| ())?;
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
conn.await
|
||||||
|
});
|
||||||
|
Ok(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn push_questions<'a>(questions: &HashSet<String>, uri: &PushEndpoint<'a>) -> Result<(), ()> {
|
||||||
|
if questions.len() == 0 {
|
||||||
|
log!(debug "skipping push - no new questions");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let Ok(mut conn) = connect_to_push_endpoint(&uri.addr).await else {
|
||||||
|
log!(err "could not connect to push endpoint");
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
|
let Ok(req) = make_push_request(questions, uri) else {
|
||||||
|
log!(err "could not construct push questions request");
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
|
let Ok(res) = conn.send_request(req).await else {
|
||||||
|
log!(err "could not send questions request to push endpoint");
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
|
if res.status() != hyper::StatusCode::OK {
|
||||||
|
log!(err "got non-200 response from push endpoint");
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
log!(info "successfully pushed new questions");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn load_config(path: &str) -> Result<config::Config, String> {
|
fn load_config(path: &str) -> Result<config::Config, String> {
|
||||||
let Ok(file_contents) = std::fs::read_to_string(path) else {
|
let Ok(file_contents) = std::fs::read_to_string(path) else {
|
||||||
return Err("could not read the config file".to_string());
|
return Err("could not read the config file".to_string());
|
||||||
|
Reference in New Issue
Block a user