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

@@ -87,6 +87,8 @@ pub fn routes() -> Router<Arc<AppState>> {
.route("/ui/cargo/{name}", get(cargo_detail))
.route("/ui/pypi", get(pypi_list))
.route("/ui/pypi/{name}", get(pypi_detail))
.route("/ui/go", get(go_list))
.route("/ui/go/{*name}", get(go_detail))
// API endpoints for HTMX
.route("/api/ui/stats", get(api_stats))
.route("/api/ui/dashboard", get(api_dashboard))
@@ -298,3 +300,41 @@ async fn pypi_detail(
let detail = get_pypi_detail(&state.storage, &name).await;
Html(render_package_detail("pypi", &name, &detail, lang))
}
// Go pages
async fn go_list(
State(state): State<Arc<AppState>>,
Query(query): Query<ListQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang_from_list(&query, headers.get("cookie").and_then(|v| v.to_str().ok()));
let page = query.page.unwrap_or(1).max(1);
let limit = query.limit.unwrap_or(DEFAULT_PAGE_SIZE).min(100);
let all_modules = state.repo_index.get("go", &state.storage).await;
let (modules, total) = paginate(&all_modules, page, limit);
Html(render_registry_list_paginated(
"go",
"Go Modules",
&modules,
page,
limit,
total,
lang,
))
}
async fn go_detail(
State(state): State<Arc<AppState>>,
Path(name): Path<String>,
Query(query): Query<LangQuery>,
headers: axum::http::HeaderMap,
) -> impl IntoResponse {
let lang = extract_lang(
&Query(query),
headers.get("cookie").and_then(|v| v.to_str().ok()),
);
let detail = get_go_detail(&state.storage, &name).await;
Html(render_package_detail("go", &name, &detail, lang))
}