Harden scraper inputs and exports
This commit is contained in:
+110
-38
@@ -99,6 +99,27 @@ def normalize_media_url(media_path: Optional[str]) -> Optional[str]:
|
||||
return "/media/" + urllib.parse.quote(str(relative).replace("\\", "/"))
|
||||
|
||||
|
||||
def normalize_channel_id(value: Any) -> str:
|
||||
channel_id = str(value or "").strip().lstrip("@")
|
||||
if (
|
||||
not channel_id
|
||||
or "/" in channel_id
|
||||
or "\\" in channel_id
|
||||
or channel_id in {".", ".."}
|
||||
or any(ord(ch) < 32 for ch in channel_id)
|
||||
):
|
||||
raise ValueError("channel_id contains unsupported path characters")
|
||||
return channel_id
|
||||
|
||||
|
||||
def export_account_state(state: Dict[str, Any], include_secrets: bool = False) -> Dict[str, Any]:
|
||||
exported = dict(state)
|
||||
if not include_secrets and "api_hash" in exported:
|
||||
exported["api_hash_present"] = bool(exported.get("api_hash"))
|
||||
exported["api_hash"] = None
|
||||
return exported
|
||||
|
||||
|
||||
def channel_db_path(account_id: Optional[str], channel_id: str) -> Path:
|
||||
if account_id:
|
||||
return account_data_dir(DATA_DIR, account_id) / channel_id / f"{channel_id}.db"
|
||||
@@ -1832,7 +1853,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_json(list_channels_snapshot())
|
||||
if path.startswith("/api/channels/") and path.endswith("/messages"):
|
||||
parts = path.split("/")
|
||||
channel_id = urllib.parse.unquote(parts[3]).lstrip("@")
|
||||
try:
|
||||
channel_id = normalize_channel_id(urllib.parse.unquote(parts[3]))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
limit = max(1, min(int(query.get("limit", ["120"])[0]), 300))
|
||||
before = query.get("before")
|
||||
before_message_id = int(before[0]) if before else None
|
||||
@@ -1912,16 +1936,20 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
if sub == ["health"]:
|
||||
return self.send_json(account_health_summary(account_id, self.app.job_runner))
|
||||
if sub == ["export"]:
|
||||
include_secrets = query.get("include_secrets", ["0"])[0].lower() in {"1", "true", "yes"}
|
||||
payload = {
|
||||
"version": 1,
|
||||
"account_id": account_id,
|
||||
"state": load_account(DATA_DIR, account_id),
|
||||
"state": export_account_state(load_account(DATA_DIR, account_id), include_secrets=include_secrets),
|
||||
}
|
||||
return self.send_json(payload)
|
||||
if sub == ["channels"]:
|
||||
return self._handle_get_account_channels(account_id)
|
||||
if len(sub) >= 3 and sub[0] == "channels" and sub[-1] == "messages":
|
||||
channel_id = urllib.parse.unquote(sub[1]).lstrip("@")
|
||||
try:
|
||||
channel_id = normalize_channel_id(urllib.parse.unquote(sub[1]))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
return self._handle_get_account_channel_messages(account_id, channel_id, query)
|
||||
if sub == ["continuous"]:
|
||||
return self.send_json(
|
||||
@@ -2117,9 +2145,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "Legacy auth is not available in multi-account mode. Use /api/accounts/{id}/auth/password instead.")
|
||||
|
||||
if path == "/api/channels/add":
|
||||
channel_id = str(body.get("channel_id", "")).strip()
|
||||
if not channel_id:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "channel_id is required")
|
||||
try:
|
||||
channel_id = normalize_channel_id(body.get("channel_id"))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
if self.app.legacy_account_id:
|
||||
return self._handle_account_channel_add(self.app.legacy_account_id, channel_id, body.get("name"))
|
||||
state = load_state()
|
||||
@@ -2131,7 +2160,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_json({"ok": True, "channel_id": channel_id})
|
||||
|
||||
if path == "/api/channels/remove":
|
||||
channel_id = str(body.get("channel_id", "")).strip()
|
||||
try:
|
||||
channel_id = normalize_channel_id(body.get("channel_id"))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
if self.app.legacy_account_id:
|
||||
return self._handle_account_channel_remove(self.app.legacy_account_id, channel_id)
|
||||
state = load_state()
|
||||
@@ -2147,10 +2179,13 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
if account_id:
|
||||
payload["account_id"] = account_id
|
||||
if channel_id:
|
||||
payload["channel_id"] = str(channel_id)
|
||||
try:
|
||||
payload["channel_id"] = normalize_channel_id(channel_id)
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
job = self.app.job_runner.create_job(
|
||||
"scrape_channel",
|
||||
f"Scrape channel {channel_id}" + (f" [{account_id}]" if account_id else ""),
|
||||
f"Scrape channel {payload['channel_id']}" + (f" [{account_id}]" if account_id else ""),
|
||||
payload,
|
||||
)
|
||||
else:
|
||||
@@ -2173,9 +2208,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_json(job.to_dict(), status=HTTPStatus.ACCEPTED)
|
||||
|
||||
if path == "/api/jobs/export-channel":
|
||||
channel_id = str(body.get("channel_id", "")).strip()
|
||||
if not channel_id:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "channel_id is required")
|
||||
try:
|
||||
channel_id = normalize_channel_id(body.get("channel_id"))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
payload = {"channel_id": channel_id}
|
||||
if self.app.legacy_account_id:
|
||||
payload["account_id"] = self.app.legacy_account_id
|
||||
@@ -2187,9 +2223,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_json(job.to_dict(), status=HTTPStatus.ACCEPTED)
|
||||
|
||||
if path == "/api/jobs/rescrape-media":
|
||||
channel_id = str(body.get("channel_id", "")).strip()
|
||||
if not channel_id:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "channel_id is required")
|
||||
try:
|
||||
channel_id = normalize_channel_id(body.get("channel_id"))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
payload = {"channel_id": channel_id}
|
||||
if self.app.legacy_account_id:
|
||||
payload["account_id"] = self.app.legacy_account_id
|
||||
@@ -2201,9 +2238,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_json(job.to_dict(), status=HTTPStatus.ACCEPTED)
|
||||
|
||||
if path == "/api/jobs/fix-media":
|
||||
channel_id = str(body.get("channel_id", "")).strip()
|
||||
if not channel_id:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "channel_id is required")
|
||||
try:
|
||||
channel_id = normalize_channel_id(body.get("channel_id"))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
payload = {"channel_id": channel_id}
|
||||
if self.app.legacy_account_id:
|
||||
payload["account_id"] = self.app.legacy_account_id
|
||||
@@ -2293,8 +2331,8 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
"label": str(body.get("label") or state.get("label") or account_id).strip(),
|
||||
"api_id": state.get("api_id"),
|
||||
"api_hash": state.get("api_hash"),
|
||||
"channels": state.get("channels") if isinstance(state.get("channels"), dict) else {},
|
||||
"channel_names": state.get("channel_names") if isinstance(state.get("channel_names"), dict) else {},
|
||||
"channels": self._clean_imported_channels(state.get("channels")),
|
||||
"channel_names": self._clean_imported_channel_names(state.get("channel_names")),
|
||||
"scrape_media": bool(state.get("scrape_media", True)),
|
||||
"forwarding_rules": state.get("forwarding_rules") if isinstance(state.get("forwarding_rules"), list) else [],
|
||||
"continuous_scraping": state.get("continuous_scraping") if isinstance(state.get("continuous_scraping"), dict) else {
|
||||
@@ -2337,9 +2375,9 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
if sub == ["auth", "password"]:
|
||||
return self._handle_post_account_auth_password(account_id, body)
|
||||
if sub == ["channels", "add"]:
|
||||
return self._handle_account_channel_add(account_id, str(body.get("channel_id", "")).strip(), body.get("name"))
|
||||
return self._handle_account_channel_add(account_id, body.get("channel_id"), body.get("name"))
|
||||
if sub == ["channels", "remove"]:
|
||||
return self._handle_account_channel_remove(account_id, str(body.get("channel_id", "")).strip())
|
||||
return self._handle_account_channel_remove(account_id, body.get("channel_id"))
|
||||
if sub == ["jobs", "scrape"]:
|
||||
return self._handle_account_job_scrape(account_id, body)
|
||||
if sub == ["jobs", "export"]:
|
||||
@@ -2412,10 +2450,35 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
return self.send_json(payload)
|
||||
|
||||
def _handle_account_channel_add(self, account_id: str, channel_id: str, name: Optional[str]) -> None:
|
||||
channel_id = channel_id.lstrip("@")
|
||||
if not channel_id:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "channel_id is required")
|
||||
def _clean_imported_channels(self, channels: Any) -> Dict[str, Any]:
|
||||
if not isinstance(channels, dict):
|
||||
return {}
|
||||
cleaned: Dict[str, Any] = {}
|
||||
for raw_channel_id, last_message_id in channels.items():
|
||||
try:
|
||||
channel_id = normalize_channel_id(raw_channel_id)
|
||||
except ValueError:
|
||||
continue
|
||||
cleaned[channel_id] = last_message_id
|
||||
return cleaned
|
||||
|
||||
def _clean_imported_channel_names(self, channel_names: Any) -> Dict[str, str]:
|
||||
if not isinstance(channel_names, dict):
|
||||
return {}
|
||||
cleaned: Dict[str, str] = {}
|
||||
for raw_channel_id, name in channel_names.items():
|
||||
try:
|
||||
channel_id = normalize_channel_id(raw_channel_id)
|
||||
except ValueError:
|
||||
continue
|
||||
cleaned[channel_id] = str(name).strip()
|
||||
return cleaned
|
||||
|
||||
def _handle_account_channel_add(self, account_id: str, channel_id: Any, name: Optional[str]) -> None:
|
||||
try:
|
||||
channel_id = normalize_channel_id(channel_id)
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
store = get_account_store(DATA_DIR, account_id)
|
||||
def mutate(state: Dict[str, Any]) -> None:
|
||||
if channel_id not in state.setdefault("channels", {}):
|
||||
@@ -2426,9 +2489,11 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
self.app.continuous_orchestrator.refresh_account(account_id)
|
||||
return self.send_json({"ok": True, "channel_id": channel_id})
|
||||
|
||||
def _handle_account_channel_remove(self, account_id: str, channel_id: str) -> None:
|
||||
if not channel_id:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "channel_id is required")
|
||||
def _handle_account_channel_remove(self, account_id: str, channel_id: Any) -> None:
|
||||
try:
|
||||
channel_id = normalize_channel_id(channel_id)
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
store = get_account_store(DATA_DIR, account_id)
|
||||
existed = [False]
|
||||
def mutate(state: Dict[str, Any]) -> None:
|
||||
@@ -2444,6 +2509,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
def _handle_account_job_scrape(self, account_id: str, body: Dict[str, Any]) -> None:
|
||||
channel_id = body.get("channel_id")
|
||||
if channel_id:
|
||||
try:
|
||||
channel_id = normalize_channel_id(channel_id)
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
job = self.app.job_runner.create_job(
|
||||
"scrape_channel",
|
||||
f"Scrape channel {channel_id} [{account_id}]",
|
||||
@@ -2466,9 +2535,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_json(job.to_dict(), status=HTTPStatus.ACCEPTED)
|
||||
|
||||
def _handle_account_job_export_channel(self, account_id: str, body: Dict[str, Any]) -> None:
|
||||
channel_id = str(body.get("channel_id", "")).strip()
|
||||
if not channel_id:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "channel_id is required")
|
||||
try:
|
||||
channel_id = normalize_channel_id(body.get("channel_id"))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
job = self.app.job_runner.create_job(
|
||||
"export_channel",
|
||||
f"Export channel {channel_id} [{account_id}]",
|
||||
@@ -2477,9 +2547,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_json(job.to_dict(), status=HTTPStatus.ACCEPTED)
|
||||
|
||||
def _handle_account_job_rescrape_media(self, account_id: str, body: Dict[str, Any]) -> None:
|
||||
channel_id = str(body.get("channel_id", "")).strip()
|
||||
if not channel_id:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "channel_id is required")
|
||||
try:
|
||||
channel_id = normalize_channel_id(body.get("channel_id"))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
job = self.app.job_runner.create_job(
|
||||
"rescrape_media",
|
||||
f"Rescrape media for {channel_id} [{account_id}]",
|
||||
@@ -2488,9 +2559,10 @@ class TelegramScraperRequestHandler(BaseHTTPRequestHandler):
|
||||
return self.send_json(job.to_dict(), status=HTTPStatus.ACCEPTED)
|
||||
|
||||
def _handle_account_job_fix_media(self, account_id: str, body: Dict[str, Any]) -> None:
|
||||
channel_id = str(body.get("channel_id", "")).strip()
|
||||
if not channel_id:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, "channel_id is required")
|
||||
try:
|
||||
channel_id = normalize_channel_id(body.get("channel_id"))
|
||||
except ValueError as exc:
|
||||
return self.send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
||||
job = self.app.job_runner.create_job(
|
||||
"fix_missing_media",
|
||||
f"Fix missing media for {channel_id} [{account_id}]",
|
||||
|
||||
Reference in New Issue
Block a user