Every build writes the exact source it was built from, uncommitted work and new files included, as source.tar.gz next to the app, named after that tree. Docker builds, which have no git, pack the build context and name it by a hash of its files. The sign-in page and Settings > About link to it instead of a repository that can drift. What users, operators and packagers see no longer names the upstream server: - interface text, in all nine catalogues, with a token-session line for Security; - server messages; - the settings, now MAIL_SERVER_URL, MAIL_SERVERS_FILE, ADMIN_URL and MAIL_SERVER_FOLLOW_ADVERTISED_URLS, and mail-servers.example.json; - the Tenants notice, which is gone; - the README, CONTRIBUTING and SECURITY. ihasmail's own FEATURES, KNOWN-ISSUES and ROADMAP stay with public ihasmail, and INBUXA.md is folded into the README.
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
process.env.MAIL_SERVER_URL = "http://127.0.0.1:1";
|
|
process.env.APP_SECRET = "test-secret-for-ics-proxy";
|
|
|
|
const { safeFetch, safeFetchStatus } = await import("./imageproxy.js");
|
|
|
|
/**
|
|
* Subscribing to a calendar makes the server fetch a URL a stranger published,
|
|
* which is the second time this app knocks on a door somebody else chose. It
|
|
* goes through the same guard as the first — these tests are about that guard
|
|
* being reached, and about `webcal:` not being a way around it.
|
|
*/
|
|
|
|
test("a calendar URL is refused before any connection when it points somewhere private", async () => {
|
|
for (const url of [
|
|
"http://127.0.0.1/calendar.ics",
|
|
"http://169.254.169.254/latest/meta-data/", // cloud metadata
|
|
"http://[::1]/calendar.ics",
|
|
"http://10.0.0.1/c.ics",
|
|
"https://192.168.1.1/c.ics",
|
|
]) {
|
|
const got = await safeFetch(url, 500);
|
|
assert.equal(got, "forbidden_target", url);
|
|
}
|
|
});
|
|
|
|
test("webcal: is treated as https rather than waved through", async () => {
|
|
// Every subscription URL people are given is a webcal: one. It has to be
|
|
// understood, and it must not be a way past the address check.
|
|
const got = await safeFetch("webcal://127.0.0.1/calendar.ics", 500);
|
|
assert.equal(got, "forbidden_target");
|
|
});
|
|
|
|
test("schemes that are not http, https or webcal are refused", async () => {
|
|
for (const url of ["file:///etc/passwd", "ftp://example.com/c.ics", "gopher://example.com", "data:text/calendar,BEGIN:VCALENDAR"]) {
|
|
const got = await safeFetch(url, 500);
|
|
assert.equal(got, "bad_scheme", url);
|
|
}
|
|
});
|
|
|
|
test("a URL carrying credentials is refused", async () => {
|
|
// Credentials in a subscription URL would be sent by the server on the
|
|
// reader's behalf to a host the reader may not have looked at.
|
|
assert.equal(await safeFetch("http://user:[email protected]/c.ics", 500), "bad_url");
|
|
});
|
|
|
|
test("nonsense is refused rather than guessed at", async () => {
|
|
for (const url of ["", "not a url", "://missing-scheme"]) {
|
|
assert.equal(await safeFetch(url, 500), "bad_url", JSON.stringify(url));
|
|
}
|
|
});
|
|
|
|
test("each refusal has a status that says which kind it was", () => {
|
|
assert.equal(safeFetchStatus("forbidden_target"), 403);
|
|
assert.equal(safeFetchStatus("bad_scheme"), 400);
|
|
assert.equal(safeFetchStatus("bad_url"), 400);
|
|
assert.equal(safeFetchStatus("bad_redirect"), 400);
|
|
assert.equal(safeFetchStatus("dns_failure"), 502);
|
|
assert.equal(safeFetchStatus("fetch_failed"), 502);
|
|
});
|