// Init translations for current crate. rust_i18n::i18n!("src/locales"); use std::collections::HashMap; use std::convert::Infallible; use std::fs; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; use std::sync::mpsc::{channel, Sender}; use std::sync::Arc; use std::{thread, time::Duration}; use serde_derive::{Deserialize, Serialize}; use hyper::server::conn::AddrStream; use hyper::service::{make_service_fn, service_fn}; use hyper::{header::HeaderValue, Body, Method, Request, Response, Server, StatusCode}; use rusqlite::{Connection, Transaction}; use rust_i18n::t; enum VustQuery { Get, Like, Commit, } struct VustMessage { req_type: VustQuery, ip: SocketAddr, path: String, res: Sender>, } fn respond(sc: StatusCode, mes: String) -> Response { Response::builder() .status(sc) .body(Body::from(mes)) .unwrap() } fn wrap_cors(mut res: Response, config: Arc) -> Response { res.headers_mut().insert( "Access-Control-Allow-Origin", HeaderValue::from_str(&config.cors_hosts).unwrap(), ); res.headers_mut().insert( "Access-Control-Allow-Methods", HeaderValue::from_static("*"), ); res.headers_mut().insert( "Access-Control-Allow-Headers", HeaderValue::from_static("*"), ); res } fn handle( req: Request, addr: SocketAddr, tx: Sender, config: Arc, ) -> Response { const PREFIX_PATH: &str = "/like/"; const DEFAULT_LOCALE: &str = "fr"; let path: String = req.uri().path().to_string(); rust_i18n::set_locale(match req.headers().contains_key("lang") { true => match req.headers().get("lang") { Some(h) => match h.to_str() { Ok(l) => l, Err(_) => DEFAULT_LOCALE, }, None => DEFAULT_LOCALE, }, false => DEFAULT_LOCALE, }); let addr = match req.headers().contains_key("real-ip") { false => addr, true => { let real_ip_str = match req.headers().get("real-ip") { Some(ip) => ip.to_str(), None => { return respond(StatusCode::SERVICE_UNAVAILABLE, t!("hidden_ip").to_string()); } }; let real_ip_str = match real_ip_str { Ok(addr) => addr, Err(s) => { return respond( StatusCode::SERVICE_UNAVAILABLE, format!("{}: {}.", t!("invalid_ip"), s), ); } }; let ip: IpAddr = match real_ip_str.parse() { Ok(addr) => addr, Err(_) => { return respond( StatusCode::SERVICE_UNAVAILABLE, format!("{} {}.", t!("invalid_ip"), real_ip_str), ); } }; SocketAddr::new(ip, 0) } }; let query_type = match *req.method() { Method::POST => VustQuery::Like, Method::PUT => VustQuery::Commit, Method::GET => VustQuery::Get, Method::OPTIONS => { return respond(StatusCode::OK, "WHAT ?".to_string()); } _ => { return respond( StatusCode::METHOD_NOT_ALLOWED, t!("method_not_allowed").to_string(), ); } }; if !path.starts_with(PREFIX_PATH) { return respond( StatusCode::BAD_REQUEST, format!("{} {}.", t!("bad_endpoint"), PREFIX_PATH), ); } let path: String = path.chars().skip(PREFIX_PATH.len()).collect(); if path.contains("/") || path.is_empty() || path.len() > 128 || !config.list_articles.contains(&path) { return respond(StatusCode::BAD_REQUEST, t!("bad_request").to_string()); } let article_key_db = if config.aliases.contains_key(&path) { config.aliases.get(&path).unwrap().to_owned() } else { path }; let (ttx, rx) = channel(); let message = VustMessage { req_type: query_type, ip: addr, path: article_key_db, res: ttx, }; tx.send(message).unwrap(); match *req.method() { Method::PUT => respond(StatusCode::OK, t!("done").to_string()), _ => rx.recv().unwrap(), } } fn do_get(tr: &Transaction, path: String) -> Response { for _ in 1..3 { let mut req_prepared = tr .prepare("SELECT cast(SUM(number) as text) FROM likes WHERE path = ?") .unwrap(); let mut rows = req_prepared .query(rusqlite::params![path.as_str()]) .unwrap(); let first_row = rows.next(); // Error while fetching // By example busy database if first_row.is_err() { continue; } let first_row = first_row.unwrap(); // Empty row! Nobody like what I do. 😭 if first_row.is_none() { return respond(StatusCode::OK, "0".to_string()); } let first_row = first_row.unwrap(); match first_row.get(0) { Ok(nb_likes) => return respond(StatusCode::OK, nb_likes), // In case of NULL or not an Integer value: Err(_) => return respond(StatusCode::OK, "⋅".to_string()), } } respond(StatusCode::OK, "❓".to_string()) } fn do_like(tr: &Transaction, ip: SocketAddr, path: String) -> Response { let hash_ip = match ip.ip() { IpAddr::V4(ip) => blake3::hash(&ip.octets()).to_hex(), IpAddr::V6(ip) => blake3::hash(&ip.octets()).to_hex(), }; for _ in 1..7 { let mut req_prepared = tr.prepare("SELECT number, cast(lastMod as UNSIGNED INT), cast(unixepoch() as UNSIGNED INT) FROM likes WHERE ip_hash = ? and path = ?").unwrap(); // , path.as_str()]).unwrap(); let mut rows = req_prepared .query(rusqlite::params![hash_ip.as_str(), path.as_str()]) .unwrap(); let first_row = rows.next(); match first_row { Ok(None) => { let res = tr.execute( "INSERT OR IGNORE INTO likes VALUES (?, ?, unixepoch(), 1)", [hash_ip.as_str(), path.as_str()], ); if res.is_err() { eprintln!("Error doing the request {:?}", res.err()); continue; } return respond(StatusCode::OK, t!("thanks").to_string()); } Ok(Some(t)) => { let number: u64 = t.get(0).unwrap(); let time: u64 = t.get(1).unwrap(); let now: u64 = t.get(2).unwrap(); if number > 31 { return respond( StatusCode::RANGE_NOT_SATISFIABLE, format!("{} 💕 x ({})", t!("too_many_hears"), number).to_string(), ); } let limite = (1 << number) / 10; // 2^likes / Cst let dtime = now - time; if dtime < limite { let time_remaining = limite - dtime; return respond( StatusCode::TOO_MANY_REQUESTS, format!( "{} {}s {}", t!("wait_prefix"), time_remaining, t!("wait_message_suffix") ), ); } let res = tr.execute("UPDATE likes SET number = number + 1, lastMod = unixepoch() WHERE ip_hash = ? and path = ?", [hash_ip.as_str(), path.as_str()]); if res.is_err() { eprintln!("Error doing the request {:?}", res.err()); continue; } return respond( StatusCode::OK, format!("{} 💕 x {}", t!("thanks"), number + 1).to_string(), ); } Err(_) => continue, }; } respond( StatusCode::INTERNAL_SERVER_ERROR, t!("unknow_error").to_string(), ) } #[derive(Serialize, Deserialize)] struct Config { ip: String, port: u16, cors_hosts: String, list_articles: Vec, aliases: HashMap, } fn get_config() -> String { let path = if std::path::Path::new("vust.conf").exists() { "vust.conf" } else if std::path::Path::new("/etc/vust.conf").exists() { "/etc/vust.conf" } else { return r#" ip = "127.0.0.1" port = 3000 # A commas separated list of hosts cors_hosts = '*' list_articles = [ 'bizarreries-du-langage-c', 'retour-sur-laoc-2021-semaine-1', '2FA-discord-sur-pc', 'duckduckgo-google-en-mieux', 'c-language-quirks', 'formats-images-web', 'web-image-formats', 'les-trains-et-la-publicit%C3%A9', 'rail-and-advertising', ] aliases.c-language-quirks='bizarreries-du-langage-c' aliases.web-image-formats='formats-images-web' aliases.rail-and-advertising='les-trains-et-la-publicit%C3%A9' "# .to_string(); }; fs::read_to_string(path).expect("Unable to read config file") } #[tokio::main] async fn main() { let config: Arc = Arc::new(toml::from_str(get_config().as_str()).unwrap()); let ip = IpAddr::from_str(config.ip.as_str()).expect("Invalid IP address"); let addr = SocketAddr::new(ip, config.port); eprintln!("Listening on {}", addr); let (tx, rx) = channel(); let ttx = tx.clone(); thread::spawn(move || { let tx = ttx.clone(); loop { let (txx, _) = channel(); let res = tx.send(VustMessage { req_type: VustQuery::Commit, ip: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), path: "".to_string(), res: txx, }); if res.is_ok() || res.is_err() { thread::sleep(Duration::from_secs(10)); } } }); // This thread handle the sqlite connection thread::spawn(move || { let mut conn = Connection::open("likes.db").unwrap(); loop { let tr = conn.transaction().unwrap(); let mut should_commit = false; loop { let recv: VustMessage = rx.recv().unwrap(); match recv.req_type { VustQuery::Like => { let res = do_like(&tr, recv.ip, recv.path); let res = recv.res.send(res); if res.is_ok() { should_commit = true; } continue; } VustQuery::Get => { let res = recv.res.send(do_get(&tr, recv.path)); if res.is_ok() || res.is_err() { continue; } } VustQuery::Commit => { if should_commit { break; } } } } tr.commit().unwrap(); } }); // The closure passed to `make_service_fn` is executed each time a new // connection is established and returns a future that resolves to a // service. let make_service = make_service_fn(|conn: &AddrStream| { // The closure passed to `service_fn` is executed each time a request // arrives on the connection and returns a future that resolves // to a response. let remote_addr = conn.remote_addr(); let tx = tx.clone(); let config = config.clone(); async move { Ok::<_, Infallible>(service_fn(move |req| { let tx = tx.clone(); let config = config.clone(); async move { Ok::<_, Infallible>(wrap_cors( handle(req, remote_addr, tx, config.clone()), config.clone(), )) } })) } }); // Start the server. if let Err(e) = Server::bind(&addr).serve(make_service).await { eprintln!("Error: {:#}", e); std::process::exit(1); } }