110 lines
4.5 KiB
Python
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))
|