init
This commit is contained in:
182
lib/forum/public_site.ex
Normal file
182
lib/forum/public_site.ex
Normal 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
|
||||
Reference in New Issue
Block a user