diff options
Diffstat (limited to 'internal/modules/handler.go')
| -rw-r--r-- | internal/modules/handler.go | 473 |
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"` +} |