johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! 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());
}