penguin/flatgit

flatgit showcase

internal/server/git_http.go

raw ยท 4014 bytes

package server

import (
	"bytes"
	"log/slog"
	"net/http"
	"os"
	"os/exec"
	"strings"

	"github.com/tgckpg/flatgit/internal/config"
)

type gitHTTP struct {
	gitCommand string
	logger     *slog.Logger

	// Exact HTTP repository base path -> bare mirror directory.
	//
	// Example:
	//   /penguin/flatgit -> /var/lib/flatgit/repos/...
	repos map[string]string
}

func newGitHTTP(
	gitCommand string,
	repos []config.Repo,
	logger *slog.Logger,
) *gitHTTP {
	if gitCommand == "" {
		gitCommand = "git"
	}

	g := &gitHTTP{
		gitCommand: gitCommand,
		logger:     logger,
		repos:      make(map[string]string, len(repos)*2),
	}

	for _, repo := range repos {
		base := strings.TrimSuffix(repo.RepoBase(), "/")

		g.logger.Info(
			"register git repository",
			"base", base,
			"mirror", repo.MirrorDir,
		)

		g.repos[base] = repo.MirrorDir

		// Supporting the conventional .git form is basically free.
		g.repos[base+".git"] = repo.MirrorDir
	}

	return g
}

// ServeHTTP handles only the two endpoints needed for read-only Smart HTTP.
//
// Returns true if the request belonged to the Git transport.
// Everything else should fall through to Flatgit's static server.
func (g *gitHTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) bool {
	path := strings.TrimSuffix(r.URL.Path, "/")

	/*
		GET /owner/repo/info/refs?service=git-upload-pack
	*/
	if r.Method == http.MethodGet &&
		strings.HasSuffix(path, "/info/refs") {

		base := strings.TrimSuffix(path, "/info/refs")
		mirror, ok := g.repos[base]

		g.logger.Info(
			"git info refs lookup",
			"path", path,
			"base", base,
			"found", ok,
			"mirror", mirror,
		)

		if !ok {
			return false
		}

		if r.URL.Query().Get("service") != "git-upload-pack" {
			http.Error(w, "unsupported git service", http.StatusForbidden)
			return true
		}

		g.infoRefs(w, r, mirror)
		return true
	}

	/*
		POST /owner/repo/git-upload-pack
	*/
	if r.Method == http.MethodPost &&
		strings.HasSuffix(path, "/git-upload-pack") {

		base := strings.TrimSuffix(path, "/git-upload-pack")
		mirror, ok := g.repos[base]
		if !ok {
			return false
		}

		g.uploadPack(w, r, mirror)
		return true
	}

	return false
}

func (g *gitHTTP) command(r *http.Request, args ...string) *exec.Cmd {
	cmd := exec.CommandContext(r.Context(), g.gitCommand, args...)

	cmd.Env = append(
		os.Environ(),

		// upload-pack is read-only. Don't let Git perform optional
		// repository maintenance or locking as a side effect.
		"GIT_OPTIONAL_LOCKS=0",
	)

	// Git HTTP protocol negotiation, particularly protocol v2.
	if protocol := r.Header.Get("Git-Protocol"); protocol != "" {
		cmd.Env = append(cmd.Env, "GIT_PROTOCOL="+protocol)
	}

	return cmd
}

func (g *gitHTTP) infoRefs(
	w http.ResponseWriter,
	r *http.Request,
	mirror string,
) {
	cmd := g.command(
		r,
		"upload-pack",
		"--strict",
		"--http-backend-info-refs",
		mirror,
	)

	var stderr bytes.Buffer

	cmd.Stdout = w
	cmd.Stderr = &stderr

	w.Header().Set(
		"Content-Type",
		"application/x-git-upload-pack-advertisement",
	)
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Pragma", "no-cache")
	w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")

	if err := cmd.Run(); err != nil {
		g.logger.Error(
			"git upload-pack advertisement failed",
			"repo", mirror,
			"error", err,
			"stderr", strings.TrimSpace(stderr.String()),
		)
	}
}

func (g *gitHTTP) uploadPack(
	w http.ResponseWriter,
	r *http.Request,
	mirror string,
) {
	cmd := g.command(
		r,
		"upload-pack",
		"--strict",
		"--stateless-rpc",
		mirror,
	)

	var stderr bytes.Buffer

	cmd.Stdin = r.Body
	cmd.Stdout = w
	cmd.Stderr = &stderr

	w.Header().Set(
		"Content-Type",
		"application/x-git-upload-pack-result",
	)
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Pragma", "no-cache")
	w.Header().Set("Expires", "Fri, 01 Jan 1980 00:00:00 GMT")

	if err := cmd.Run(); err != nil {
		g.logger.Error(
			"git upload-pack failed",
			"repo", mirror,
			"error", err,
			"stderr", strings.TrimSpace(stderr.String()),
		)
	}
}