Serve RSS feeds through app with host rewrites

This commit is contained in:
Abel Luck 2026-03-31 17:30:24 +02:00
parent 3f28e46ff6
commit e7b00b4129
3 changed files with 121 additions and 12 deletions

View file

@ -31,6 +31,7 @@ from repub.model import (
delete_job_source,
delete_source,
initialize_database,
load_feed_url,
load_job_enabled,
load_settings_form,
load_source_form,
@ -151,15 +152,19 @@ def create_app(*, dev_mode: bool = False) -> Quart:
@app.get("/feeds/<path:feed_path>")
async def published_feed(feed_path: str) -> Response:
if Path(feed_path).suffix == ".rss":
return _rss_feed_response(
_read_feed_text(
feeds_dir=Path(app.config["REPUB_FEEDS_DIR"]),
feed_path=feed_path,
)
)
if not bool(app.config["REPUB_DEV_MODE"]):
return Response(status=404)
response = await send_from_directory(
return await send_from_directory(
str(Path(app.config["REPUB_FEEDS_DIR"])),
feed_path,
)
if Path(feed_path).suffix == ".rss":
response.mimetype = "application/rss+xml"
return response
@app.get("/static/<string:asset_name>-<string:asset_hash>.<string:extension>")
async def versioned_static_asset(
@ -582,6 +587,42 @@ def _load_sidebar_counts(app: Quart) -> dict[str, int]:
}
def _rss_feed_response(feed_text: str | None) -> Response:
if feed_text is None:
return Response(status=404)
return Response(feed_text, mimetype="application/rss+xml")
def _read_feed_text(*, feeds_dir: Path, feed_path: str) -> str | None:
resolved_path = _resolve_feed_path(feeds_dir=feeds_dir, feed_path=feed_path)
if resolved_path is None:
return None
return _rewrite_feed_text(
resolved_path.read_text(encoding="utf-8"),
configured_feed_url=load_feed_url(),
request_host=request.host,
)
def _resolve_feed_path(*, feeds_dir: Path, feed_path: str) -> Path | None:
base_dir = feeds_dir.resolve()
candidate_path = (base_dir / feed_path).resolve()
try:
candidate_path.relative_to(base_dir)
except ValueError:
return None
return candidate_path if candidate_path.is_file() else None
def _rewrite_feed_text(
feed_text: str, *, configured_feed_url: str, request_host: str
) -> str:
configured_origin = configured_feed_url.rstrip("/")
if configured_origin == "":
return feed_text
return feed_text.replace(configured_origin, f"https://{request_host}")
async def _clean_tab_state_periodically(app: Quart) -> None:
while True:
await asyncio.sleep(TAB_STATE_CLEAN_INTERVAL.total_seconds())