aboutsummaryrefslogtreecommitdiff
path: root/internal/modules/handler.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/modules/handler.go')
-rw-r--r--internal/modules/handler.go473
1 files changed, 473 insertions, 0 deletions
diff --git a/internal/modules/handler.go b/internal/modules/handler.go
new file mode 100644
index 0000000..9fa1094
--- /dev/null
+++ b/internal/modules/handler.go
@@ -0,0 +1,473 @@
+package modules
+
+import (
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "net/http"
+ "path/filepath"
+ "strings"
+
+ "git.ofmax.li/go-git-server/internal/admin"
+ "github.com/go-chi/chi/v5"
+)
+
+// ModuleHandler handles Go module proxy requests and go-import metadata
+type ModuleHandler struct {
+ reposDir string
+ serverHost string
+}
+
+// NewModuleHandler creates a new module handler with explicit routes for known repos
+func NewModuleHandler(reposDir, serverHost string, config *admin.ServerRepos) http.Handler {
+ handler := &ModuleHandler{
+ reposDir: reposDir,
+ serverHost: serverHost,
+ }
+
+ r := chi.NewRouter()
+
+ if config == nil {
+ slog.Warn("no server config provided, falling back to catch-all routing")
+ r.Get("/*", handler.handleAllRequests)
+ return r
+ }
+
+ // Register explicit routes only for repositories configured as Go modules
+ for _, repo := range config.Repos {
+ if !repo.GoModule {
+ slog.Debug("skipping non-Go module repo", "repo", repo.Name)
+ continue
+ }
+
+ // Use repo name as module path
+ modulePath := repo.Name
+
+ r.Get("/"+modulePath+"/@v/list", handler.createVersionListHandler(modulePath))
+ r.Get("/"+modulePath+"/@v/*", handler.createGenericVersionHandler(modulePath))
+ r.Get("/"+modulePath+"/@latest", handler.createLatestVersionHandler(modulePath))
+ r.Get("/"+modulePath, handler.createGoImportHandler(modulePath))
+ r.Get("/"+modulePath+"/", handler.createGoImportHandler(modulePath))
+
+ slog.Debug("registered Go module routes", "module", modulePath)
+ }
+
+ return r
+}
+
+// Handler creators that capture the module path
+func (h *ModuleHandler) createVersionListHandler(modulePath string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.handleVersionListForModule(w, r, modulePath)
+ }
+}
+
+func (h *ModuleHandler) createLatestVersionHandler(modulePath string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.handleLatestVersionForModule(w, r, modulePath)
+ }
+}
+
+func (h *ModuleHandler) createGoImportHandler(modulePath string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ h.handleGoImportForModule(w, r, modulePath)
+ }
+}
+
+func (h *ModuleHandler) createGenericVersionHandler(modulePath string) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path
+
+ if strings.HasSuffix(path, ".info") {
+ version := ExtractVersion(path)
+ h.handleVersionInfoForModule(w, r, modulePath, version)
+ } else if strings.HasSuffix(path, ".mod") {
+ version := ExtractVersion(path)
+ h.handleModFileForModule(w, r, modulePath, version)
+ } else if strings.HasSuffix(path, ".zip") {
+ version := ExtractVersion(path)
+ h.handleModuleZipForModule(w, r, modulePath, version)
+ } else {
+ http.NotFound(w, r)
+ }
+ }
+}
+
+// handleAllRequests routes to the appropriate handler based on the URL path
+func (h *ModuleHandler) handleAllRequests(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path
+
+ // Route to specific handlers based on path patterns
+ if strings.HasSuffix(path, "/@v/list") {
+ h.handleVersionList(w, r)
+ return
+ }
+
+ if strings.HasSuffix(path, "/@latest") {
+ h.handleLatestVersion(w, r)
+ return
+ }
+
+ if strings.Contains(path, "/@v/") {
+ if strings.HasSuffix(path, ".info") {
+ h.handleVersionInfo(w, r)
+ return
+ }
+ if strings.HasSuffix(path, ".mod") {
+ h.handleModFile(w, r)
+ return
+ }
+ if strings.HasSuffix(path, ".zip") {
+ h.handleModuleZip(w, r)
+ return
+ }
+ }
+
+ // Default to go-import handler for all other requests
+ h.handleGoImport(w, r)
+}
+
+// New handler methods that accept module path as parameter
+func (h *ModuleHandler) handleVersionListForModule(w http.ResponseWriter, r *http.Request, modulePath string) {
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ versions, err := h.getVersions(repoPath)
+ if err != nil {
+ slog.Error("failed to get versions", "module", modulePath, "error", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+
+ for _, version := range versions {
+ fmt.Fprintln(w, version)
+ }
+
+ slog.Debug("served version list", "module", modulePath, "count", len(versions))
+}
+
+func (h *ModuleHandler) handleLatestVersionForModule(w http.ResponseWriter, r *http.Request, modulePath string) {
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ version, err := h.getLatestVersion(repoPath)
+ if err != nil {
+ slog.Error("failed to get latest version", "module", modulePath, "error", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ timestamp, err := h.getVersionTimestamp(repoPath, version)
+ if err != nil {
+ slog.Error("failed to get version timestamp", "module", modulePath, "version", version, "error", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ info := VersionInfo{
+ Version: version,
+ Time: timestamp,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ if err := json.NewEncoder(w).Encode(info); err != nil {
+ slog.Error("failed to encode JSON response", "error", err)
+ }
+
+ slog.Debug("served latest version", "module", modulePath, "version", version)
+}
+
+func (h *ModuleHandler) handleVersionInfoForModule(w http.ResponseWriter, r *http.Request, modulePath, version string) {
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ timestamp, err := h.getVersionTimestamp(repoPath, version)
+ if err != nil {
+ slog.Error("failed to get version timestamp", "module", modulePath, "version", version, "error", err)
+ // Check if it's a repository access issue (500) vs version not found (404)
+ if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "failed to open repository") {
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ } else {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ }
+ return
+ }
+
+ info := VersionInfo{
+ Version: version,
+ Time: timestamp,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ if err := json.NewEncoder(w).Encode(info); err != nil {
+ slog.Error("failed to encode JSON response", "error", err)
+ }
+
+ slog.Debug("served version info", "module", modulePath, "version", version)
+}
+
+func (h *ModuleHandler) handleModFileForModule(w http.ResponseWriter, r *http.Request, modulePath, version string) {
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ modContent, err := h.getModFile(repoPath, version)
+ if err != nil {
+ slog.Error("failed to get mod file", "module", modulePath, "version", version, "error", err)
+ // Check if it's a repository access issue (500) vs version not found (404)
+ if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "failed to open repository") {
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ } else {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ }
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ if _, err := w.Write(modContent); err != nil {
+ slog.Error("failed to write mod file response", "error", err)
+ }
+
+ slog.Debug("served mod file", "module", modulePath, "version", version)
+}
+
+func (h *ModuleHandler) handleModuleZipForModule(w http.ResponseWriter, r *http.Request, modulePath, version string) {
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ zipData, err := h.getModuleZip(repoPath, version)
+ if err != nil {
+ slog.Error("failed to get module zip", "module", modulePath, "version", version, "error", err)
+ // Check if it's a repository access issue (500) vs version not found (404)
+ if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "failed to open repository") {
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ } else {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ }
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/zip")
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s@%s.zip",
+ strings.ReplaceAll(modulePath, "/", "-"), version))
+ w.WriteHeader(http.StatusOK)
+ if _, err := w.Write(zipData); err != nil {
+ slog.Error("failed to write zip response", "error", err)
+ }
+
+ slog.Debug("served module zip", "module", modulePath, "version", version, "size", len(zipData))
+}
+
+func (h *ModuleHandler) handleGoImportForModule(w http.ResponseWriter, r *http.Request, modulePath string) {
+ // Only handle if go-get=1 parameter is present
+ if r.URL.Query().Get("go-get") != "1" {
+ http.NotFound(w, r)
+ return
+ }
+
+ // Generate HTML with go-import meta tag
+ html := fmt.Sprintf(`<!DOCTYPE html>
+<html>
+<head>
+ <meta name="go-import" content="%s git https://%s/%s">
+ <meta name="go-source" content="%s https://%s/%s https://%s/%s/tree/{/dir} https://%s/%s/blob/{/dir}/{file}#L{line}">
+</head>
+<body>
+ go get %s
+</body>
+</html>`,
+ modulePath, h.serverHost, modulePath,
+ modulePath, h.serverHost, modulePath, h.serverHost, modulePath, h.serverHost, modulePath,
+ modulePath)
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ if _, err := w.Write([]byte(html)); err != nil {
+ slog.Error("failed to write go-import response", "error", err)
+ }
+
+ slog.Debug("served go-import", "module", modulePath)
+}
+
+// handleGoImport serves the go-import meta tag for module discovery
+func (h *ModuleHandler) handleGoImport(w http.ResponseWriter, r *http.Request) {
+ // Only handle if go-get=1 parameter is present
+ if r.URL.Query().Get("go-get") != "1" {
+ http.NotFound(w, r)
+ return
+ }
+
+ modulePath := ExtractModulePath(r.URL.Path)
+
+ // Generate HTML with go-import meta tag
+ html := fmt.Sprintf(`<!DOCTYPE html>
+<html>
+<head>
+ <meta name="go-import" content="%s git https://%s/%s">
+ <meta name="go-source" content="%s https://%s/%s https://%s/%s/tree/{/dir} https://%s/%s/blob/{/dir}/{file}#L{line}">
+</head>
+<body>
+ go get %s
+</body>
+</html>`,
+ modulePath, h.serverHost, modulePath,
+ modulePath, h.serverHost, modulePath, h.serverHost, modulePath, h.serverHost, modulePath,
+ modulePath)
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ if _, err := w.Write([]byte(html)); err != nil {
+ slog.Error("failed to write go-import response", "error", err)
+ }
+
+ slog.Debug("served go-import", "module", modulePath)
+}
+
+// handleVersionList returns a list of available versions
+func (h *ModuleHandler) handleVersionList(w http.ResponseWriter, r *http.Request) {
+ modulePath := ExtractModulePath(r.URL.Path)
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ versions, err := h.getVersions(repoPath)
+ if err != nil {
+ slog.Error("failed to get versions", "module", modulePath, "error", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+
+ for _, version := range versions {
+ fmt.Fprintln(w, version)
+ }
+
+ slog.Debug("served version list", "module", modulePath, "count", len(versions))
+}
+
+// handleLatestVersion returns the latest version information
+func (h *ModuleHandler) handleLatestVersion(w http.ResponseWriter, r *http.Request) {
+ modulePath := ExtractModulePath(r.URL.Path)
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ version, err := h.getLatestVersion(repoPath)
+ if err != nil {
+ slog.Error("failed to get latest version", "module", modulePath, "error", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ timestamp, err := h.getVersionTimestamp(repoPath, version)
+ if err != nil {
+ slog.Error("failed to get version timestamp", "module", modulePath, "version", version, "error", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ info := VersionInfo{
+ Version: version,
+ Time: timestamp,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ if err := json.NewEncoder(w).Encode(info); err != nil {
+ slog.Error("failed to encode JSON response", "error", err)
+ }
+
+ slog.Debug("served latest version", "module", modulePath, "version", version)
+}
+
+// handleVersionInfo returns version metadata
+func (h *ModuleHandler) handleVersionInfo(w http.ResponseWriter, r *http.Request) {
+ modulePath := ExtractModulePath(r.URL.Path)
+ version := ExtractVersion(r.URL.Path)
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ timestamp, err := h.getVersionTimestamp(repoPath, version)
+ if err != nil {
+ slog.Error("failed to get version timestamp", "module", modulePath, "version", version, "error", err)
+ // Check if it's a repository access issue (500) vs version not found (404)
+ if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "failed to open repository") {
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ } else {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ }
+ return
+ }
+
+ info := VersionInfo{
+ Version: version,
+ Time: timestamp,
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ if err := json.NewEncoder(w).Encode(info); err != nil {
+ slog.Error("failed to encode JSON response", "error", err)
+ }
+
+ slog.Debug("served version info", "module", modulePath, "version", version)
+}
+
+// handleModFile returns the go.mod file for a specific version
+func (h *ModuleHandler) handleModFile(w http.ResponseWriter, r *http.Request) {
+ modulePath := ExtractModulePath(r.URL.Path)
+ version := ExtractVersion(r.URL.Path)
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ modContent, err := h.getModFile(repoPath, version)
+ if err != nil {
+ slog.Error("failed to get mod file", "module", modulePath, "version", version, "error", err)
+ // Check if it's a repository access issue (500) vs version not found (404)
+ if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "failed to open repository") {
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ } else {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ }
+ return
+ }
+
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ if _, err := w.Write(modContent); err != nil {
+ slog.Error("failed to write mod file response", "error", err)
+ }
+
+ slog.Debug("served mod file", "module", modulePath, "version", version)
+}
+
+// handleModuleZip returns a zip archive of the module source
+func (h *ModuleHandler) handleModuleZip(w http.ResponseWriter, r *http.Request) {
+ modulePath := ExtractModulePath(r.URL.Path)
+ version := ExtractVersion(r.URL.Path)
+ repoPath := filepath.Join(h.reposDir, modulePath+".git")
+
+ zipData, err := h.getModuleZip(repoPath, version)
+ if err != nil {
+ slog.Error("failed to get module zip", "module", modulePath, "version", version, "error", err)
+ // Check if it's a repository access issue (500) vs version not found (404)
+ if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "failed to open repository") {
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ } else {
+ http.Error(w, "Not Found", http.StatusNotFound)
+ }
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/zip")
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s@%s.zip",
+ strings.ReplaceAll(modulePath, "/", "-"), version))
+ w.WriteHeader(http.StatusOK)
+ if _, err := w.Write(zipData); err != nil {
+ slog.Error("failed to write zip response", "error", err)
+ }
+
+ slog.Debug("served module zip", "module", modulePath, "version", version, "size", len(zipData))
+}
+
+// VersionInfo represents module version metadata
+type VersionInfo struct {
+ Version string `json:"Version"`
+ Time string `json:"Time"`
+}