fix(logo): don't let a non-SVG / proxy error response break the logo route
get_logo_svg() cached whatever LOGO_URL returned without validating it, and get_logo() then ran str.format() over it. Any literal '{' in the payload - an HTML error page from a proxy, captive portal or CDN, e.g. a Cloudflare 523 page whose CSS uses braces - raised ValueError: unexpected '{' in field name and broke the route for every request until restart.
Validate that the fetched markup is SVG, fall back to the bundled web/common/media/rgthree.svg, substitute {bg}/{fg} with str.replace instead of str.format, and never serve non-SVG content from the route.
This commit is contained in:
+22
-2
@@ -45,6 +45,19 @@ if not LOGO_URL.endswith('.svg'):
|
||||
raise ValueError('Bad logo url.')
|
||||
|
||||
LOGO_SVG = None
|
||||
|
||||
|
||||
def _local_logo_svg():
|
||||
"""The logo bundled with this repository, used when the remote one is unavailable."""
|
||||
import os
|
||||
try:
|
||||
path = os.path.join(_THIS_DIR, '..', 'web', 'common', 'media', 'rgthree.svg')
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"></svg>'
|
||||
|
||||
|
||||
async def get_logo_svg():
|
||||
import aiohttp
|
||||
global LOGO_SVG
|
||||
@@ -62,9 +75,16 @@ async def get_logo_svg():
|
||||
'Expires': '0'
|
||||
}
|
||||
async with session.get(LOGO_URL, headers=headers) as resp:
|
||||
LOGO_SVG = await resp.text()
|
||||
fetched = await resp.text()
|
||||
# The response is not guaranteed to be our SVG: a proxy, a captive portal or a
|
||||
# CDN error page answers with HTML (Cloudflare pages contain `{` in their CSS,
|
||||
# which later blows up `str.format` in the route). Only accept real SVG markup
|
||||
# and otherwise fall back to the bundled logo below.
|
||||
if '<svg' not in fetched.lower():
|
||||
raise ValueError('remote logo is not SVG')
|
||||
LOGO_SVG = fetched
|
||||
LOGO_SVG = re.sub(r'(id="bg".*fill=)"[^\"]+"', r'\1"{bg}"', LOGO_SVG)
|
||||
LOGO_SVG = re.sub(r'(id="fg".*fill=)"[^\"]+"', r'\1"{fg}"', LOGO_SVG)
|
||||
except Exception:
|
||||
LOGO_SVG = '<svg></svg>'
|
||||
LOGO_SVG = _local_logo_svg()
|
||||
return LOGO_SVG
|
||||
|
||||
@@ -45,7 +45,12 @@ async def get_logo(request, as_markup=False):
|
||||
h = get_param(request, 'h')
|
||||
css_class = get_param(request, 'cssClass')
|
||||
svg = await get_logo_svg()
|
||||
resp = svg.format(bg=bg, fg=fg)
|
||||
# `str.format` raises on any stray `{` in the markup, which turns a cached
|
||||
# non-SVG response into a 500 for the whole route. Substitute the two
|
||||
# placeholders directly, and never serve non-SVG content.
|
||||
if '<svg' not in svg.lower():
|
||||
svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"></svg>'
|
||||
resp = svg.replace('{bg}', bg).replace('{fg}', fg)
|
||||
if w is not None:
|
||||
resp = re.sub(r'(<svg[^\>]*?)width="[^\"]+"', r'\1', resp)
|
||||
if str(w).isnumeric():
|
||||
|
||||
Reference in New Issue
Block a user