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