zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/common/http_server.go (about)

     1  package common
     2  
     3  import (
     4  	"net/http"
     5  	"net/url"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/gorilla/mux"
    11  	jsoniter "github.com/json-iterator/go"
    12  
    13  	"zotregistry.io/zot/pkg/api/config"
    14  	"zotregistry.io/zot/pkg/api/constants"
    15  	apiErr "zotregistry.io/zot/pkg/api/errors"
    16  	reqCtx "zotregistry.io/zot/pkg/requestcontext"
    17  )
    18  
    19  func AllowedMethods(methods ...string) []string {
    20  	return append(methods, http.MethodOptions)
    21  }
    22  
    23  func AddExtensionSecurityHeaders() mux.MiddlewareFunc { //nolint:varnamelen
    24  	return func(next http.Handler) http.Handler {
    25  		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
    26  			resp.Header().Set("X-Content-Type-Options", "nosniff")
    27  
    28  			next.ServeHTTP(resp, req)
    29  		})
    30  	}
    31  }
    32  
    33  func ACHeadersMiddleware(config *config.Config, allowedMethods ...string) mux.MiddlewareFunc {
    34  	allowedMethodsValue := strings.Join(allowedMethods, ",")
    35  
    36  	return func(next http.Handler) http.Handler {
    37  		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
    38  			resp.Header().Set("Access-Control-Allow-Methods", allowedMethodsValue)
    39  			resp.Header().Set("Access-Control-Allow-Headers", "Authorization,content-type,"+constants.SessionClientHeaderName)
    40  
    41  			if config.IsBasicAuthnEnabled() {
    42  				resp.Header().Set("Access-Control-Allow-Credentials", "true")
    43  			}
    44  
    45  			if req.Method == http.MethodOptions {
    46  				return
    47  			}
    48  
    49  			next.ServeHTTP(resp, req)
    50  		})
    51  	}
    52  }
    53  
    54  func CORSHeadersMiddleware(allowOrigin string) mux.MiddlewareFunc {
    55  	return func(next http.Handler) http.Handler {
    56  		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
    57  			AddCORSHeaders(allowOrigin, response)
    58  
    59  			next.ServeHTTP(response, request)
    60  		})
    61  	}
    62  }
    63  
    64  func AddCORSHeaders(allowOrigin string, response http.ResponseWriter) {
    65  	if allowOrigin == "" {
    66  		response.Header().Set("Access-Control-Allow-Origin", "*")
    67  	} else {
    68  		response.Header().Set("Access-Control-Allow-Origin", allowOrigin)
    69  	}
    70  }
    71  
    72  // AuthzOnlyAdminsMiddleware permits only admin user access if auth is enabled.
    73  func AuthzOnlyAdminsMiddleware(conf *config.Config) mux.MiddlewareFunc {
    74  	return func(next http.Handler) http.Handler {
    75  		return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
    76  			if !conf.IsBasicAuthnEnabled() {
    77  				next.ServeHTTP(response, request)
    78  
    79  				return
    80  			}
    81  
    82  			// get userAccessControl built in previous authn/authz middlewares
    83  			userAc, err := reqCtx.UserAcFromContext(request.Context())
    84  			if err != nil { // should not happen as this has been previously checked for errors
    85  				AuthzFail(response, request, userAc.GetUsername(), conf.HTTP.Realm, conf.HTTP.Auth.FailDelay)
    86  
    87  				return
    88  			}
    89  
    90  			// reject non-admin access if authentication is enabled
    91  			if userAc != nil && !userAc.IsAdmin() {
    92  				AuthzFail(response, request, userAc.GetUsername(), conf.HTTP.Realm, conf.HTTP.Auth.FailDelay)
    93  
    94  				return
    95  			}
    96  
    97  			next.ServeHTTP(response, request)
    98  		})
    99  	}
   100  }
   101  
   102  func AuthzFail(w http.ResponseWriter, r *http.Request, identity, realm string, delay int) {
   103  	time.Sleep(time.Duration(delay) * time.Second)
   104  
   105  	// don't send auth headers if request is coming from UI
   106  	if r.Header.Get(constants.SessionClientHeaderName) != constants.SessionClientHeaderValue {
   107  		if realm == "" {
   108  			realm = "Authorization Required"
   109  		}
   110  
   111  		realm = "Basic realm=" + strconv.Quote(realm)
   112  
   113  		w.Header().Set("WWW-Authenticate", realm)
   114  	}
   115  
   116  	w.Header().Set("Content-Type", "application/json")
   117  
   118  	if identity == "" {
   119  		WriteJSON(w, http.StatusUnauthorized, apiErr.NewErrorList(apiErr.NewError(apiErr.UNAUTHORIZED)))
   120  	} else {
   121  		WriteJSON(w, http.StatusForbidden, apiErr.NewErrorList(apiErr.NewError(apiErr.DENIED)))
   122  	}
   123  }
   124  
   125  func WriteJSON(response http.ResponseWriter, status int, data interface{}) {
   126  	json := jsoniter.ConfigCompatibleWithStandardLibrary
   127  
   128  	body, err := json.Marshal(data)
   129  	if err != nil {
   130  		panic(err)
   131  	}
   132  
   133  	WriteData(response, status, constants.DefaultMediaType, body)
   134  }
   135  
   136  func WriteData(w http.ResponseWriter, status int, mediaType string, data []byte) {
   137  	w.Header().Set("Content-Type", mediaType)
   138  	w.WriteHeader(status)
   139  	_, _ = w.Write(data)
   140  }
   141  
   142  func QueryHasParams(values url.Values, params []string) bool {
   143  	for _, param := range params {
   144  		if !values.Has(param) {
   145  			return false
   146  		}
   147  	}
   148  
   149  	return true
   150  }