defmodule Forum.Router do use Plug.Router use Joken.Config require Logger plug Plug.Logger plug :log_request plug :fetch_cookies plug :verify_session_host plug :match plug :dispatch defp verify_session_host(conn, _opts) do if conn.host == "session.frm.so" do case authenticate(conn) do {:ok, claims} -> conn |> Plug.Conn.assign(:claims, claims) :error -> conn |> send_resp(401, "unauthorized") |> halt() end else conn end end defp authenticate(conn) do with token when is_binary(token) <- conn.cookies["auth_token"], {:ok, claims} <- verify_token(token) do {:ok, claims} else _ -> :error end end defp verify_token(token) do secret = Application.fetch_env!(:forum, :auth)[:jwt_secret] signer = Joken.Signer.create("HS256", secret) case Joken.verify_and_validate(%{}, token, signer) do {:ok, claims} -> {:ok, claims} {:error, _reason} = err -> err end end get "/admin", host: "session.frm.so", do: handle_admin(conn) get "/admin/graphyellow.svg", host: "session.frm.so", do: admin_asset(conn, "ui/graphyellow.svg", "image/svg+xml") get "/admin/*path", host: "session.frm.so", do: handle_admin(conn) get "/", host: "session.frm.so" do user_id = conn.assigns.claims["id"] email = conn.assigns.claims["email"] conn |> WebSockAdapter.upgrade(Forum.WsHandler, %{user_id: user_id, email: email}, timeout: :infinity) |> halt() end match _ do send_resp(conn, 404, "not found") end defp handle_admin(conn) do if websocket_upgrade?(conn) do conn |> WebSockAdapter.upgrade(Forum.WsHandler, %{}, timeout: :infinity) |> halt() else admin_page(conn) end end defp websocket_upgrade?(conn) do connection = get_req_header(conn, "connection") |> Enum.join(",") |> String.downcase() upgrade = get_req_header(conn, "upgrade") |> List.first() |> to_string() |> String.downcase() String.contains?(connection, "upgrade") and upgrade == "websocket" end defp admin_page(conn) do html = File.read!(Forum.Assets.path("ui/admin.html")) conn |> put_resp_content_type("text/html") |> send_resp(200, html) end defp admin_asset(conn, path, content_type) do conn |> put_resp_content_type(content_type) |> send_file(200, Forum.Assets.path(path)) end defp log_request(conn, _opts) do started_at = System.monotonic_time(:microsecond) Plug.Conn.register_before_send(conn, fn conn -> duration_us = System.monotonic_time(:microsecond) - started_at Forum.LogStore.add(%{ "source_ip" => remote_ip(conn), "host" => conn.host || "", "method" => conn.method, "path" => conn.request_path, "query_string" => conn.query_string, "status" => conn.status || 0, "duration_ms" => Float.round(duration_us / 1_000, 2) }) conn end) end defp remote_ip(conn) do case Plug.Conn.get_req_header(conn, "x-real-ip") do [ip | _] -> ip [] -> fallback_ip(conn) end end defp fallback_ip(%{remote_ip: remote_ip}) when is_tuple(remote_ip) do remote_ip |> :inet.ntoa() |> to_string() end defp fallback_ip(_), do: "" end