This commit is contained in:
Sam
2026-06-10 11:51:56 -05:00
commit 66ba338b81
57 changed files with 5509 additions and 0 deletions

182
lib/forum/public_site.ex Normal file
View File

@@ -0,0 +1,182 @@
defmodule Forum.PublicSite do
@moduledoc """
A per-network process for serving public static websites on separate ports.
"""
use GenServer
def child_spec(opts) do
network = opts |> Keyword.fetch!(:network) |> normalize_network()
%{
id: {:public_site, network},
start: {__MODULE__, :start_link, [[network: network]]},
restart: :permanent,
shutdown: 5_000,
type: :worker
}
end
def start_link(opts) do
network = Keyword.fetch!(opts, :network)
GenServer.start_link(__MODULE__, network, name: via(network))
end
def normalize_network(network) when is_binary(network) do
network
|> String.downcase()
|> String.split(":", parts: 2)
|> hd()
end
def serve(pid, request) do
GenServer.call(pid, {:serve, request})
end
@impl true
def init(network) do
{:ok, %{network: normalize_network(network), started_at: DateTime.utc_now()}}
end
@impl true
def handle_call({:serve, request}, _from, state) do
response = do_serve(state, request)
{:reply, response, state}
end
defp do_serve(%{network: network}, request) do
roots = public_roots(network)
case roots do
[] ->
%{
status: 404,
content_type: "text/plain",
body: "No public website configured for #{network}"
}
[primary | fallbacks] ->
static_response(primary, request.path, fallbacks)
end
end
defp static_response(root, path, fallback_roots \\ []) do
file_path = resolve_path(root, path)
cond do
file_path == :invalid ->
%{status: 403, content_type: "text/plain", body: "Forbidden"}
File.dir?(file_path) and File.regular?(Path.join(file_path, "index.html")) ->
send_file(Path.join(file_path, "index.html"))
File.regular?(file_path) ->
send_file(file_path)
versioned = find_versioned_file([root | fallback_roots], path) ->
send_file(versioned)
fallback = find_fallback_file(fallback_roots, path) ->
send_file(fallback)
true ->
%{status: 404, content_type: "text/plain", body: "Not found"}
end
end
defp find_versioned_file(roots, path) do
case path |> String.trim_leading("/") |> Path.split() do
[_version | rest] when rest != [] ->
stripped_path = "/" <> Path.join(rest)
find_fallback_file(roots, stripped_path)
_ ->
nil
end
end
defp find_fallback_file(roots, path) do
Enum.find_value(roots, fn root ->
file_path = resolve_path(root, path)
cond do
file_path == :invalid ->
nil
File.dir?(file_path) and File.regular?(Path.join(file_path, "index.html")) ->
Path.join(file_path, "index.html")
File.regular?(file_path) ->
file_path
true ->
nil
end
end)
end
defp resolve_path(root, path) do
relative =
path
|> String.trim_leading("/")
|> URI.decode()
candidate = Path.expand(relative, root)
rel_to_root = Path.relative_to(candidate, Path.expand(root))
if rel_to_root == ".." or String.starts_with?(rel_to_root, "../") do
:invalid
else
candidate
end
end
defp send_file(path) do
%{status: 200, content_type: content_type(path), body: File.read!(path)}
end
defp content_type(path) do
case Path.extname(path) do
".html" -> "text/html"
".css" -> "text/css"
".js" -> "application/javascript"
".json" -> "application/json"
".svg" -> "image/svg+xml"
".png" -> "image/png"
".jpg" -> "image/jpeg"
".jpeg" -> "image/jpeg"
".webp" -> "image/webp"
".gif" -> "image/gif"
".ico" -> "image/x-icon"
".txt" -> "text/plain"
_ -> "application/octet-stream"
end
end
defp public_roots("cs") do
["networks/cs/website"]
|> Enum.map(&Forum.Assets.path/1)
|> Enum.filter(&File.dir?/1)
end
defp public_roots("comalyr") do
[
"networks/comalyr/comalyr.com/ui/desktop",
"networks/comalyr/comalyr.com/ui"
]
|> Enum.map(&Forum.Assets.path/1)
|> Enum.filter(&File.dir?/1)
end
defp public_roots(network) do
[
Path.join(["networks", network, "website"]),
Path.join(["networks", network, "public"]),
Path.join(["networks", network, "www"])
]
|> Enum.map(&Forum.Assets.path/1)
|> Enum.filter(&File.dir?/1)
end
defp via(network),
do: {:via, Registry, {Forum.ProcessRegistry, {:public_site, normalize_network(network)}}}
end