Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,10 @@ func runServe() {
logger := setupLogger(cfg.Log.Level, cfg.Log.Format)

// Create and start server
srv, err := server.New(cfg, logger)
srv, err := server.New(cfg, logger, server.BuildInfo{
Version: Version,
Commit: Commit,
})
if err != nil {
logger.Error("failed to create server", "error", err)
os.Exit(1)
Expand Down
4 changes: 4 additions & 0 deletions internal/server/browse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,10 @@ func TestHandleBrowseSourcePage(t *testing.T) {
}
}

if !strings.Contains(body, "proxy test-version (test-commit)") {
t.Error("browse source footer should contain proxy build information, not the package version")
}

// Check that the escapeHTML function is present for XSS protection
if !strings.Contains(body, "function escapeHTML(str)") {
t.Error("browse source page missing escapeHTML function for XSS protection")
Expand Down
15 changes: 11 additions & 4 deletions internal/server/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@ package server

import "net/http"

// Layout carries per-request fields consumed by the shared base template
// (canonical URL, og:url). It is embedded in every page data struct so that
// templates can reference {{.UIBaseURL}} and {{.CanonicalPath}} alongside the
// page's own fields.
// BuildInfo identifies the running proxy binary.
type BuildInfo struct {
Version string
Commit string
}

// Layout carries shared fields consumed by the base template. It is embedded
// in every page data struct so templates can access canonical URL and build
// information alongside the page's own fields.
type Layout struct {
BuildInfo BuildInfo
UIBaseURL string
CanonicalPath string
}

func (s *Server) layoutFor(r *http.Request) Layout {
return Layout{
BuildInfo: s.buildInfo,
UIBaseURL: s.cfg.UIBaseURL,
CanonicalPath: r.URL.Path,
}
Expand Down
4 changes: 3 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ type Server struct {
db *database.DB
storage storage.Storage
logger *slog.Logger
buildInfo BuildInfo
http *http.Server
templates *Templates
cancel context.CancelFunc
Expand All @@ -100,7 +101,7 @@ type Server struct {
}

// New creates a new Server with the given configuration.
func New(cfg *config.Config, logger *slog.Logger) (*Server, error) {
func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server, error) {
var activityLog *accesslog.Logger
if cfg.AccessLog.Path != "" {
var err error
Expand Down Expand Up @@ -169,6 +170,7 @@ func New(cfg *config.Config, logger *slog.Logger) (*Server, error) {
db: db,
storage: store,
logger: logger,
buildInfo: buildInfo,
templates: &Templates{},
healthCache: hc,
accessLog: activityLog,
Expand Down
15 changes: 13 additions & 2 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func newTestServer(t *testing.T) *testServer {
db: db,
storage: store,
logger: logger,
buildInfo: BuildInfo{Version: "test-version", Commit: "test-commit"},
templates: &Templates{},
healthCache: hc,
}
Expand Down Expand Up @@ -313,6 +314,9 @@ func TestDashboard(t *testing.T) {
if !strings.Contains(body, "Cached Artifacts") {
t.Error("dashboard should contain stats")
}
if !strings.Contains(body, "proxy test-version (test-commit)") {
t.Error("dashboard footer should contain build information")
}
if !strings.Contains(body, "Popular Packages") {
t.Error("dashboard should contain popular packages section")
}
Expand Down Expand Up @@ -598,6 +602,9 @@ func TestVersionShowWithHitCount(t *testing.T) {
if !strings.Contains(body, "42 cache hits") {
t.Error("expected page to show hit count")
}
if !strings.Contains(body, "proxy test-version (test-commit)") {
t.Error("version show footer should contain proxy build information, not the package version")
}
}

func TestSearchWithNullValues(t *testing.T) {
Expand Down Expand Up @@ -1327,10 +1334,14 @@ func TestNewServer_StorageConnectivityCheck(t *testing.T) {

logger := slog.New(slog.NewTextHandler(io.Discard, nil))

srv, err := New(cfg, logger)
buildInfo := BuildInfo{Version: "test-version", Commit: "test-commit"}
srv, err := New(cfg, logger, buildInfo)
if err != nil {
t.Fatalf("New() failed: %v", err)
}
if srv.buildInfo != buildInfo {
t.Errorf("build info = %#v, want %#v", srv.buildInfo, buildInfo)
}

// On Windows, OpenBucket normalises to file:///C:/path; on Unix the
// absolute path already starts with /, so file:// + /path == file:///path.
Expand All @@ -1354,7 +1365,7 @@ func TestNewServer_InvalidAccessLogFailsBeforeDatabaseInit(t *testing.T) {
}

logger := slog.New(slog.NewTextHandler(io.Discard, nil))
if _, err := New(cfg, logger); err == nil {
if _, err := New(cfg, logger, BuildInfo{}); err == nil {
t.Fatal("New() succeeded with invalid access log path")
} else if !strings.Contains(err.Error(), "initializing access log") {
t.Fatalf("New() error = %v, want access log initialization error", err)
Expand Down
5 changes: 5 additions & 0 deletions internal/server/templates/layout/footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ <h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">About</h
<i data-lucide="github" class="w-4 h-4"></i><span>github.com/git-pkgs/proxy</span>
</a>
</p>
{{if .BuildInfo.Version}}
<p class="text-xs text-gray-500 dark:text-gray-500 mt-2">
proxy {{.BuildInfo.Version}}{{if .BuildInfo.Commit}} ({{.BuildInfo.Commit}}){{end}}
</p>
{{end}}
</div>
<div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">Resources</h3>
Expand Down
58 changes: 58 additions & 0 deletions internal/server/templates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,64 @@ func TestRenderEmitsCanonicalAndOG(t *testing.T) {
}
}

func TestFooterUsesBuildInfoWhenPageDefinesVersion(t *testing.T) {
templates := &Templates{}
buildInfo := BuildInfo{Version: "proxy-build-1.2.3", Commit: "abc123def"}
wantFooter := "proxy proxy-build-1.2.3 (abc123def)"

tests := []struct {
name string
page string
data any
shadow string
}{
{
name: "version show page",
page: "version_show",
data: VersionShowData{
Layout: Layout{BuildInfo: buildInfo},
Package: &database.Package{
PURL: "pkg:npm/lodash",
Ecosystem: "npm",
Name: "lodash",
},
Version: &database.Version{
PURL: "pkg:npm/lodash@9.9.9",
PackagePURL: "pkg:npm/lodash",
},
},
shadow: "9.9.9",
},
{
name: "browse source page",
page: "browse_source",
data: BrowseSourceData{
Layout: Layout{BuildInfo: buildInfo},
Ecosystem: "npm",
PackageName: "lodash",
Version: "9.9.9",
},
shadow: "9.9.9",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
if err := templates.Render(w, tt.page, tt.data); err != nil {
t.Fatalf("Render(%q) failed: %v", tt.page, err)
}
body := w.Body.String()
if !strings.Contains(body, wantFooter) {
t.Errorf("footer missing build info %q", wantFooter)
}
if strings.Contains(body, "proxy "+tt.shadow) {
t.Errorf("footer used page Version %q instead of BuildInfo", tt.shadow)
}
})
}
}

func TestRenderOmitsCanonicalWhenUIBaseURLUnset(t *testing.T) {
templates := &Templates{}

Expand Down