fix
This commit is contained in:
@@ -33,9 +33,7 @@ let
|
|||||||
// lib.optionalAttrs (bg != null) { bg = bg; }
|
// lib.optionalAttrs (bg != null) { bg = bg; }
|
||||||
// lib.optionalAttrs (fg != null) { fg = fg; };
|
// lib.optionalAttrs (fg != null) { fg = fg; };
|
||||||
hostMappings = lib.mapAttrs' (mapping: profile:
|
hostMappings = lib.mapAttrs' (mapping: profile:
|
||||||
lib.nameValuePair
|
lib.nameValuePair mapping (normalizeProfile profile)
|
||||||
(if lib.hasInfix "." mapping then mapping else "${mapping}.${serverCfg.domain}")
|
|
||||||
(normalizeProfile profile)
|
|
||||||
) (containerCfg.extra.mappings or {});
|
) (containerCfg.extra.mappings or {});
|
||||||
traefikAssetPathRegexp =
|
traefikAssetPathRegexp =
|
||||||
"^/(.*/)?"
|
"^/(.*/)?"
|
||||||
@@ -49,6 +47,7 @@ let
|
|||||||
configFile = pkgs.writeText "favicon-config.json" (builtins.toJSON {
|
configFile = pkgs.writeText "favicon-config.json" (builtins.toJSON {
|
||||||
inherit cacheControl;
|
inherit cacheControl;
|
||||||
borderRadius = borderRadius;
|
borderRadius = borderRadius;
|
||||||
|
domain = serverCfg.domain;
|
||||||
mappings = hostMappings;
|
mappings = hostMappings;
|
||||||
default =
|
default =
|
||||||
if containerCfg.extra ? default then normalizeProfile containerCfg.extra.default
|
if containerCfg.extra ? default then normalizeProfile containerCfg.extra.default
|
||||||
@@ -83,18 +82,36 @@ let
|
|||||||
APP_CONFIG = json.load(fh)
|
APP_CONFIG = json.load(fh)
|
||||||
with open(LOGO_PATH, "rb") as fh:
|
with open(LOGO_PATH, "rb") as fh:
|
||||||
LOGO_BYTES = fh.read()
|
LOGO_BYTES = fh.read()
|
||||||
|
MAPPINGS = APP_CONFIG.get("mappings", {})
|
||||||
|
DEFAULT_PROFILE = APP_CONFIG.get("default")
|
||||||
|
APP_DOMAIN = (APP_CONFIG.get("domain", "") or "").strip().lower()
|
||||||
|
CACHE_CONTROL = APP_CONFIG.get("cacheControl", DEFAULT_CACHE_CONTROL)
|
||||||
LOGO_HASH = hashlib.sha256(LOGO_BYTES).hexdigest()
|
LOGO_HASH = hashlib.sha256(LOGO_BYTES).hexdigest()
|
||||||
|
|
||||||
def _normalize_host(host):
|
def _normalize_host(host):
|
||||||
return (host or "").split(":", 1)[0].strip().lower()
|
host = (host or "").split(",", 1)[0].split(":", 1)[0].strip().lower()
|
||||||
|
if APP_DOMAIN and host.endswith(f".{APP_DOMAIN}"):
|
||||||
|
return host[:-(len(APP_DOMAIN) + 1)]
|
||||||
|
return host
|
||||||
|
|
||||||
|
def _request_host(headers):
|
||||||
|
forwarded = headers.get("X-Forwarded-Host", "")
|
||||||
|
original = headers.get("X-Original-Host", "")
|
||||||
|
host = forwarded or original or headers.get("Host", "")
|
||||||
|
return _normalize_host(host)
|
||||||
|
|
||||||
def _pick_profile(host):
|
def _pick_profile(host):
|
||||||
mappings = APP_CONFIG.get("mappings", {})
|
return MAPPINGS.get(host) or DEFAULT_PROFILE
|
||||||
return mappings.get(host) or APP_CONFIG.get("default")
|
|
||||||
|
|
||||||
def _color(value, fallback):
|
def _color(value, fallback):
|
||||||
return value if isinstance(value, str) and value else fallback
|
return value if isinstance(value, str) and value else fallback
|
||||||
|
|
||||||
|
def _resolved_profile(profile):
|
||||||
|
return {
|
||||||
|
"bg": _color(profile.get("bg") or profile.get("background"), "#111827"),
|
||||||
|
"fg": _color(profile.get("fg") or profile.get("foreground"), "#f8fafc"),
|
||||||
|
}
|
||||||
|
|
||||||
def _replace_svg_color(svg, attribute, color):
|
def _replace_svg_color(svg, attribute, color):
|
||||||
if attribute in {"fill", "stroke"}:
|
if attribute in {"fill", "stroke"}:
|
||||||
svg = re.sub(
|
svg = re.sub(
|
||||||
@@ -122,51 +139,44 @@ let
|
|||||||
svg = _replace_svg_color(svg, "stroke", color)
|
svg = _replace_svg_color(svg, "stroke", color)
|
||||||
return "data:image/svg+xml;base64," + base64.b64encode(svg.encode("utf-8")).decode("ascii")
|
return "data:image/svg+xml;base64," + base64.b64encode(svg.encode("utf-8")).decode("ascii")
|
||||||
|
|
||||||
def _border_radius():
|
border_radius = str(APP_CONFIG.get("borderRadius", "8")).strip()
|
||||||
value = APP_CONFIG.get("borderRadius", "8")
|
if not border_radius.endswith("px"):
|
||||||
text = str(value).strip()
|
border_radius = f"{border_radius}px"
|
||||||
if text.endswith("px"):
|
|
||||||
return text
|
|
||||||
return f"{text}px"
|
|
||||||
|
|
||||||
def _render_svg(profile):
|
def _render_svg(colors):
|
||||||
bg = _color(profile.get("bg") or profile.get("background"), "#111827")
|
logo_data_uri = _tinted_logo_data_uri(colors["fg"])
|
||||||
fg = _color(profile.get("fg") or profile.get("foreground"), "#f8fafc")
|
|
||||||
border_radius = _border_radius()
|
|
||||||
logo_data_uri = _tinted_logo_data_uri(fg)
|
|
||||||
|
|
||||||
canvas = 256
|
canvas = 256
|
||||||
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{canvas}" height="{canvas}" viewBox="0 0 {canvas} {canvas}">
|
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{canvas}" height="{canvas}" viewBox="0 0 {canvas} {canvas}">
|
||||||
<rect x="0" y="0" width="{canvas}" height="{canvas}" rx="{border_radius}" ry="{border_radius}" fill="{bg}" />
|
<rect x="0" y="0" width="{canvas}" height="{canvas}" rx="{border_radius}" ry="{border_radius}" fill="{colors["bg"]}" />
|
||||||
<image href="{logo_data_uri}" x="0" y="0" width="{canvas}" height="{canvas}" preserveAspectRatio="xMidYMid meet" />
|
<image href="{logo_data_uri}" x="0" y="0" width="{canvas}" height="{canvas}" preserveAspectRatio="xMidYMid meet" />
|
||||||
</svg>"""
|
</svg>"""
|
||||||
|
|
||||||
def _cache_key(host, profile):
|
def _cache_key(host, colors):
|
||||||
bg = _color(profile.get("bg") or profile.get("background"), "#111827")
|
|
||||||
fg = _color(profile.get("fg") or profile.get("foreground"), "#f8fafc")
|
|
||||||
cache_inputs = {
|
cache_inputs = {
|
||||||
"asset_size": ASSET_SIZE,
|
"asset_size": ASSET_SIZE,
|
||||||
"bg": bg,
|
"bg": colors["bg"],
|
||||||
"border_radius": _border_radius(),
|
"border_radius": border_radius,
|
||||||
"fg": fg,
|
"fg": colors["fg"],
|
||||||
"host": host,
|
"host": host,
|
||||||
"logo_hash": LOGO_HASH,
|
"logo_hash": LOGO_HASH,
|
||||||
}
|
}
|
||||||
payload = json.dumps(cache_inputs, sort_keys=True, separators=(",", ":"))
|
payload = json.dumps(cache_inputs, sort_keys=True, separators=(",", ":"))
|
||||||
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
|
||||||
|
|
||||||
def _cache_name(host, profile):
|
def _cache_name(host, colors):
|
||||||
safe_host = re.sub(r"[^a-z0-9.-]+", "_", host or "default")
|
safe_host = re.sub(r"[^a-z0-9.-]+", "_", host or "default")
|
||||||
return f"{safe_host}-{_cache_key(host, profile)}.ico"
|
return f"{safe_host}-{_cache_key(host, colors)}.ico"
|
||||||
|
|
||||||
def _generate_asset(host, profile):
|
def _generate_asset(host, profile):
|
||||||
cache_name = _cache_name(host, profile)
|
colors = _resolved_profile(profile)
|
||||||
|
cache_name = _cache_name(host, colors)
|
||||||
target = CACHE_DIR / cache_name
|
target = CACHE_DIR / cache_name
|
||||||
if target.exists():
|
if target.exists():
|
||||||
return target
|
return target
|
||||||
|
|
||||||
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
svg = _render_svg(profile).encode("utf-8")
|
svg = _render_svg(colors).encode("utf-8")
|
||||||
png_bytes = cairosvg.svg2png(bytestring=svg, output_width=ASSET_SIZE, output_height=ASSET_SIZE)
|
png_bytes = cairosvg.svg2png(bytestring=svg, output_width=ASSET_SIZE, output_height=ASSET_SIZE)
|
||||||
image = Image.open(BytesIO(png_bytes))
|
image = Image.open(BytesIO(png_bytes))
|
||||||
image.save(target, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)])
|
image.save(target, format="ICO", sizes=[(ASSET_SIZE, ASSET_SIZE)])
|
||||||
@@ -177,7 +187,7 @@ let
|
|||||||
server_version = "favicon-router/1.0"
|
server_version = "favicon-router/1.0"
|
||||||
|
|
||||||
def _serve(self, include_body):
|
def _serve(self, include_body):
|
||||||
host = _normalize_host(self.headers.get("Host", ""))
|
host = _request_host(self.headers)
|
||||||
profile = _pick_profile(host)
|
profile = _pick_profile(host)
|
||||||
if not profile:
|
if not profile:
|
||||||
self.send_error(404, "No favicon mapping for host")
|
self.send_error(404, "No favicon mapping for host")
|
||||||
@@ -188,7 +198,7 @@ let
|
|||||||
if self.headers.get("If-None-Match") == etag:
|
if self.headers.get("If-None-Match") == etag:
|
||||||
self.send_response(304)
|
self.send_response(304)
|
||||||
self.send_header("ETag", etag)
|
self.send_header("ETag", etag)
|
||||||
self.send_header("Cache-Control", APP_CONFIG.get("cacheControl", DEFAULT_CACHE_CONTROL))
|
self.send_header("Cache-Control", CACHE_CONTROL)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -196,7 +206,7 @@ let
|
|||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "image/x-icon")
|
self.send_header("Content-Type", "image/x-icon")
|
||||||
self.send_header("Content-Length", str(len(payload)))
|
self.send_header("Content-Length", str(len(payload)))
|
||||||
self.send_header("Cache-Control", APP_CONFIG.get("cacheControl", DEFAULT_CACHE_CONTROL))
|
self.send_header("Cache-Control", CACHE_CONTROL)
|
||||||
self.send_header("ETag", etag)
|
self.send_header("ETag", etag)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
if include_body:
|
if include_body:
|
||||||
|
|||||||
Reference in New Issue
Block a user