~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
222 lines · 9.2 KB Raw History
//! Repositories' websites, deployed with `knotless repo site` and answered on
//! their own domain by an appview that follows the reference PDS.

#[path = "../../knotless/tests/common/mod.rs"]
mod common;
use common::*;

use knotless_serve::index::Index;
use knotless_serve::sites::{Sites, hosted};
use knotless_serve::{App, router};
use std::time::Duration;

const DOMAIN: &str = "knotless.page";
const HOME: &str = "https://pages.example";

/// What a request to `base` as `host` got: its status, headers and body.
fn get(
    base: &str,
    host: &str,
    path: &str,
    headers: &[(&str, &str)],
) -> (u16, reqwest::header::HeaderMap, String) {
    let client = reqwest::blocking::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .build()
        .unwrap();
    let mut req = client
        .get(format!("{base}{path}"))
        .header("x-forwarded-host", host);
    for (name, value) in headers {
        req = req.header(*name, *value);
    }
    let res = req.send().unwrap();
    let (status, headers) = (res.status().as_u16(), res.headers().clone());
    (status, headers, res.text().unwrap())
}

#[test]
fn sites_are_served_on_their_domain() {
    let mut s = Sandbox::new();
    let handle = s.account.handle.clone();
    let did = s.account.did.clone();
    s.set("KNOTLESS_IDENTIFIER", &handle);
    s.set("KNOTLESS_CHUNK_SIZE", "1000");
    let rt = tokio::runtime::Runtime::new().unwrap();
    let listener = rt
        .block_on(tokio::net::TcpListener::bind("127.0.0.1:0"))
        .unwrap();
    let base = format!("http://{}", listener.local_addr().unwrap());
    let index = Index::open(&s.path("index.db")).unwrap();
    let app = App::build(ctx(&s), Duration::ZERO, None, Some(index));
    let sites = Sites::new(app.clone(), DOMAIN.into(), format!("{HOME}/"));
    let routes = hosted(router(app.clone()), sites);
    rt.spawn(async move { axum::serve(listener, routes).await.unwrap() });
    let ws = net().pds.replace("http://", "ws://");
    rt.spawn(knotless_serve::firehose::watch(app.clone(), ws));

    let url = s.url("site");
    let work = s.work("w", 1);
    s.git(&work, &["push", "-q", &url, "main"]);
    s.git(&work, &["push", "-q", &s.url("bare"), "main"]);
    let dist = s.path("dist");
    let write = |path: &str, bytes: &[u8]| {
        let at = dist.join(path);
        std::fs::create_dir_all(at.parent().unwrap()).unwrap();
        std::fs::write(at, bytes).unwrap();
    };
    let script: String = (0..400).map(|i| format!("let a{i} = {i};\n")).collect();
    write("index.html", b"<h1>home</h1>");
    write("app.js", script.as_bytes());
    write("docs/index.html", b"docs");
    write("404.html", b"gone");
    write("data.bin", b"\x00\x01");
    let dir = dist.to_string_lossy().into_owned();
    let deploy = ["repo", "site", &url, &dir, "--not-found", "404.html"];
    let (code, out) = s.cli(&deploy, b"");
    assert_eq!(code, 0, "{out}");

    let host = format!("{}.{DOMAIN}", handle.replace('-', "--").replace('.', "-"));
    let plc = did.strip_prefix("did:plc:").unwrap();
    let by_did = format!("{plc}.{DOMAIN}");
    wait_until(|| get(&base, &host, "/site/", &[]).0 == 200);

    // Its repository's pages name it, with nothing said of what it was
    // deployed from; a repository with none names none.
    let shown = |name: &str| -> serde_json::Value {
        let repo = format!("at://{did}/dev.knotless.repo/{name}");
        let asked = format!("{base}/xrpc/dev.knotless.getRepo?repo={repo}");
        let answer: serde_json::Value = reqwest::blocking::get(asked).unwrap().json().unwrap();
        answer["site"].clone()
    };
    let site = shown("site");
    assert_eq!(site["url"], format!("https://{host}/site/"), "{site}");
    assert!(site["deployedAt"].is_string(), "{site}");
    assert!(site.get("commit").is_none() && site.get("check").is_none());
    assert!(shown("bare").is_null());

    // A page, as its record says, with its hash as its ETag.
    let (status, headers, body) = get(&base, &host, "/site/", &[]);
    assert_eq!((status, body.as_str()), (200, "<h1>home</h1>"));
    assert_eq!(headers["content-type"], "text/html; charset=utf-8");
    assert_eq!(headers["x-content-type-options"], "nosniff");
    assert_eq!(headers["cache-control"], "public, max-age=60");
    let etag = headers["etag"].to_str().unwrap().to_string();
    let (status, _, body) = get(&base, &host, "/site/", &[("if-none-match", &etag)]);
    assert_eq!((status, body.as_str()), (304, ""));
    // Joined from its parts, and typed by its extension, or as bytes.
    let (status, headers, body) = get(&base, &by_did, "/site/app.js", &[]);
    assert_eq!((status, body), (200, script));
    assert_eq!(headers["content-type"], "text/javascript; charset=utf-8");
    let (_, headers, _) = get(&base, &host, "/site/data.bin", &[]);
    assert_eq!(headers["content-type"], "application/octet-stream");

    // Directories, with and without their `/`.
    let location = |path: &str, host: &str| {
        let (status, headers, _) = get(&base, host, path, &[]);
        assert_eq!(status, 302, "{path}");
        headers["location"].to_str().unwrap().to_string()
    };
    assert_eq!(location("/site", &host), "/site/");
    assert_eq!(location("/site/docs", &host), "docs/");
    assert_eq!(get(&base, &host, "/site/docs/", &[]).2, "docs");
    // The owner's root is their page.
    assert_eq!(location("/", &host), format!("{HOME}/{handle}"));
    assert_eq!(location("/", &by_did), format!("{HOME}/{did}"));

    // What is not there: the site's 404 page, or a plain one.
    let (status, _, body) = get(&base, &host, "/site/nope.js", &[]);
    assert_eq!((status, body.as_str()), (404, "gone"));
    for (host, path) in [
        (host.as_str(), "/bare/"),
        (host.as_str(), "/nothing/"),
        (host.as_str(), "/site/%ff"),
        // Not redirected to `/\evil.example/`, which a browser reads as
        // another host.
        (host.as_str(), "/%5Cevil.example"),
        ("short.knotless.page", "/site/"),
        ("nobody-test.knotless.page", "/site/"),
    ] {
        let (status, _, body) = get(&base, host, path, &[]);
        assert_eq!(
            (status, body.as_str()),
            (404, "no such site\n"),
            "{host}{path}"
        );
    }

    // Only reads, and only on its domain: anything else is the server's.
    let res = reqwest::blocking::Client::new()
        .post(format!("{base}/site/"))
        .header("x-forwarded-host", &host)
        .send()
        .unwrap();
    assert_eq!(res.status().as_u16(), 405);
    assert_eq!(res.headers()["allow"], "GET, HEAD");
    assert_eq!(get(&base, "localhost", "/healthz", &[]).2, "ok");

    // Deployed again as a single-page app: its routes are its page, and a
    // script still missing is not.
    write("index.html", b"<h1>app</h1>");
    let sha = "19fa13fcd6bd147877c93f9b1ac81a68b884d733";
    let runner = "did:plc:runnerofsitestest2345abc";
    let check = format!("at://{runner}/dev.knotless.check/built");
    let again = [
        "repo",
        "site",
        &url,
        &dir,
        "--fallback",
        "index.html",
        "--commit",
        sha,
        "--check",
        &check,
    ];
    assert_eq!(s.cli(&again, b"").0, 0);
    wait_until(|| get(&base, &host, "/site/", &[]).2 == "<h1>app</h1>");
    // Its commit as given, and its check only once the repository names
    // the runner it is from.
    wait_until(|| shown("site")["commit"] == sha);
    assert!(shown("site").get("check").is_none());
    let (key, mut repo) = s
        .records("dev.knotless.repo")
        .into_iter()
        .find(|(_, r)| r["name"] == "site")
        .unwrap();
    repo["runners"] = serde_json::json!([{ "did": runner }]);
    s.put_record("dev.knotless.repo", &key, repo);
    wait_until(|| shown("site")["check"] == check.as_str());
    let (status, _, body) = get(&base, &host, "/site/settings/me", &[]);
    assert_eq!((status, body.as_str()), (200, "<h1>app</h1>"));
    assert_eq!(get(&base, &host, "/site/gone.js", &[]).0, 404);

    // A file that is not what its record says is not served.
    let (rkey, mut record) = s.records("dev.knotless.site").remove(0);
    let files = record["files"].as_array_mut().unwrap();
    let docs = files
        .iter_mut()
        .find(|f| f["path"] == "docs/index.html")
        .unwrap();
    docs["sha256"] = serde_json::json!("0".repeat(64));
    s.put_record("dev.knotless.site", &rkey, record);
    wait_until(|| get(&base, &host, "/site/docs/", &[]).0 == 502);

    // The operator taking down its repository, the site's record or its
    // owner takes the site down with them, and restoring brings it back.
    let index = app.index.as_ref().unwrap();
    let repo = format!("at://{did}/dev.knotless.repo/site");
    let record = format!("at://{did}/dev.knotless.site/{rkey}");
    for subject in [&repo, &record, &did] {
        let why = serde_json::json!({"subject": subject});
        index.take_down(HOME, subject, &why, true).unwrap();
        wait_until(|| get(&base, &host, "/site/", &[]).0 == 404);
        index.take_down(HOME, subject, &why, false).unwrap();
        wait_until(|| get(&base, &host, "/site/", &[]).0 == 200);
    }

    // Taken down by its owner, it is gone, and its pages name none.
    assert_eq!(s.cli(&["repo", "site", &url, "--down"], b"").0, 0);
    wait_until(|| get(&base, &host, "/site/", &[]).0 == 404);
    wait_until(|| shown("site").is_null());
}