diff options
| author | Max Resnick <max@ofmax.li> | 2025-09-20 23:59:46 -0700 |
|---|---|---|
| committer | Max Resnick <max@ofmax.li> | 2025-09-20 23:59:46 -0700 |
| commit | de0e66a14419b608f6d81ebd12598fceb07a91ea (patch) | |
| tree | 9d59c9b889c9384d7a02cf358d9d1d4efb950e7a | |
| parent | e81cbe44ae496f32c98011c739718d4df7570f73 (diff) | |
| download | go-git-server-de0e66a14419b608f6d81ebd12598fceb07a91ea.tar.gz | |
fix: some things
| -rw-r--r-- | README.md | 23 | ||||
| -rw-r--r-- | cmd/main.go | 1 | ||||
| -rw-r--r-- | internal/admin/model_test.go | 4 | ||||
| -rw-r--r-- | internal/modules/handler.go | 107 | ||||
| -rw-r--r-- | internal/modules/handler_test.go | 316 | ||||
| -rw-r--r-- | tests/test_gitserver.yaml | 11 |
6 files changed, 433 insertions, 29 deletions
@@ -90,14 +90,37 @@ basepath: ./repos repos: - name: myrepo public: false + go_module: false # Git-only repository permissions: - role: maintainers mode: 1 git_web_config: owner: username description: Repository description +- name: mylib + public: true + go_module: true # Enable Go module proxy for this repo + permissions: + - role: admin + mode: 1 + - role: maintainers + mode: 1 ``` +**Repository Configuration Options:** + +- `go_module: true` - Enables Go module proxy endpoints for `go get` compatibility + - Serves go-import meta tags for module discovery + - Provides module proxy endpoints (@v/list, @latest, .info, .mod, .zip) + - Generates version metadata from git tags +- `go_module: false` - Standard Git repository (default) +- `public: true` - Repository accessible without authentication for read operations +- `public: false` - Requires authentication for all operations + +**Permission modes:** +- `mode: 1` - Read/write access +- `mode: 0` - Read-only access + The server will automatically use the repository directory path as the base path when using the default configuration if the management repository is not found. ### Authentication Model (auth_model.ini) diff --git a/cmd/main.go b/cmd/main.go index 0cd9eec..1619a17 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -110,6 +110,7 @@ func main() { ReadHeaderTimeout: 5 * time.Second, Handler: router, } + slog.Info("running") slog.Error("error while running exiting", slog.Any("error", server.ListenAndServe())) os.Exit(1) } diff --git a/internal/admin/model_test.go b/internal/admin/model_test.go index 082b3f0..d36c9d0 100644 --- a/internal/admin/model_test.go +++ b/internal/admin/model_test.go @@ -146,8 +146,8 @@ func TestLoadServerConfig(t *testing.T) { if err != nil { t.Fatal(err) } - if len(loadedFile.Repos) != 2 { - t.Fatalf("expected to find 2 repos found %d", len(loadedFile.Repos)) + if len(loadedFile.Repos) != 3 { + t.Fatalf("expected to find 3 repos found %d", len(loadedFile.Repos)) } }) diff --git a/internal/modules/handler.go b/internal/modules/handler.go index 9fa1094..c01f716 100644 --- a/internal/modules/handler.go +++ b/internal/modules/handler.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "net/http" + "net/url" "path/filepath" "strings" @@ -18,6 +19,74 @@ type ModuleHandler struct { serverHost string } +// normalizeImportPath creates a properly formatted full import path +func (h *ModuleHandler) normalizeImportPath(modulePath string) (string, error) { + if h.serverHost == "" { + return "", fmt.Errorf("serverHost cannot be empty") + } + if modulePath == "" { + return "", fmt.Errorf("modulePath cannot be empty") + } + if strings.Contains(modulePath, "..") { + return "", fmt.Errorf("modulePath cannot contain '..'") + } + + // Clean the inputs + host := strings.TrimRight(h.serverHost, "/") + module := strings.Trim(modulePath, "/") + + // Validate that module path doesn't contain invalid characters + if strings.ContainsAny(module, " \t\n\r") { + return "", fmt.Errorf("modulePath cannot contain whitespace characters") + } + + return host + "/" + module, nil +} + +// buildRepoURL creates a proper repository URL +func (h *ModuleHandler) buildRepoURL(modulePath string) (string, error) { + _, err := h.normalizeImportPath(modulePath) + if err != nil { + return "", err + } + + // Use proper URL building to ensure valid URLs + repoURL := &url.URL{ + Scheme: "https", + Host: h.serverHost, + Path: "/" + strings.Trim(modulePath, "/"), + } + + return repoURL.String(), nil +} + +// generateGoImportHTML creates the HTML response for go-import requests +func (h *ModuleHandler) generateGoImportHTML(modulePath string) (string, error) { + fullImportPath, err := h.normalizeImportPath(modulePath) + if err != nil { + return "", fmt.Errorf("failed to normalize import path: %w", err) + } + + repoURL, err := h.buildRepoURL(modulePath) + if err != nil { + return "", fmt.Errorf("failed to build repository URL: %w", err) + } + + return fmt.Sprintf(`<!DOCTYPE html> +<html> +<head> + <meta name="go-import" content="%s git %s"> + <meta name="go-source" content="%s %s %s/tree/{/dir} %s/blob/{/dir}/{file}#L{line}"> +</head> +<body> + go get %s +</body> +</html>`, + fullImportPath, repoURL, + fullImportPath, repoURL, repoURL, repoURL, + fullImportPath), nil +} + // NewModuleHandler creates a new module handler with explicit routes for known repos func NewModuleHandler(reposDir, serverHost string, config *admin.ServerRepos) http.Handler { handler := &ModuleHandler{ @@ -266,19 +335,12 @@ func (h *ModuleHandler) handleGoImportForModule(w http.ResponseWriter, r *http.R } // 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) + html, err := h.generateGoImportHTML(modulePath) + if err != nil { + slog.Error("failed to generate go-import HTML", "module", modulePath, "error", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) @@ -300,19 +362,12 @@ func (h *ModuleHandler) handleGoImport(w http.ResponseWriter, r *http.Request) { 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) + html, err := h.generateGoImportHTML(modulePath) + if err != nil { + slog.Error("failed to generate go-import HTML", "module", modulePath, "error", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) diff --git a/internal/modules/handler_test.go b/internal/modules/handler_test.go index b173f98..943c74c 100644 --- a/internal/modules/handler_test.go +++ b/internal/modules/handler_test.go @@ -65,7 +65,7 @@ func TestHandleGoImport_ConfiguredModule(t *testing.T) { } // Check for correct module path and VCS info - expectedContent := `content="mymodule git https://git.example.com/mymodule"` + expectedContent := `content="git.example.com/mymodule git https://git.example.com/mymodule"` if !strings.Contains(body, expectedContent) { t.Errorf("response should contain %s", expectedContent) } @@ -548,3 +548,317 @@ func TestModuleMiddleware(t *testing.T) { } }) } + +// Tests for new helper functions +func TestNormalizeImportPath(t *testing.T) { + handler := &ModuleHandler{ + reposDir: "/tmp/repos", + serverHost: "git.example.com", + } + + tests := []struct { + name string + serverHost string + modulePath string + expected string + shouldErr bool + }{ + { + name: "valid_simple_path", + serverHost: "git.example.com", + modulePath: "mymodule", + expected: "git.example.com/mymodule", + shouldErr: false, + }, + { + name: "trailing_slash_on_host", + serverHost: "git.example.com/", + modulePath: "mymodule", + expected: "git.example.com/mymodule", + shouldErr: false, + }, + { + name: "leading_slash_on_module", + serverHost: "git.example.com", + modulePath: "/mymodule", + expected: "git.example.com/mymodule", + shouldErr: false, + }, + { + name: "nested_module_path", + serverHost: "git.example.com", + modulePath: "company/mymodule", + expected: "git.example.com/company/mymodule", + shouldErr: false, + }, + { + name: "empty_server_host", + serverHost: "", + modulePath: "mymodule", + expected: "", + shouldErr: true, + }, + { + name: "empty_module_path", + serverHost: "git.example.com", + modulePath: "", + expected: "", + shouldErr: true, + }, + { + name: "path_traversal_attempt", + serverHost: "git.example.com", + modulePath: "../../../etc/passwd", + expected: "", + shouldErr: true, + }, + { + name: "whitespace_in_module_path", + serverHost: "git.example.com", + modulePath: "my module", + expected: "", + shouldErr: true, + }, + { + name: "tab_in_module_path", + serverHost: "git.example.com", + modulePath: "my\tmodule", + expected: "", + shouldErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Update handler for this test + handler.serverHost = tt.serverHost + + result, err := handler.normalizeImportPath(tt.modulePath) + + if tt.shouldErr { + if err == nil { + t.Errorf("expected error but got none") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + } + }) + } +} + +func TestBuildRepoURL(t *testing.T) { + handler := &ModuleHandler{ + reposDir: "/tmp/repos", + serverHost: "git.example.com", + } + + tests := []struct { + name string + modulePath string + expected string + shouldErr bool + }{ + { + name: "simple_module", + modulePath: "mymodule", + expected: "https://git.example.com/mymodule", + shouldErr: false, + }, + { + name: "nested_module", + modulePath: "company/mymodule", + expected: "https://git.example.com/company/mymodule", + shouldErr: false, + }, + { + name: "invalid_module_path", + modulePath: "", + expected: "", + shouldErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := handler.buildRepoURL(tt.modulePath) + + if tt.shouldErr { + if err == nil { + t.Errorf("expected error but got none") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + } + }) + } +} + +func TestGenerateGoImportHTML(t *testing.T) { + handler := &ModuleHandler{ + reposDir: "/tmp/repos", + serverHost: "git.example.com", + } + + tests := []struct { + name string + modulePath string + shouldErr bool + expectedHTML []string // Strings that should be present in the HTML + }{ + { + name: "valid_module", + modulePath: "mymodule", + shouldErr: false, + expectedHTML: []string{ + `<meta name="go-import" content="git.example.com/mymodule git https://git.example.com/mymodule">`, + `<meta name="go-source" content="git.example.com/mymodule https://git.example.com/mymodule`, + `go get git.example.com/mymodule`, + }, + }, + { + name: "invalid_module", + modulePath: "", + shouldErr: true, + expectedHTML: nil, + }, + { + name: "module_with_path_traversal", + modulePath: "../../../etc/passwd", + shouldErr: true, + expectedHTML: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := handler.generateGoImportHTML(tt.modulePath) + + if tt.shouldErr { + if err == nil { + t.Errorf("expected error but got none") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + for _, expected := range tt.expectedHTML { + if !strings.Contains(result, expected) { + t.Errorf("HTML should contain %q, got:\n%s", expected, result) + } + } + } + }) + } +} + +// Integration test that simulates the full go-get flow +func TestGoGetIntegration(t *testing.T) { + config := &admin.ServerRepos{ + Repos: []*admin.GitRepo{ + { + Name: "mymodule", + GoModule: true, + }, + }, + } + + handler := NewModuleHandler("/tmp/repos", "git.example.com", config) + + // Test the complete go-get flow: + // 1. Initial request with go-get=1 + // 2. Verify correct meta tags are returned + // 3. Verify import path format is correct + + req := httptest.NewRequest("GET", "/mymodule?go-get=1", nil) + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", w.Code) + } + + body := w.Body.String() + + // Verify the response contains proper go-import directive + expectedImport := `content="git.example.com/mymodule git https://git.example.com/mymodule"` + if !strings.Contains(body, expectedImport) { + t.Errorf("response should contain correct go-import directive: %s\nGot: %s", expectedImport, body) + } + + // Verify the response contains proper go-source directive + expectedSource := `content="git.example.com/mymodule https://git.example.com/mymodule` + if !strings.Contains(body, expectedSource) { + t.Errorf("response should contain correct go-source directive: %s\nGot: %s", expectedSource, body) + } + + // Verify the response contains proper body content + expectedBody := `go get git.example.com/mymodule` + if !strings.Contains(body, expectedBody) { + t.Errorf("response should contain correct body content: %s\nGot: %s", expectedBody, body) + } + + // Verify content type + contentType := w.Header().Get("Content-Type") + if contentType != "text/html; charset=utf-8" { + t.Errorf("expected content type text/html; charset=utf-8, got %s", contentType) + } + + // Test that the import path format matches Go's expectations: + // - Should be domain/path format + // - Should not have protocol prefix in the import path + // - Should use HTTPS for the repository URL + if strings.Contains(body, `content="mymodule git`) { + t.Error("import path should include domain, not just module name") + } + + if strings.Contains(body, `content="https://git.example.com/mymodule git`) { + t.Error("import path should not include protocol") + } +} + +// Test error handling in go-import generation +func TestGoImportErrorHandling(t *testing.T) { + // Test with invalid server configuration + handler := &ModuleHandler{ + reposDir: "/tmp/repos", + serverHost: "", // Invalid empty host + } + + req := httptest.NewRequest("GET", "/mymodule?go-get=1", nil) + w := httptest.NewRecorder() + + handler.handleGoImport(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500 for invalid server config, got %d", w.Code) + } + + // Test with invalid module path characters + handler.serverHost = "git.example.com" + + req = httptest.NewRequest("GET", "/my%20module?go-get=1", nil) + w = httptest.NewRecorder() + + handler.handleGoImport(w, req) + + // Should handle URL-encoded paths gracefully or return appropriate error + if w.Code == http.StatusOK { + body := w.Body.String() + // Verify it doesn't contain malformed content + if strings.Contains(body, "%20") { + t.Error("response should not contain URL-encoded characters in meta tags") + } + } +} diff --git a/tests/test_gitserver.yaml b/tests/test_gitserver.yaml index 70d8eed..4a39bfa 100644 --- a/tests/test_gitserver.yaml +++ b/tests/test_gitserver.yaml @@ -17,3 +17,14 @@ repos: permissions: - role: maintainers mode: 1 +- name: environ + public: true + go_module: true + git_web_config: + owner: grumps + description: >- + A wrapper to git http-backend providing authentcation and authorization + inspired by gitolite. + permissions: + - role: maintainers + mode: 1 |