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 import APIRouter, Response
|
||||||
from fastapi.responses import FileResponse, RedirectResponse
|
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
|
from ..web.styles import ADMIN_CSS, BASE_CSS
|
||||||
|
|
||||||
ASSET_DIR = Path(__file__).parents[1] / "assets"
|
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)
|
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")
|
@router.get("/app.css")
|
||||||
def app_css() -> Response:
|
def app_css() -> Response:
|
||||||
return Response(MINIFIED_BASE_CSS, media_type="text/css", headers=ASSET_CACHE_HEADERS)
|
return Response(MINIFIED_BASE_CSS, media_type="text/css", headers=ASSET_CACHE_HEADERS)
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class DdnsSettings(BaseModel):
|
|||||||
"/social-preview.webp",
|
"/social-preview.webp",
|
||||||
"/theme.js",
|
"/theme.js",
|
||||||
"/uninstall.sh",
|
"/uninstall.sh",
|
||||||
|
"/visitor-ip.js",
|
||||||
}
|
}
|
||||||
if any(path == reserved or path.startswith(f"{reserved}/") for reserved in reserved_paths):
|
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")
|
raise ValueError("admin_path must not conflict with a public route")
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ def _page(
|
|||||||
{preload_logo_html}
|
{preload_logo_html}
|
||||||
<script src="/theme.js?v=2"></script>
|
<script src="/theme.js?v=2"></script>
|
||||||
<script src="/clipboard.js?v=3" defer></script>
|
<script src="/clipboard.js?v=3" defer></script>
|
||||||
|
<script src="/visitor-ip.js?v=1" defer></script>
|
||||||
{extra_scripts_html}
|
{extra_scripts_html}
|
||||||
<link href="/bootstrap-subset.css?v=3" rel="stylesheet">
|
<link href="/bootstrap-subset.css?v=3" rel="stylesheet">
|
||||||
<link href="/app.css?v=13" rel="stylesheet">
|
<link href="/app.css?v=13" rel="stylesheet">
|
||||||
|
|||||||
@@ -190,3 +190,55 @@ document.querySelectorAll("[data-admin-table]").forEach((tableShell) => {
|
|||||||
render();
|
render();
|
||||||
});
|
});
|
||||||
""".strip()
|
""".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'; "
|
"style-src 'self'; "
|
||||||
"img-src 'self' data:; "
|
"img-src 'self' data:; "
|
||||||
"font-src 'self'; "
|
"font-src 'self'; "
|
||||||
"connect-src 'self'; "
|
"connect-src 'self' https://api4.ipify.org https://api6.ipify.org; "
|
||||||
"form-action 'self'; "
|
"form-action 'self'; "
|
||||||
"frame-ancestors 'none'"
|
"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 response.status_code == 200
|
||||||
assert '<script src="/theme.js?v=2"></script>' in response.text
|
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="/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 '<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="/bootstrap-subset.css?v=3" rel="stylesheet">' in response.text
|
||||||
assert '<link href="/app.css?v=13" 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 "<style>" not in response.text
|
||||||
assert "script-src 'self' 'unsafe-inline'" in response.headers["content-security-policy"]
|
assert "script-src 'self' 'unsafe-inline'" in response.headers["content-security-policy"]
|
||||||
assert "style-src 'self'" 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"]
|
assert "style-src 'self' 'unsafe-inline'" not in response.headers["content-security-policy"]
|
||||||
bootstrap_css = client.get("/bootstrap-subset.css")
|
bootstrap_css = client.get("/bootstrap-subset.css")
|
||||||
assert bootstrap_css.status_code == 200
|
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 bootstrap_css.headers["cache-control"] == "public, max-age=31536000, immutable"
|
||||||
assert client.get("/theme.js").status_code == 200
|
assert client.get("/theme.js").status_code == 200
|
||||||
assert client.get("/clipboard.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
|
assert client.get("/admin-tables.js").status_code == 200
|
||||||
app_css = client.get("/app.css")
|
app_css = client.get("/app.css")
|
||||||
assert app_css.status_code == 200
|
assert app_css.status_code == 200
|
||||||
|
|||||||
Reference in new issue
Block a user