first commit
This commit is contained in:
commit
58cfd95600
35 files changed
+5357
No files matched your search
+109
@@ -0,0 +1,109 @@
|
||||
"""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))
|
||||
Reference in new issue
Block a user