aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-04-28 18:31:22 +0200
committermetamuffin <metamuffin@disroot.org>2024-04-28 18:31:22 +0200
commit26632e3474fd4c7aba1b5c180cf8e6b72c7dde6a (patch)
tree0357dccfbd1e52e199792394e461d7eb355fded1 /src
downloadmeta-adservices-26632e3474fd4c7aba1b5c180cf8e6b72c7dde6a.tar
meta-adservices-26632e3474fd4c7aba1b5c180cf8e6b72c7dde6a.tar.bz2
meta-adservices-26632e3474fd4c7aba1b5c180cf8e6b72c7dde6a.tar.zst
works
Diffstat (limited to 'src')
-rw-r--r--src/embed.rs90
-rw-r--r--src/error.rs27
-rw-r--r--src/info.rs58
-rw-r--r--src/main.rs62
-rw-r--r--src/state.rs105
-rw-r--r--src/style.css1
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 @@
+