diff options
author | metamuffin <metamuffin@disroot.org> | 2024-04-28 18:31:22 +0200 |
---|---|---|
committer | metamuffin <metamuffin@disroot.org> | 2024-04-28 18:31:22 +0200 |
commit | 26632e3474fd4c7aba1b5c180cf8e6b72c7dde6a (patch) | |
tree | 0357dccfbd1e52e199792394e461d7eb355fded1 /src | |
download | meta-adservices-26632e3474fd4c7aba1b5c180cf8e6b72c7dde6a.tar meta-adservices-26632e3474fd4c7aba1b5c180cf8e6b72c7dde6a.tar.bz2 meta-adservices-26632e3474fd4c7aba1b5c180cf8e6b72c7dde6a.tar.zst |
works
Diffstat (limited to 'src')
-rw-r--r-- | src/embed.rs | 90 | ||||
-rw-r--r-- | src/error.rs | 27 | ||||
-rw-r--r-- | src/info.rs | 58 | ||||
-rw-r--r-- | src/main.rs | 62 | ||||
-rw-r--r-- | src/state.rs | 105 | ||||
-rw-r--r-- | src/style.css | 1 |
6 files changed, 343 insertions, 0 deletions
diff --git a/src/embed.rs b/src/embed.rs new file mode 100644 index 0000000..02dea2f --- /dev/null +++ b/src/embed.rs @@ -0,0 +1,90 @@ +use crate::{error::MyResult, state::Logic, Template}; +use anyhow::anyhow; +use log::warn; +use markup::{doctype, DynRender}; +use rand::{seq::IndexedRandom, thread_rng}; +use rocket::{ + get, + http::{Header, Status}, + response::{self, Responder}, + tokio::fs::File, + uri, Request, Response, State, +}; +use std::net::IpAddr; + +#[get("/v1/embed?<s>")] +pub async fn r_embed<'a>( + state: &State<Logic>, + addr: IpAddr, + s: &str, +) -> MyResult<Template<DynRender<'a>>> { + let key = state + .ad_keys + .choose(&mut thread_rng()) + .ok_or(anyhow!("no ads configured"))?; + let ad = state.config.ads.get(key).unwrap(); + + let image_url = uri!(r_image(key)).to_string(); + let target_url = ad.target.clone(); + + if let Err(e) = state.register_impression(s, &key, addr).await { + warn!("could not register impression: {e}") + } + + Ok(Template(markup::new! { + @doctype() + html { + head { + title { "advertisement by meta adservices" } + style { + "*{user-select:none}" + "body,p{margin:0px}" + "div,p{position:absolute}" + "p{font-size:11px;bottom:0px;right:0px;color:#fff;background-color:#0009}" + "body{width:728px;height:90px;overflow:hidden;position:relative;background-color:grey}" + "div{pointer-events:none;opacity:0;transition:opacity 0.5s;top:-64px;left:-300px;width:374px;background-color:#fffa}" + "button:hover div{opacity:1}" + "button{border:none;width:22px;height:22px}" + } + } + body { + a[href=&target_url,target="_blank"] { img[src=&image_url, width=728, height=90]; } + p { + "Ad by meta " + a[href="/", target="_blank"]{ button { + "🛈" + div { + "advertisement by meta adservices." br; + "meta adservices is a non-profit non-registered organisation" br; + "eager to flooding the internet with useless ads." br; + i{"(click for more information)"} + } + } } + } + } + } + })) +} + +#[get("/v1/image?<k>")] +pub async fn r_image(state: &State<Logic>, k: &str) -> MyResult<CachedFile> { + let info = state + .config + .ads + .get(&k.to_owned()) + .ok_or(anyhow!("ad does not exist"))?; + Ok(CachedFile( + File::open(state.config.image_base.join(&info.image)).await?, + )) +} + +pub struct CachedFile(File); +impl<'r> Responder<'r, 'static> for CachedFile { + fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'static> { + Response::build() + .status(Status::Ok) + .header(Header::new("cache-control", "max-age=3600, public")) + .streamed_body(self.0) + .ok() + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..04d7e38 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,27 @@ +use std::fmt::Display; + +use rocket::{ + response::{self, Responder}, + Request, +}; +use thiserror::Error; + +pub type MyResult<T> = Result<T, MyError>; + +#[derive(Debug, Error)] +pub enum MyError { + Anyhow(#[from] anyhow::Error), + Io(#[from] std::io::Error), +} + +impl<'r> Responder<'r, 'static> for MyError { + fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { + format!("{self}").respond_to(req) + } +} + +impl Display for MyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} diff --git a/src/info.rs b/src/info.rs new file mode 100644 index 0000000..433bb13 --- /dev/null +++ b/src/info.rs @@ -0,0 +1,58 @@ +use crate::{s_file, Template}; +use markup::{doctype, DynRender}; +use rocket::{get, http::ContentType}; + +#[get("/")] +pub fn r_index<'a>() -> Template<DynRender<'a>> { + Template(markup::new! { + @doctype() + html { + head { + title { "meta adservices" } + meta[name="viewport", content="width=device-width, initial-scale=1.0"]; + meta[name="description", content="advertise now with meta adservices"]; + link[rel="stylesheet", href="/style.css"]; + } + body { + h1 { "meta adservices" } + p { + "meta adservices is the leading provider of useless images within iframes. " + } + + h2 { "sample ad" } + iframe[src="/v1/embed?s=adservices.metamuffin.org", style="border:none;width:728px;height:90px;"] {} + h2 { "privacy" } + p { "data used by meta adservices: " } + ul { + li { "Your IP address" } + li { "The Advertisement site (where the ad was shown)" } + } + p { "data stored by meta adservices: " } + ul { + li { "Your IP address is used within a bloom filter in volatile memory." } + li { "Your requests increment a persistent public counter per ad site (according to a weighting function)." } + } + } + } + }) +} + +#[get("/style.css")] +pub fn r_style() -> (ContentType, String) { + (ContentType::CSS, s_file!("style.css")) +} + +#[cfg(not(debug_assertions))] +#[macro_export] +macro_rules! s_file { + ($path: literal) => { + include_str!($path).to_string(), + }; +} +#[cfg(debug_assertions)] +#[macro_export] +macro_rules! s_file { + ($path: literal) => { + std::fs::read_to_string(concat!("src/", $path)).unwrap() + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8a20982 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,62 @@ +#![feature(ip_bits)] +pub mod embed; +pub mod error; +pub mod info; +pub mod state; +use embed::*; +use info::*; + +use markup::Render; +use rocket::{ + catch, catchers, + fairing::AdHoc, + http::{ContentType, Header, Status}, + response::{self, Responder}, + routes, Request, Response, +}; +use state::{Config, Logic}; +use std::io::Cursor; + +#[rocket::main] +async fn main() { + env_logger::init_from_env("LOG"); + + let config = std::env::args() + .nth(1) + .expect("first arg needs to be the config"); + let config = rocket::tokio::fs::read_to_string(config) + .await + .expect("could not read config"); + let config: Config = serde_yaml::from_str(config.as_str()).expect("config invalid"); + + let state = Logic::new(config); + + let _ = rocket::build() + .attach(AdHoc::on_response("set server header", |_req, res| { + res.set_header(Header::new("server", "meta adservices")); + Box::pin(async {}) + })) + .manage(state) + .mount("/", routes![r_index, r_embed, r_style, r_image]) + .register("/", catchers![r_catch]) + .launch() + .await + .unwrap(); +} + +pub struct Template<T>(pub T); +impl<'r, T: Render> Responder<'r, 'static> for Template<T> { + fn respond_to(self, _req: &'r Request<'_>) -> response::Result<'static> { + let mut out = String::new(); + self.0.render(&mut out).unwrap(); + Response::build() + .header(ContentType::HTML) + .streamed_body(Cursor::new(out)) + .ok() + } +} + +#[catch(default)] +pub fn r_catch<'a>(status: Status, _request: &Request) -> String { + format!("{status}; This ad is likely misconfigured. Server could also be broken...") +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..4de21b4 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,105 @@ +use anyhow::Context; +use redb::{Database, ReadableTable, TableDefinition}; +use rocket::tokio::sync::Mutex; +use serde::Deserialize; +use std::{collections::HashMap, net::IpAddr, path::PathBuf}; + +#[derive(Deserialize)] +pub struct AdInfo { + pub image: PathBuf, + pub target: String, +} + +#[derive(Deserialize)] +pub struct Config { + bloom_filter_size: usize, + impression_weight_falloff: f64, + pub image_base: PathBuf, + database_path: PathBuf, + pub ads: HashMap<String, AdInfo>, +} + +pub struct Logic { + pub config: Config, + database: Database, + impressions_by_addr: Mutex<Vec<u16>>, + pub ad_keys: Vec<String>, +} + +static T_TOTAL: TableDefinition<'static, (), u128> = TableDefinition::new("t"); +static T_IMPRESSIONS_RAW: TableDefinition<'static, &str, u128> = TableDefinition::new("ir"); +static T_IMPRESSIONS_WEIGHTED: TableDefinition<'static, &str, f64> = TableDefinition::new("iw"); +static T_ADS: TableDefinition<'static, &str, u128> = TableDefinition::new("a"); + +impl Logic { + pub fn new(config: Config) -> Self { + Self { + impressions_by_addr: vec![0; config.bloom_filter_size].into(), + database: { + let db = Database::create(&config.database_path).expect("database open failed"); + { + let txn = db.begin_write().unwrap(); + txn.open_table(T_IMPRESSIONS_RAW).unwrap(); + txn.open_table(T_IMPRESSIONS_WEIGHTED).unwrap(); + txn.open_table(T_ADS).unwrap(); + } + db + }, + ad_keys: config.ads.keys().map(String::from).collect(), + config, + } + } + + pub async fn register_impression( + &self, + site: &str, + adid: &str, + address: IpAddr, + ) -> anyhow::Result<()> { + let num_impressions = { + let mut bloom = self.impressions_by_addr.lock().await; + let ind = (xorshift(xorshift(xorshift( + match address { + IpAddr::V4(a) => a.to_ipv6_mapped(), + IpAddr::V6(a) => a, + } + .to_bits() as u64, + ))) % bloom.len() as u64) as usize; + bloom[ind] = bloom[ind].saturating_add(1); + bloom[ind] + } as f64; + + let weight = self.config.impression_weight_falloff.powf(num_impressions); + + let txn = self.database.begin_write().context("database failure")?; + { + let mut raw = txn.open_table(T_TOTAL)?; + let v = raw.get(())?.map(|g| g.value()).unwrap_or_default(); + raw.insert((), v + 1)?; + } + { + let mut raw = txn.open_table(T_IMPRESSIONS_RAW)?; + let v = raw.get(site)?.map(|g| g.value()).unwrap_or_default(); + raw.insert(site, v + 1)?; + } + { + let mut raw = txn.open_table(T_IMPRESSIONS_WEIGHTED)?; + let v = raw.get(site)?.map(|g| g.value()).unwrap_or_default(); + raw.insert(site, v + weight)?; + } + { + let mut raw = txn.open_table(T_ADS)?; + let v = raw.get(site)?.map(|g| g.value()).unwrap_or_default(); + raw.insert(adid, v + 1)?; + } + txn.commit().context("database failure")?; + Ok(()) + } +} + +fn xorshift(mut x: u64) -> u64 { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + x +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/style.css @@ -0,0 +1 @@ + |