caddy-git-server
Provides a git_server caddy module for serving git repositories.
package gitserver
import (
"fmt"
"net/http"
"path/filepath"
"strings"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"go.uber.org/zap"
)
// Serve a git client
func (gs *GitServer) serveGitClient(repoPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
// Only dumb protocol is implemented at the moment
return gs.serveGitDumb(repoPath, w, r, next)
}
// Serve dumb git client files. These are generated on-the-fly
func (gs *GitServer) serveGitDumb(repoPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
// repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// root := repl.ReplaceAll(gs.Root, ".")
// Detect 'info/refs' and generate and serve
if strings.HasSuffix(r.URL.Path, "info/refs") {
// Try to open repo
repo, err := git.PlainOpen(repoPath)
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, fmt.Errorf("could not load repository"))
}
// Log the clone attempt
gs.logger.Info("git clone attempt",
zap.String("path", r.RequestURI),
zap.String("git_repo", repoPath),
zap.String("git_protocol", r.Header.Get("Git-Protocol")),
zap.String("git_client", r.UserAgent()),
)
var refs []string
// Collect all heads in repo
repoHeads, err := repo.Branches()
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
// Write heads to connection
repoHeads.ForEach(func(r *plumbing.Reference) error {
fmt.Fprintf(w, "%s\t%s\n", r.Hash().String(), r.Name().String())
refs = append(refs, r.String())
return nil
})
// Collect all tags in repo
repoTags, err := repo.Tags()
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
// Write tags to connection
repoTags.ForEach(func(r *plumbing.Reference) error {
fmt.Fprintf(w, "%s\t%s\n", r.Hash().String(), r.Name().String())
refs = append(refs, r.String())
return nil
})
gs.logger.Debug("generating dumb info/refs",
zap.String("git_repo", repoPath),
zap.String("req_path", r.URL.Path),
zap.String("refs", strings.Join(refs, ",")),
)
// The approach below is without a git library //
// // Find all ref files in GIT_DIR/refs/*/*
// refPath := filepath.Join(gitDir, "refs/*/*")
// refFiles, err := filepath.Glob(refPath)
// if err != nil {
// return err
// }
// // Generate 'info/refs'
// var infoRefs string
// for _, s := range refFiles {
// refDirs, refName := filepath.Split(s)
// _, refDir := filepath.Split(strings.TrimSuffix(refDirs, "/"))
// refName = filepath.Join("refs", refDir, refName)
// refHash, err := os.ReadFile(s)
// if err != nil {
// return err
// }
// infoRefs += strings.TrimSpace(string(refHash)) + "\t" + refName + "\n"
// }
// // Write info/refs to connection and close it
// fmt.Fprintf(w, "%s", infoRefs)
// //
return nil
}
// Detect 'objects/info/packs' and generate and serve
if strings.HasSuffix(r.URL.Path, "objects/info/packs") {
// Try to open repo
_, err := git.PlainOpen(repoPath)
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
// Get packs in repo
packFiles, err := filepath.Glob(filepath.Join(repoPath, "objects/pack/*.pack"))
if err != nil {
return caddyhttp.Error(http.StatusInternalServerError, err)
}
// Write pack file response
for _, packFile := range packFiles {
fmt.Fprintf(w, "P %s\n", filepath.Base(packFile))
}
return nil
}
// Serve the file if it exists
return gs.FileServer.ServeHTTP(w, r, next)
}