defmodule GitGudWeb.AptBrowseTest do
@moduledoc """
The Debian repository as a browsable tree.
A real mirror is clickable all the way down; ours only answered exact
paths, so you couldn't see what was published without already knowing
its name.
Two properties are worth pinning. The listing must never advertise a
path that 404s — it is derived from what the repository actually
serves, and a test walks every link to prove it. And it must never
show a repository the caller couldn't have read directly.
"""
use GitGudWeb.ConnCase, async: false
alias GitGud.Packages
alias GitGud.Repositories
alias GitGud.Repositories.Storage
import GitGud.AccountsFixtures
import GitGud.ForgeFixtures
defp deb(fields) do
control = Enum.map_join(fields, fn {k, v} -> "#{k}: #{v}\n" end)
ar([
{"debian-binary", "2.0\n"},
{"control.tar.gz", :zlib.gzip(tar([{~c"./control", control}]))},
{"data.tar.gz", :zlib.gzip(tar([{~c"./usr/bin/thing", "#!/bin/sh\n"}]))}
])
end
defp tar(members) do
path = Path.join(System.tmp_dir!(), "t-#{System.unique_integer([:positive])}.tar")
:ok = :erl_tar.create(String.to_charlist(path), members, [])
File.read!(path)
end
defp ar(members) do
body =
Enum.map_join(members, fn {name, data} ->
String.pad_trailing(name, 16) <>
String.pad_trailing("0", 12) <>
String.pad_trailing("0", 6) <>
String.pad_trailing("0", 6) <>
String.pad_trailing("100644", 8) <>
String.pad_trailing(Integer.to_string(byte_size(data)), 10) <>
<<0x60, 0x0A>> <> data <> if(rem(byte_size(data), 2) == 1, do: "\n", else: "")
end)
"!<arch>\n" <> body
end
defp publish(repo, package, opts \\ []) do
version = Keyword.get(opts, :version, "1.0.0-1")
arch = Keyword.get(opts, :arch, "amd64")
fields = [
Package: package,
Version: version,
Architecture: arch,
Maintainer: "Someone <someone@example.com>",
Description: "the #{package} package"
]
{:ok, _p, _v, _f} =
Packages.publish_file(repo, "deb", package, version, %{
filename: "#{package}_#{version}_#{arch}.deb",
body: deb(fields),
metadata: %{
"dist" => Keyword.get(opts, :dist, "stable"),
"component" => "main",
"architecture" => arch,
"control" => Map.new(fields, fn {k, v} -> {to_string(k), v} end)
}
})
:ok
end
setup do
{user, repo} = repository_fixture(%{visibility: "public"})
publish(repo, "widget")
{:ok, user: user, repo: repo, root: "/pkg/deb/#{Storage.owner_handle(user)}/#{repo.name}"}
end
describe "the tree" do
test "the root lists dists, pool and the key", %{conn: conn, root: root} do
conn = get(conn, root <> "/")
assert conn.status == 200
assert get_resp_header(conn, "content-type") == ["text/html; charset=utf-8"]
assert conn.resp_body =~ "dists/"
assert conn.resp_body =~ "pool/"
assert conn.resp_body =~ "key.asc"
end
test "a suite lists its index files and components", %{conn: conn, root: root} do
body = get(conn, root <> "/dists/stable/").resp_body
assert body =~ "InRelease"
assert body =~ "Release.gpg"
assert body =~ "main/"
end
test "an architecture lists the Packages files", %{conn: conn, root: root} do
body = get(conn, root <> "/dists/stable/main/binary-amd64/").resp_body
assert body =~ "Packages"
assert body =~ "Packages.xz"
end
test "the pool is navigable down to the .deb, with its size", %{
conn: conn,
repo: repo,
root: root
} do
assert get(conn, root <> "/pool/").resp_body =~ "main/"
assert get(build_conn(), root <> "/pool/main/").resp_body =~ "#{repo.name}/"
assert get(build_conn(), root <> "/pool/main/#{repo.name}/").resp_body =~ "w/"
leaf =
get(build_conn(), root <> "/pool/main/#{repo.name}/w/widget/").resp_body
assert leaf =~ "widget_1.0.0-1_amd64.deb"
# A size column, not just a name.
assert leaf =~ ~r/\d+(\.\d+)? (B|KB|MB)/
end
test "every directory offers a parent link except the root", %{conn: conn, root: root} do
refute get(conn, root <> "/").resp_body =~ ">../<"
assert get(build_conn(), root <> "/dists/").resp_body =~ ">../<"
end
test "a path that is neither a file nor a directory is still a 404", %{
conn: conn,
root: root
} do
assert get(conn, root <> "/nonsense/").status == 404
assert get(build_conn(), root <> "/dists/nosuchsuite/").status == 404
assert get(build_conn(), root <> "/pool/main/nosuchrepo/").status == 404
end
end
describe "the listing never advertises a path that 404s" do
test "walking every link from the root reaches only things that exist", %{
conn: conn,
repo: repo,
root: root
} do
publish(repo, "libthing", arch: "all")
publish(repo, "gadget", dist: "unstable")
# Breadth-first over every href the tree offers, following only
# links below this repository's root.
{visited, checked} = walk([root <> "/"], MapSet.new(), 0)
assert checked > 10, "the crawl should have covered the whole tree, saw #{checked}"
assert MapSet.size(visited) > 10
# Sanity: the crawl actually reached the leaves.
assert Enum.any?(visited, &String.ends_with?(&1, ".deb"))
assert Enum.any?(visited, &String.ends_with?(&1, "/InRelease"))
assert Enum.any?(visited, &String.ends_with?(&1, "/Packages.xz"))
_ = conn
end
defp walk([], visited, checked), do: {visited, checked}
defp walk([path | rest], visited, checked) do
if MapSet.member?(visited, path) do
walk(rest, visited, checked)
else
conn = get(build_conn(), path)
assert conn.status == 200,
"#{path} was linked from a listing but answered #{conn.status}"
visited = MapSet.put(visited, path)
next =
if String.ends_with?(path, "/") do
conn.resp_body
|> then(&Regex.scan(~r/href="([^"]+)"/, &1))
|> Enum.map(fn [_, href] -> URI.parse(href).path end)
# Don't climb back out through `../`.
|> Enum.reject(&(byte_size(&1) <= byte_size(path)))
else
[]
end
walk(rest ++ next, visited, checked + 1)
end
end
end
describe "permissions" do
test "a private repository's tree challenges an anonymous browser", %{conn: conn} do
{user, private} = repository_fixture(%{visibility: "private"})
publish(private, "secret")
root = "/pkg/deb/#{Storage.owner_handle(user)}/#{private.name}"
conn = get(conn, root <> "/")
assert conn.status == 401
assert [_] = get_resp_header(conn, "www-authenticate")
end
test "and serves it to a token holder", %{conn: conn} do
{user, private} = repository_fixture(%{visibility: "private"})
publish(private, "secret")
{:ok, _t, token} = Packages.create_registry_token(private, "ro", ["pull"])
root = "/pkg/deb/#{Storage.owner_handle(user)}/#{private.name}"
conn =
conn
|> put_req_header("authorization", token)
|> get(root <> "/pool/main/#{private.name}/s/secret/")
assert conn.status == 200
assert conn.resp_body =~ "secret_1.0.0-1_amd64.deb"
end
test "a namespace listing hides repositories the browser can't read" do
owner = user_fixture()
{:ok, org} =
GitGud.Organizations.create_organization(owner, %{
"handle" => "org#{System.unique_integer([:positive])}",
"name" => "Org"
})
{:ok, open} =
Repositories.create_repository_for_org(org, owner, %{
"name" => unique_repo_name(),
"visibility" => "public"
})
{:ok, closed} =
Repositories.create_repository_for_org(org, owner, %{
"name" => unique_repo_name(),
"visibility" => "private"
})
publish(open, "public-thing")
publish(closed, "secret-thing")
root = "/pkg/deb/#{org.handle}/_all"
anon = get(build_conn(), root <> "/pool/main/").resp_body
assert anon =~ "#{open.name}/"
refute anon =~ "#{closed.name}/"
# …and the path itself is not merely hidden from the index.
assert get(build_conn(), root <> "/pool/main/#{closed.name}/").status == 404
end
end
describe "apt is unaffected" do
test "the index files it fetches are still their own content types", %{
conn: conn,
root: root
} do
release = get(conn, root <> "/dists/stable/Release")
assert get_resp_header(release, "content-type") == ["text/plain; charset=utf-8"]
assert release.resp_body =~ "Suite: stable"
xz = get(build_conn(), root <> "/dists/stable/main/binary-amd64/Packages.xz")
assert get_resp_header(xz, "content-type") == ["application/x-xz"]
end
test "the pool path from the index is the file, not a listing", %{conn: conn, root: root} do
index = get(conn, root <> "/dists/stable/main/binary-amd64/Packages").resp_body
[_, filename] = Regex.run(~r/^Filename: (\S+)$/m, index)
served = get(build_conn(), root <> "/" <> filename)
assert served.status == 200
refute served.resp_body =~ "<!DOCTYPE html>"
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
9.2 KiB · text
History
b868907