// Init translations for current crate. rust_i18n::i18n!("src/locales"); use std::convert::Infallible; use std::net::{IpAddr, SocketAddr, Ipv4Addr}; use std::collections::HashMap; use std::str::FromStr; use std::sync::mpsc::{Sender, channel}; use std::{thread, time::Duration}; use std::sync::Arc; use std::fs; use serde_derive::{Serialize, Deserialize}; use toml; use hyper::server::conn::AddrStream; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server, StatusCode, Method, header::HeaderValue}; use rusqlite::{Connection, Transaction}; use blake3; 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").to_string(), s.to_string())); } }; let ip :IpAddr = match real_ip_str.parse() { Ok(addr) => addr, Err(_) => { return respond(StatusCode::SERVICE_UNAVAILABLE, format!("{} {}.", t!("invalid_ip").to_string(), 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.len() > 128 || path.len() == 0 || !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 a 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); } }