|
|
|
@@ -3,14 +3,11 @@ from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import base64
|
|
|
|
import base64
|
|
|
|
import hashlib
|
|
|
|
import hashlib
|
|
|
|
import hmac
|
|
|
|
|
|
|
|
import html as html_mod
|
|
|
|
import html as html_mod
|
|
|
|
import ipaddress
|
|
|
|
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import re
|
|
|
|
import shlex
|
|
|
|
import shlex
|
|
|
|
import socket
|
|
|
|
|
|
|
|
import subprocess
|
|
|
|
import subprocess
|
|
|
|
import threading
|
|
|
|
import threading
|
|
|
|
import time
|
|
|
|
import time
|
|
|
|
@@ -109,7 +106,7 @@ def _check_basic_auth(req: Request) -> bool:
|
|
|
|
if ":" not in raw:
|
|
|
|
if ":" not in raw:
|
|
|
|
return False
|
|
|
|
return False
|
|
|
|
user, pw = raw.split(":", 1)
|
|
|
|
user, pw = raw.split(":", 1)
|
|
|
|
return hmac.compare_digest(user, BASIC_AUTH_USER) and hmac.compare_digest(pw, BASIC_AUTH_PASS)
|
|
|
|
return user == BASIC_AUTH_USER and pw == BASIC_AUTH_PASS
|
|
|
|
|
|
|
|
|
|
|
|
def _auth_challenge() -> HTMLResponse:
|
|
|
|
def _auth_challenge() -> HTMLResponse:
|
|
|
|
return HTMLResponse(
|
|
|
|
return HTMLResponse(
|
|
|
|
@@ -240,26 +237,8 @@ def is_demo_link(name: str) -> bool:
|
|
|
|
lower = name.lower().replace("-", "_").replace(".", " ")
|
|
|
|
lower = name.lower().replace("-", "_").replace(".", " ")
|
|
|
|
return any(pat in lower for pat in DEMO_PATTERNS)
|
|
|
|
return any(pat in lower for pat in DEMO_PATTERNS)
|
|
|
|
|
|
|
|
|
|
|
|
def _is_ssrf_target(url: str) -> bool:
|
|
|
|
|
|
|
|
"""Return True if the URL resolves to a private/loopback address (SSRF protection)."""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
host = urllib.parse.urlparse(url).hostname or ""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
addr = ipaddress.ip_address(host)
|
|
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
host = socket.gethostbyname(host)
|
|
|
|
|
|
|
|
addr = ipaddress.ip_address(host)
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
return addr.is_private or addr.is_loopback or addr.is_link_local or addr.is_reserved
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def check_url_reachable(url: str) -> Optional[str]:
|
|
|
|
def check_url_reachable(url: str) -> Optional[str]:
|
|
|
|
"""Try a HEAD request to verify the URL is reachable. Returns error string or None."""
|
|
|
|
"""Try a HEAD request to verify the URL is reachable. Returns error string or None."""
|
|
|
|
if _is_ssrf_target(url):
|
|
|
|
|
|
|
|
return "URL zeigt auf eine interne/private Adresse (nicht erlaubt)"
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
req = urllib.request.Request(url, method="HEAD")
|
|
|
|
req = urllib.request.Request(url, method="HEAD")
|
|
|
|
req.add_header("User-Agent", "Mozilla/5.0")
|
|
|
|
req.add_header("User-Agent", "Mozilla/5.0")
|
|
|
|
@@ -456,22 +435,13 @@ def format_proxy_lines(raw: str, scheme: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
return "\n".join(dedup)
|
|
|
|
return "\n".join(dedup)
|
|
|
|
|
|
|
|
|
|
|
|
_PROXY_FETCH_LIMIT = 2 * 1024 * 1024 # 2 MB cap to prevent memory exhaustion
|
|
|
|
|
|
|
|
_proxy_cache: Dict[str, Tuple[float, str]] = {}
|
|
|
|
|
|
|
|
_PROXY_CACHE_TTL = 300.0 # 5 minutes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_proxy_list(url: str) -> str:
|
|
|
|
def fetch_proxy_list(url: str) -> str:
|
|
|
|
now = time.time()
|
|
|
|
|
|
|
|
cached_ts, cached_text = _proxy_cache.get(url, (0.0, ""))
|
|
|
|
|
|
|
|
if cached_text and now - cached_ts < _PROXY_CACHE_TTL:
|
|
|
|
|
|
|
|
return cached_text
|
|
|
|
|
|
|
|
req = urllib.request.Request(url)
|
|
|
|
req = urllib.request.Request(url)
|
|
|
|
log_connection(f"HTTP GET {url} (no-proxy)")
|
|
|
|
log_connection(f"HTTP GET {url} (no-proxy)")
|
|
|
|
with NO_PROXY_OPENER.open(req, timeout=20) as resp:
|
|
|
|
with NO_PROXY_OPENER.open(req, timeout=20) as resp:
|
|
|
|
text = resp.read(_PROXY_FETCH_LIMIT).decode("utf-8", "replace")
|
|
|
|
text = resp.read().decode("utf-8", "replace")
|
|
|
|
if "\n" not in text and re.search(r"\s", text):
|
|
|
|
if "\n" not in text and re.search(r"\s", text):
|
|
|
|
text = re.sub(r"\s+", "\n", text.strip())
|
|
|
|
return re.sub(r"\s+", "\n", text.strip())
|
|
|
|
_proxy_cache[url] = (now, text)
|
|
|
|
|
|
|
|
return text
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
def build_jdproxies_payload(text: str) -> Dict[str, Any]:
|
|
|
|
def build_jdproxies_payload(text: str) -> Dict[str, Any]:
|
|
|
|
@@ -1198,6 +1168,7 @@ def index():
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/submit")
|
|
|
|
@app.post("/submit")
|
|
|
|
def submit(url: str = Form(...), package_name: str = Form(""), library: str = Form("auto")):
|
|
|
|
def submit(url: str = Form(...), package_name: str = Form(""), library: str = Form("auto")):
|
|
|
|
|
|
|
|
try:
|
|
|
|
ensure_env()
|
|
|
|
ensure_env()
|
|
|
|
url = url.strip()
|
|
|
|
url = url.strip()
|
|
|
|
package_name = (package_name or "").strip() or "WebGUI"
|
|
|
|
package_name = (package_name or "").strip() or "WebGUI"
|
|
|
|
@@ -1211,10 +1182,7 @@ def submit(url: str = Form(...), package_name: str = Form(""), library: str = Fo
|
|
|
|
log_connection(f"URL-Check fehlgeschlagen: {url} -> {url_err}")
|
|
|
|
log_connection(f"URL-Check fehlgeschlagen: {url} -> {url_err}")
|
|
|
|
return HTMLResponse(render_page(f"Link nicht erreichbar: {url_err}"), status_code=400)
|
|
|
|
return HTMLResponse(render_page(f"Link nicht erreichbar: {url_err}"), status_code=400)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
dev = get_device()
|
|
|
|
dev = get_device()
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
return HTMLResponse(render_page(f"JDownloader nicht erreichbar: {e}"), status_code=503)
|
|
|
|
|
|
|
|
resp = dev.linkgrabber.add_links([{
|
|
|
|
resp = dev.linkgrabber.add_links([{
|
|
|
|
"links": url,
|
|
|
|
"links": url,
|
|
|
|
"autostart": True,
|
|
|
|
"autostart": True,
|
|
|
|
@@ -1222,9 +1190,18 @@ def submit(url: str = Form(...), package_name: str = Form(""), library: str = Fo
|
|
|
|
"packageName": package_name,
|
|
|
|
"packageName": package_name,
|
|
|
|
}])
|
|
|
|
}])
|
|
|
|
|
|
|
|
|
|
|
|
jobid = str(resp.get("id", ""))
|
|
|
|
jobid = ""
|
|
|
|
|
|
|
|
if isinstance(resp, dict):
|
|
|
|
|
|
|
|
jobid = str(resp.get("id", "")).strip()
|
|
|
|
|
|
|
|
elif isinstance(resp, (str, int)):
|
|
|
|
|
|
|
|
jobid = str(resp).strip()
|
|
|
|
|
|
|
|
elif isinstance(resp, list) and resp and isinstance(resp[0], dict):
|
|
|
|
|
|
|
|
jobid = str(resp[0].get("id", "")).strip()
|
|
|
|
|
|
|
|
|
|
|
|
if not jobid:
|
|
|
|
if not jobid:
|
|
|
|
return HTMLResponse(render_page(f"Unerwartete Antwort von add_links: {resp}"), status_code=500)
|
|
|
|
msg = f"Unerwartete Antwort von add_links (kein Job-ID): {resp!r}"
|
|
|
|
|
|
|
|
log_connection(msg)
|
|
|
|
|
|
|
|
return HTMLResponse(render_page(msg), status_code=502)
|
|
|
|
|
|
|
|
|
|
|
|
with lock:
|
|
|
|
with lock:
|
|
|
|
jobs[jobid] = Job(
|
|
|
|
jobs[jobid] = Job(
|
|
|
|
@@ -1241,6 +1218,9 @@ def submit(url: str = Form(...), package_name: str = Form(""), library: str = Fo
|
|
|
|
t.start()
|
|
|
|
t.start()
|
|
|
|
|
|
|
|
|
|
|
|
return RedirectResponse(url="/", status_code=303)
|
|
|
|
return RedirectResponse(url="/", status_code=303)
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
|
|
log_connection(f"Submit-Fehler: {e}")
|
|
|
|
|
|
|
|
return HTMLResponse(render_page(f"Interner Fehler beim Absenden: {e}"), status_code=500)
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/cancel/{jobid}")
|
|
|
|
@app.post("/cancel/{jobid}")
|
|
|
|
def cancel(jobid: str):
|
|
|
|
def cancel(jobid: str):
|
|
|
|
|