aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Resnick <max@ofmax.li>2025-09-20 23:59:46 -0700
committerMax Resnick <max@ofmax.li>2025-09-20 23:59:46 -0700
commitde0e66a14419b608f6d81ebd12598fceb07a91ea (patch)
tree9d59c9b889c9384d7a02cf358d9d1d4efb950e7a
parente81cbe44ae496f32c98011c739718d4df7570f73 (diff)
downloadgo-git-server-de0e66a14419b608f6d81ebd12598fceb07a91ea.tar.gz
fix: some things
-rw-r--r--README.md23
-rw-r--r--cmd/main.go1
-rw-r--r--internal/admin/model_test.go4
-rw-r--r--internal/modules/handler.go107
-rw-r--r--internal/modules/handler_test.go316
-rw-r--r--tests/test_gitserver.yaml11
6 files changed, 433 insertions, 29 deletions
diff --git a/README.md b/README.md
index 848a46e..a543923 100644
--- a/README.md
+++ b/README.md
@@ -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