"""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/') 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))