package handler import ( "errors" "fmt" "io" "net/http" "path" "strings" "nf-repo/internal/model" "nf-repo/internal/opt" "nf-repo/internal/tool/rerr" "nf-repo/internal/verify" "github.com/loveuer/nf" "github.com/loveuer/nf/nft/log" ) func handleBlobs(c *nf.Ctx) error { elem := strings.Split(c.Path(), "/") elem = elem[1:] if elem[len(elem)-1] == "" { elem = elem[:len(elem)-1] } // Must have a path of form /v2/{name}/blobs/{upload,sha256:} if len(elem) < 4 { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "NAME_INVALID", Message: "blobs must be attached to a repo", }) } target := elem[len(elem)-1] service := elem[len(elem)-2] digest := c.Query("digest") contentRange := c.Get("Content-Range") rangeHeader := c.Get("Range") repo := c.Request.URL.Host + path.Join(elem[1:len(elem)-2]...) log.Debug("handleBlob: path = %s, method = %s, target = %s, service = %s, repo = %s, digest = %s", c.Path(), c.Method(), target, service, repo, digest) switch c.Method() { case http.MethodHead: h, err := model.NewHash(target) if err != nil { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "NAME_INVALID", Message: "invalid digest", }) } var size int64 size, err = b.blobHandler.Stat(c.Request.Context(), repo, h) if errors.Is(err, opt.ErrNotFound) { return rerr.Error(c, rerr.ErrBlobUnknown) } else if err != nil { var re model.RedirectError if errors.As(err, &re) { http.Redirect(c.Writer, c.Request, re.Location, re.Code) return nil } return rerr.Error(c, rerr.ErrInternal(err)) } c.Set("Content-Length", fmt.Sprint(size)) c.Set("Docker-Content-Digest", h.String()) return c.SendStatus(http.StatusOK) case http.MethodGet: h, err := model.NewHash(target) if err != nil { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "NAME_INVALID", Message: "invalid digest", }) } var size int64 var r io.Reader size, err = b.blobHandler.Stat(c.Request.Context(), repo, h) if errors.Is(err, opt.ErrNotFound) { log.Error("handleBlobs: mirror registry get repo not found, repo = %s", repo) return rerr.Error(c, rerr.ErrBlobUnknown) } else if err != nil { var re model.RedirectError if errors.As(err, &re) { http.Redirect(c.Writer, c.Request, re.Location, re.Code) return nil } return rerr.Error(c, rerr.ErrInternal(err)) } rc, err := b.blobHandler.Get(c.Request.Context(), repo, h) if errors.Is(err, opt.ErrNotFound) { return rerr.Error(c, rerr.ErrBlobUnknown) } else if err != nil { var re model.RedirectError if errors.As(err, &re) { http.Redirect(c.Writer, c.Request, re.Location, re.Code) return nil } return rerr.Error(c, rerr.ErrInternal(err)) } defer rc.Close() r = rc if rangeHeader != "" { start, end := int64(0), int64(0) if _, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); err != nil { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusRequestedRangeNotSatisfiable, Code: "BLOB_UNKNOWN", Message: "We don't understand your Range", }) } n := (end + 1) - start if ra, ok := r.(io.ReaderAt); ok { if end+1 > size { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusRequestedRangeNotSatisfiable, Code: "BLOB_UNKNOWN", Message: fmt.Sprintf("range end %d > %d size", end+1, size), }) } r = io.NewSectionReader(ra, start, n) } else { if _, err := io.CopyN(io.Discard, r, start); err != nil { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusRequestedRangeNotSatisfiable, Code: "BLOB_UNKNOWN", Message: fmt.Sprintf("Failed to discard %d bytes", start), }) } r = io.LimitReader(r, n) } c.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size)) c.Set("Content-Length", fmt.Sprint(n)) c.Set("Docker-Content-Digest", h.String()) c.Status(http.StatusPartialContent) } else { c.Set("Content-Length", fmt.Sprint(size)) c.Set("Docker-Content-Digest", h.String()) c.Status(http.StatusOK) } _, err = io.Copy(c.Writer, r) return err case http.MethodPost: // It is weird that this is "target" instead of "service", but // that's how the index math works out above. if target != "uploads" { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "METHOD_UNKNOWN", Message: fmt.Sprintf("POST to /blobs must be followed by /uploads, got %s", target), }) } if digest != "" { h, err := model.NewHash(digest) if err != nil { return rerr.Error(c, rerr.ErrDigestInvalid) } vrc, err := verify.ReadCloser(c.Request.Body, c.Request.ContentLength, h) if err != nil { return rerr.Error(c, rerr.ErrInternal(err)) } defer vrc.Close() if err = b.blobHandler.Put(c.Request.Context(), repo, h, vrc); err != nil { if errors.As(err, &verify.Error{}) { log.Info("Digest mismatch: %v", err) return rerr.Error(c, rerr.ErrDigestMismatch) } return rerr.Error(c, rerr.ErrInternal(err)) } c.Set("Docker-Content-Digest", h.String()) return c.SendStatus(http.StatusCreated) } id := b.uploadHandler.UploadId() c.Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-2]...), "blobs/uploads", id)) c.Set("Range", "0-0") return c.SendStatus(http.StatusAccepted) case http.MethodPatch: if service != "uploads" { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "METHOD_UNKNOWN", Message: fmt.Sprintf("PATCH to /blobs must be followed by /uploads, got %s", service), }) } start, end := 0, 0 if contentRange != "" { if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusRequestedRangeNotSatisfiable, Code: "BLOB_UPLOAD_UNKNOWN", Message: "We don't understand your Content-Range", }) } if end != start+int(c.Request.ContentLength) { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusRequestedRangeNotSatisfiable, Code: "BLOB_UPLOAD_INVALID", Message: "blob upload content range mismatch content length", }) } } end = start + int(c.Request.ContentLength) var ( length int re *rerr.RepositoryError ) if length, re = b.uploadHandler.Write(c.Request.Context(), target, c.Request.Body, start, end); re != nil { return rerr.Error(c, re) } c.Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target)) c.Set("Range", fmt.Sprintf("0-%d", length-1)) return c.SendStatus(http.StatusNoContent) case http.MethodPut: if service != "uploads" { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "METHOD_UNKNOWN", Message: fmt.Sprintf("PUT to /blobs must be followed by /uploads, got %s", service), }) } if digest == "" { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "DIGEST_INVALID", Message: "digest not specified", }) } hash, err := model.NewHash(digest) if err != nil { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "NAME_INVALID", Message: "invalid digest", }) } var re *rerr.RepositoryError if re = b.uploadHandler.Done(c.Request.Context(), b.blobHandler, target, c.Request.Body, int(c.Request.ContentLength), repo, hash); re != nil { return rerr.Error(c, re) } c.Set("Docker-Content-Digest", hash.String()) return c.SendStatus(http.StatusCreated) case http.MethodDelete: h, err := model.NewHash(target) if err != nil { return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "NAME_INVALID", Message: "invalid digest", }) } if err := b.blobHandler.Delete(c.Request.Context(), repo, h); err != nil { return rerr.Error(c, rerr.ErrInternal(err)) } return c.SendStatus(http.StatusAccepted) default: return rerr.Error(c, &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "METHOD_UNKNOWN", Message: "We don't understand your method + url", }) } }