diff --git a/src/core/controllers/controllers_test.go b/src/core/controllers/controllers_test.go index 1381a26d3..8aa6dd1e6 100644 --- a/src/core/controllers/controllers_test.go +++ b/src/core/controllers/controllers_test.go @@ -32,7 +32,7 @@ import ( "github.com/goharbor/harbor/src/common/models" utilstest "github.com/goharbor/harbor/src/common/utils/test" "github.com/goharbor/harbor/src/core/config" - "github.com/goharbor/harbor/src/core/proxy" + "github.com/goharbor/review/harbor/src/core/proxy" "github.com/stretchr/testify/assert" ) diff --git a/src/core/controllers/proxy.go b/src/core/controllers/proxy.go index 1ddaf9ca7..5fc7b31f5 100644 --- a/src/core/controllers/proxy.go +++ b/src/core/controllers/proxy.go @@ -2,7 +2,7 @@ package controllers import ( "github.com/astaxie/beego" - "github.com/goharbor/harbor/src/core/proxy" + "github.com/goharbor/harbor/src/core/middlewares" ) // RegistryProxy is the endpoint on UI for a reverse proxy pointing to registry @@ -14,10 +14,11 @@ type RegistryProxy struct { func (p *RegistryProxy) Handle() { req := p.Ctx.Request rw := p.Ctx.ResponseWriter - proxy.Handle(rw, req) + middlewares.Handle(rw, req) } // Render ... func (p *RegistryProxy) Render() error { return nil } + diff --git a/src/core/main.go b/src/core/main.go index 2c68141dd..d2b010097 100644 --- a/src/core/main.go +++ b/src/core/main.go @@ -35,7 +35,7 @@ import ( _ "github.com/goharbor/harbor/src/core/auth/uaa" "github.com/goharbor/harbor/src/core/config" "github.com/goharbor/harbor/src/core/filter" - "github.com/goharbor/harbor/src/core/proxy" + "github.com/goharbor/harbor/src/core/middlewares" "github.com/goharbor/harbor/src/core/service/token" "github.com/goharbor/harbor/src/replication" ) @@ -158,7 +158,9 @@ func main() { } log.Info("Init proxy") - proxy.Init() + if err := middlewares.Init(); err != nil { + log.Errorf("init proxy error, %v", err) + } // go proxy.StartProxy() beego.Run() } diff --git a/src/core/middlewares/blobquota/handler.go b/src/core/middlewares/blobquota/handler.go new file mode 100644 index 000000000..876fd2ef3 --- /dev/null +++ b/src/core/middlewares/blobquota/handler.go @@ -0,0 +1,104 @@ +package blobquota + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/middlewares/util" + "github.com/opencontainers/go-digest" + "github.com/pkg/errors" + "net/http" + "time" +) + +type blobQuotaHandler struct { + next http.Handler +} + +func New(next http.Handler) http.Handler { + return &blobQuotaHandler{ + next: next, + } +} + +func (bqh blobQuotaHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if req.Method == http.MethodPut { + match, _ := util.MatchPutBlobURL(req) + if match { + dgstStr := req.FormValue("digest") + if dgstStr == "" { + http.Error(rw, util.MarshalError("InternalServerError", "blob digest missing"), http.StatusInternalServerError) + return + } + dgst, err := digest.Parse(dgstStr) + if err != nil { + http.Error(rw, util.MarshalError("InternalServerError", "blob digest parsing failed"), http.StatusInternalServerError) + return + } + // ToDo lock digest with redis + + // ToDo read placeholder from config + state, err := hmacKey("placeholder").unpackUploadState(req.FormValue("_state")) + if err != nil { + http.Error(rw, util.MarshalError("InternalServerError", "failed to decode state"), http.StatusInternalServerError) + return + } + log.Infof("we need to insert blob data into DB.") + log.Infof("blob digest, %v", dgst) + log.Infof("blob size, %v", state.Offset) + } + + } + bqh.next.ServeHTTP(rw, req) +} + +// blobUploadState captures the state serializable state of the blob upload. +type blobUploadState struct { + // name is the primary repository under which the blob will be linked. + Name string + + // UUID identifies the upload. + UUID string + + // offset contains the current progress of the upload. + Offset int64 + + // StartedAt is the original start time of the upload. + StartedAt time.Time +} + +type hmacKey string + +var errInvalidSecret = errors.New("invalid secret") + +// unpackUploadState unpacks and validates the blob upload state from the +// token, using the hmacKey secret. +func (secret hmacKey) unpackUploadState(token string) (blobUploadState, error) { + var state blobUploadState + + tokenBytes, err := base64.URLEncoding.DecodeString(token) + if err != nil { + return state, err + } + mac := hmac.New(sha256.New, []byte(secret)) + + if len(tokenBytes) < mac.Size() { + return state, errInvalidSecret + } + + macBytes := tokenBytes[:mac.Size()] + messageBytes := tokenBytes[mac.Size():] + + mac.Write(messageBytes) + if !hmac.Equal(mac.Sum(nil), macBytes) { + return state, errInvalidSecret + } + + if err := json.Unmarshal(messageBytes, &state); err != nil { + return state, err + } + + return state, nil +} diff --git a/src/core/middlewares/chain.go b/src/core/middlewares/chain.go new file mode 100644 index 000000000..69d15a2af --- /dev/null +++ b/src/core/middlewares/chain.go @@ -0,0 +1,110 @@ +package middlewares + +import ( + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/middlewares/blobquota" + "github.com/goharbor/harbor/src/core/middlewares/contenttrust" + "github.com/goharbor/harbor/src/core/middlewares/listrepo" + "github.com/goharbor/harbor/src/core/middlewares/multiplmanifest" + "github.com/goharbor/harbor/src/core/middlewares/readonly" + "github.com/goharbor/harbor/src/core/middlewares/regquota" + "github.com/goharbor/harbor/src/core/middlewares/url" + "github.com/goharbor/harbor/src/core/middlewares/vulnerable" + "github.com/justinas/alice" + "net/http" +) + +type DefaultCreator struct { + middlewares []string +} + +func New(middlewares []string) *DefaultCreator { + return &DefaultCreator{ + middlewares: middlewares, + } +} + +// CreateChain ... +func (b *DefaultCreator) Create() *alice.Chain { + chain := alice.New() + for _, mName := range b.middlewares { + middlewareName := mName + chain = chain.Append(func(next http.Handler) http.Handler { + constructor := b.getMiddleware(middlewareName) + if constructor == nil { + log.Errorf("cannot init middle %s", middlewareName) + return nil + } + return constructor(next) + }) + } + return &chain +} + +func (b *DefaultCreator) getMiddleware(mName string) alice.Constructor { + var middleware alice.Constructor + + if mName == READONLY { + middleware = func(next http.Handler) http.Handler { + return readonly.New(next) + } + } + if mName == URL { + if middleware != nil { + return nil + } + middleware = func(next http.Handler) http.Handler { + return url.New(next) + } + } + if mName == MUITIPLEMANIFEST { + if middleware != nil { + return nil + } + middleware = func(next http.Handler) http.Handler { + return multiplmanifest.New(next) + } + } + if mName == LISTREPO { + if middleware != nil { + return nil + } + middleware = func(next http.Handler) http.Handler { + return listrepo.New(next) + } + } + if mName == CONTENTTRUST { + if middleware != nil { + return nil + } + middleware = func(next http.Handler) http.Handler { + return contenttrust.New(next) + } + } + if mName == VULNERABLE { + if middleware != nil { + return nil + } + middleware = func(next http.Handler) http.Handler { + return vulnerable.New(next) + } + } + if mName == REGQUOTA { + if middleware != nil { + return nil + } + middleware = func(next http.Handler) http.Handler { + return regquota.New(next) + } + } + if mName == BLOBQUOTA { + if middleware != nil { + return nil + } + middleware = func(next http.Handler) http.Handler { + return blobquota.New(next) + } + } + + return middleware +} diff --git a/src/core/middlewares/config.go b/src/core/middlewares/config.go new file mode 100644 index 000000000..c881b5075 --- /dev/null +++ b/src/core/middlewares/config.go @@ -0,0 +1,16 @@ +package middlewares + +// const variables +const ( + READONLY = "readonly" + URL = "url" + MUITIPLEMANIFEST = "manifest" + LISTREPO = "listrepo" + CONTENTTRUST = "contenttrust" + VULNERABLE = "vulnerable" + REGQUOTA = "regquota" + BLOBQUOTA = "blobquota" +) + +// sequential organization +var Middlewares = []string{READONLY, URL, MUITIPLEMANIFEST, LISTREPO, CONTENTTRUST, VULNERABLE, BLOBQUOTA, REGQUOTA} diff --git a/src/core/middlewares/contenttrust/handler.go b/src/core/middlewares/contenttrust/handler.go new file mode 100644 index 000000000..5b9d7b4ed --- /dev/null +++ b/src/core/middlewares/contenttrust/handler.go @@ -0,0 +1,89 @@ +package contenttrust + +import ( + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/common/utils/notary" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/core/middlewares/util" + "net/http" + "strings" +) + +var NotaryEndpoint = "" + +type contentTrustHandler struct { + next http.Handler +} + +func New(next http.Handler) http.Handler { + return &contentTrustHandler{ + next: next, + } +} + +func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + imgRaw := req.Context().Value(util.ImageInfoCtxKey) + if imgRaw == nil || !config.WithNotary() { + cth.next.ServeHTTP(rw, req) + return + } + img, _ := req.Context().Value(util.ImageInfoCtxKey).(util.ImageInfo) + if img.Digest == "" { + cth.next.ServeHTTP(rw, req) + return + } + if !util.GetPolicyChecker().ContentTrustEnabled(img.ProjectName) { + cth.next.ServeHTTP(rw, req) + return + } + match, err := matchNotaryDigest(img) + if err != nil { + http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", "Failed in communication with Notary please check the log"), http.StatusInternalServerError) + return + } + if !match { + log.Debugf("digest mismatch, failing the response.") + http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", "The image is not signed in Notary."), http.StatusPreconditionFailed) + return + } + cth.next.ServeHTTP(rw, req) +} + +func matchNotaryDigest(img util.ImageInfo) (bool, error) { + if NotaryEndpoint == "" { + NotaryEndpoint = config.InternalNotaryEndpoint() + } + targets, err := notary.GetInternalTargets(NotaryEndpoint, util.TokenUsername, img.Repository) + if err != nil { + return false, err + } + for _, t := range targets { + if isDigest(img.Reference) { + d, err := notary.DigestFromTarget(t) + if err != nil { + return false, err + } + if img.Digest == d { + return true, nil + } + } else { + if t.Tag == img.Reference { + log.Debugf("found reference: %s in notary, try to match digest.", img.Reference) + d, err := notary.DigestFromTarget(t) + if err != nil { + return false, err + } + if img.Digest == d { + return true, nil + } + } + } + } + log.Debugf("image: %#v, not found in notary", img) + return false, nil +} + +// A sha256 is a string with 64 characters. +func isDigest(ref string) bool { + return strings.HasPrefix(ref, "sha256:") && len(ref) == 71 +} diff --git a/src/core/middlewares/inlet.go b/src/core/middlewares/inlet.go new file mode 100644 index 000000000..55f579a06 --- /dev/null +++ b/src/core/middlewares/inlet.go @@ -0,0 +1,25 @@ +package middlewares + +import ( + "errors" + "github.com/goharbor/harbor/src/core/middlewares/registryproxy" + "net/http" +) + +var head http.Handler + +// Init initialize the Proxy instance and handler chain. +func Init() error { + ph := registryproxy.New() + if ph == nil { + return errors.New("get nil when to create proxy") + } + handlerChain := New(Middlewares).Create() + head = handlerChain.Then(ph) + return nil +} + +// Handle handles the request. +func Handle(rw http.ResponseWriter, req *http.Request) { + head.ServeHTTP(rw, req) +} diff --git a/src/core/middlewares/interface.go b/src/core/middlewares/interface.go new file mode 100644 index 000000000..86381d8cd --- /dev/null +++ b/src/core/middlewares/interface.go @@ -0,0 +1,7 @@ +package middlewares + +import "github.com/justinas/alice" + +type ChainCreator interface { + Create(middlewares []string) *alice.Chain +} diff --git a/src/core/middlewares/listrepo/handler.go b/src/core/middlewares/listrepo/handler.go new file mode 100644 index 000000000..ea41a6557 --- /dev/null +++ b/src/core/middlewares/listrepo/handler.go @@ -0,0 +1,88 @@ +package listrepo + +import ( + "encoding/json" + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/middlewares/util" + "net/http" + "net/http/httptest" + "regexp" + "strconv" +) + +const ( + catalogURLPattern = `/v2/_catalog` +) + +type listReposHandler struct { + next http.Handler +} + +func New(next http.Handler) http.Handler { + return &listReposHandler{ + next: next, + } +} + +func (lrh listReposHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + var rec *httptest.ResponseRecorder + listReposFlag := matchListRepos(req) + if listReposFlag { + rec = httptest.NewRecorder() + lrh.next.ServeHTTP(rec, req) + if rec.Result().StatusCode != http.StatusOK { + util.CopyResp(rec, rw) + return + } + var ctlg struct { + Repositories []string `json:"repositories"` + } + decoder := json.NewDecoder(rec.Body) + if err := decoder.Decode(&ctlg); err != nil { + log.Errorf("Decode repositories error: %v", err) + util.CopyResp(rec, rw) + return + } + var entries []string + for repo := range ctlg.Repositories { + log.Debugf("the repo in the response %s", ctlg.Repositories[repo]) + exist := dao.RepositoryExists(ctlg.Repositories[repo]) + if exist { + entries = append(entries, ctlg.Repositories[repo]) + } + } + type Repos struct { + Repositories []string `json:"repositories"` + } + resp := &Repos{Repositories: entries} + respJSON, err := json.Marshal(resp) + if err != nil { + log.Errorf("Encode repositories error: %v", err) + util.CopyResp(rec, rw) + return + } + + for k, v := range rec.Header() { + rw.Header()[k] = v + } + clen := len(respJSON) + rw.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(clen)) + rw.Write(respJSON) + return + } + lrh.next.ServeHTTP(rw, req) +} + +// matchListRepos checks if the request looks like a request to list repositories. +func matchListRepos(req *http.Request) bool { + if req.Method != http.MethodGet { + return false + } + re := regexp.MustCompile(catalogURLPattern) + s := re.FindStringSubmatch(req.URL.Path) + if len(s) == 1 { + return true + } + return false +} diff --git a/src/core/middlewares/multiplmanifest/handler.go b/src/core/middlewares/multiplmanifest/handler.go new file mode 100644 index 000000000..bd18e09f2 --- /dev/null +++ b/src/core/middlewares/multiplmanifest/handler.go @@ -0,0 +1,33 @@ +package multiplmanifest + +import ( + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/middlewares/util" + "net/http" + "strings" +) + +type MultipleManifestHandler struct { + next http.Handler +} + +func New(next http.Handler) http.Handler { + return &MultipleManifestHandler{ + next: next, + } +} + +// The handler is responsible for blocking request to upload manifest list by docker client, which is not supported so far by Harbor. +func (mh MultipleManifestHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + match, _, _ := util.MatchManifestURL(req) + if match { + contentType := req.Header.Get("Content-type") + // application/vnd.docker.distribution.manifest.list.v2+json + if strings.Contains(contentType, "manifest.list.v2") { + log.Debugf("Content-type: %s is not supported, failing the response.", contentType) + http.Error(rw, util.MarshalError("UNSUPPORTED_MEDIA_TYPE", "Manifest.list is not supported."), http.StatusUnsupportedMediaType) + return + } + } + mh.next.ServeHTTP(rw, req) +} diff --git a/src/core/middlewares/readonly/hanlder.go b/src/core/middlewares/readonly/hanlder.go new file mode 100644 index 000000000..1bdb8b659 --- /dev/null +++ b/src/core/middlewares/readonly/hanlder.go @@ -0,0 +1,29 @@ +package readonly + +import ( + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/core/middlewares/util" + "net/http" +) + +type readonlyHandler struct { + next http.Handler +} + +func New(next http.Handler) http.Handler { + return &readonlyHandler{ + next: next, + } +} + +func (rh readonlyHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if config.ReadOnly() { + if req.Method == http.MethodDelete || req.Method == http.MethodPost || req.Method == http.MethodPatch || req.Method == http.MethodPut { + log.Warningf("The request is prohibited in readonly mode, url is: %s", req.URL.Path) + http.Error(rw, util.MarshalError("DENIED", "The system is in read only mode. Any modification is prohibited."), http.StatusForbidden) + return + } + } + rh.next.ServeHTTP(rw, req) +} diff --git a/src/core/middlewares/registryproxy/handler.go b/src/core/middlewares/registryproxy/handler.go new file mode 100644 index 000000000..eb0e43b05 --- /dev/null +++ b/src/core/middlewares/registryproxy/handler.go @@ -0,0 +1,107 @@ +package registryproxy + +import ( + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/core/middlewares/util" + "net/http" + "net/http/httputil" + "net/url" + "strings" +) + +type proxyHandler struct { + handler http.Handler +} + +func New(urls ...string) http.Handler { + var registryURL string + var err error + if len(urls) > 1 { + log.Errorf("the parm, urls should have only 0 or 1 elements") + return nil + } + if len(urls) == 0 { + registryURL, err = config.RegistryURL() + if err != nil { + log.Error(err) + return nil + } + } else { + registryURL = urls[0] + } + targetURL, err := url.Parse(registryURL) + if err != nil { + log.Error(err) + return nil + } + + return &proxyHandler{ + handler: &httputil.ReverseProxy{ + Director: func(req *http.Request) { + director(targetURL, req) + }, + ModifyResponse: modifyResponse, + }, + } + +} + +// Overwrite the http requests +func director(target *url.URL, req *http.Request) { + targetQuery := target.RawQuery + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) + if targetQuery == "" || req.URL.RawQuery == "" { + req.URL.RawQuery = targetQuery + req.URL.RawQuery + } else { + req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery + } + if _, ok := req.Header["User-Agent"]; !ok { + // explicitly disable User-Agent so it's not set to default value + req.Header.Set("User-Agent", "") + } +} + +// Modify the http response +func modifyResponse(res *http.Response) error { + + if res.Request.Method == http.MethodPut { + // PUT manifest + matchMF, _, _ := util.MatchManifestURL(res.Request) + if matchMF { + if res.StatusCode == http.StatusCreated { + log.Infof("we need to insert data here ... ") + } else if res.StatusCode >= 202 || res.StatusCode <= 511 { + log.Infof("we need to roll back data here ... ") + } + } + + // PUT blob + matchBB, _ := util.MatchPutBlobURL(res.Request) + if matchBB { + if res.StatusCode != http.StatusCreated { + log.Infof("we need to rollback DB and unlock digest ... ") + } + } + } + + return nil +} + +func singleJoiningSlash(a, b string) string { + aslash := strings.HasSuffix(a, "/") + bslash := strings.HasPrefix(b, "/") + switch { + case aslash && bslash: + return a + b[1:] + case !aslash && !bslash: + return a + "/" + b + } + return a + b +} + +func (ph proxyHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + ph.handler.ServeHTTP(rw, req) +} diff --git a/src/core/middlewares/regquota/handler.go b/src/core/middlewares/regquota/handler.go new file mode 100644 index 000000000..fe2b132ea --- /dev/null +++ b/src/core/middlewares/regquota/handler.go @@ -0,0 +1,53 @@ +package regquota + +import ( + "bytes" + "fmt" + "github.com/docker/distribution" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/middlewares/util" + "io/ioutil" + "net/http" +) + +type regQuotaHandler struct { + next http.Handler +} + +func New(next http.Handler) http.Handler { + return ®QuotaHandler{ + next: next, + } +} + +//PATCH manifest ... +func (rqh regQuotaHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + match, _, _ := util.MatchManifestURL(req) + if match { + var mfSize int64 + var mfDigest string + mediaType := req.Header.Get("Content-Type") + if req.Method == http.MethodPut && mediaType == "application/vnd.docker.distribution.manifest.v2+json" { + data, err := ioutil.ReadAll(req.Body) + if err != nil { + log.Warningf("Error occured when to copy manifest body %v", err) + http.Error(rw, util.MarshalError("InternalServerError", fmt.Sprintf("Error occured when to decode manifest body %v", err)), http.StatusInternalServerError) + return + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(data)) + + _, desc, err := distribution.UnmarshalManifest(mediaType, data) + if err != nil { + log.Warningf("Error occured when to Unmarshal Manifest %v", err) + http.Error(rw, util.MarshalError("InternalServerError", fmt.Sprintf("Error occured when to Unmarshal Manifest %v", err)), http.StatusInternalServerError) + return + } + mfDigest = desc.Digest.String() + mfSize = desc.Size + log.Infof("manifest digest... %s", mfDigest) + log.Infof("manifest size... %v", mfSize) + } + } + + rqh.next.ServeHTTP(rw, req) +} diff --git a/src/core/middlewares/url/handler.go b/src/core/middlewares/url/handler.go new file mode 100644 index 000000000..cfd893ef3 --- /dev/null +++ b/src/core/middlewares/url/handler.go @@ -0,0 +1,58 @@ +package url + +import ( + "context" + "fmt" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/middlewares/util" + coreutils "github.com/goharbor/harbor/src/core/utils" + "net/http" + "strings" +) + +type urlHandler struct { + next http.Handler +} + +func New(next http.Handler) http.Handler { + return &urlHandler{ + next: next, + } +} + +func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + log.Debugf("in url handler, path: %s", req.URL.Path) + flag, repository, reference := util.MatchPullManifest(req) + if flag { + components := strings.SplitN(repository, "/", 2) + if len(components) < 2 { + http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Bad repository name: %s", repository)), http.StatusBadRequest) + return + } + + client, err := coreutils.NewRepositoryClientForUI(util.TokenUsername, repository) + if err != nil { + log.Errorf("Error creating repository Client: %v", err) + http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Failed due to internal Error: %v", err)), http.StatusInternalServerError) + return + } + digest, _, err := client.ManifestExist(reference) + if err != nil { + log.Errorf("Failed to get digest for reference: %s, error: %v", reference, err) + http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Failed due to internal Error: %v", err)), http.StatusInternalServerError) + return + } + + img := util.ImageInfo{ + Repository: repository, + Reference: reference, + ProjectName: components[0], + Digest: digest, + } + + log.Debugf("image info of the request: %#v", img) + ctx := context.WithValue(req.Context(), util.ImageInfoCtxKey, img) + req = req.WithContext(ctx) + } + uh.next.ServeHTTP(rw, req) +} diff --git a/src/core/middlewares/util/util.go b/src/core/middlewares/util/util.go new file mode 100644 index 000000000..6eb6ade6a --- /dev/null +++ b/src/core/middlewares/util/util.go @@ -0,0 +1,144 @@ +package util + +import ( + "encoding/json" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/clair" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/core/promgr" + "net/http" + "net/http/httptest" + "regexp" + "strings" +) + +type contextKey string + +const ( + manifestURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)manifests/([\w][\w.:-]{0,127})` + blobURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)blobs/uploads/` + ImageInfoCtxKey = contextKey("ImageInfo") + // TODO: temp solution, remove after vmware/harbor#2242 is resolved. + TokenUsername = "harbor-core" +) + +// ImageInfo +type ImageInfo struct { + Repository string + Reference string + ProjectName string + Digest string +} + +// JSONError wraps a concrete Code and Message, it's readable for docker deamon. +type JSONError struct { + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Detail string `json:"detail,omitempty"` +} + +// MarshalError ... +func MarshalError(code, msg string) string { + var tmpErrs struct { + Errors []JSONError `json:"errors,omitempty"` + } + tmpErrs.Errors = append(tmpErrs.Errors, JSONError{ + Code: code, + Message: msg, + Detail: msg, + }) + str, err := json.Marshal(tmpErrs) + if err != nil { + log.Debugf("failed to marshal json error, %v", err) + return msg + } + return string(str) +} + +// MatchManifestURL ... +func MatchManifestURL(req *http.Request) (bool, string, string) { + re := regexp.MustCompile(manifestURLPattern) + s := re.FindStringSubmatch(req.URL.Path) + if len(s) == 3 { + s[1] = strings.TrimSuffix(s[1], "/") + return true, s[1], s[2] + } + return false, "", "" +} + +// MatchPutBlobURL ... +func MatchPutBlobURL(req *http.Request) (bool, string) { + if req.Method != http.MethodPut { + return false, "" + } + re := regexp.MustCompile(blobURLPattern) + s := re.FindStringSubmatch(req.URL.Path) + if len(s) == 2 { + s[1] = strings.TrimSuffix(s[1], "/") + return true, s[1] + } + return false, "" +} + +// MatchPullManifest checks if the request looks like a request to pull manifest. If it is returns the image and tag/sha256 digest as 2nd and 3rd return values +func MatchPullManifest(req *http.Request) (bool, string, string) { + if req.Method != http.MethodGet { + return false, "", "" + } + return MatchManifestURL(req) +} + +// CopyResp ... +func CopyResp(rec *httptest.ResponseRecorder, rw http.ResponseWriter) { + for k, v := range rec.Header() { + rw.Header()[k] = v + } + rw.WriteHeader(rec.Result().StatusCode) + rw.Write(rec.Body.Bytes()) +} + +// PolicyChecker checks the policy of a project by project name, to determine if it's needed to check the image's status under this project. +type PolicyChecker interface { + // contentTrustEnabled returns whether a project has enabled content trust. + ContentTrustEnabled(name string) bool + // vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity. + VulnerablePolicy(name string) (bool, models.Severity) +} + +// PmsPolicyChecker ... +type PmsPolicyChecker struct { + pm promgr.ProjectManager +} + +// ContentTrustEnabled ... +func (pc PmsPolicyChecker) ContentTrustEnabled(name string) bool { + project, err := pc.pm.Get(name) + if err != nil { + log.Errorf("Unexpected error when getting the project, error: %v", err) + return true + } + return project.ContentTrustEnabled() +} + +// VulnerablePolicy ... +func (pc PmsPolicyChecker) VulnerablePolicy(name string) (bool, models.Severity) { + project, err := pc.pm.Get(name) + if err != nil { + log.Errorf("Unexpected error when getting the project, error: %v", err) + return true, models.SevUnknown + } + return project.VulPrevented(), clair.ParseClairSev(project.Severity()) +} + +// NewPMSPolicyChecker returns an instance of an pmsPolicyChecker +func NewPMSPolicyChecker(pm promgr.ProjectManager) PolicyChecker { + return &PmsPolicyChecker{ + pm: pm, + } +} + +// GetPolicyChecker ... +func GetPolicyChecker() PolicyChecker { + return NewPMSPolicyChecker(config.GlobalProjectMgr) +} diff --git a/src/core/middlewares/vulnerable/handler.go b/src/core/middlewares/vulnerable/handler.go new file mode 100644 index 000000000..44762ac5e --- /dev/null +++ b/src/core/middlewares/vulnerable/handler.go @@ -0,0 +1,57 @@ +package vulnerable + +import ( + "fmt" + "github.com/goharbor/harbor/src/common/dao" + "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/common/utils/log" + "github.com/goharbor/harbor/src/core/config" + "github.com/goharbor/harbor/src/core/middlewares/util" + "net/http" +) + +type vulnerableHandler struct { + next http.Handler +} + +func New(next http.Handler) http.Handler { + return &vulnerableHandler{ + next: next, + } +} + +func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + imgRaw := req.Context().Value(util.ImageInfoCtxKey) + if imgRaw == nil || !config.WithClair() { + vh.next.ServeHTTP(rw, req) + return + } + img, _ := req.Context().Value(util.ImageInfoCtxKey).(util.ImageInfo) + if img.Digest == "" { + vh.next.ServeHTTP(rw, req) + return + } + projectVulnerableEnabled, projectVulnerableSeverity := util.GetPolicyChecker().VulnerablePolicy(img.ProjectName) + if !projectVulnerableEnabled { + vh.next.ServeHTTP(rw, req) + return + } + overview, err := dao.GetImgScanOverview(img.Digest) + if err != nil { + log.Errorf("failed to get ImgScanOverview with repo: %s, reference: %s, digest: %s. Error: %v", img.Repository, img.Reference, img.Digest, err) + http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", "Failed to get ImgScanOverview."), http.StatusPreconditionFailed) + return + } + // severity is 0 means that the image fails to scan or not scanned successfully. + if overview == nil || overview.Sev == 0 { + http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", "Cannot get the image severity."), http.StatusPreconditionFailed) + return + } + imageSev := overview.Sev + if imageSev >= int(projectVulnerableSeverity) { + log.Debugf("the image severity: %q is higher then project setting: %q, failing the response.", models.Severity(imageSev), projectVulnerableSeverity) + http.Error(rw, util.MarshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("The severity of vulnerability of the image: %q is equal or higher than the threshold in project setting: %q.", models.Severity(imageSev), projectVulnerableSeverity)), http.StatusPreconditionFailed) + return + } + vh.next.ServeHTTP(rw, req) +} diff --git a/src/core/proxy/interceptor_test.go b/src/core/proxy/interceptor_test.go deleted file mode 100644 index ab73f27bf..000000000 --- a/src/core/proxy/interceptor_test.go +++ /dev/null @@ -1,226 +0,0 @@ -package proxy - -import ( - "github.com/goharbor/harbor/src/common" - "github.com/goharbor/harbor/src/common/models" - notarytest "github.com/goharbor/harbor/src/common/utils/notary/test" - testutils "github.com/goharbor/harbor/src/common/utils/test" - "github.com/goharbor/harbor/src/core/config" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "net/http" - "net/http/httptest" - "os" - "testing" -) - -var endpoint = "10.117.4.142" -var notaryServer *httptest.Server - -var admiralEndpoint = "http://127.0.0.1:8282" -var token = "" - -func TestMain(m *testing.M) { - notaryServer = notarytest.NewNotaryServer(endpoint) - defer notaryServer.Close() - NotaryEndpoint = notaryServer.URL - var defaultConfig = map[string]interface{}{ - common.ExtEndpoint: "https://" + endpoint, - common.WithNotary: true, - common.TokenExpiration: 30, - } - config.InitWithSettings(defaultConfig) - result := m.Run() - if result != 0 { - os.Exit(result) - } -} - -func TestMatchPullManifest(t *testing.T) { - assert := assert.New(t) - req1, _ := http.NewRequest("POST", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil) - res1, _, _ := MatchPullManifest(req1) - assert.False(res1, "%s %v is not a request to pull manifest", req1.Method, req1.URL) - - req2, _ := http.NewRequest("GET", "http://192.168.0.3:80/v2/library/ubuntu/manifests/14.04", nil) - res2, repo2, tag2 := MatchPullManifest(req2) - assert.True(res2, "%s %v is a request to pull manifest", req2.Method, req2.URL) - assert.Equal("library/ubuntu", repo2) - assert.Equal("14.04", tag2) - - req3, _ := http.NewRequest("GET", "https://192.168.0.5:443/v1/library/ubuntu/manifests/14.04", nil) - res3, _, _ := MatchPullManifest(req3) - assert.False(res3, "%s %v is not a request to pull manifest", req3.Method, req3.URL) - - req4, _ := http.NewRequest("GET", "https://192.168.0.5/v2/library/ubuntu/manifests/14.04", nil) - res4, repo4, tag4 := MatchPullManifest(req4) - assert.True(res4, "%s %v is a request to pull manifest", req4.Method, req4.URL) - assert.Equal("library/ubuntu", repo4) - assert.Equal("14.04", tag4) - - req5, _ := http.NewRequest("GET", "https://myregistry.com/v2/path1/path2/golang/manifests/1.6.2", nil) - res5, repo5, tag5 := MatchPullManifest(req5) - assert.True(res5, "%s %v is a request to pull manifest", req5.Method, req5.URL) - assert.Equal("path1/path2/golang", repo5) - assert.Equal("1.6.2", tag5) - - req6, _ := http.NewRequest("GET", "https://myregistry.com/v2/myproject/registry/manifests/sha256:ca4626b691f57d16ce1576231e4a2e2135554d32e13a85dcff380d51fdd13f6a", nil) - res6, repo6, tag6 := MatchPullManifest(req6) - assert.True(res6, "%s %v is a request to pull manifest", req6.Method, req6.URL) - assert.Equal("myproject/registry", repo6) - assert.Equal("sha256:ca4626b691f57d16ce1576231e4a2e2135554d32e13a85dcff380d51fdd13f6a", tag6) - - req7, _ := http.NewRequest("GET", "https://myregistry.com/v2/myproject/manifests/sha256:ca4626b691f57d16ce1576231e4a2e2135554d32e13a85dcff380d51fdd13f6a", nil) - res7, repo7, tag7 := MatchPullManifest(req7) - assert.True(res7, "%s %v is a request to pull manifest", req7.Method, req7.URL) - assert.Equal("myproject", repo7) - assert.Equal("sha256:ca4626b691f57d16ce1576231e4a2e2135554d32e13a85dcff380d51fdd13f6a", tag7) -} - -func TestMatchPushManifest(t *testing.T) { - assert := assert.New(t) - req1, _ := http.NewRequest("POST", "http://127.0.0.1:5000/v2/library/ubuntu/manifests/14.04", nil) - res1, _, _ := MatchPushManifest(req1) - assert.False(res1, "%s %v is not a request to push manifest", req1.Method, req1.URL) - - req2, _ := http.NewRequest("PUT", "http://192.168.0.3:80/v2/library/ubuntu/manifests/14.04", nil) - res2, repo2, tag2 := MatchPushManifest(req2) - assert.True(res2, "%s %v is a request to push manifest", req2.Method, req2.URL) - assert.Equal("library/ubuntu", repo2) - assert.Equal("14.04", tag2) - - req3, _ := http.NewRequest("GET", "https://192.168.0.5:443/v1/library/ubuntu/manifests/14.04", nil) - res3, _, _ := MatchPushManifest(req3) - assert.False(res3, "%s %v is not a request to push manifest", req3.Method, req3.URL) - - req4, _ := http.NewRequest("PUT", "https://192.168.0.5/v2/library/ubuntu/manifests/14.04", nil) - res4, repo4, tag4 := MatchPushManifest(req4) - assert.True(res4, "%s %v is a request to push manifest", req4.Method, req4.URL) - assert.Equal("library/ubuntu", repo4) - assert.Equal("14.04", tag4) - - req5, _ := http.NewRequest("PUT", "https://myregistry.com/v2/path1/path2/golang/manifests/1.6.2", nil) - res5, repo5, tag5 := MatchPushManifest(req5) - assert.True(res5, "%s %v is a request to push manifest", req5.Method, req5.URL) - assert.Equal("path1/path2/golang", repo5) - assert.Equal("1.6.2", tag5) - - req6, _ := http.NewRequest("PUT", "https://myregistry.com/v2/myproject/registry/manifests/sha256:ca4626b691f57d16ce1576231e4a2e2135554d32e13a85dcff380d51fdd13f6a", nil) - res6, repo6, tag6 := MatchPushManifest(req6) - assert.True(res6, "%s %v is a request to push manifest", req6.Method, req6.URL) - assert.Equal("myproject/registry", repo6) - assert.Equal("sha256:ca4626b691f57d16ce1576231e4a2e2135554d32e13a85dcff380d51fdd13f6a", tag6) - - req7, _ := http.NewRequest("PUT", "https://myregistry.com/v2/myproject/manifests/sha256:ca4626b691f57d16ce1576231e4a2e2135554d32e13a85dcff380d51fdd13f6a", nil) - res7, repo7, tag7 := MatchPushManifest(req7) - assert.True(res7, "%s %v is a request to push manifest", req7.Method, req7.URL) - assert.Equal("myproject", repo7) - assert.Equal("sha256:ca4626b691f57d16ce1576231e4a2e2135554d32e13a85dcff380d51fdd13f6a", tag7) - - req8, _ := http.NewRequest("PUT", "http://192.168.0.3:80/v2/library/ubuntu/manifests/14.04", nil) - res8, repo8, tag8 := MatchPushManifest(req8) - assert.True(res8, "%s %v is a request to push manifest", req8.Method, req8.URL) - assert.Equal("library/ubuntu", repo8) - assert.Equal("14.04", tag8) -} - -func TestMatchListRepos(t *testing.T) { - assert := assert.New(t) - req1, _ := http.NewRequest("POST", "http://127.0.0.1:5000/v2/_catalog", nil) - res1 := MatchListRepos(req1) - assert.False(res1, "%s %v is not a request to list repos", req1.Method, req1.URL) - - req2, _ := http.NewRequest("GET", "http://127.0.0.1:5000/v2/_catalog", nil) - res2 := MatchListRepos(req2) - assert.True(res2, "%s %v is a request to list repos", req2.Method, req2.URL) - - req3, _ := http.NewRequest("GET", "https://192.168.0.5:443/v1/_catalog", nil) - res3 := MatchListRepos(req3) - assert.False(res3, "%s %v is not a request to pull manifest", req3.Method, req3.URL) - -} - -func TestPMSPolicyChecker(t *testing.T) { - var defaultConfigAdmiral = map[string]interface{}{ - common.ExtEndpoint: "https://" + endpoint, - common.WithNotary: true, - common.TokenExpiration: 30, - common.DatabaseType: "postgresql", - common.PostGreSQLHOST: "127.0.0.1", - common.PostGreSQLPort: 5432, - common.PostGreSQLUsername: "postgres", - common.PostGreSQLPassword: "root123", - common.PostGreSQLDatabase: "registry", - } - - if err := config.Init(); err != nil { - panic(err) - } - testutils.InitDatabaseFromEnv() - - config.Upload(defaultConfigAdmiral) - - name := "project_for_test_get_sev_low" - id, err := config.GlobalProjectMgr.Create(&models.Project{ - Name: name, - OwnerID: 1, - Metadata: map[string]string{ - models.ProMetaEnableContentTrust: "true", - models.ProMetaPreventVul: "true", - models.ProMetaSeverity: "low", - }, - }) - require.Nil(t, err) - defer func(id int64) { - if err := config.GlobalProjectMgr.Delete(id); err != nil { - t.Logf("failed to delete project %d: %v", id, err) - } - }(id) - - contentTrustFlag := getPolicyChecker().contentTrustEnabled("project_for_test_get_sev_low") - assert.True(t, contentTrustFlag) - projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy("project_for_test_get_sev_low") - assert.True(t, projectVulnerableEnabled) - assert.Equal(t, projectVulnerableSeverity, models.SevLow) -} - -func TestMatchNotaryDigest(t *testing.T) { - assert := assert.New(t) - // The data from common/utils/notary/helper_test.go - img1 := imageInfo{"notary-demo/busybox", "1.0", "notary-demo", "sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7"} - img2 := imageInfo{"notary-demo/busybox", "2.0", "notary-demo", "sha256:12345678"} - - res1, err := matchNotaryDigest(img1) - assert.Nil(err, "Unexpected error: %v, image: %#v", err, img1) - assert.True(res1) - - res2, err := matchNotaryDigest(img2) - assert.Nil(err, "Unexpected error: %v, image: %#v, take 2", err, img2) - assert.False(res2) -} - -func TestCopyResp(t *testing.T) { - assert := assert.New(t) - rec1 := httptest.NewRecorder() - rec2 := httptest.NewRecorder() - rec1.Header().Set("X-Test", "mytest") - rec1.WriteHeader(418) - copyResp(rec1, rec2) - assert.Equal(418, rec2.Result().StatusCode) - assert.Equal("mytest", rec2.Header().Get("X-Test")) -} - -func TestMarshalError(t *testing.T) { - assert := assert.New(t) - js1 := marshalError("PROJECT_POLICY_VIOLATION", "Not Found") - assert.Equal("{\"errors\":[{\"code\":\"PROJECT_POLICY_VIOLATION\",\"message\":\"Not Found\",\"detail\":\"Not Found\"}]}", js1) - js2 := marshalError("DENIED", "The action is denied") - assert.Equal("{\"errors\":[{\"code\":\"DENIED\",\"message\":\"The action is denied\",\"detail\":\"The action is denied\"}]}", js2) -} - -func TestIsDigest(t *testing.T) { - assert := assert.New(t) - assert.False(isDigest("latest")) - assert.True(isDigest("sha256:1359608115b94599e5641638bac5aef1ddfaa79bb96057ebf41ebc8d33acf8a7")) -} diff --git a/src/core/proxy/interceptors.go b/src/core/proxy/interceptors.go deleted file mode 100644 index fb656edce..000000000 --- a/src/core/proxy/interceptors.go +++ /dev/null @@ -1,411 +0,0 @@ -package proxy - -import ( - "encoding/json" - "github.com/goharbor/harbor/src/common/dao" - "github.com/goharbor/harbor/src/common/models" - "github.com/goharbor/harbor/src/common/utils/clair" - "github.com/goharbor/harbor/src/common/utils/log" - "github.com/goharbor/harbor/src/common/utils/notary" - "github.com/goharbor/harbor/src/core/config" - "github.com/goharbor/harbor/src/core/promgr" - coreutils "github.com/goharbor/harbor/src/core/utils" - "github.com/goharbor/harbor/src/pkg/scan" - - "context" - "fmt" - "net/http" - "net/http/httptest" - "regexp" - "strconv" - "strings" -) - -type contextKey string - -const ( - manifestURLPattern = `^/v2/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)manifests/([\w][\w.:-]{0,127})` - catalogURLPattern = `/v2/_catalog` - imageInfoCtxKey = contextKey("ImageInfo") - // TODO: temp solution, remove after vmware/harbor#2242 is resolved. - tokenUsername = "harbor-core" -) - -// Record the docker deamon raw response. -var rec *httptest.ResponseRecorder - -// NotaryEndpoint , exported for testing. -var NotaryEndpoint = "" - -// MatchPullManifest checks if the request looks like a request to pull manifest. If it is returns the image and tag/sha256 digest as 2nd and 3rd return values -func MatchPullManifest(req *http.Request) (bool, string, string) { - // TODO: add user agent check. - if req.Method != http.MethodGet { - return false, "", "" - } - return matchManifestURL(req) -} - -// MatchPushManifest checks if the request looks like a request to push manifest. If it is returns the image and tag/sha256 digest as 2nd and 3rd return values -func MatchPushManifest(req *http.Request) (bool, string, string) { - if req.Method != http.MethodPut { - return false, "", "" - } - return matchManifestURL(req) -} - -func matchManifestURL(req *http.Request) (bool, string, string) { - re := regexp.MustCompile(manifestURLPattern) - s := re.FindStringSubmatch(req.URL.Path) - if len(s) == 3 { - s[1] = strings.TrimSuffix(s[1], "/") - return true, s[1], s[2] - } - return false, "", "" -} - -// MatchListRepos checks if the request looks like a request to list repositories. -func MatchListRepos(req *http.Request) bool { - if req.Method != http.MethodGet { - return false - } - re := regexp.MustCompile(catalogURLPattern) - s := re.FindStringSubmatch(req.URL.Path) - if len(s) == 1 { - return true - } - return false -} - -// policyChecker checks the policy of a project by project name, to determine if it's needed to check the image's status under this project. -type policyChecker interface { - // contentTrustEnabled returns whether a project has enabled content trust. - contentTrustEnabled(name string) bool - // vulnerablePolicy returns whether a project has enabled vulnerable, and the project's severity. - vulnerablePolicy(name string) (bool, models.Severity) -} - -type pmsPolicyChecker struct { - pm promgr.ProjectManager -} - -func (pc pmsPolicyChecker) contentTrustEnabled(name string) bool { - project, err := pc.pm.Get(name) - if err != nil { - log.Errorf("Unexpected error when getting the project, error: %v", err) - return true - } - return project.ContentTrustEnabled() -} -func (pc pmsPolicyChecker) vulnerablePolicy(name string) (bool, models.Severity) { - project, err := pc.pm.Get(name) - if err != nil { - log.Errorf("Unexpected error when getting the project, error: %v", err) - return true, models.SevUnknown - } - return project.VulPrevented(), clair.ParseClairSev(project.Severity()) -} - -// newPMSPolicyChecker returns an instance of an pmsPolicyChecker -func newPMSPolicyChecker(pm promgr.ProjectManager) policyChecker { - return &pmsPolicyChecker{ - pm: pm, - } -} - -func getPolicyChecker() policyChecker { - return newPMSPolicyChecker(config.GlobalProjectMgr) -} - -type imageInfo struct { - repository string - reference string - projectName string - digest string -} - -type urlHandler struct { - next http.Handler -} - -func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - log.Debugf("in url handler, path: %s", req.URL.Path) - flag, repository, reference := MatchPullManifest(req) - if flag { - components := strings.SplitN(repository, "/", 2) - if len(components) < 2 { - http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Bad repository name: %s", repository)), http.StatusBadRequest) - return - } - - client, err := coreutils.NewRepositoryClientForUI(tokenUsername, repository) - if err != nil { - log.Errorf("Error creating repository Client: %v", err) - http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Failed due to internal Error: %v", err)), http.StatusInternalServerError) - return - } - digest, _, err := client.ManifestExist(reference) - if err != nil { - log.Errorf("Failed to get digest for reference: %s, error: %v", reference, err) - http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("Failed due to internal Error: %v", err)), http.StatusInternalServerError) - return - } - - img := imageInfo{ - repository: repository, - reference: reference, - projectName: components[0], - digest: digest, - } - - log.Debugf("image info of the request: %#v", img) - ctx := context.WithValue(req.Context(), imageInfoCtxKey, img) - req = req.WithContext(ctx) - } - uh.next.ServeHTTP(rw, req) -} - -type readonlyHandler struct { - next http.Handler -} - -func (rh readonlyHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - if config.ReadOnly() { - if req.Method == http.MethodDelete || req.Method == http.MethodPost || req.Method == http.MethodPatch || req.Method == http.MethodPut { - log.Warningf("The request is prohibited in readonly mode, url is: %s", req.URL.Path) - http.Error(rw, marshalError("DENIED", "The system is in read only mode. Any modification is prohibited."), http.StatusForbidden) - return - } - } - rh.next.ServeHTTP(rw, req) -} - -type multipleManifestHandler struct { - next http.Handler -} - -// The handler is responsible for blocking request to upload manifest list by docker client, which is not supported so far by Harbor. -func (mh multipleManifestHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - match, _, _ := MatchPushManifest(req) - if match { - contentType := req.Header.Get("Content-type") - // application/vnd.docker.distribution.manifest.list.v2+json - if strings.Contains(contentType, "manifest.list.v2") { - log.Debugf("Content-type: %s is not supported, failing the response.", contentType) - http.Error(rw, marshalError("UNSUPPORTED_MEDIA_TYPE", "Manifest.list is not supported."), http.StatusUnsupportedMediaType) - return - } - } - mh.next.ServeHTTP(rw, req) -} - -type listReposHandler struct { - next http.Handler -} - -func (lrh listReposHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - listReposFlag := MatchListRepos(req) - if listReposFlag { - rec = httptest.NewRecorder() - lrh.next.ServeHTTP(rec, req) - if rec.Result().StatusCode != http.StatusOK { - copyResp(rec, rw) - return - } - var ctlg struct { - Repositories []string `json:"repositories"` - } - decoder := json.NewDecoder(rec.Body) - if err := decoder.Decode(&ctlg); err != nil { - log.Errorf("Decode repositories error: %v", err) - copyResp(rec, rw) - return - } - var entries []string - for repo := range ctlg.Repositories { - log.Debugf("the repo in the response %s", ctlg.Repositories[repo]) - exist := dao.RepositoryExists(ctlg.Repositories[repo]) - if exist { - entries = append(entries, ctlg.Repositories[repo]) - } - } - type Repos struct { - Repositories []string `json:"repositories"` - } - resp := &Repos{Repositories: entries} - respJSON, err := json.Marshal(resp) - if err != nil { - log.Errorf("Encode repositories error: %v", err) - copyResp(rec, rw) - return - } - - for k, v := range rec.Header() { - rw.Header()[k] = v - } - clen := len(respJSON) - rw.Header().Set(http.CanonicalHeaderKey("Content-Length"), strconv.Itoa(clen)) - rw.Write(respJSON) - return - } - lrh.next.ServeHTTP(rw, req) -} - -type contentTrustHandler struct { - next http.Handler -} - -func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - imgRaw := req.Context().Value(imageInfoCtxKey) - if imgRaw == nil || !config.WithNotary() { - cth.next.ServeHTTP(rw, req) - return - } - img, _ := req.Context().Value(imageInfoCtxKey).(imageInfo) - if img.digest == "" { - cth.next.ServeHTTP(rw, req) - return - } - if !getPolicyChecker().contentTrustEnabled(img.projectName) { - cth.next.ServeHTTP(rw, req) - return - } - match, err := matchNotaryDigest(img) - if err != nil { - http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Failed in communication with Notary please check the log"), http.StatusInternalServerError) - return - } - if !match { - log.Debugf("digest mismatch, failing the response.") - http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "The image is not signed in Notary."), http.StatusPreconditionFailed) - return - } - cth.next.ServeHTTP(rw, req) -} - -type vulnerableHandler struct { - next http.Handler -} - -func (vh vulnerableHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - imgRaw := req.Context().Value(imageInfoCtxKey) - if imgRaw == nil || !config.WithClair() { - vh.next.ServeHTTP(rw, req) - return - } - img, _ := req.Context().Value(imageInfoCtxKey).(imageInfo) - if img.digest == "" { - vh.next.ServeHTTP(rw, req) - return - } - projectVulnerableEnabled, projectVulnerableSeverity := getPolicyChecker().vulnerablePolicy(img.projectName) - if !projectVulnerableEnabled { - vh.next.ServeHTTP(rw, req) - return - } - // TODO: Get whitelist based on project setting - wl, err := dao.GetSysCVEWhitelist() - if err != nil { - log.Errorf("Failed to get the whitelist, error: %v", err) - http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Failed to get CVE whitelist."), http.StatusPreconditionFailed) - return - } - vl, err := scan.VulnListByDigest(img.digest) - if err != nil { - log.Errorf("Failed to get the vulnerability list, error: %v", err) - http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", "Failed to get vulnerabilities."), http.StatusPreconditionFailed) - return - } - filtered := vl.ApplyWhitelist(*wl) - msg := vh.filterMsg(img, filtered) - log.Info(msg) - if int(vl.Severity()) >= int(projectVulnerableSeverity) { - log.Debugf("the image severity: %q is higher then project setting: %q, failing the response.", vl.Severity(), projectVulnerableSeverity) - http.Error(rw, marshalError("PROJECT_POLICY_VIOLATION", fmt.Sprintf("The severity of vulnerability of the image: %q is equal or higher than the threshold in project setting: %q.", vl.Severity(), projectVulnerableSeverity)), http.StatusPreconditionFailed) - return - } - vh.next.ServeHTTP(rw, req) -} - -func (vh vulnerableHandler) filterMsg(img imageInfo, filtered scan.VulnerabilityList) string { - filterMsg := fmt.Sprintf("Image: %s/%s:%s, digest: %s, vulnerabilities fitered by whitelist:", img.projectName, img.repository, img.reference, img.digest) - if len(filtered) == 0 { - filterMsg = fmt.Sprintf("%s none.", filterMsg) - } - for _, v := range filtered { - filterMsg = fmt.Sprintf("%s ID: %s, severity: %s;", filterMsg, v.ID, v.Severity) - } - return filterMsg -} - -func matchNotaryDigest(img imageInfo) (bool, error) { - if NotaryEndpoint == "" { - NotaryEndpoint = config.InternalNotaryEndpoint() - } - targets, err := notary.GetInternalTargets(NotaryEndpoint, tokenUsername, img.repository) - if err != nil { - return false, err - } - for _, t := range targets { - if isDigest(img.reference) { - d, err := notary.DigestFromTarget(t) - if err != nil { - return false, err - } - if img.digest == d { - return true, nil - } - } else { - if t.Tag == img.reference { - log.Debugf("found reference: %s in notary, try to match digest.", img.reference) - d, err := notary.DigestFromTarget(t) - if err != nil { - return false, err - } - if img.digest == d { - return true, nil - } - } - } - } - log.Debugf("image: %#v, not found in notary", img) - return false, nil -} - -// A sha256 is a string with 64 characters. -func isDigest(ref string) bool { - return strings.HasPrefix(ref, "sha256:") && len(ref) == 71 -} - -func copyResp(rec *httptest.ResponseRecorder, rw http.ResponseWriter) { - for k, v := range rec.Header() { - rw.Header()[k] = v - } - rw.WriteHeader(rec.Result().StatusCode) - rw.Write(rec.Body.Bytes()) -} - -func marshalError(code, msg string) string { - var tmpErrs struct { - Errors []JSONError `json:"errors,omitempty"` - } - tmpErrs.Errors = append(tmpErrs.Errors, JSONError{ - Code: code, - Message: msg, - Detail: msg, - }) - - str, err := json.Marshal(tmpErrs) - if err != nil { - log.Debugf("failed to marshal json error, %v", err) - return msg - } - return string(str) -} - -// JSONError wraps a concrete Code and Message, it's readable for docker deamon. -type JSONError struct { - Code string `json:"code,omitempty"` - Message string `json:"message,omitempty"` - Detail string `json:"detail,omitempty"` -} diff --git a/src/core/proxy/proxy.go b/src/core/proxy/proxy.go deleted file mode 100644 index bc0d7f44a..000000000 --- a/src/core/proxy/proxy.go +++ /dev/null @@ -1,56 +0,0 @@ -package proxy - -import ( - "github.com/goharbor/harbor/src/core/config" - - "fmt" - "net/http" - "net/http/httputil" - "net/url" -) - -// Proxy is the instance of the reverse proxy in this package. -var Proxy *httputil.ReverseProxy - -var handlers handlerChain - -type handlerChain struct { - head http.Handler -} - -// Init initialize the Proxy instance and handler chain. -func Init(urls ...string) error { - var err error - var registryURL string - if len(urls) > 1 { - return fmt.Errorf("the parm, urls should have only 0 or 1 elements") - } - if len(urls) == 0 { - registryURL, err = config.RegistryURL() - if err != nil { - return err - } - } else { - registryURL = urls[0] - } - targetURL, err := url.Parse(registryURL) - if err != nil { - return err - } - Proxy = httputil.NewSingleHostReverseProxy(targetURL) - handlers = handlerChain{ - head: readonlyHandler{ - next: urlHandler{ - next: multipleManifestHandler{ - next: listReposHandler{ - next: contentTrustHandler{ - next: vulnerableHandler{ - next: Proxy, - }}}}}}} - return nil -} - -// Handle handles the request. -func Handle(rw http.ResponseWriter, req *http.Request) { - handlers.head.ServeHTTP(rw, req) -} diff --git a/src/go.mod b/src/go.mod index 3ce265cce..9c6b4efcb 100644 --- a/src/go.mod +++ b/src/go.mod @@ -46,6 +46,7 @@ require ( github.com/gorilla/mux v1.6.2 github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/jinzhu/gorm v1.9.8 // indirect + github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/lib/pq v1.1.0 diff --git a/src/go.sum b/src/go.sum index 222c78148..c63861893 100644 --- a/src/go.sum +++ b/src/go.sum @@ -169,6 +169,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da h1:5y58+OCjoHCYB8182mpf/dEsq0vwTKPOo4zGfH0xW9A= +github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da/go.mod h1:oLH0CmIaxCGXD67VKGR5AacGXZSMznlmeqM8RzPrcY8= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= diff --git a/src/vendor/github.com/justinas/alice/.travis.yml b/src/vendor/github.com/justinas/alice/.travis.yml new file mode 100644 index 000000000..dc6bea671 --- /dev/null +++ b/src/vendor/github.com/justinas/alice/.travis.yml @@ -0,0 +1,17 @@ +language: go + +matrix: + include: + - go: 1.0.x + - go: 1.1.x + - go: 1.2.x + - go: 1.3.x + - go: 1.4.x + - go: 1.5.x + - go: 1.6.x + - go: 1.7.x + - go: 1.8.x + - go: 1.9.x + - go: tip + allow_failures: + - go: tip diff --git a/src/vendor/github.com/justinas/alice/LICENSE b/src/vendor/github.com/justinas/alice/LICENSE new file mode 100644 index 000000000..0d0d352ec --- /dev/null +++ b/src/vendor/github.com/justinas/alice/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Justinas Stankevicius + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/vendor/github.com/justinas/alice/README.md b/src/vendor/github.com/justinas/alice/README.md new file mode 100644 index 000000000..e4f9157c0 --- /dev/null +++ b/src/vendor/github.com/justinas/alice/README.md @@ -0,0 +1,98 @@ +# Alice + +[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](http://godoc.org/github.com/justinas/alice) +[![Build Status](https://travis-ci.org/justinas/alice.svg?branch=master)](https://travis-ci.org/justinas/alice) +[![Coverage](http://gocover.io/_badge/github.com/justinas/alice)](http://gocover.io/github.com/justinas/alice) + +Alice provides a convenient way to chain +your HTTP middleware functions and the app handler. + +In short, it transforms + +```go +Middleware1(Middleware2(Middleware3(App))) +``` + +to + +```go +alice.New(Middleware1, Middleware2, Middleware3).Then(App) +``` + +### Why? + +None of the other middleware chaining solutions +behaves exactly like Alice. +Alice is as minimal as it gets: +in essence, it's just a for loop that does the wrapping for you. + +Check out [this blog post](http://justinas.org/alice-painless-middleware-chaining-for-go/) +for explanation how Alice is different from other chaining solutions. + +### Usage + +Your middleware constructors should have the form of + +```go +func (http.Handler) http.Handler +``` + +Some middleware provide this out of the box. +For ones that don't, it's trivial to write one yourself. + +```go +func myStripPrefix(h http.Handler) http.Handler { + return http.StripPrefix("/old", h) +} +``` + +This complete example shows the full power of Alice. + +```go +package main + +import ( + "net/http" + "time" + + "github.com/throttled/throttled" + "github.com/justinas/alice" + "github.com/justinas/nosurf" +) + +func timeoutHandler(h http.Handler) http.Handler { + return http.TimeoutHandler(h, 1*time.Second, "timed out") +} + +func myApp(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello world!")) +} + +func main() { + th := throttled.Interval(throttled.PerSec(10), 1, &throttled.VaryBy{Path: true}, 50) + myHandler := http.HandlerFunc(myApp) + + chain := alice.New(th.Throttle, timeoutHandler, nosurf.NewPure).Then(myHandler) + http.ListenAndServe(":8000", chain) +} +``` + +Here, the request will pass [throttled](https://github.com/PuerkitoBio/throttled) first, +then an http.TimeoutHandler we've set up, +then [nosurf](https://github.com/justinas/nosurf) +and will finally reach our handler. + +Note that Alice makes **no guarantees** for +how one or another piece of middleware will behave. +Once it passes the execution to the outer layer of middleware, +it has no saying in whether middleware will execute the inner handlers. +This is intentional behavior. + +Alice works with Go 1.0 and higher. + +### Contributing + +0. Find an issue that bugs you / open a new one. +1. Discuss. +2. Branch off, commit, test. +3. Make a pull request / attach the commits to the issue. diff --git a/src/vendor/github.com/justinas/alice/chain.go b/src/vendor/github.com/justinas/alice/chain.go new file mode 100644 index 000000000..da0e2b580 --- /dev/null +++ b/src/vendor/github.com/justinas/alice/chain.go @@ -0,0 +1,112 @@ +// Package alice provides a convenient way to chain http handlers. +package alice + +import "net/http" + +// A constructor for a piece of middleware. +// Some middleware use this constructor out of the box, +// so in most cases you can just pass somepackage.New +type Constructor func(http.Handler) http.Handler + +// Chain acts as a list of http.Handler constructors. +// Chain is effectively immutable: +// once created, it will always hold +// the same set of constructors in the same order. +type Chain struct { + constructors []Constructor +} + +// New creates a new chain, +// memorizing the given list of middleware constructors. +// New serves no other function, +// constructors are only called upon a call to Then(). +func New(constructors ...Constructor) Chain { + return Chain{append(([]Constructor)(nil), constructors...)} +} + +// Then chains the middleware and returns the final http.Handler. +// New(m1, m2, m3).Then(h) +// is equivalent to: +// m1(m2(m3(h))) +// When the request comes in, it will be passed to m1, then m2, then m3 +// and finally, the given handler +// (assuming every middleware calls the following one). +// +// A chain can be safely reused by calling Then() several times. +// stdStack := alice.New(ratelimitHandler, csrfHandler) +// indexPipe = stdStack.Then(indexHandler) +// authPipe = stdStack.Then(authHandler) +// Note that constructors are called on every call to Then() +// and thus several instances of the same middleware will be created +// when a chain is reused in this way. +// For proper middleware, this should cause no problems. +// +// Then() treats nil as http.DefaultServeMux. +func (c Chain) Then(h http.Handler) http.Handler { + if h == nil { + h = http.DefaultServeMux + } + + for i := range c.constructors { + h = c.constructors[len(c.constructors)-1-i](h) + } + + return h +} + +// ThenFunc works identically to Then, but takes +// a HandlerFunc instead of a Handler. +// +// The following two statements are equivalent: +// c.Then(http.HandlerFunc(fn)) +// c.ThenFunc(fn) +// +// ThenFunc provides all the guarantees of Then. +func (c Chain) ThenFunc(fn http.HandlerFunc) http.Handler { + if fn == nil { + return c.Then(nil) + } + return c.Then(fn) +} + +// Append extends a chain, adding the specified constructors +// as the last ones in the request flow. +// +// Append returns a new chain, leaving the original one untouched. +// +// stdChain := alice.New(m1, m2) +// extChain := stdChain.Append(m3, m4) +// // requests in stdChain go m1 -> m2 +// // requests in extChain go m1 -> m2 -> m3 -> m4 +func (c Chain) Append(constructors ...Constructor) Chain { + newCons := make([]Constructor, 0, len(c.constructors)+len(constructors)) + newCons = append(newCons, c.constructors...) + newCons = append(newCons, constructors...) + + return Chain{newCons} +} + +// Extend extends a chain by adding the specified chain +// as the last one in the request flow. +// +// Extend returns a new chain, leaving the original one untouched. +// +// stdChain := alice.New(m1, m2) +// ext1Chain := alice.New(m3, m4) +// ext2Chain := stdChain.Extend(ext1Chain) +// // requests in stdChain go m1 -> m2 +// // requests in ext1Chain go m3 -> m4 +// // requests in ext2Chain go m1 -> m2 -> m3 -> m4 +// +// Another example: +// aHtmlAfterNosurf := alice.New(m2) +// aHtml := alice.New(m1, func(h http.Handler) http.Handler { +// csrf := nosurf.New(h) +// csrf.SetFailureHandler(aHtmlAfterNosurf.ThenFunc(csrfFail)) +// return csrf +// }).Extend(aHtmlAfterNosurf) +// // requests to aHtml hitting nosurfs success handler go m1 -> nosurf -> m2 -> target-handler +// // requests to aHtml hitting nosurfs failure handler go m1 -> nosurf -> m2 -> csrfFail +func (c Chain) Extend(chain Chain) Chain { + return c.Append(chain.constructors...) +} diff --git a/src/vendor/modules.txt b/src/vendor/modules.txt index fbe305814..fc7a8ec94 100644 --- a/src/vendor/modules.txt +++ b/src/vendor/modules.txt @@ -114,6 +114,8 @@ github.com/gorilla/handlers github.com/gorilla/mux # github.com/json-iterator/go v1.1.6 github.com/json-iterator/go +# github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da +github.com/justinas/alice # github.com/konsorten/go-windows-terminal-sequences v1.0.2 github.com/konsorten/go-windows-terminal-sequences # github.com/lib/pq v1.1.0