2024-04-10 22:10:09 +08:00
|
|
|
package handler
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
2024-12-23 00:07:44 -08:00
|
|
|
"path"
|
|
|
|
"strings"
|
|
|
|
|
2024-04-10 22:10:09 +08:00
|
|
|
"nf-repo/internal/model"
|
|
|
|
"nf-repo/internal/opt"
|
2024-12-23 00:07:44 -08:00
|
|
|
"nf-repo/internal/tool/rerr"
|
2024-04-10 22:10:09 +08:00
|
|
|
"nf-repo/internal/verify"
|
2024-12-23 00:07:44 -08:00
|
|
|
|
|
|
|
"github.com/loveuer/nf"
|
|
|
|
"github.com/loveuer/nf/nft/log"
|
2024-04-10 22:10:09 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
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]...)
|
|
|
|
|
2024-12-23 00:07:44 -08:00
|
|
|
log.Debug("handleBlob: path = %s, method = %s, target = %s, service = %s, repo = %s, digest = %s",
|
|
|
|
c.Path(), c.Method(), target, service, repo, digest)
|
2024-04-10 22:10:09 +08:00
|
|
|
|
|
|
|
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) {
|
2024-12-23 00:07:44 -08:00
|
|
|
http.Redirect(c.Writer, c.Request, re.Location, re.Code)
|
2024-04-10 22:10:09 +08:00
|
|
|
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) {
|
2024-12-23 00:07:44 -08:00
|
|
|
log.Error("handleBlobs: mirror registry get repo not found, repo = %s", repo)
|
2024-04-10 22:10:09 +08:00
|
|
|
return rerr.Error(c, rerr.ErrBlobUnknown)
|
|
|
|
} else if err != nil {
|
|
|
|
var re model.RedirectError
|
|
|
|
if errors.As(err, &re) {
|
2024-12-23 00:07:44 -08:00
|
|
|
http.Redirect(c.Writer, c.Request, re.Location, re.Code)
|
2024-04-10 22:10:09 +08:00
|
|
|
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) {
|
2024-12-23 00:07:44 -08:00
|
|
|
http.Redirect(c.Writer, c.Request, re.Location, re.Code)
|
2024-04-10 22:10:09 +08:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-12-23 00:07:44 -08:00
|
|
|
_, err = io.Copy(c.Writer, r)
|
2024-04-10 22:10:09 +08:00
|
|
|
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{}) {
|
2024-12-23 00:07:44 -08:00
|
|
|
log.Info("Digest mismatch: %v", err)
|
2024-04-10 22:10:09 +08:00
|
|
|
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",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|