Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244package gitserver
import ( "context" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strconv" "strings" "testing"
"github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/migrator/config" "tangled.org/core/migrator/db")
func testServer(t *testing.T) (*Server, *db.DB, int64) { t.Helper() database, err := db.Make(context.Background(), filepath.Join(t.TempDir(), "gitserver.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { database.Close() }) _, jobs, _, err := database.CreateBatch(context.Background(), db.CreateBatchInput{ ID: "batch", OwnerDid: "did:plc:alice", RequestID: "request", RequestDigest: "digest", Jobs: []db.CreateJobInput{{Name: "repo", KnotDid: "did:web:knot.example", SourceURL: "https://github.com/alice/repo.git"}}, }) if err != nil { t.Fatal(err) } if err := database.UpdateJobStatus(context.Background(), jobs[0].ID, db.StatusImporting, nil); err != nil { t.Fatal(err) } workDir := t.TempDir() mirror := filepath.Join(workDir, "job-"+strconv.FormatInt(jobs[0].ID, 10), "repo.git") for _, directory := range []string{"objects/info", "objects/pack", "refs/heads", "refs/tags"} { if err := os.MkdirAll(filepath.Join(mirror, directory), 0700); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(mirror, "HEAD"), []byte("ref: refs/heads/main\n"), 0600); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(mirror, "config"), []byte("[core]\nrepositoryformatversion = 0\nbare = true\n"), 0600); err != nil { t.Fatal(err) } cfg := &config.Config{WorkDir: workDir} cfg.ServiceDid, _ = syntax.ParseDID("did:web:migrator.example") return New(database, cfg), database, jobs[0].ID}
func request(server *Server, method, target, contentType string) *httptest.ResponseRecorder { return requestBody(server, method, target, contentType, "")}
func requestBody(server *Server, method, target, contentType, body string) *httptest.ResponseRecorder { req := httptest.NewRequest(method, target, strings.NewReader(body)) if contentType != "" { req.Header.Set("Content-Type", contentType) } rec := httptest.NewRecorder() server.ServeHTTP(rec, req) return rec}
func TestCapabilityMapsOnlyItsExactRepositoryAndRevokesToNotFound(t *testing.T) { server, database, jobID := testServer(t) token := strings.Repeat("a", 43) if err := database.SetJobCapability(context.Background(), jobID, token); err != nil { t.Fatal(err) } path := "/git/" + token + "/repo.git/info/refs?service=git-upload-pack" if rec := request(server, http.MethodGet, path, ""); rec.Code != http.StatusOK { t.Fatalf("active exact capability: got %d", rec.Code) } else if !strings.HasPrefix(rec.Body.String(), "001e# service=git-upload-pack\n0000") { t.Fatalf("invalid smart-http advertisement %q", rec.Body.String()) } v2req := httptest.NewRequest(http.MethodGet, path, nil) v2req.Header.Set("Git-Protocol", "version=2") v2rec := httptest.NewRecorder() server.ServeHTTP(v2rec, v2req) if !strings.Contains(v2rec.Body.String(), "version 2") { t.Fatalf("Git-Protocol was not passed to upload-pack: %q", v2rec.Body.String()) } if rec := request(server, http.MethodGet, "/git/"+token+"/other.git/info/refs?service=git-upload-pack", ""); rec.Code != http.StatusNotFound { t.Fatalf("wrong repository name: got %d", rec.Code) } if err := database.RevokeJobCapability(context.Background(), jobID); err != nil { t.Fatal(err) } if rec := request(server, http.MethodGet, path, ""); rec.Code != http.StatusNotFound { t.Fatalf("revoked capability: got %d", rec.Code) }}
func TestSmartHTTPRejectsTraversalAndUnsupportedOperations(t *testing.T) { server, database, jobID := testServer(t) token := strings.Repeat("b", 43) if err := database.SetJobCapability(context.Background(), jobID, token); err != nil { t.Fatal(err) } paths := []string{ "/git/" + token + "/../repo.git/info/refs?service=git-upload-pack", "/git/" + token + "/repo.git/HEAD", "/git/" + token + "/repo.git/info/refs?service=git-receive-pack", "/git/" + token + "/repo.git/info/refs?service=git-upload-pack&extra=1", } for _, path := range paths { if rec := request(server, http.MethodGet, path, ""); rec.Code != http.StatusNotFound { t.Fatalf("%s: got %d", path, rec.Code) } } postPath := "/git/" + token + "/repo.git/git-upload-pack" if rec := request(server, http.MethodPost, postPath, "application/json"); rec.Code != http.StatusNotFound { t.Fatalf("wrong upload-pack content type: got %d", rec.Code) }}
const lfsOID = "ab12cd34ab12cd34ab12cd34ab12cd34ab12cd34ab12cd34ab12cd34ab12cd34"
func stageLFSObject(t *testing.T, server *Server, database *db.DB, jobID int64) (token, objectPath string) { t.Helper() token = strings.Repeat("c", 43) if err := database.SetJobCapability(context.Background(), jobID, token); err != nil { t.Fatal(err) } mirror := filepath.Join(server.cfg.WorkDir, "job-"+strconv.FormatInt(jobID, 10), "repo.git") objectPath = filepath.Join(mirror, "lfs", "objects", lfsOID[:2], lfsOID[2:4], lfsOID) if err := os.MkdirAll(filepath.Dir(objectPath), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(objectPath, []byte("lfs media bytes"), 0600); err != nil { t.Fatal(err) } return token, objectPath}
func lfsBatchBody(objects string) string { return `{"operation":"download","transfers":["basic"],"objects":[` + objects + `]}`}
func TestLFSBatchServesTheMirrorAndTheObjectDownloads(t *testing.T) { server, database, jobID := testServer(t) token, objectPath := stageLFSObject(t, server, database, jobID) media, err := os.ReadFile(objectPath) if err != nil { t.Fatal(err) } present := `{"oid":"` + lfsOID + `","size":` + strconv.Itoa(len(media)) + `}` absent := `{"oid":"` + strings.Repeat("ef", 32) + `","size":1}` invalid := `{"oid":"../../etc/passwd","size":1}` base := "/git/" + token + "/repo.git"
rec := requestBody(server, http.MethodPost, base+"/info/lfs/objects/batch", lfsContentType, lfsBatchBody(present+","+absent+","+invalid)) if rec.Code != http.StatusOK { t.Fatalf("batch: got %d %s", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Content-Type"); !strings.HasPrefix(got, lfsContentType) { t.Fatalf("batch content type = %q", got) } var payload struct { Objects []struct { Oid string `json:"oid"` Size *int64 `json:"size"` Actions *struct { Download *struct { Href string `json:"href"` } `json:"download"` } `json:"actions"` Error *struct { Code int `json:"code"` Message string `json:"message"` } `json:"error"` } `json:"objects"` } if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatal(err) } if len(payload.Objects) != 3 { t.Fatalf("batch answered %d objects: %s", len(payload.Objects), rec.Body.String()) } found := payload.Objects[0] if found.Oid != lfsOID || found.Error != nil || found.Actions == nil || found.Actions.Download == nil { t.Fatalf("present object answered wrong: %+v", found) } if found.Size == nil || *found.Size != int64(len(media)) { t.Fatalf("present object size = %v, want %d (a knot batch parser requires size on every object)", found.Size, len(media)) } wantHref := "https://migrator.example" + base + "/info/lfs/objects/" + lfsOID if found.Actions.Download.Href != wantHref { t.Fatalf("download href = %q, want %q", found.Actions.Download.Href, wantHref) }
missing := payload.Objects[1] if missing.Oid != strings.Repeat("ef", 32) || missing.Error == nil || missing.Error.Code != http.StatusNotFound { t.Fatalf("absent object answered wrong: %+v", missing) } rejected := payload.Objects[2] if rejected.Error == nil || rejected.Error.Code != http.StatusUnprocessableEntity { t.Fatalf("invalid oid answered wrong: %+v", rejected) } // the knot's batch parser requires size on every answered object, errors included for _, object := range payload.Objects { if object.Size == nil { t.Fatalf("object %q answered without size: %s", object.Oid, rec.Body.String()) } }
objectRec := request(server, http.MethodGet, base+"/info/lfs/objects/"+lfsOID, "") if objectRec.Code != http.StatusOK || objectRec.Body.String() != string(media) { t.Fatalf("object download: got %d %q", objectRec.Code, objectRec.Body.String()) } absentRec := request(server, http.MethodGet, base+"/info/lfs/objects/"+strings.Repeat("ef", 32), "") if absentRec.Code != http.StatusNotFound { t.Fatalf("absent object download: got %d", absentRec.Code) }}
func TestLFSRoutesRejectWrongShapes(t *testing.T) { server, database, jobID := testServer(t) token, _ := stageLFSObject(t, server, database, jobID) base := "/git/" + token + "/repo.git" if rec := requestBody(server, http.MethodGet, base+"/info/lfs/objects/batch", lfsContentType, lfsBatchBody("")); rec.Code != http.StatusNotFound { t.Fatalf("GET batch: got %d", rec.Code) } if rec := requestBody(server, http.MethodPost, base+"/info/lfs/objects/batch", "text/plain", lfsBatchBody("")); rec.Code != http.StatusNotFound { t.Fatalf("batch with a non-lfs content type: got %d", rec.Code) } if rec := requestBody(server, http.MethodPost, base+"/info/lfs/objects/batch", lfsContentType, `{"operation":"upload","objects":[]}`); rec.Code != http.StatusBadRequest { t.Fatalf("upload batch: got %d", rec.Code) } if rec := requestBody(server, http.MethodPost, base+"/info/lfs/objects/"+lfsOID, lfsContentType, lfsBatchBody("")); rec.Code != http.StatusNotFound { t.Fatalf("POST object: got %d", rec.Code) } if rec := request(server, http.MethodGet, base+"/info/lfs/objects/"+strings.Repeat("z", 64), ""); rec.Code != http.StatusNotFound { t.Fatalf("non-oid path: got %d", rec.Code) } if rec := request(server, http.MethodGet, "/git/"+token+"/../repo.git/info/lfs/objects/"+lfsOID, ""); rec.Code != http.StatusNotFound { t.Fatalf("traversal name: got %d", rec.Code) }}