Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logging
import os
from urllib.parse import quote

from fastapi import FastAPI, HTTPException, Query
import requests
import humanize
Expand Down Expand Up @@ -72,7 +74,7 @@ def health():
@app.get("/freshrss/unread")
def freshrss_unread(
n: int = Query(default=10, ge=1),
category: str | None = Query(default=None),
category: str | None = Query(default=None, max_length=200),
):
token = get_greader_token()
headers = {"Authorization": f"GoogleLogin auth={token}"}
Expand All @@ -81,8 +83,12 @@ def freshrss_unread(
"output": "json",
"n": n,
}
category_label = category if isinstance(category, str) and category else None
stream_id = f"user/-/label/{category_label}" if category_label else "user/-/state/com.google/reading-list"
category_label = category.strip() if isinstance(category, str) else None
stream_id = (
f"user/-/label/{quote(category_label, safe='')}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle dot-only labels before building the path

When the normalized category is . or .., quote(..., safe='') leaves it unchanged because dots are always URL-safe. The locked Requests 2.33.1 then resolves these as path segments while preparing the request: label/. becomes label/, and label/.. becomes user/-/, so FreshRSS receives a different stream ID rather than the requested category. Reject these labels or construct the request so their path segment cannot be normalized; the mocked requests.get tests currently inspect the URL before this preparation occurs.

Useful? React with 👍 / 👎.

if category_label
else "user/-/state/com.google/reading-list"
)
# Using the same host as before but with the right endpoint
url = f"{FRESHRSS_HOST}/api/greader.php/reader/api/0/stream/contents/{stream_id}"
try:
Expand Down
30 changes: 30 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,36 @@ def fake_get(url, headers, params, timeout):
assert result[0]["url"] == ""


@pytest.mark.parametrize(
("category", "expected_stream"),
[
(" Tech News ", "user/-/label/Tech%20News"),
("Tech/News", "user/-/label/Tech%2FNews"),
("100% News", "user/-/label/100%25%20News"),
("What?", "user/-/label/What%3F"),
("日本語", "user/-/label/%E6%97%A5%E6%9C%AC%E8%AA%9E"),
(" \t\n ", "user/-/state/com.google/reading-list"),
],
)
def test_freshrss_unread_normalizes_and_encodes_category(monkeypatch, category, expected_stream):
main = import_app(monkeypatch)
monkeypatch.setattr(main, "get_greader_token", lambda: "token-123")
captured = {}

def fake_get(url, **kwargs):
captured["url"] = url
return FakeResponse(payload={"items": []})

monkeypatch.setattr(main.requests, "get", fake_get)

main.freshrss_unread(category=category)

assert captured["url"] == (
"https://freshrss.example.test/api/greader.php/reader/api/0/stream/contents/"
f"{expected_stream}"
)


def test_freshrss_unread_wraps_upstream_http_errors(monkeypatch):
main = import_app(monkeypatch)
monkeypatch.setattr(main, "get_greader_token", lambda: "token-123")
Expand Down