defmodule GitGudWeb.BadgeController do
@moduledoc """
A repository's status badges as images:
/r/:owner/:name/badge/ci.svg
/r/:owner/:name/badge/coverage/dot/2x/gg/forest.png
/r/:owner/:name/badge/workflow/audit/outline.svg
`GitGud.Badges.Spec` reads the path. The badge sheet
(`GitGudWeb.RepoLive.Badges`) previews and builds these URLs.
Visibility follows the repository, as for `RepoAuth`: a public or
internal repository's badges are anyone's, a private one's only its
readers', and a missing repository and a forbidden one both answer
404. A package badge follows the registry's rule instead
(`Packages.Auth`), so a private repository that publishes a format
publicly has public badges for those packages. A badge is fetched by an `<img>`, which sends cookies on GitGud's
own pages and nothing anywhere else, so a private repository's badges
only work here.
Caching is short: a badge reports state that changes with every push,
and image proxies (GitHub's camo among them) honor `max-age`. The
ETag lets a revalidation come back as a 304.
"""
use GitGudWeb, :controller
alias GitGud.Badges
alias GitGud.Badges.Spec
alias GitGud.Organizations
alias GitGud.Packages
alias GitGud.Repositories
@max_age 120
def show(conn, %{"owner" => owner, "name" => name, "path" => path}) do
user = conn.assigns[:current_scope] && conn.assigns.current_scope.user
with {:ok, spec} <- Spec.parse_path(path),
repo when not is_nil(repo) <- Repositories.find_by_path(owner, name),
true <- readable?(user, repo, spec),
{:ok, content_type, body} <- Badges.render(repo, spec) do
send_badge(conn, "#{cache_scope(repo, spec)}, max-age=#{@max_age}", content_type, body)
else
_ -> conn |> put_status(:not_found) |> text("")
end
end
@doc """
An example badge for the sheet: `/badges/sample/ci/failing/dot/forest.svg`.
The examples are a fixed set and never change, so they cache for a day.
"""
def sample(conn, %{"kind" => kind, "key" => key, "path" => path}) do
# The options are parsed under a stand-in kind: what's drawn comes
# from the sample, and `workflow` would want a name segment here.
with {:ok, spec} <- Spec.parse_path(["mark" | path]),
{:ok, content} <- Badges.sample(kind, key),
{:ok, content_type, body} <- Badges.render_content(content, spec) do
send_badge(conn, "public, max-age=86400", content_type, body)
else
_ -> conn |> put_status(:not_found) |> text("")
end
end
defp readable?(user, repo, %Spec{kind: "package", registry: registry}) do
principal = if user, do: {:user, user, :all}, else: :anonymous
Packages.Auth.authorize(principal, repo, :read, registry) == true
end
defp readable?(user, repo, _spec), do: Organizations.can?(user, repo, :read) == true
# What a stranger may see can be cached by anyone; the rest only by the
# reader's own browser.
defp cache_scope(repo, %Spec{kind: "package", registry: registry}) do
if repo.visibility != "private" or Packages.Auth.public_format?(repo, registry),
do: "public",
else: "private"
end
defp cache_scope(repo, _spec),
do: if(repo.visibility == "private", do: "private", else: "public")
defp send_badge(conn, cache_control, content_type, body) do
etag =
~s("#{:crypto.hash(:sha256, body) |> Base.url_encode64(padding: false) |> binary_part(0, 22)}")
conn =
conn
|> put_resp_header("etag", etag)
|> put_resp_header("cache-control", cache_control)
|> put_resp_header("x-content-type-options", "nosniff")
# The SVG is ours and carries no script, but it's served from the
# app's origin; opening it directly shouldn't be able to run any.
|> put_resp_header(
"content-security-policy",
"default-src 'none'; style-src 'unsafe-inline'"
)
if etag in get_req_header(conn, "if-none-match") do
send_resp(conn, 304, "")
else
conn |> put_resp_content_type(content_type, nil) |> send_resp(200, body)
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
4.0 KiB · text
History
0d6bdf0