Add a Random image endpoint and SPA page (with fastfetch mode)
Backend: GET /api/random/ (aliases /random and /random/) returns a random library image with: - rating=s,q,e filtering (comma separated, default any); - fastfetch mode (?fastfetch=1 or any User-Agent containing "fastfetch") that only considers png/jpg/gif - what terminal viewers can show; - JSON with j_id, filename, extension, rating, size, e621 id plus absolute url/download_url/thumbnail_url. Authenticated callers get signed URLs so fastfetch and image viewers can load them without headers; guests get unsigned URLs and never receive hidden_from_guests items. Tests: apps/library/tests/test_random.py (8 tests) covering the response contract, guest signatures, image-only default, the fastfetch format restriction (flag and User-Agent), rating filters, guest visibility and the short alias. Frontend: /random page with rating pills, R to roll, Open/Download and a library link, plus navigation and command palette entries; needs a backend, hidden in local mode. nginx: /random negotiates on Accept so browsers keep getting the SPA while scripts get the JSON (verified with the proxy and frontend containers). Also fixes a regression from the SSRF change: the guest download proxy still referenced the removed 'parsed' variable on its success path, so every proxied download would have 500'd. Redirect hops are now covered by tests with a mocked requests.get.
This commit is contained in:
@@ -17,6 +17,7 @@ import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import signing
|
||||
@@ -433,6 +434,28 @@ class RemoteUrlTests(SecurityTestCase):
|
||||
"https://static1.e621.net/data/x.png",
|
||||
)
|
||||
|
||||
@mock.patch("requests.get")
|
||||
def test_redirects_off_the_allowlist_are_refused(self, mocked_get):
|
||||
redirect = mock.Mock(is_redirect=True, is_permanent_redirect=False)
|
||||
redirect.headers = {"Location": "http://127.0.0.1:8000/health"}
|
||||
mocked_get.return_value = redirect
|
||||
|
||||
with self.assertRaises(services.RemoteUrlError):
|
||||
services.open_remote("https://static1.e621.net/x.png")
|
||||
# The internal address was never requested: only the first hop was.
|
||||
self.assertEqual(mocked_get.call_count, 1)
|
||||
redirect.close.assert_called()
|
||||
|
||||
@mock.patch("requests.get")
|
||||
def test_redirects_within_the_allowlist_are_followed(self, mocked_get):
|
||||
redirect = mock.Mock(is_redirect=True, is_permanent_redirect=False)
|
||||
redirect.headers = {"Location": "https://static2.e621.net/x.png"}
|
||||
final = mock.Mock(is_redirect=False, is_permanent_redirect=False)
|
||||
mocked_get.side_effect = [redirect, final]
|
||||
|
||||
self.assertIs(services.open_remote("https://static1.e621.net/x.png"), final)
|
||||
self.assertEqual(mocked_get.call_count, 2)
|
||||
|
||||
def test_download_creation_rejects_internal_urls(self):
|
||||
uploader = self.client_for("sec-uploader")
|
||||
for url in ("http://127.0.0.1:1/", "http://192.168.1.1/", "file:///etc/passwd"):
|
||||
|
||||
Reference in New Issue
Block a user