Files
2026-10-15 14:15:10 +02:00

110 lines
4.5 KiB
Python

"""Validation, safe paths and Internet Archive integration."""
import html
import re
import socket
import ipaddress
from urllib.parse import urlparse, quote, unquote, parse_qs
from pathlib import Path, PurePosixPath
FORMATS = {'xex', 'god', 'raw'}
ARCHIVE_EXTS = ('.zip', '.7z', '.rar', '.tar', '.tar.gz', '.tgz', '.tar.xz', '.tar.bz2')
SAFE_ID = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]{0,150}$')
VIMM_PAGE = re.compile(r'^/vault/[0-9]+/?$')
def safe_title(name):
value = re.sub(r'[^\w .()\[\]-]+', '_', name, flags=re.UNICODE).strip(' ._')[:78]
return value or 'Gra'
def normalize_magnet(value):
# Pasted magnet URLs often contain HTML entities, hard wraps or Markdown.
url = html.unescape(value).strip()
url = re.sub(r"(?i)^['\"`]*\*{0,2}magnet\*{0,2}:\?", 'magnet:?', url)
if not url.lower().startswith('magnet:?'):
return url
url = re.sub(r"\\\s*", '', url)
url = re.sub(r"\s+", '', url)
return html.unescape(url.strip("'\"`"))
def validate_url(value):
url = normalize_magnet(value)
if url.lower().startswith('magnet:?'):
if len(url) > 12000:
raise ValueError('Link magnet jest zbyt dlugi')
params = parse_qs(urlparse(url).query, keep_blank_values=True)
xt = params.get('xt', [])
if not any(re.fullmatch(r'urn:btih:(?:[0-9a-fA-F]{40}|[A-Za-z2-7]{32})', item, re.I)
or re.fullmatch(r'urn:btmh:1220[0-9a-fA-F]{64}', item, re.I)
for item in xt):
raise ValueError('Nieprawidlowy hash xt w magnet (BTIH: 40 znakow HEX lub 32 Base32)')
return 'magnet', url
parsed = urlparse(url)
if parsed.scheme not in ('https', 'http') or not parsed.hostname or parsed.username or parsed.password:
raise ValueError('Podaj adres HTTP/HTTPS albo link magnet')
try:
port = parsed.port
except ValueError as exc:
raise ValueError('Niepoprawny numer portu w URL') from exc
if port not in (None, 80, 443):
raise ValueError('Niestandardowy port w URL jest niedozwolony')
host = parsed.hostname.lower().strip('.')
if host in ('localhost',) or host.endswith('.localhost') or host.endswith('.local'):
raise ValueError('Adres lokalny nie jest dozwolony')
try:
ip = ipaddress.ip_address(host)
except ValueError:
ip = None
if ip is not None and not ip.is_global:
raise ValueError('Adres prywatny lub zarezerwowany jest niedozwolony')
if host in ('vimm.net', 'www.vimm.net') and VIMM_PAGE.fullmatch(parsed.path):
return 'vimm', url
return 'url', url
def parse_archive_id(value):
value = value.strip()
u = urlparse(value)
if u.scheme in ('http', 'https') and u.hostname in ('archive.org', 'www.archive.org'):
pieces = [unquote(x) for x in u.path.split('/') if x]
if len(pieces) >= 2 and pieces[0] in ('details', 'download', 'metadata'):
value = pieces[1]
else:
raise ValueError('Link musi wskazywac element archive.org/details/<identyfikator>')
if not SAFE_ID.fullmatch(value):
raise ValueError('Bledny identyfikator elementu Archive.org')
return value
def archive_download_url(identifier, filename):
identifier = parse_archive_id(identifier)
if (not filename or filename.startswith('/') or '\\' in filename or '\x00' in filename
or any(p in ('..', '.') for p in PurePosixPath(filename).parts)):
raise ValueError('Niedozwolona sciezka pliku Archive.org')
return f'https://archive.org/download/{quote(identifier)}/{quote(filename, safe="/")}'
def safe_import_file(filename, root=Path('/data/imports')):
# User selects from server-side imports (including subdirectories).
if not filename or '\\' in filename or filename.startswith('/') or '\x00' in filename:
raise ValueError('Niepoprawna sciezka pliku')
item = root.joinpath(*PurePosixPath(filename).parts)
if not item.resolve().is_relative_to(root.resolve()):
raise ValueError('Plik spoza katalogu imports')
if not item.is_file() or item.is_symlink():
raise ValueError('Wskazany plik nie istnieje')
return item
def is_archive(filename):
name = filename.lower()
return name.endswith(ARCHIVE_EXTS) or bool(re.search(r'\.(?:7z|zip)\.001$', name)) or bool(re.search(r'\.part0*1\.rar$', name))
def is_additional_volume(filename):
name = filename.lower()
return bool(re.search(r'\.(?:7z|zip)\.0*0?[2-9][0-9]*$', name) or
re.search(r'\.part0*[2-9][0-9]*\.rar$', name) or
re.search(r'\.r\d\d$', name))