Archived
Update visible examples with visitor IP
This commit is contained in:
1 parent
8de260dfc7
commit
03ef2d9af6
6 files changed
+67
-2
No files matched your search
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
from fastapi import APIRouter, Response
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
|
||||
from ..web.scripts import ADMIN_TABLES_JS, CLIPBOARD_JS, THEME_JS
|
||||
from ..web.scripts import ADMIN_TABLES_JS, CLIPBOARD_JS, THEME_JS, VISITOR_IP_JS
|
||||
from ..web.styles import ADMIN_CSS, BASE_CSS
|
||||
|
||||
ASSET_DIR = Path(__file__).parents[1] / "assets"
|
||||
@@ -107,6 +107,11 @@ def admin_tables_js() -> Response:
|
||||
return Response(ADMIN_TABLES_JS, media_type="application/javascript", headers=ASSET_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/visitor-ip.js")
|
||||
def visitor_ip_js() -> Response:
|
||||
return Response(VISITOR_IP_JS, media_type="application/javascript", headers=ASSET_CACHE_HEADERS)
|
||||
|
||||
|
||||
@router.get("/app.css")
|
||||
def app_css() -> Response:
|
||||
return Response(MINIFIED_BASE_CSS, media_type="text/css", headers=ASSET_CACHE_HEADERS)
|
||||
|
||||
@@ -85,6 +85,7 @@ class DdnsSettings(BaseModel):
|
||||
"/social-preview.webp",
|
||||
"/theme.js",
|
||||
"/uninstall.sh",
|
||||
"/visitor-ip.js",
|
||||
}
|
||||
if any(path == reserved or path.startswith(f"{reserved}/") for reserved in reserved_paths):
|
||||
raise ValueError("admin_path must not conflict with a public route")
|
||||
|
||||
@@ -137,6 +137,7 @@ def _page(
|
||||
{preload_logo_html}
|
||||
<script src="/theme.js?v=2"></script>
|
||||
<script src="/clipboard.js?v=3" defer></script>
|
||||
<script src="/visitor-ip.js?v=1" defer></script>
|
||||
{extra_scripts_html}
|
||||
<link href="/bootstrap-subset.css?v=3" rel="stylesheet">
|
||||
<link href="/app.css?v=13" rel="stylesheet">
|
||||
|
||||
@@ -190,3 +190,55 @@ document.querySelectorAll("[data-admin-table]").forEach((tableShell) => {
|
||||
render();
|
||||
});
|
||||
""".strip()
|
||||
|
||||
|
||||
VISITOR_IP_JS = """
|
||||
(() => {
|
||||
const ipv4Placeholder = "203.0.113.10";
|
||||
const ipv6Placeholder = "2001:db8::10";
|
||||
const needsIpv4 = () => document.body.textContent.includes(ipv4Placeholder) || document.querySelector(`[data-copy*="${ipv4Placeholder}"]`);
|
||||
const needsIpv6 = () => document.body.textContent.includes(ipv6Placeholder) || document.querySelector(`[data-copy*="${ipv6Placeholder}"]`);
|
||||
|
||||
const fetchIp = async (url) => {
|
||||
try {
|
||||
const response = await fetch(url, { cache: "no-store", mode: "cors" });
|
||||
if (!response.ok) return "";
|
||||
const payload = await response.json();
|
||||
return typeof payload.ip === "string" ? payload.ip : "";
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const replaceInTextNodes = (search, replacement) => {
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
const nodes = [];
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.nodeValue.includes(search)) nodes.push(walker.currentNode);
|
||||
}
|
||||
nodes.forEach((node) => {
|
||||
node.nodeValue = node.nodeValue.split(search).join(replacement);
|
||||
});
|
||||
};
|
||||
|
||||
const replaceInCopyValues = (search, replacement) => {
|
||||
document.querySelectorAll("[data-copy]").forEach((element) => {
|
||||
const value = element.dataset.copy || "";
|
||||
if (value.includes(search)) element.dataset.copy = value.split(search).join(replacement);
|
||||
});
|
||||
};
|
||||
|
||||
const replaceEverywhere = (search, replacement) => {
|
||||
if (!replacement || replacement === search) return;
|
||||
replaceInTextNodes(search, replacement);
|
||||
replaceInCopyValues(search, replacement);
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const tasks = [];
|
||||
if (needsIpv4()) tasks.push(fetchIp("https://api4.ipify.org?format=json").then((ip) => replaceEverywhere(ipv4Placeholder, ip)));
|
||||
if (needsIpv6()) tasks.push(fetchIp("https://api6.ipify.org?format=json").then((ip) => replaceEverywhere(ipv6Placeholder, ip)));
|
||||
await Promise.allSettled(tasks);
|
||||
});
|
||||
})();
|
||||
""".strip()
|
||||
@@ -132,7 +132,7 @@ def content_security_policy(path: str) -> str:
|
||||
"style-src 'self'; "
|
||||
"img-src 'self' data:; "
|
||||
"font-src 'self'; "
|
||||
"connect-src 'self'; "
|
||||
"connect-src 'self' https://api4.ipify.org https://api6.ipify.org; "
|
||||
"form-action 'self'; "
|
||||
"frame-ancestors 'none'"
|
||||
)
|
||||
|
||||
@@ -552,6 +552,7 @@ def test_application_pages_use_stricter_script_csp(tmp_path: Path) -> None:
|
||||
assert response.status_code == 200
|
||||
assert '<script src="/theme.js?v=2"></script>' in response.text
|
||||
assert '<script src="/clipboard.js?v=3" defer></script>' in response.text
|
||||
assert '<script src="/visitor-ip.js?v=1" defer></script>' in response.text
|
||||
assert '<script src="/admin-tables.js?v=2" defer></script>' not in response.text
|
||||
assert '<link href="/bootstrap-subset.css?v=3" rel="stylesheet">' in response.text
|
||||
assert '<link href="/app.css?v=13" rel="stylesheet">' in response.text
|
||||
@@ -560,6 +561,7 @@ def test_application_pages_use_stricter_script_csp(tmp_path: Path) -> None:
|
||||
assert "<style>" not in response.text
|
||||
assert "script-src 'self' 'unsafe-inline'" in response.headers["content-security-policy"]
|
||||
assert "style-src 'self'" in response.headers["content-security-policy"]
|
||||
assert "connect-src 'self' https://api4.ipify.org https://api6.ipify.org" in response.headers["content-security-policy"]
|
||||
assert "style-src 'self' 'unsafe-inline'" not in response.headers["content-security-policy"]
|
||||
bootstrap_css = client.get("/bootstrap-subset.css")
|
||||
assert bootstrap_css.status_code == 200
|
||||
@@ -567,6 +569,10 @@ def test_application_pages_use_stricter_script_csp(tmp_path: Path) -> None:
|
||||
assert bootstrap_css.headers["cache-control"] == "public, max-age=31536000, immutable"
|
||||
assert client.get("/theme.js").status_code == 200
|
||||
assert client.get("/clipboard.js").status_code == 200
|
||||
visitor_ip_js = client.get("/visitor-ip.js")
|
||||
assert visitor_ip_js.status_code == 200
|
||||
assert "api4.ipify.org" in visitor_ip_js.text
|
||||
assert "api6.ipify.org" in visitor_ip_js.text
|
||||
assert client.get("/admin-tables.js").status_code == 200
|
||||
app_css = client.get("/app.css")
|
||||
assert app_css.status_code == 200
|
||||
|
||||
Reference in new issue
Block a user