Files
zot/pkg/common/http_server.go

176 lines
5.1 KiB
Go

package common
import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
jsoniter "github.com/json-iterator/go"
"zotregistry.dev/zot/v2/pkg/api/config"
"zotregistry.dev/zot/v2/pkg/api/constants"
apiErr "zotregistry.dev/zot/v2/pkg/api/errors"
reqCtx "zotregistry.dev/zot/v2/pkg/requestcontext"
)
func AllowedMethods(methods ...string) []string {
return append(methods, http.MethodOptions)
}
func AddExtensionSecurityHeaders() mux.MiddlewareFunc { //nolint:varnamelen
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(resp, req)
})
}
}
func ACHeadersMiddleware(config *config.Config, allowedMethods ...string) mux.MiddlewareFunc {
allowedMethodsValue := strings.Join(allowedMethods, ",")
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
resp.Header().Set("Access-Control-Allow-Methods", allowedMethodsValue)
resp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
// Access-Control-Allow-Credentials must not be "true" when
// Access-Control-Allow-Origin is the wildcard "*" (CORS spec §3.2).
// Only advertise credentials support when an explicit origin is set.
authConfig := config.CopyAuthConfig()
allowOrigin := strings.TrimSpace(config.GetAllowOrigin())
if authConfig.IsBasicAuthnEnabled() && allowOrigin != "" && allowOrigin != "*" {
resp.Header().Set("Access-Control-Allow-Credentials", "true")
}
if req.Method == http.MethodOptions {
return
}
next.ServeHTTP(resp, req)
})
}
}
func CORSHeadersMiddleware(allowOrigin string) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
AddCORSHeaders(allowOrigin, response)
next.ServeHTTP(response, request)
})
}
}
func AddCORSHeaders(allowOrigin string, response http.ResponseWriter) {
if allowOrigin == "" {
response.Header().Set("Access-Control-Allow-Origin", "*")
} else {
response.Header().Set("Access-Control-Allow-Origin", allowOrigin)
}
}
// AuthzOnlyAdminsMiddleware permits only admin user access if auth is enabled.
func AuthzOnlyAdminsMiddleware(conf *config.Config) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
// Get auth config safely
authConfig := conf.CopyAuthConfig()
if !authConfig.IsBasicAuthnEnabled() {
next.ServeHTTP(response, request)
return
}
realm := conf.GetRealm()
failDelay := authConfig.GetFailDelay()
// get userAccessControl built in previous authn/authz middlewares
userAc, err := reqCtx.UserAcFromContext(request.Context())
if err != nil { // should not happen as this has been previously checked for errors
AuthzFail(response, request, userAc.GetUsername(), realm, failDelay)
return
}
// reject non-admin access if authentication is enabled
if userAc != nil && !userAc.IsAdmin() {
AuthzFail(response, request, userAc.GetUsername(), realm, failDelay)
return
}
next.ServeHTTP(response, request)
})
}
}
func AuthzFail(w http.ResponseWriter, r *http.Request, identity, realm string, delay int) {
AuthzFailWithReason(w, r, identity, realm, delay, "")
}
// AuthzFailWithReason behaves like AuthzFail but, when reason is non-empty,
// embeds it in the response body's error detail under the "reason" key. This
// lets policy conditions surface the operator-authored Message to the client
// alongside the standard DENIED error code.
func AuthzFailWithReason(w http.ResponseWriter, r *http.Request, identity, realm string, delay int, reason string) {
time.Sleep(time.Duration(delay) * time.Second)
// don't send auth headers if request is coming from UI
if r.Header.Get(constants.SessionClientHeaderName) != constants.SessionClientHeaderValue {
if realm == "" {
realm = "Authorization Required"
}
realm = "Basic realm=" + strconv.Quote(realm)
w.Header().Set("WWW-Authenticate", realm)
}
w.Header().Set("Content-Type", "application/json")
if identity == "" {
WriteJSON(w, http.StatusUnauthorized, apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED)))
return
}
denied := apiErr.NewError(apiErr.DENIED)
if reason != "" {
denied.AddDetail(map[string]string{"reason": reason})
}
WriteJSON(w, http.StatusForbidden, apiErr.NewErrorList(denied))
}
func WriteJSON(response http.ResponseWriter, status int, data any) {
json := jsoniter.ConfigCompatibleWithStandardLibrary
body, err := json.Marshal(data)
if err != nil {
panic(err)
}
WriteData(response, status, constants.DefaultMediaType, body)
}
func WriteData(w http.ResponseWriter, status int, mediaType string, data []byte) {
w.Header().Set("Content-Type", mediaType)
w.WriteHeader(status)
_, _ = w.Write(data)
}
func QueryHasParams(values url.Values, params []string) bool {
for _, param := range params {
if !values.Has(param) {
return false
}
}
return true
}