first commit

This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 14:15:10 +02:00
commit 58cfd95600
35 files changed
+5357

No files matched your search

+181
View File
@@ -0,0 +1,181 @@
"""Safe, small bencode parser and ephemeral magnet metadata inspection state."""
import json
import os
import re
import shutil
import time
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
from . import store
ID = re.compile(r'^[a-f0-9]{32}$')
MAX_METADATA = 32 * 1024 * 1024
class MetadataError(ValueError):
pass
def decode_bencode(raw):
if len(raw) > MAX_METADATA:
raise MetadataError('Metadane torrenta są zbyt duże')
position = 0
count = 0
def decode(depth=0):
nonlocal position, count
if depth > 24:
raise MetadataError('Metadane mają zbyt duże zagnieżdżenie')
count += 1
if count > 600000 or position >= len(raw):
raise MetadataError('Uszkodzone metadane torrenta')
start = raw[position]
if start == 105: # i<number>e
end = raw.find(b'e', position + 1)
if end < 0 or end - position > 25:
raise MetadataError('Błędna liczba bencode')
try:
number = int(raw[position + 1:end])
except ValueError as exc:
raise MetadataError('Błędna liczba bencode') from exc
position = end + 1
return number
if start in (108, 100): # l, d
position += 1
output = [] if start == 108 else {}
while position < len(raw) and raw[position] != 101:
if start == 108:
output.append(decode(depth + 1))
else:
key = decode(depth + 1)
if not isinstance(key, bytes):
raise MetadataError('Niepoprawny klucz bencode')
output[key] = decode(depth + 1)
if position >= len(raw):
raise MetadataError('Nieukończone metadane')
position += 1
return output
if 48 <= start <= 57: # <len>:<bytes>
colon = raw.find(b':', position, min(len(raw), position + 22))
if colon < 0:
raise MetadataError('Błędny ciąg bencode')
try:
length = int(raw[position:colon])
except ValueError as exc:
raise MetadataError('Błędna długość bencode') from exc
position = colon + 1
if length < 0 or length > len(raw) - position:
raise MetadataError('Niepoprawna długość pola')
result = raw[position:position + length]
position += length
return result
raise MetadataError('Uszkodzone metadane torrenta')
result = decode()
if position != len(raw) or not isinstance(result, dict):
raise MetadataError('Niepoprawny format torrenta')
return result
def _segment(raw):
if not isinstance(raw, bytes) or not raw or len(raw) > 255 or raw in (b'.', b'..'):
raise MetadataError('Niedozwolona nazwa w torrencie')
if any(char in raw for char in (b'/', b'\\', b'\0')):
raise MetadataError('Niebezpieczna ścieżka w torrencie')
return raw.decode('utf-8', errors='replace')
def read_torrent(path):
try:
data = decode_bencode(Path(path).read_bytes())
info = data[b'info']
name = _segment(info.get(b'name.utf-8') or info[b'name'])
result = []
if b'files' in info:
files = info[b'files']
if not isinstance(files, list) or len(files) > 50000:
raise MetadataError('Nieprawidłowa lista plików torrenta')
for index, file in enumerate(files, start=1):
parts = file.get(b'path.utf-8') or file[b'path']
if not isinstance(parts, list) or not parts:
raise MetadataError('Błędna ścieżka pliku')
length = file[b'length']
if not isinstance(length, int) or length < 0:
raise MetadataError('Nieprawidłowy rozmiar pliku')
result.append({'index': index, 'name': '/'.join([name] + [_segment(x) for x in parts]),
'size': length, 'padding': b'p' in file.get(b'attr', b'')})
else:
length = info[b'length']
if not isinstance(length, int) or length < 0:
raise MetadataError('Nieprawidłowy rozmiar pliku')
result.append({'index': 1, 'name': name, 'size': length, 'padding': False})
if not result:
raise MetadataError('Torrent nie zawiera plików')
return {'name': name, 'files': result}
except (KeyError, TypeError, OSError, ValueError) as exc:
if isinstance(exc, MetadataError):
raise
raise MetadataError('Nie można odczytać plików z torrenta') from exc
def suggested_indices(magnet, max_index):
"""magnet so indexes are zero-based; aria2 select-file indexes are one-based."""
so = parse_qs(urlsplit(magnet).query).get('so', [''])[0]
suggested = set()
for chunk in so.split(',')[:1000]:
try:
if '-' in chunk:
a, b = (int(x) for x in chunk.split('-', 1))
if b < a or b - a > 1000:
continue
values = range(a, b + 1)
else:
values = [int(chunk)]
suggested.update(n + 1 for n in values if 0 <= n < max_index)
except ValueError:
continue
return sorted(suggested)
def inspect_dir(preview_id):
if not ID.fullmatch(preview_id):
raise MetadataError('Nieprawidłowy identyfikator metadanych')
return store.DATA / 'metadata' / preview_id
def write_request(preview_id, data):
path = inspect_dir(preview_id)
path.mkdir(mode=0o700, parents=True, exist_ok=True)
filename = path / 'request.json'
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY
with os.fdopen(os.open(filename, flags, 0o600), 'w', encoding='utf-8') as f:
json.dump({'created': time.time(), **data}, f)
def load_request(preview_id):
try:
info = json.loads((inspect_dir(preview_id) / 'request.json').read_text(encoding='utf-8'))
if not isinstance(info['gid'], str) or not isinstance(info['magnet'], str):
raise MetadataError('Błędne dane podglądu')
return info
except FileNotFoundError as exc:
raise MetadataError('Nie znaleziono podglądu magneta') from exc
except (ValueError, KeyError, TypeError) as exc:
raise MetadataError('Uszkodzone dane podglądu') from exc
def torrent_file(preview_id):
matches = list(inspect_dir(preview_id).glob('*.torrent'))
return matches[0] if len(matches) == 1 and matches[0].stat().st_size <= MAX_METADATA else None
def cleanup_old(max_age=86400):
root = store.DATA / 'metadata'
if not root.exists():
return
now = time.time()
for child in root.iterdir():
if (ID.fullmatch(child.name) and child.is_dir() and not child.is_symlink()
and now - child.stat().st_mtime > max_age):
shutil.rmtree(child)