Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234package gitserver
import ( "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "regexp" "strings"
"tangled.org/core/migrator/config" "tangled.org/core/migrator/db" migratorgit "tangled.org/core/migrator/git")
const lfsContentType = "application/vnd.git-lfs+json"
var lfsOIDPattern = regexp.MustCompile(`^[a-f0-9]{64}$`)
var tokenPattern = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
type Server struct { db *db.DB cfg *config.Config}
func New(database *db.DB, cfg *config.Config) *Server { return &Server{db: database, cfg: cfg}}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { token, name, operation, ok := parsePath(r.URL.Path) if !ok || !tokenPattern.MatchString(token) { http.NotFound(w, r) return } job, err := s.db.ResolveJobCapability(r.Context(), token, name) if err != nil { http.NotFound(w, r) return }
mirror := filepath.Join(s.cfg.WorkDir, fmt.Sprintf("job-%d", job.ID), "repo.git") switch { case operation == "info/refs": services := r.URL.Query()["service"] if r.Method != http.MethodGet || len(services) != 1 || services[0] != "git-upload-pack" || len(r.URL.Query()) != 1 { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("001e# service=git-upload-pack\n0000")) if err := s.uploadPack(r, w, mirror, true); err != nil { return } case operation == "git-upload-pack": if r.Method != http.MethodPost || r.Header.Get("Content-Type") != "application/x-git-upload-pack-request" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/x-git-upload-pack-result") w.Header().Set("Cache-Control", "no-store") if err := s.uploadPack(r, w, mirror, false); err != nil { return } case operation == "lfs-batch": if r.Method != http.MethodPost || !lfsJSON(r.Header.Get("Content-Type")) { http.NotFound(w, r) return } s.lfsBatch(w, r, token, name, mirror) // the download route carries the oid itself as its operation case lfsOIDPattern.MatchString(operation): if r.Method != http.MethodGet { http.NotFound(w, r) return } s.lfsObject(w, r, mirror, operation) default: http.NotFound(w, r) }}
func parsePath(path string) (token, name, operation string, ok bool) { parts := strings.Split(path, "/") if len(parts) < 5 || parts[0] != "" || parts[1] != "git" { return "", "", "", false } token = parts[2] if !strings.HasSuffix(parts[3], ".git") { return "", "", "", false } name = strings.TrimSuffix(parts[3], ".git") rest := strings.Join(parts[4:], "/") switch { case rest == "info/refs" || rest == "git-upload-pack": operation = rest case rest == "info/lfs/objects/batch": operation = "lfs-batch" case strings.HasPrefix(rest, "info/lfs/objects/"): oid := strings.TrimPrefix(rest, "info/lfs/objects/") if !lfsOIDPattern.MatchString(oid) { return "", "", "", false } operation = oid default: return "", "", "", false } if name == "" || strings.ContainsAny(name, `/\\`) || strings.Contains(name, "..") { return "", "", "", false } return token, name, operation, true}
func lfsJSON(contentType string) bool { return strings.HasPrefix(contentType, lfsContentType) || strings.HasPrefix(contentType, "application/json")}
func lfsObjectPath(mirror, oid string) string { return filepath.Join(mirror, "lfs", "objects", oid[:2], oid[2:4], oid)}
type lfsBatchRequest struct { Operation string `json:"operation"` Objects []lfsOid `json:"objects"`}
type lfsOid struct { Oid string `json:"oid"` Size int64 `json:"size"`}
type lfsObject struct { Oid string `json:"oid"` // knot batch parser requires size on every object Size int64 `json:"size"` Authenticated bool `json:"authenticated,omitempty"` Actions map[string]lfsAction `json:"actions,omitempty"` Error *lfsObjectError `json:"error,omitempty"`}
type lfsAction struct { Href string `json:"href"` Header map[string]string `json:"header,omitempty"`}
type lfsObjectError struct { Code int `json:"code"` Message string `json:"message"`}
func resolveLFSObject(mirror, hrefBase string, ref lfsOid) lfsObject { if !lfsOIDPattern.MatchString(ref.Oid) { return lfsObject{Oid: ref.Oid, Error: &lfsObjectError{Code: 422, Message: "invalid oid"}} } st, err := os.Stat(lfsObjectPath(mirror, ref.Oid)) if err != nil { return lfsObject{Oid: ref.Oid, Error: &lfsObjectError{Code: 404, Message: "object not found in the import"}} } return lfsObject{ Oid: ref.Oid, Size: st.Size(), Authenticated: true, Actions: map[string]lfsAction{"download": {Href: hrefBase + ref.Oid}}, }}
func (s *Server) lfsBatch(w http.ResponseWriter, r *http.Request, token, name, mirror string) { body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 4<<20)) if err != nil { http.Error(w, "unreadable batch request", http.StatusBadRequest) return } var batch lfsBatchRequest if err := json.Unmarshal(body, &batch); err != nil || batch.Operation != "download" { http.Error(w, "only a download batch is supported", http.StatusBadRequest) return }
hrefBase := strings.TrimSuffix(s.cfg.ServiceURL(), "/") + fmt.Sprintf("/git/%s/%s.git/info/lfs/objects/", token, name) objects := make([]lfsObject, len(batch.Objects)) for i, ref := range batch.Objects { objects[i] = resolveLFSObject(mirror, hrefBase, ref) }
w.Header().Set("Content-Type", lfsContentType) w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(struct { Transfer string `json:"transfer,omitempty"` Objects []lfsObject `json:"objects"` }{Objects: objects})}
func (s *Server) lfsObject(w http.ResponseWriter, r *http.Request, mirror, oid string) { f, err := os.Open(lfsObjectPath(mirror, oid)) if err != nil { http.NotFound(w, r) return } defer f.Close() st, err := f.Stat() if err != nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/octet-stream") http.ServeContent(w, r, oid, st.ModTime(), f)}
func (s *Server) uploadPack(r *http.Request, w http.ResponseWriter, mirror string, advertise bool) error { args := []string{"upload-pack", "--stateless-rpc"} if advertise { args = append(args, "--advertise-refs") } args = append(args, mirror) cmd := exec.CommandContext(r.Context(), "git", args...) cmd.Env = migratorgit.HardenedGitEnv("", "", s.cfg.WorkDir, "", "") if protocol := r.Header.Get("Git-Protocol"); protocol != "" { cmd.Env = append(cmd.Env, "GIT_PROTOCOL="+protocol) } if !advertise { cmd.Stdin = http.MaxBytesReader(w, r.Body, 64<<20) } cmd.Stdout = w return cmd.Run()}