fix: more thorough handling for internal image url fixes

pull/375/head
Antti Kettunen 1 month ago
parent 569c827ab4
commit 7285c6f55a
No known key found for this signature in database
GPG Key ID: C6B2A3D250359BD7

@ -18,7 +18,7 @@ import sqlite3
import types
import collections
from pathlib import Path
from urllib.parse import urljoin
from urllib.parse import quote, urljoin, urlparse
from concurrent.futures import ThreadPoolExecutor, as_completed
from flask import Flask, render_template, request, jsonify, redirect, send_file, Response, session, g, abort
@ -11127,7 +11127,7 @@ def maintain_search_history():
return jsonify({"success": False, "error": str(e)}), 500
def fix_artist_image_url(thumb_url):
"""Convert localhost URLs to proper server URLs using config"""
"""Convert media-server image URLs into browser-safe URLs."""
if not thumb_url:
return None
@ -11136,6 +11136,11 @@ def fix_artist_image_url(thumb_url):
needs_fixing = (
thumb_url.startswith('http://localhost:') or
thumb_url.startswith('https://localhost:') or
thumb_url.startswith('http://127.0.0.1:') or
thumb_url.startswith('https://127.0.0.1:') or
thumb_url.startswith('http://host.docker.internal:') or
thumb_url.startswith('https://host.docker.internal:') or
(thumb_url.startswith('http://') and _is_internal_image_host(thumb_url)) or
thumb_url.startswith('/library/') or # Plex relative paths
thumb_url.startswith('/Items/') or # Jellyfin relative paths
thumb_url.startswith('/api/') or # Old Navidrome API paths
@ -11166,7 +11171,7 @@ def fix_artist_image_url(thumb_url):
# Construct proper Plex URL with token
fixed_url = f"{plex_base_url.rstrip('/')}{path}?X-Plex-Token={plex_token}"
logger.info(f"Fixed URL: {fixed_url}")
return fixed_url
return _browser_safe_image_url(fixed_url)
elif active_server == 'jellyfin':
jellyfin_config = config_manager.get_jellyfin_config()
@ -11192,7 +11197,7 @@ def fix_artist_image_url(thumb_url):
else:
fixed_url = f"{jellyfin_base_url.rstrip('/')}{path}"
logger.info(f"Fixed URL: {fixed_url}")
return fixed_url
return _browser_safe_image_url(fixed_url)
elif active_server == 'navidrome':
navidrome_config = config_manager.get_navidrome_config()
@ -11225,16 +11230,57 @@ def fix_artist_image_url(thumb_url):
# Construct proper Navidrome Subsonic URL
fixed_url = f"{navidrome_base_url.rstrip('/')}{path}{separator}{auth_params}"
logger.info(f"Fixed URL: {fixed_url}")
return fixed_url
return _browser_safe_image_url(fixed_url)
logger.warning(f"No configuration found for {active_server} or unsupported server type")
# Return original URL if no fixing needed/possible
return thumb_url
# Return a browser-safe URL even if no server-specific rebuild was possible.
return _browser_safe_image_url(thumb_url)
except Exception as e:
logger.error(f"Error fixing image URL '{thumb_url}': {e}")
return thumb_url
return _browser_safe_image_url(thumb_url)
def _is_internal_image_host(url: str) -> bool:
"""Return True when an image URL points at a host the browser likely cannot reach directly."""
try:
parsed = urlparse(url)
host = (parsed.hostname or '').strip('[]').lower()
if not host:
return False
if host in {'localhost', '127.0.0.1', '::1', 'host.docker.internal'}:
return True
# Single-label hosts are usually Docker service names or local LAN aliases.
if '.' not in host:
return True
try:
ip = ipaddress.ip_address(host)
return ip.is_loopback or ip.is_private or ip.is_link_local or ip.is_reserved
except ValueError:
return False
except Exception:
return False
def _browser_safe_image_url(url: str) -> str:
"""Return a browser-safe image URL, proxying internal hosts through SoulSync."""
if not url:
return url
if url.startswith('/api/image-proxy?url='):
return url
if url.startswith('http://') or url.startswith('https://'):
if _is_internal_image_host(url):
return f"/api/image-proxy?url={quote(url, safe='')}"
return url
# Relative media-server paths should already have been expanded before this point.
return url
@app.route('/api/library/history')
def get_library_history():
@ -45220,8 +45266,7 @@ def image_proxy():
url = request.args.get('url', '')
if not url or not url.startswith('http'):
return '', 400
# Only allow known image CDNs
from urllib.parse import urlparse
host = urlparse(url).hostname or ''
allowed_hosts = [
'i.scdn.co', 'mosaic.scdn.co', # Spotify
@ -45230,8 +45275,9 @@ def image_proxy():
'is1-ssl.mzstatic.com', 'is2-ssl.mzstatic.com', 'is3-ssl.mzstatic.com',
'is4-ssl.mzstatic.com', 'is5-ssl.mzstatic.com', # iTunes/Apple
'img.discogs.com', 'i.discogs.com', # Discogs
'localhost', '127.0.0.1', 'host.docker.internal', # Local/Docker media servers
]
if not any(host == h or host.endswith('.' + h) for h in allowed_hosts):
if not any(host == h or host.endswith('.' + h) for h in allowed_hosts) and not _is_internal_image_host(url):
return '', 403
try:
resp = requests.get(url, timeout=10, stream=True, headers={

Loading…
Cancel
Save