feat: add Go module proxy (GOPROXY protocol) (#59)

* feat: add Go module proxy (GOPROXY protocol) (#47)

Implements caching proxy for Go modules with 5 standard endpoints:
- GET /go/{module}/@v/list — list versions
- GET /go/{module}/@v/{version}.info — version metadata
- GET /go/{module}/@v/{version}.mod — go.mod file
- GET /go/{module}/@v/{version}.zip — module zip
- GET /go/{module}/@latest — latest version info

Features:
- Module path encoding/decoding per Go spec (!x → X)
- Immutable caching (.info/.mod/.zip never overwritten)
- Mutable endpoints (@v/list, @latest) refreshed from upstream
- Configurable upstream (default: proxy.golang.org)
- Separate timeout for .zip downloads (default: 120s)
- Size limit for zips (default: 100MB)
- Path traversal protection
- Dashboard integration (stats, mount points, index)
- 25 unit tests (encoding, path splitting, safety, content-type)

Closes #47

* style: cargo fmt

* feat(ui): add Go pages, compact cards, fix icons

- Go in sidebar + list/detail pages with go get command
- Dashboard: fix fallback icon (was Docker whale for Go)
- Compact registry cards: lg:grid-cols-6, all 6 in one row
- Cargo icon: crate boxes instead of truck
- Go icon: stylized Go text (sidebar + dashboard)

* fix(go): URL-decode path + send encoded paths to upstream

Go client sends %21 for ! in module paths. Axum wildcard does not
auto-decode, so we percent-decode manually. Upstream proxy.golang.org
expects encoded paths (with !), not decoded uppercase.

Tested: full Pusk build (22 modules, 135MB cached) including
SherClockHolmes/webpush-go with triple uppercase encoding.

* style: cargo fmt
This commit is contained in:
2026-03-27 21:16:00 +03:00
committed by GitHub
parent a09f83ffdb
commit c8dc141b2f
11 changed files with 751 additions and 36 deletions

View File

@@ -24,22 +24,8 @@ pub fn render_dashboard(data: &DashboardResponse, lang: Lang) -> String {
.registry_stats
.iter()
.map(|r| {
let icon = match r.name.as_str() {
"docker" => icons::DOCKER,
"maven" => icons::MAVEN,
"npm" => icons::NPM,
"cargo" => icons::CARGO,
"pypi" => icons::PYPI,
_ => icons::DOCKER,
};
let display_name = match r.name.as_str() {
"docker" => "Docker",
"maven" => "Maven",
"npm" => "npm",
"cargo" => "Cargo",
"pypi" => "PyPI",
_ => &r.name,
};
let icon = get_registry_icon(&r.name);
let display_name = get_registry_title(&r.name);
render_registry_card(
display_name,
icon,
@@ -155,7 +141,7 @@ pub fn render_dashboard(data: &DashboardResponse, lang: Lang) -> String {
{}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4 mb-6">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3 mb-6">
{}
</div>
@@ -669,6 +655,7 @@ pub fn render_package_detail(
"pip install {} --index-url http://127.0.0.1:4000/simple",
name
),
"go" => format!("GOPROXY=http://127.0.0.1:4000/go go get {}", name),
_ => String::new(),
};
@@ -835,6 +822,7 @@ fn get_registry_icon(registry_type: &str) -> &'static str {
"npm" => icons::NPM,
"cargo" => icons::CARGO,
"pypi" => icons::PYPI,
"go" => icons::GO,
_ => {
r#"<path fill="currentColor" d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>"#
}
@@ -848,6 +836,7 @@ fn get_registry_title(registry_type: &str) -> &'static str {
"npm" => "npm Registry",
"cargo" => "Cargo Registry",
"pypi" => "PyPI Repository",
"go" => "Go Modules",
_ => "Registry",
}
}