From 103b724962a73af92d2d854a0dadfe5784173b03 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 20 May 2026 20:58:03 +0200 Subject: [PATCH 1/6] EnsureHostMatchesPlug: add missing behavior declaration --- lib/pleroma/web/plugs/ensure_host_matches_plug.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/web/plugs/ensure_host_matches_plug.ex b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex index ab2129f9e..960e86d4f 100644 --- a/lib/pleroma/web/plugs/ensure_host_matches_plug.ex +++ b/lib/pleroma/web/plugs/ensure_host_matches_plug.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.Plugs.EnsureHostMatchesPlug do @moduledoc "Ensures Host header matches instance" + @behaviour Plug alias Pleroma.Web.Endpoint From a30298c1eb174858fd9aa8a5919e90349f35670c Mon Sep 17 00:00:00 2001 From: Phantasm Date: Wed, 20 May 2026 20:58:41 +0200 Subject: [PATCH 2/6] StaticNotFoundPlug: Move 404 on non-existing static files to a new plug This fixes preferred frontends not working (returning 404 on most static frontend files). --- changelog.d/fix-preferred-frontend.fix | 1 + lib/pleroma/web/endpoint.ex | 8 ++++ lib/pleroma/web/plugs/instance_static.ex | 35 ++--------------- .../web/plugs/static_not_found_plug.ex | 38 +++++++++++++++++++ 4 files changed, 51 insertions(+), 31 deletions(-) create mode 100644 changelog.d/fix-preferred-frontend.fix create mode 100644 lib/pleroma/web/plugs/static_not_found_plug.ex diff --git a/changelog.d/fix-preferred-frontend.fix b/changelog.d/fix-preferred-frontend.fix new file mode 100644 index 000000000..07bf4d0c6 --- /dev/null +++ b/changelog.d/fix-preferred-frontend.fix @@ -0,0 +1 @@ +Fix preferred frontend not working due to InstanceStatic plug prematurely returning HTTP 404 diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 07b33f866..4582fd0fd 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -139,6 +139,14 @@ defmodule Pleroma.Web.Endpoint do from: {:pleroma, "priv/static/adminfe/"} ) + plug(Pleroma.Web.Plugs.StaticNotFoundPlug, + at: "/", + cache_control_for_etags: @static_cache_disabled, + headers: %{ + "cache-control" => @static_cache_disabled + } + ) + # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do diff --git a/lib/pleroma/web/plugs/instance_static.ex b/lib/pleroma/web/plugs/instance_static.ex index 948188287..d2a674d39 100644 --- a/lib/pleroma/web/plugs/instance_static.ex +++ b/lib/pleroma/web/plugs/instance_static.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do require Pleroma.Constants - import Plug.Conn, only: [put_resp_header: 3, put_status: 2, send_resp: 3, halt: 1] + import Plug.Conn, only: [put_resp_header: 3] @moduledoc """ This is a shim to call `Plug.Static` but with runtime `from` configuration. @@ -51,37 +51,10 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do |> Map.put(:from, from) |> Map.put(:content_types, false) - conn = - conn - |> set_content_type(conn.request_path) - |> Plug.Static.call(opts) + conn = set_content_type(conn, conn.request_path) - if conn.halted do - conn - else - path = String.trim_leading(conn.request_path, "/") - - if not File.exists?(file_path(path)) do - conn - |> put_status(:not_found) - |> send_404() - |> halt() - else - conn - end - end - end - - defp send_404(conn) do - if String.ends_with?(String.downcase(conn.request_path), ".json") do - conn - |> put_resp_header("content-type", "application/json") - |> send_resp(404, Jason.encode!(%{error: "not found"})) - else - conn - |> put_resp_header("content-type", "text/plain") - |> send_resp(404, "Not found") - end + # Call Plug.Static with our sanitized content-type + Plug.Static.call(conn, opts) end defp set_content_type(conn, "/emoji/" <> filepath) do diff --git a/lib/pleroma/web/plugs/static_not_found_plug.ex b/lib/pleroma/web/plugs/static_not_found_plug.ex new file mode 100644 index 000000000..e2e7a622c --- /dev/null +++ b/lib/pleroma/web/plugs/static_not_found_plug.ex @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Plugs.StaticNotFoundPlug do + @moduledoc """ + A small plug that enforces returning 404 on static files when they don't exist. + """ + @behaviour Plug + + require Pleroma.Constants + import Plug.Conn, only: [put_resp_header: 3, send_resp: 3, halt: 1] + + def init(opts), do: opts + + for only <- Pleroma.Constants.static_only_files() do + def call(%{request_path: "/" <> unquote(only) <> _} = conn, _opts) do + send_404(conn) + end + end + + def call(conn, _) do + conn + end + + defp send_404(conn) do + if String.ends_with?(String.downcase(conn.request_path), ".json") do + conn + |> put_resp_header("content-type", "application/json") + |> send_resp(404, Jason.encode!(%{error: "not found"})) + else + conn + |> put_resp_header("content-type", "text/plain") + |> send_resp(404, "Not found") + end + |> halt() + end +end From 9129c6579594a06d7c993ba028d9995e8e86b6e7 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sat, 23 May 2026 23:31:24 +0200 Subject: [PATCH 3/6] StaticNotFoundPlug: Move tests out from InstanceStatic --- .../web/plugs/instance_static_test.exs | 43 ---------------- .../web/plugs/static_not_found_plug_test.exs | 49 +++++++++++++++++++ 2 files changed, 49 insertions(+), 43 deletions(-) create mode 100644 test/pleroma/web/plugs/static_not_found_plug_test.exs diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs index 017c49d1e..b5a5a3334 100644 --- a/test/pleroma/web/plugs/instance_static_test.exs +++ b/test/pleroma/web/plugs/instance_static_test.exs @@ -137,47 +137,4 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do # It should be preserved because "image" is in the allowed_mime_types list assert content_type == "image/jpeg" end - - describe "404s for missing files in static-only paths" do - test "returns 404 for non-existent static-only JSON files" do - conn = get(build_conn(), "/static/non-existent.json") - - assert conn.status == 404 - assert ["application/json"] = get_resp_header(conn, "content-type") - assert Jason.decode!(conn.resp_body) == %{"error" => "not found"} - end - - test "returns 404 for non-existent static-only non-JSON files" do - conn = get(build_conn(), "/static/non-existent.txt") - - assert conn.status == 404 - assert conn.resp_body == "Not found" - assert ["text/plain"] = get_resp_header(conn, "content-type") - end - - test "returns 404 for non-existent .css files" do - conn = get(build_conn(), "/static/non-existent.css") - - assert conn.status == 404 - assert conn.resp_body == "Not found" - # Verifies that we forced text/plain for the error body, even though the path was .css - assert ["text/plain"] = get_resp_header(conn, "content-type") - end - - test "returns 404 for non-existent files without an extension" do - conn = get(build_conn(), "/static/non-existent") - - assert conn.status == 404 - assert conn.resp_body == "Not found" - assert ["text/plain"] = get_resp_header(conn, "content-type") - end - - test "returns 200 (falls through to SPA) for non-static-only paths" do - # /some-route is NOT in static_only_files, so it should still fall through to the SPA. - conn = get(build_conn(), "/some-route") - - assert conn.status == 200 - assert ["text/html; charset=utf-8"] = get_resp_header(conn, "content-type") - end - end end diff --git a/test/pleroma/web/plugs/static_not_found_plug_test.exs b/test/pleroma/web/plugs/static_not_found_plug_test.exs new file mode 100644 index 000000000..a5d063c53 --- /dev/null +++ b/test/pleroma/web/plugs/static_not_found_plug_test.exs @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2026 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +defmodule Pleroma.Web.Plugs.StaticNotFoundPlugTest do + use Pleroma.Web.ConnCase + + describe "404s for missing files in static-only paths" do + test "returns 404 for non-existent static-only JSON files" do + conn = get(build_conn(), "/static/non-existent.json") + + assert conn.status == 404 + assert ["application/json"] = get_resp_header(conn, "content-type") + assert Jason.decode!(conn.resp_body) == %{"error" => "not found"} + end + + test "returns 404 for non-existent static-only non-JSON files" do + conn = get(build_conn(), "/static/non-existent.txt") + + assert conn.status == 404 + assert conn.resp_body == "Not found" + assert ["text/plain"] = get_resp_header(conn, "content-type") + end + + test "returns 404 for non-existent .css files" do + conn = get(build_conn(), "/static/non-existent.css") + + assert conn.status == 404 + assert conn.resp_body == "Not found" + # Verifies that we forced text/plain for the error body, even though the path was .css + assert ["text/plain"] = get_resp_header(conn, "content-type") + end + + test "returns 404 for non-existent files without an extension" do + conn = get(build_conn(), "/static/non-existent") + + assert conn.status == 404 + assert conn.resp_body == "Not found" + assert ["text/plain"] = get_resp_header(conn, "content-type") + end + + test "returns 200 (falls through to SPA) for non-static-only paths" do + # /some-route is NOT in static_only_files, so it should still fall through to the SPA. + conn = get(build_conn(), "/some-route") + + assert conn.status == 200 + assert ["text/html; charset=utf-8"] = get_resp_header(conn, "content-type") + end + end +end From 00ca971390f07dc87b2206ededd06493ca20e6c6 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Sun, 24 May 2026 00:49:32 +0200 Subject: [PATCH 4/6] FrontendStaticPlug: Add test for static files outside of index --- test/pleroma/web/plugs/frontend_static_plug_test.exs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs index b7c06eacd..7dc7e52cf 100644 --- a/test/pleroma/web/plugs/frontend_static_plug_test.exs +++ b/test/pleroma/web/plugs/frontend_static_plug_test.exs @@ -122,16 +122,27 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do clear_config([:frontends, :pickable], ["#{name}/#{ref}"]) path = "#{@dir}/frontends/#{name}/#{ref}" + path2 = "#{@dir}/frontends/#{name}/#{ref}/packs/html" Pleroma.Backports.mkdir_p!(path) File.write!("#{path}/index.html", "from frontend plug") + Pleroma.Backports.mkdir_p!(path2) + File.write!("#{path2}/custom.html", "from frontend plug") + index = conn |> put_req_cookie("preferred_frontend", "#{name}/#{ref}") |> get("/") assert html_response(index, 200) == "from frontend plug" + + custom = + conn + |> put_req_cookie("preferred_frontend", "#{name}/#{ref}") + |> get("/packs/html/custom.html") + + assert html_response(custom, 200) == "from frontend plug" end test "only returns content from pickable frontends", %{conn: conn} do From 85a0f2e602914b1ada62ce77b90fd7233c907ada Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 1 Jun 2026 21:20:01 +0200 Subject: [PATCH 5/6] StaticNotFoundPlug: Remove unused plug options --- lib/pleroma/web/endpoint.ex | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 4582fd0fd..8552f5d0a 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -139,13 +139,7 @@ defmodule Pleroma.Web.Endpoint do from: {:pleroma, "priv/static/adminfe/"} ) - plug(Pleroma.Web.Plugs.StaticNotFoundPlug, - at: "/", - cache_control_for_etags: @static_cache_disabled, - headers: %{ - "cache-control" => @static_cache_disabled - } - ) + plug(Pleroma.Web.Plugs.StaticNotFoundPlug) # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. From 1d6664b1f46c7dae805ff5b3d7ea79265a58bb84 Mon Sep 17 00:00:00 2001 From: Phantasm Date: Mon, 1 Jun 2026 21:50:20 +0200 Subject: [PATCH 6/6] InstanceStaticPlug: Add more override and fallback tests --- .../web/plugs/instance_static_test.exs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs index b5a5a3334..1d67e8dc3 100644 --- a/test/pleroma/web/plugs/instance_static_test.exs +++ b/test/pleroma/web/plugs/instance_static_test.exs @@ -61,6 +61,28 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do File.write!(@dir <> "/static/kaniini.html", "

rabbit hugs as a service

") index = get(build_conn(), "/static/kaniini.html") assert html_response(index, 200) == "

rabbit hugs as a service

" + + File.write!(@dir <> "/static/kaniini the bnuuy.html", "

rabbit hugs as a service

") + index = get(build_conn(), "/static/kaniini%20the%20bnuuy.html") + assert html_response(index, 200) == "

rabbit hugs as a service

" + end + + test "overrides /static/styles files with spaces in URL" do + Pleroma.Backports.mkdir_p!(@dir <> "/static/styles") + File.write(@dir <> "/static/styles/Redmond DX.iss", "overridden") + + index = get(build_conn(), "/static/styles/Redmond%20DX.iss") + assert index.status == 200 + assert index.resp_body == File.read!(@dir <> "/static/styles/Redmond DX.iss") + assert index.halted == true + end + + test "serves fallback /static/styles files with spaces in URL" do + index = get(build_conn(), "/static/styles/Redmond%20DX.iss") + + assert index.status == 200 + assert index.resp_body == File.read!("priv/static/static/styles/Redmond DX.iss") + assert index.halted == true end test "does not sanitize dangerous files in general, as there can be html and javascript files legitimately in this folder" do