37 "github.com/ThomsonReutersEikon/go-ntlm/ntlm"
42 largeObjects = newLfsStorage()
43 server *httptest.Server
44 serverTLS *httptest.Server
45 serverClientCert *httptest.Server
47 // maps OIDs to content strings. Both the LFS and Storage test servers below
49 oidHandlers map[string]string
51 // These magic strings tell the test lfs server change their behavior so the
52 // integration tests can check those use cases. Tests will create objects with
53 // the magic strings as the contents.
55 // printf "status:lfs:404" > 404.dat
57 contentHandlers = []string{
58 "status-batch-403", "status-batch-404", "status-batch-410", "status-batch-422", "status-batch-500",
59 "status-storage-403", "status-storage-404", "status-storage-410", "status-storage-422", "status-storage-500", "status-storage-503",
60 "status-batch-resume-206", "batch-resume-fail-fallback", "return-expired-action", "return-expired-action-forever", "return-invalid-size",
61 "object-authenticated", "storage-download-retry", "storage-upload-retry", "unknown-oid",
62 "send-verify-action", "send-deprecated-links",
67 repoDir = os.Getenv("LFSTEST_DIR")
69 mux := http.NewServeMux()
70 server = httptest.NewServer(mux)
71 serverTLS = httptest.NewTLSServer(mux)
72 serverClientCert = httptest.NewUnstartedServer(mux)
74 //setup Client Cert server
75 rootKey, rootCert := generateCARootCertificates()
76 _, clientCertPEM, clientKeyPEM := generateClientCertificates(rootCert, rootKey)
78 certPool := x509.NewCertPool()
79 certPool.AddCert(rootCert)
81 serverClientCert.TLS = &tls.Config{
82 Certificates: []tls.Certificate{serverTLS.TLS.Certificates[0]},
83 ClientAuth: tls.RequireAndVerifyClientCert,
86 serverClientCert.StartTLS()
88 ntlmSession, err := ntlm.CreateServerSession(ntlm.Version2, ntlm.ConnectionOrientedMode)
90 fmt.Println("Error creating ntlm session:", err)
93 ntlmSession.SetUserInfo("ntlmuser", "ntlmpass", "NTLMDOMAIN")
95 stopch := make(chan bool)
97 mux.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
101 mux.HandleFunc("/storage/", storageHandler)
102 mux.HandleFunc("/verify", verifyHandler)
103 mux.HandleFunc("/redirect307/", redirect307Handler)
104 mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
105 fmt.Fprintf(w, "%s\n", time.Now().String())
107 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
113 if strings.Contains(r.URL.Path, "/info/lfs") {
114 if !skipIfBadAuth(w, r, id, ntlmSession) {
121 debug(id, "git http-backend %s %s", r.Method, r.URL)
125 urlname := writeTestStateFile([]byte(server.URL), "LFSTEST_URL", "lfstest-gitserver")
126 defer os.RemoveAll(urlname)
128 sslurlname := writeTestStateFile([]byte(serverTLS.URL), "LFSTEST_SSL_URL", "lfstest-gitserver-ssl")
129 defer os.RemoveAll(sslurlname)
131 clientCertUrlname := writeTestStateFile([]byte(serverClientCert.URL), "LFSTEST_CLIENT_CERT_URL", "lfstest-gitserver-ssl")
132 defer os.RemoveAll(clientCertUrlname)
134 block := &pem.Block{}
135 block.Type = "CERTIFICATE"
136 block.Bytes = serverTLS.TLS.Certificates[0].Certificate[0]
137 pembytes := pem.EncodeToMemory(block)
139 certname := writeTestStateFile(pembytes, "LFSTEST_CERT", "lfstest-gitserver-cert")
140 defer os.RemoveAll(certname)
142 cccertname := writeTestStateFile(clientCertPEM, "LFSTEST_CLIENT_CERT", "lfstest-gitserver-client-cert")
143 defer os.RemoveAll(cccertname)
145 ckcertname := writeTestStateFile(clientKeyPEM, "LFSTEST_CLIENT_KEY", "lfstest-gitserver-client-key")
146 defer os.RemoveAll(ckcertname)
148 debug("init", "server url: %s", server.URL)
149 debug("init", "server tls url: %s", serverTLS.URL)
150 debug("init", "server client cert url: %s", serverClientCert.URL)
153 debug("init", "git server done")
156 // writeTestStateFile writes contents to either the file referenced by the
157 // environment variable envVar, or defaultFilename if that's not set. Returns
158 // the filename that was used
159 func writeTestStateFile(contents []byte, envVar, defaultFilename string) string {
160 f := os.Getenv(envVar)
164 file, err := os.Create(f)
173 type lfsObject struct {
174 Oid string `json:"oid,omitempty"`
175 Size int64 `json:"size,omitempty"`
176 Authenticated bool `json:"authenticated,omitempty"`
177 Actions map[string]*lfsLink `json:"actions,omitempty"`
178 Links map[string]*lfsLink `json:"_links,omitempty"`
179 Err *lfsError `json:"error,omitempty"`
182 type lfsLink struct {
183 Href string `json:"href"`
184 Header map[string]string `json:"header,omitempty"`
185 ExpiresAt time.Time `json:"expires_at,omitempty"`
186 ExpiresIn int `json:"expires_in,omitempty"`
189 type lfsError struct {
190 Code int `json:"code,omitempty"`
191 Message string `json:"message"`
194 func writeLFSError(w http.ResponseWriter, code int, msg string) {
195 by, err := json.Marshal(&lfsError{Message: msg})
197 http.Error(w, "json encoding error: "+err.Error(), 500)
201 w.Header().Set("Content-Type", "application/vnd.git-lfs+json")
206 // handles any requests with "{name}.server.git/info/lfs" in the path
207 func lfsHandler(w http.ResponseWriter, r *http.Request, id string) {
208 repo, err := repoFromLfsUrl(r.URL.Path)
211 w.Write([]byte(err.Error()))
215 debug(id, "git lfs %s %s repo: %s", r.Method, r.URL, repo)
216 w.Header().Set("Content-Type", "application/vnd.git-lfs+json")
219 if strings.HasSuffix(r.URL.String(), "batch") {
220 lfsBatchHandler(w, r, id, repo)
222 locksHandler(w, r, repo)
225 lfsDeleteHandler(w, r, id, repo)
227 if strings.Contains(r.URL.String(), "/locks") {
228 locksHandler(w, r, repo)
231 w.Write([]byte("lock request"))
238 func lfsUrl(repo, oid string) string {
239 return server.URL + "/storage/" + oid + "?r=" + repo
243 retries = make(map[string]uint32)
247 func incrementRetriesFor(api, direction, repo, oid string, check bool) (after uint32, ok bool) {
248 // fmtStr formats a string like "<api>-<direction>-[check]-<retry>",
249 // i.e., "legacy-upload-check-retry", or "storage-download-retry".
252 fmtStr = "%s-%s-check-retry"
254 fmtStr = "%s-%s-retry"
257 if oidHandlers[oid] != fmt.Sprintf(fmtStr, api, direction) {
262 defer retriesMu.Unlock()
264 retryKey := strings.Join([]string{direction, repo, oid}, ":")
267 retries := retries[retryKey]
272 func lfsDeleteHandler(w http.ResponseWriter, r *http.Request, id, repo string) {
273 parts := strings.Split(r.URL.Path, "/")
274 oid := parts[len(parts)-1]
276 largeObjects.Delete(repo, oid)
277 debug(id, "DELETE:", oid)
281 type batchReq struct {
282 Transfers []string `json:"transfers"`
283 Operation string `json:"operation"`
284 Objects []lfsObject `json:"objects"`
285 Ref *Ref `json:"ref,omitempty"`
288 func (r *batchReq) RefName() string {
295 type batchResp struct {
296 Transfer string `json:"transfer,omitempty"`
297 Objects []lfsObject `json:"objects"`
300 func lfsBatchHandler(w http.ResponseWriter, r *http.Request, id, repo string) {
301 checkingObject := r.Header.Get("X-Check-Object") == "1"
302 if !checkingObject && repo == "batchunsupported" {
307 if !checkingObject && repo == "badbatch" {
312 if repo == "netrctest" {
313 user, pass, err := extractAuth(r.Header.Get("Authorization"))
314 if err != nil || (user != "netrcuser" || pass != "netrcpass") {
320 if missingRequiredCreds(w, r, repo) {
324 buf := &bytes.Buffer{}
325 tee := io.TeeReader(r.Body, buf)
327 err := json.NewDecoder(tee).Decode(objs)
328 io.Copy(ioutil.Discard, r.Body)
332 debug(id, buf.String())
338 if strings.HasSuffix(repo, "branch-required") {
339 parts := strings.Split(repo, "-")
340 lenParts := len(parts)
341 if lenParts > 3 && "refs/heads/"+parts[lenParts-3] != objs.RefName() {
343 json.NewEncoder(w).Encode(struct {
344 Message string `json:"message"`
345 }{fmt.Sprintf("Expected ref %q, got %q", "refs/heads/"+parts[lenParts-3], objs.RefName())})
351 testingChunked := testingChunkedTransferEncoding(r)
352 testingTus := testingTusUploadInBatchReq(r)
353 testingTusInterrupt := testingTusUploadInterruptedInBatchReq(r)
354 testingCustomTransfer := testingCustomTransfer(r)
355 var transferChoice string
356 var searchForTransfer string
358 searchForTransfer = "tus"
359 } else if testingCustomTransfer {
360 searchForTransfer = "testcustom"
362 if len(searchForTransfer) > 0 {
363 for _, t := range objs.Transfers {
364 if t == searchForTransfer {
365 transferChoice = searchForTransfer
371 for _, obj := range objs.Objects {
372 handler := oidHandlers[obj.Oid]
373 action := objs.Operation
377 Actions: make(map[string]*lfsLink),
380 // Clobber the OID if told to do so.
381 if handler == "unknown-oid" {
382 o.Oid = "unknown-oid"
387 exists := largeObjects.Has(repo, obj.Oid)
389 if action == "download" {
391 o.Err = &lfsError{Code: 404, Message: fmt.Sprintf("Object %v does not exist", obj.Oid)}
396 // not an error but don't add an action
401 if handler == "object-authenticated" {
402 o.Authenticated = true
406 case "status-batch-403":
407 o.Err = &lfsError{Code: 403, Message: "welp"}
408 case "status-batch-404":
409 o.Err = &lfsError{Code: 404, Message: "welp"}
410 case "status-batch-410":
411 o.Err = &lfsError{Code: 410, Message: "welp"}
412 case "status-batch-422":
413 o.Err = &lfsError{Code: 422, Message: "welp"}
414 case "status-batch-500":
415 o.Err = &lfsError{Code: 500, Message: "welp"}
416 default: // regular 200 response
417 if handler == "return-invalid-size" {
421 if handler == "send-deprecated-links" {
422 o.Links = make(map[string]*lfsLink)
427 Href: lfsUrl(repo, obj.Oid),
428 Header: map[string]string{},
430 a = serveExpired(a, repo, handler)
432 if handler == "send-deprecated-links" {
435 o.Actions[action] = a
439 if handler == "send-verify-action" {
440 o.Actions["verify"] = &lfsLink{
441 Href: server.URL + "/verify",
442 Header: map[string]string{
449 if testingChunked && addAction {
450 if handler == "send-deprecated-links" {
451 o.Links[action].Header["Transfer-Encoding"] = "chunked"
453 o.Actions[action].Header["Transfer-Encoding"] = "chunked"
456 if testingTusInterrupt && addAction {
457 if handler == "send-deprecated-links" {
458 o.Links[action].Header["Lfs-Tus-Interrupt"] = "true"
460 o.Actions[action].Header["Lfs-Tus-Interrupt"] = "true"
467 ores := batchResp{Transfer: transferChoice, Objects: res}
469 by, err := json.Marshal(ores)
474 debug(id, "RESPONSE: 200")
475 debug(id, string(by))
481 // emu guards expiredRepos
484 // expiredRepos is a map keyed by repository name, valuing to whether or not it
485 // has yet served an expired object.
486 var expiredRepos = map[string]bool{}
488 // serveExpired marks the given repo as having served an expired object, making
489 // it unable for that same repository to return an expired object in the future,
490 func serveExpired(a *lfsLink, repo, handler string) *lfsLink {
492 dur = -5 * time.Minute
493 at = time.Now().Add(dur)
496 if handler == "return-expired-action-forever" ||
497 (handler == "return-expired-action" && canServeExpired(repo)) {
500 expiredRepos[repo] = true
508 case "expired-absolute":
510 case "expired-relative":
520 // canServeExpired returns whether or not a repository is capable of serving an
521 // expired object. In other words, canServeExpired returns whether or not the
522 // given repo has yet served an expired object.
523 func canServeExpired(repo string) bool {
527 return !expiredRepos[repo]
530 // Persistent state across requests
531 var batchResumeFailFallbackStorageAttempts = 0
532 var tusStorageAttempts = 0
536 verifyCounts = make(map[string]int)
537 verifyRetryRe = regexp.MustCompile(`verify-fail-(\d+)-times?$`)
540 func verifyHandler(w http.ResponseWriter, r *http.Request) {
541 repo := r.Header.Get("repo")
543 Oid string `json:"oid"`
544 Size int64 `json:"size"`
547 if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
548 writeLFSError(w, http.StatusUnprocessableEntity, err.Error())
553 if matches := verifyRetryRe.FindStringSubmatch(repo); len(matches) < 2 {
556 max, _ = strconv.Atoi(matches[1])
559 key := strings.Join([]string{repo, payload.Oid}, ":")
562 verifyCounts[key] = verifyCounts[key] + 1
563 count := verifyCounts[key]
567 writeLFSError(w, http.StatusServiceUnavailable, fmt.Sprintf(
568 "intentionally failing verify request %d (out of %d)", count, max,
574 // handles any /storage/{oid} requests
575 func storageHandler(w http.ResponseWriter, r *http.Request) {
581 repo := r.URL.Query().Get("r")
582 parts := strings.Split(r.URL.Path, "/")
583 oid := parts[len(parts)-1]
584 if missingRequiredCreds(w, r, repo) {
588 debug(id, "storage %s %s repo: %s", r.Method, oid, repo)
591 switch oidHandlers[oid] {
592 case "status-storage-403":
595 case "status-storage-404":
598 case "status-storage-410":
601 case "status-storage-422":
604 case "status-storage-500":
607 case "status-storage-503":
608 writeLFSError(w, 503, "LFS is temporarily unavailable")
610 case "object-authenticated":
611 if len(r.Header.Get("Authorization")) > 0 {
613 w.Write([]byte("Should not send authentication"))
616 case "storage-upload-retry":
617 if retries, ok := incrementRetriesFor("storage", "upload", repo, oid, false); ok && retries < 3 {
619 w.Write([]byte("malformed content"))
625 if testingChunkedTransferEncoding(r) {
627 for _, value := range r.TransferEncoding {
628 if value == "chunked" {
634 debug(id, "Chunked transfer encoding expected")
639 buf := &bytes.Buffer{}
641 io.Copy(io.MultiWriter(hash, buf), r.Body)
642 oid := hex.EncodeToString(hash.Sum(nil))
643 if !strings.HasSuffix(r.URL.Path, "/"+oid) {
648 largeObjects.Set(repo, oid, buf.Bytes())
651 parts := strings.Split(r.URL.Path, "/")
652 oid := parts[len(parts)-1]
657 if by, ok := largeObjects.Get(repo, oid); ok {
658 if len(by) == len("storage-download-retry") && string(by) == "storage-download-retry" {
659 if retries, ok := incrementRetriesFor("storage", "download", repo, oid, false); ok && retries < 3 {
661 by = []byte("malformed content")
663 } else if len(by) == len("status-batch-resume-206") && string(by) == "status-batch-resume-206" {
664 // Resume if header includes range, otherwise deliberately interrupt
665 if rangeHdr := r.Header.Get("Range"); rangeHdr != "" {
666 regex := regexp.MustCompile(`bytes=(\d+)\-.*`)
667 match := regex.FindStringSubmatch(rangeHdr)
668 if match != nil && len(match) > 1 {
670 resumeAt, _ = strconv.ParseInt(match[1], 10, 32)
671 w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", resumeAt, len(by), resumeAt-int64(len(by))))
676 } else if len(by) == len("batch-resume-fail-fallback") && string(by) == "batch-resume-fail-fallback" {
677 // Fail any Range: request even though we said we supported it
678 // To make sure client can fall back
679 if rangeHdr := r.Header.Get("Range"); rangeHdr != "" {
683 if batchResumeFailFallbackStorageAttempts == 0 {
684 // Truncate output on FIRST attempt to cause resume
685 // Second attempt (without range header) is fallback, complete successfully
687 batchResumeFailFallbackStorageAttempts++
690 w.WriteHeader(statusCode)
692 w.Write(by[0:byteLimit])
693 } else if resumeAt > 0 {
694 w.Write(by[resumeAt:])
704 if !validateTusHeaders(r, id) {
708 parts := strings.Split(r.URL.Path, "/")
709 oid := parts[len(parts)-1]
711 if by, ok := largeObjects.GetIncomplete(repo, oid); ok {
712 offset = int64(len(by))
714 w.Header().Set("Upload-Offset", strconv.FormatInt(offset, 10))
718 if !validateTusHeaders(r, id) {
722 parts := strings.Split(r.URL.Path, "/")
723 oid := parts[len(parts)-1]
725 offsetHdr := r.Header.Get("Upload-Offset")
726 offset, err := strconv.ParseInt(offsetHdr, 10, 64)
728 log.Fatal("Unable to parse Upload-Offset header in request: ", err)
733 buf := &bytes.Buffer{}
734 out := io.MultiWriter(hash, buf)
736 if by, ok := largeObjects.GetIncomplete(repo, oid); ok {
737 if offset != int64(len(by)) {
738 log.Fatal(fmt.Sprintf("Incorrect offset in request, got %d expected %d", offset, len(by)))
742 _, err := out.Write(by)
744 log.Fatal("Error reading incomplete bytes from store: ", err)
748 largeObjects.DeleteIncomplete(repo, oid)
749 debug(id, "Resuming upload of %v at byte %d", oid, offset)
752 // As a test, we intentionally break the upload from byte 0 by only
753 // reading some bytes the quitting & erroring, this forces a resume
754 // any offset > 0 will work ok
756 if r.Header.Get("Lfs-Tus-Interrupt") == "true" && offset == 0 {
757 chdr := r.Header.Get("Content-Length")
758 contentLen, err := strconv.ParseInt(chdr, 10, 64)
760 log.Fatal(fmt.Sprintf("Invalid Content-Length %q", chdr))
764 truncated := contentLen / 3
765 _, _ = io.CopyN(out, r.Body, truncated)
767 copyErr = fmt.Errorf("Simulated copy error")
769 _, copyErr = io.Copy(out, r.Body)
774 debug(id, "Incomplete upload of %v, %d bytes", oid, len(b))
775 largeObjects.SetIncomplete(repo, oid, b)
779 checkoid := hex.EncodeToString(hash.Sum(nil))
781 log.Fatal(fmt.Sprintf("Incorrect oid after calculation, got %q expected %q", checkoid, oid))
787 largeObjects.Set(repo, oid, b)
788 w.Header().Set("Upload-Offset", strconv.FormatInt(int64(len(b)), 10))
797 func validateTusHeaders(r *http.Request, id string) bool {
798 if len(r.Header.Get("Tus-Resumable")) == 0 {
799 debug(id, "Missing Tus-Resumable header in request")
805 func gitHandler(w http.ResponseWriter, r *http.Request) {
807 io.Copy(ioutil.Discard, r.Body)
811 cmd := exec.Command("git", "http-backend")
813 fmt.Sprintf("GIT_PROJECT_ROOT=%s", repoDir),
814 fmt.Sprintf("GIT_HTTP_EXPORT_ALL="),
815 fmt.Sprintf("PATH_INFO=%s", r.URL.Path),
816 fmt.Sprintf("QUERY_STRING=%s", r.URL.RawQuery),
817 fmt.Sprintf("REQUEST_METHOD=%s", r.Method),
818 fmt.Sprintf("CONTENT_TYPE=%s", r.Header.Get("Content-Type")),
821 buffer := &bytes.Buffer{}
824 cmd.Stderr = os.Stderr
826 if err := cmd.Run(); err != nil {
830 text := textproto.NewReader(bufio.NewReader(buffer))
832 code, _, _ := text.ReadCodeLine(-1)
838 headers, _ := text.ReadMIMEHeader()
840 for key, values := range headers {
841 for _, value := range values {
849 func redirect307Handler(w http.ResponseWriter, r *http.Request) {
855 // Send a redirect to info/lfs
856 // Make it either absolute or relative depending on subpath
857 parts := strings.Split(r.URL.Path, "/")
858 // first element is always blank since rooted
859 var redirectTo string
860 if parts[2] == "rel" {
861 redirectTo = "/" + strings.Join(parts[3:], "/")
862 } else if parts[2] == "abs" {
863 redirectTo = server.URL + "/" + strings.Join(parts[3:], "/")
865 debug(id, "Invalid URL for redirect: %v", r.URL)
869 w.Header().Set("Location", redirectTo)
874 Name string `json:"name"`
878 Id string `json:"id"`
879 Path string `json:"path"`
880 Owner User `json:"owner"`
881 LockedAt time.Time `json:"locked_at"`
884 type LockRequest struct {
885 Path string `json:"path"`
886 Ref *Ref `json:"ref,omitempty"`
889 func (r *LockRequest) RefName() string {
896 type LockResponse struct {
897 Lock *Lock `json:"lock"`
898 Message string `json:"message,omitempty"`
901 type UnlockRequest struct {
902 Force bool `json:"force"`
903 Ref *Ref `json:"ref,omitempty"`
906 func (r *UnlockRequest) RefName() string {
913 type UnlockResponse struct {
914 Lock *Lock `json:"lock"`
915 Message string `json:"message,omitempty"`
918 type LockList struct {
919 Locks []Lock `json:"locks"`
920 NextCursor string `json:"next_cursor,omitempty"`
921 Message string `json:"message,omitempty"`
925 Name string `json:"name,omitempty"`
928 type VerifiableLockRequest struct {
929 Ref *Ref `json:"ref,omitempty"`
930 Cursor string `json:"cursor,omitempty"`
931 Limit int `json:"limit,omitempty"`
934 func (r *VerifiableLockRequest) RefName() string {
941 type VerifiableLockList struct {
942 Ours []Lock `json:"ours"`
943 Theirs []Lock `json:"theirs"`
944 NextCursor string `json:"next_cursor,omitempty"`
945 Message string `json:"message,omitempty"`
950 repoLocks = map[string][]Lock{}
953 func addLocks(repo string, l ...Lock) {
956 repoLocks[repo] = append(repoLocks[repo], l...)
957 sort.Sort(LocksByCreatedAt(repoLocks[repo]))
960 func getLocks(repo string) []Lock {
964 locks := repoLocks[repo]
965 cp := make([]Lock, len(locks))
966 for i, l := range locks {
973 func getFilteredLocks(repo, path, cursor, limit string) ([]Lock, string, error) {
974 locks := getLocks(repo)
977 for i, l := range locks {
985 locks = locks[lastSeen:]
987 return nil, "", fmt.Errorf("cursor (%s) not found", cursor)
993 for _, l := range locks {
995 filtered = append(filtered, l)
1003 size, err := strconv.Atoi(limit)
1005 return nil, "", errors.New("unable to parse limit amount")
1008 size = int(math.Min(float64(len(locks)), 3))
1013 if size+1 < len(locks) {
1014 return locks[:size], locks[size+1].Id, nil
1018 return locks, "", nil
1021 func delLock(repo string, id string) *Lock {
1026 locks := make([]Lock, 0, len(repoLocks[repo]))
1027 for _, l := range repoLocks[repo] {
1032 locks = append(locks, l)
1034 repoLocks[repo] = locks
1038 type LocksByCreatedAt []Lock
1040 func (c LocksByCreatedAt) Len() int { return len(c) }
1041 func (c LocksByCreatedAt) Less(i, j int) bool { return c[i].LockedAt.Before(c[j].LockedAt) }
1042 func (c LocksByCreatedAt) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
1045 lockRe = regexp.MustCompile(`/locks/?$`)
1046 unlockRe = regexp.MustCompile(`locks/([^/]+)/unlock\z`)
1049 func locksHandler(w http.ResponseWriter, r *http.Request, repo string) {
1050 dec := json.NewDecoder(r.Body)
1051 enc := json.NewEncoder(w)
1055 if !lockRe.MatchString(r.URL.Path) {
1056 w.Header().Set("Content-Type", "application/json")
1057 w.WriteHeader(http.StatusNotFound)
1058 w.Write([]byte(`{"message":"unknown path: ` + r.URL.Path + `"}`))
1062 if err := r.ParseForm(); err != nil {
1063 http.Error(w, "could not parse form values", http.StatusInternalServerError)
1067 if strings.HasSuffix(repo, "branch-required") {
1068 parts := strings.Split(repo, "-")
1069 lenParts := len(parts)
1070 if lenParts > 3 && "refs/heads/"+parts[lenParts-3] != r.FormValue("refspec") {
1073 Message string `json:"message"`
1074 }{fmt.Sprintf("Expected ref %q, got %q", "refs/heads/"+parts[lenParts-3], r.FormValue("refspec"))})
1080 w.Header().Set("Content-Type", "application/json")
1081 locks, nextCursor, err := getFilteredLocks(repo,
1082 r.FormValue("path"),
1083 r.FormValue("cursor"),
1084 r.FormValue("limit"))
1087 ll.Message = err.Error()
1090 ll.NextCursor = nextCursor
1096 w.Header().Set("Content-Type", "application/json")
1097 if strings.HasSuffix(r.URL.Path, "unlock") {
1099 if matches := unlockRe.FindStringSubmatch(r.URL.Path); len(matches) > 1 {
1103 if len(lockId) == 0 {
1104 enc.Encode(&UnlockResponse{Message: "Invalid lock"})
1107 unlockRequest := &UnlockRequest{}
1108 if err := dec.Decode(unlockRequest); err != nil {
1109 enc.Encode(&UnlockResponse{Message: err.Error()})
1113 if strings.HasSuffix(repo, "branch-required") {
1114 parts := strings.Split(repo, "-")
1115 lenParts := len(parts)
1116 if lenParts > 3 && "refs/heads/"+parts[lenParts-3] != unlockRequest.RefName() {
1119 Message string `json:"message"`
1120 }{fmt.Sprintf("Expected ref %q, got %q", "refs/heads/"+parts[lenParts-3], unlockRequest.RefName())})
1125 if l := delLock(repo, lockId); l != nil {
1126 enc.Encode(&UnlockResponse{Lock: l})
1128 enc.Encode(&UnlockResponse{Message: "unable to find lock"})
1133 if strings.HasSuffix(r.URL.Path, "/locks/verify") {
1134 if strings.HasSuffix(repo, "verify-5xx") {
1138 if strings.HasSuffix(repo, "verify-501") {
1142 if strings.HasSuffix(repo, "verify-403") {
1148 case "pre_push_locks_verify_404":
1149 w.WriteHeader(http.StatusNotFound)
1150 w.Write([]byte(`{"message":"pre_push_locks_verify_404"}`))
1152 case "pre_push_locks_verify_410":
1153 w.WriteHeader(http.StatusGone)
1154 w.Write([]byte(`{"message":"pre_push_locks_verify_410"}`))
1158 reqBody := &VerifiableLockRequest{}
1159 if err := dec.Decode(reqBody); err != nil {
1160 w.WriteHeader(http.StatusBadRequest)
1162 Message string `json:"message"`
1163 }{"json decode error: " + err.Error()})
1167 if strings.HasSuffix(repo, "branch-required") {
1168 parts := strings.Split(repo, "-")
1169 lenParts := len(parts)
1170 if lenParts > 3 && "refs/heads/"+parts[lenParts-3] != reqBody.RefName() {
1173 Message string `json:"message"`
1174 }{fmt.Sprintf("Expected ref %q, got %q", "refs/heads/"+parts[lenParts-3], reqBody.RefName())})
1179 ll := &VerifiableLockList{}
1180 locks, nextCursor, err := getFilteredLocks(repo, "",
1182 strconv.Itoa(reqBody.Limit))
1184 ll.Message = err.Error()
1186 ll.NextCursor = nextCursor
1188 for _, l := range locks {
1189 if strings.Contains(l.Path, "theirs") {
1190 ll.Theirs = append(ll.Theirs, l)
1192 ll.Ours = append(ll.Ours, l)
1201 if strings.HasSuffix(r.URL.Path, "/locks") {
1202 lockRequest := &LockRequest{}
1203 if err := dec.Decode(lockRequest); err != nil {
1204 enc.Encode(&LockResponse{Message: err.Error()})
1207 if strings.HasSuffix(repo, "branch-required") {
1208 parts := strings.Split(repo, "-")
1209 lenParts := len(parts)
1210 if lenParts > 3 && "refs/heads/"+parts[lenParts-3] != lockRequest.RefName() {
1213 Message string `json:"message"`
1214 }{fmt.Sprintf("Expected ref %q, got %q", "refs/heads/"+parts[lenParts-3], lockRequest.RefName())})
1219 for _, l := range getLocks(repo) {
1220 if l.Path == lockRequest.Path {
1221 enc.Encode(&LockResponse{Message: "lock already created"})
1230 Id: fmt.Sprintf("%x", id[:]),
1231 Path: lockRequest.Path,
1232 Owner: User{Name: "Git LFS Tests"},
1233 LockedAt: time.Now(),
1236 addLocks(repo, *lock)
1238 // TODO(taylor): commit_needed case
1239 // TODO(taylor): err case
1241 enc.Encode(&LockResponse{
1251 func missingRequiredCreds(w http.ResponseWriter, r *http.Request, repo string) bool {
1252 if !strings.HasPrefix(repo, "requirecreds") {
1256 auth := r.Header.Get("Authorization")
1257 user, pass, err := extractAuth(auth)
1259 writeLFSError(w, 403, err.Error())
1263 if user != "requirecreds" || pass != "pass" {
1264 writeLFSError(w, 403, fmt.Sprintf("Got: '%s' => '%s' : '%s'", auth, user, pass))
1271 func testingChunkedTransferEncoding(r *http.Request) bool {
1272 return strings.HasPrefix(r.URL.String(), "/test-chunked-transfer-encoding")
1275 func testingTusUploadInBatchReq(r *http.Request) bool {
1276 return strings.HasPrefix(r.URL.String(), "/test-tus-upload")
1278 func testingTusUploadInterruptedInBatchReq(r *http.Request) bool {
1279 return strings.HasPrefix(r.URL.String(), "/test-tus-upload-interrupt")
1281 func testingCustomTransfer(r *http.Request) bool {
1282 return strings.HasPrefix(r.URL.String(), "/test-custom-transfer")
1285 var lfsUrlRE = regexp.MustCompile(`\A/?([^/]+)/info/lfs`)
1287 func repoFromLfsUrl(urlpath string) (string, error) {
1288 matches := lfsUrlRE.FindStringSubmatch(urlpath)
1289 if len(matches) != 2 {
1290 return "", fmt.Errorf("LFS url '%s' does not match %v", urlpath, lfsUrlRE)
1294 if strings.HasSuffix(repo, ".git") {
1295 return repo[0 : len(repo)-4], nil
1300 type lfsStorage struct {
1301 objects map[string]map[string][]byte
1302 incomplete map[string]map[string][]byte
1306 func (s *lfsStorage) Get(repo, oid string) ([]byte, bool) {
1308 defer s.mutex.Unlock()
1309 repoObjects, ok := s.objects[repo]
1314 by, ok := repoObjects[oid]
1318 func (s *lfsStorage) Has(repo, oid string) bool {
1320 defer s.mutex.Unlock()
1321 repoObjects, ok := s.objects[repo]
1326 _, ok = repoObjects[oid]
1330 func (s *lfsStorage) Set(repo, oid string, by []byte) {
1332 defer s.mutex.Unlock()
1333 repoObjects, ok := s.objects[repo]
1335 repoObjects = make(map[string][]byte)
1336 s.objects[repo] = repoObjects
1338 repoObjects[oid] = by
1341 func (s *lfsStorage) Delete(repo, oid string) {
1343 defer s.mutex.Unlock()
1344 repoObjects, ok := s.objects[repo]
1346 delete(repoObjects, oid)
1350 func (s *lfsStorage) GetIncomplete(repo, oid string) ([]byte, bool) {
1352 defer s.mutex.Unlock()
1353 repoObjects, ok := s.incomplete[repo]
1358 by, ok := repoObjects[oid]
1362 func (s *lfsStorage) SetIncomplete(repo, oid string, by []byte) {
1364 defer s.mutex.Unlock()
1365 repoObjects, ok := s.incomplete[repo]
1367 repoObjects = make(map[string][]byte)
1368 s.incomplete[repo] = repoObjects
1370 repoObjects[oid] = by
1373 func (s *lfsStorage) DeleteIncomplete(repo, oid string) {
1375 defer s.mutex.Unlock()
1376 repoObjects, ok := s.incomplete[repo]
1378 delete(repoObjects, oid)
1382 func newLfsStorage() *lfsStorage {
1384 objects: make(map[string]map[string][]byte),
1385 incomplete: make(map[string]map[string][]byte),
1386 mutex: &sync.Mutex{},
1390 func extractAuth(auth string) (string, string, error) {
1391 if strings.HasPrefix(auth, "Basic ") {
1392 decodeBy, err := base64.StdEncoding.DecodeString(auth[6:len(auth)])
1393 decoded := string(decodeBy)
1399 parts := strings.SplitN(decoded, ":", 2)
1400 if len(parts) == 2 {
1401 return parts[0], parts[1], nil
1409 func skipIfBadAuth(w http.ResponseWriter, r *http.Request, id string, ntlmSession ntlm.ServerSession) bool {
1410 auth := r.Header.Get("Authorization")
1411 if strings.Contains(r.URL.Path, "ntlm") {
1420 user, pass, err := extractAuth(auth)
1423 debug(id, "Error decoding auth: %s", err)
1432 case "netrcuser", "requirecreds":
1435 if strings.HasPrefix(r.URL.Path, "/"+pass) {
1438 debug(id, "auth attempt against: %q", r.URL.Path)
1442 debug(id, "Bad auth: %q", auth)
1446 func handleNTLM(w http.ResponseWriter, r *http.Request, authHeader string, session ntlm.ServerSession) {
1447 if strings.HasPrefix(strings.ToUpper(authHeader), "BASIC ") {
1453 w.Header().Set("Www-Authenticate", "ntlm")
1456 // ntlmNegotiateMessage from httputil pkg
1457 case "NTLM TlRMTVNTUAABAAAAB7IIogwADAAzAAAACwALACgAAAAKAAAoAAAAD1dJTExISS1NQUlOTk9SVEhBTUVSSUNB":
1458 ch, err := session.GenerateChallengeMessage()
1460 writeLFSError(w, 500, err.Error())
1464 chMsg := base64.StdEncoding.EncodeToString(ch.Bytes())
1465 w.Header().Set("Www-Authenticate", "ntlm "+chMsg)
1469 if !strings.HasPrefix(strings.ToUpper(authHeader), "NTLM ") {
1470 writeLFSError(w, 500, "bad authorization header: "+authHeader)
1474 auth := authHeader[5:] // strip "ntlm " prefix
1475 val, err := base64.StdEncoding.DecodeString(auth)
1477 writeLFSError(w, 500, "base64 decode error: "+err.Error())
1481 _, err = ntlm.ParseAuthenticateMessage(val, 2)
1483 writeLFSError(w, 500, "auth parse error: "+err.Error())
1490 oidHandlers = make(map[string]string)
1491 for _, content := range contentHandlers {
1493 h.Write([]byte(content))
1494 oidHandlers[hex.EncodeToString(h.Sum(nil))] = content
1498 func debug(reqid, msg string, args ...interface{}) {
1499 fullargs := make([]interface{}, len(args)+1)
1501 for i, a := range args {
1504 log.Printf("[%s] "+msg+"\n", fullargs...)
1507 func reqId(w http.ResponseWriter) (string, bool) {
1508 b := make([]byte, 16)
1509 _, err := rand.Read(b)
1511 http.Error(w, "error generating id: "+err.Error(), 500)
1514 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), true
1517 // https://ericchiang.github.io/post/go-tls/
1518 func generateCARootCertificates() (rootKey *rsa.PrivateKey, rootCert *x509.Certificate) {
1520 // generate a new key-pair
1521 rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
1523 log.Fatalf("generating random key: %v", err)
1526 rootCertTmpl, err := CertTemplate()
1528 log.Fatalf("creating cert template: %v", err)
1530 // describe what the certificate will be used for
1531 rootCertTmpl.IsCA = true
1532 rootCertTmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature
1533 rootCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
1534 // rootCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
1536 rootCert, _, err = CreateCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey)
1541 func generateClientCertificates(rootCert *x509.Certificate, rootKey interface{}) (clientKey *rsa.PrivateKey, clientCertPEM []byte, clientKeyPEM []byte) {
1543 // create a key-pair for the client
1544 clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
1546 log.Fatalf("generating random key: %v", err)
1549 // create a template for the client
1550 clientCertTmpl, err1 := CertTemplate()
1552 log.Fatalf("creating cert template: %v", err1)
1554 clientCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature
1555 clientCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
1557 // the root cert signs the cert by again providing its private key
1558 _, clientCertPEM, err2 := CreateCert(clientCertTmpl, rootCert, &clientKey.PublicKey, rootKey)
1560 log.Fatalf("error creating cert: %v", err2)
1563 // encode and load the cert and private key for the client
1564 clientKeyPEM = pem.EncodeToMemory(&pem.Block{
1565 Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey),
1571 // helper function to create a cert template with a serial number and other required fields
1572 func CertTemplate() (*x509.Certificate, error) {
1573 // generate a random serial number (a real cert authority would have some logic behind this)
1574 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
1575 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
1577 return nil, errors.New("failed to generate serial number: " + err.Error())
1580 tmpl := x509.Certificate{
1581 SerialNumber: serialNumber,
1582 Subject: pkix.Name{Organization: []string{"Yhat, Inc."}},
1583 SignatureAlgorithm: x509.SHA256WithRSA,
1584 NotBefore: time.Now(),
1585 NotAfter: time.Now().Add(time.Hour), // valid for an hour
1586 BasicConstraintsValid: true,
1591 func CreateCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (
1592 cert *x509.Certificate, certPEM []byte, err error) {
1594 certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv)
1598 // parse the resulting certificate so we can use it again
1599 cert, err = x509.ParseCertificate(certDER)
1603 // PEM encode the certificate (this is a standard TLS encoding)
1604 b := pem.Block{Type: "CERTIFICATE", Bytes: certDER}
1605 certPEM = pem.EncodeToMemory(&b)