relintio-agent 0.1.1

In-process protection agent for Rust web frameworks (Axum, Actix-web) powered by Relintio.
Documentation
use serde_json::Value;

pub struct ObsidianInjector {
    script_id: &'static str,
    max_body_bytes: usize,
}

impl Default for ObsidianInjector {
    fn default() -> Self {
        Self {
            script_id: "relintio-obsidian",
            max_body_bytes: 2 * 1024 * 1024,
        }
    }
}

impl ObsidianInjector {
    pub fn new() -> Self {
        Self::default()
    }

    /// Build client-side protection script based on config rules.
    pub fn build_client_script(&self, rules: &Value) -> String {
        let mut js = String::new();

        if rules.get("guard_rightclick").and_then(|v| v.as_bool()).unwrap_or(false) {
            js.push_str("document.addEventListener('contextmenu',e=>e.preventDefault());");
        }

        if rules.get("guard_devtools").and_then(|v| v.as_bool()).unwrap_or(false) {
            js.push_str("document.onkeydown=function(e){if(e.keyCode==123||(e.ctrlKey&&e.shiftKey&&e.keyCode=='I'.charCodeAt(0))||(e.ctrlKey&&e.shiftKey&&e.keyCode=='J'.charCodeAt(0))||(e.ctrlKey&&e.keyCode=='U'.charCodeAt(0)))return false;};");
        }

        if rules.get("guard_copy").and_then(|v| v.as_bool()).unwrap_or(false) {
            js.push_str("document.addEventListener('copy',function(e){try{var t=(e.clipboardData||window.clipboardData);if(!t)return;var s=window.getSelection?String(window.getSelection()):'';if(!s)return;t.setData('text/plain',s+'\\n\\n[Protected by Relintio]');e.preventDefault();}catch(_e){}});");
        }

        let mode = rules.get("obsidian_mode").and_then(|v| v.as_str()).unwrap_or("compat");

        if rules.get("obsidian_text").and_then(|v| v.as_bool()).unwrap_or(false) && mode == "aggressive" {
            js.push_str("(function(){function s(n){if(n.nodeType===3){n.nodeValue=n.nodeValue.split('').join('\\u200C');}else if(n.nodeType===1&&n.tagName!=='SCRIPT'&&n.tagName!=='STYLE'){for(var i=0;i<n.childNodes.length;i++){s(n.childNodes[i]);}}}}window.addEventListener('load',function(){try{s(document.body);}catch(_e){}},{once:true});})();");
        }

        if rules.get("obsidian_css").and_then(|v| v.as_bool()).unwrap_or(false) {
            if mode == "aggressive" {
                js.push_str("(function(){function run(){try{var n=['ax-99','bz-22','c-al'];var all=document.querySelectorAll('*');for(var j=0;j<all.length;j++){all[j].classList.add(n[Math.floor(Math.random()*n.length)]);}}catch(_e){}}window.addEventListener('load',function(){setTimeout(run,50);},{once:true});})();");
            } else {
                js.push_str("(function(){function run(){try{var roots=['#app','#__next','[data-reactroot]'];for(var i=0;i<roots.length;i++){if(document.querySelector(roots[i]))return;}var n=['ax-99','bz-22','c-al'];var all=document.querySelectorAll('*');for(var j=0;j<all.length;j++){all[j].classList.add(n[Math.floor(Math.random()*n.length)]);}}catch(_e){}}if('requestIdleCallback' in window){requestIdleCallback(run,{timeout:1200});}else{window.addEventListener('load',function(){setTimeout(run,250);});}})();");
            }
        }

        js
    }

    /// Inject script tag inside </body> of HTML response body.
    pub fn inject_into_html(&self, body: &[u8], rules: &Value) -> Vec<u8> {
        if body.is_empty() || body.len() > self.max_body_bytes {
            return body.to_vec();
        }

        // Simplistic check to see if it's text/html.
        let body_str = match std::str::from_utf8(body) {
            Ok(s) => s,
            Err(_) => return body.to_vec(),
        };

        if !body_str.to_lowercase().contains("<html") {
            return body.to_vec();
        }

        let script = self.build_client_script(rules);
        if script.is_empty() {
            return body.to_vec();
        }

        let injection = format!("<script id=\"{}\">{}</script>", self.script_id, script);

        if let Some(pos) = body_str.to_lowercase().rfind("</body>") {
            let mut new_body = Vec::with_capacity(body.len() + injection.len());
            new_body.extend_from_slice(&body[..pos]);
            new_body.extend_from_slice(injection.as_bytes());
            new_body.extend_from_slice(&body[pos..]);
            new_body
        } else {
            let mut new_body = body.to_vec();
            new_body.extend_from_slice(injection.as_bytes());
            new_body
        }
    }
}