github.com/kubewharf/katalyst-core@v0.5.3/pkg/util/process/http.go (about)

     1  /*
     2  Copyright 2022 The Katalyst Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package process
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"net"
    25  	"net/http"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	"golang.org/x/time/rate"
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	"k8s.io/klog/v2"
    34  
    35  	"github.com/kubewharf/katalyst-core/pkg/metrics"
    36  	"github.com/kubewharf/katalyst-core/pkg/util/credential"
    37  	"github.com/kubewharf/katalyst-core/pkg/util/credential/authorization"
    38  )
    39  
    40  const (
    41  	httpDefaultTimeout     = time.Second * 10
    42  	httpDefaultConnTimeout = time.Second * 3
    43  )
    44  
    45  const (
    46  	HTTPChainCredential  = "credential"
    47  	HTTPChainRateLimiter = "rateLimiter"
    48  	HTTPChainMonitor     = "monitor"
    49  )
    50  
    51  const (
    52  	HTTPRequestCount       = "http_request_count"
    53  	HTTPAuthenticateFailed = "http_request_authenticate_failed"
    54  	HTTPNoPermission       = "http_request_no_permission"
    55  	HTTPThrottled          = "http_request_throttled"
    56  
    57  	UserUnknown = "unknown"
    58  )
    59  
    60  type contextKey string
    61  
    62  const (
    63  	KeyAuthInfo contextKey = "auth"
    64  )
    65  
    66  var httpCleanupVisitorPeriod = time.Minute * 3
    67  
    68  type visitor struct {
    69  	limiter  *rate.Limiter
    70  	lastSeen time.Time
    71  }
    72  
    73  type HTTPHandler struct {
    74  	mux       sync.Mutex
    75  	cred      credential.Credential
    76  	accessCtl authorization.AccessControl
    77  
    78  	enabled              sets.String
    79  	visitors             map[string]*visitor
    80  	authInfo             map[string]string
    81  	skipAuthURLPrefix    []string
    82  	strictAuthentication bool
    83  
    84  	emitter metrics.MetricEmitter
    85  }
    86  
    87  func NewHTTPHandler(enabled []string, skipAuthURLPrefix []string, strictAuthentication bool, emitter metrics.MetricEmitter) *HTTPHandler {
    88  	return &HTTPHandler{
    89  		visitors: make(map[string]*visitor),
    90  		enabled:  sets.NewString(enabled...),
    91  		// no credential by default
    92  		cred: credential.DefaultCredential(),
    93  		// no authorization check by default
    94  		accessCtl:            authorization.DefaultAccessControl(),
    95  		skipAuthURLPrefix:    skipAuthURLPrefix,
    96  		strictAuthentication: strictAuthentication,
    97  		emitter:              emitter,
    98  	}
    99  }
   100  
   101  func (h *HTTPHandler) Run(ctx context.Context) {
   102  	if h.enabled.Has(HTTPChainRateLimiter) {
   103  		go wait.Until(h.cleanupVisitor, httpCleanupVisitorPeriod, ctx.Done())
   104  	}
   105  
   106  	if h.enabled.Has(HTTPChainCredential) {
   107  		h.cred.Run(ctx)
   108  		h.accessCtl.Run(ctx)
   109  	}
   110  }
   111  
   112  func (h *HTTPHandler) getHTTPVisitor(subject string) *rate.Limiter {
   113  	h.mux.Lock()
   114  	defer h.mux.Unlock()
   115  
   116  	v, exists := h.visitors[subject]
   117  	if !exists {
   118  		limiter := rate.NewLimiter(0.5, 1)
   119  		h.visitors[subject] = &visitor{limiter, time.Now()}
   120  		return limiter
   121  	}
   122  
   123  	v.lastSeen = time.Now()
   124  	return v.limiter
   125  }
   126  
   127  // cleanupVisitor periodically cleanups visitors if they are not called for a long time
   128  func (h *HTTPHandler) cleanupVisitor() {
   129  	h.mux.Lock()
   130  	defer h.mux.Unlock()
   131  
   132  	for addr, v := range h.visitors {
   133  		if time.Since(v.lastSeen) > httpCleanupVisitorPeriod {
   134  			delete(h.visitors, addr)
   135  		}
   136  	}
   137  }
   138  
   139  // withBasicAuth is used to verify the requests and bind authInfo to request.
   140  func (h *HTTPHandler) withCredential(f http.HandlerFunc) http.HandlerFunc {
   141  	skipAuth := func(r *http.Request) bool {
   142  		for _, prefix := range h.skipAuthURLPrefix {
   143  			if strings.HasPrefix(r.URL.Path, prefix) {
   144  				return true
   145  			}
   146  		}
   147  		return false
   148  	}
   149  
   150  	return func(w http.ResponseWriter, r *http.Request) {
   151  		if r == nil {
   152  			klog.Warningf("request is nil")
   153  			w.WriteHeader(http.StatusBadRequest)
   154  			return
   155  		}
   156  
   157  		var authInfo credential.AuthInfo
   158  		shouldSkipAuth := skipAuth(r)
   159  		if shouldSkipAuth {
   160  			f(w, r)
   161  			return
   162  		}
   163  
   164  		var err error
   165  		authInfo, err = h.cred.Auth(r)
   166  		if err != nil {
   167  			if h.strictAuthentication {
   168  				klog.Warningf("request %+v doesn't have proper auth", r.URL)
   169  				w.Header().Set("Katalyst-Authenticate", `Basic realm="Restricted"`)
   170  				w.WriteHeader(http.StatusUnauthorized)
   171  				_ = h.emitter.StoreInt64(HTTPAuthenticateFailed, 1, metrics.MetricTypeNameCount,
   172  					metrics.MetricTag{Key: "path", Val: r.URL.Path})
   173  				return
   174  			}
   175  		} else {
   176  			r = attachAuthInfo(r, authInfo)
   177  			klog.V(4).Infof("user %v request %+v  with auth type %v", authInfo.SubjectName(), r.URL, authInfo.AuthType())
   178  			if verifyErr := h.accessCtl.Verify(authInfo, authorization.PermissionTypeHttpEndpoint); verifyErr != nil &&
   179  				h.strictAuthentication {
   180  				klog.Warningf("request %+v with user %v doesn't have permission, msg: %v", r.URL, authInfo.SubjectName(), verifyErr)
   181  				w.Header().Set("Katalyst-Authenticate", `Basic realm="Restricted"`)
   182  				w.WriteHeader(http.StatusUnauthorized)
   183  				_ = h.emitter.StoreInt64(HTTPNoPermission, 1, metrics.MetricTypeNameCount,
   184  					metrics.MetricTag{Key: "path", Val: r.URL.Path},
   185  					metrics.MetricTag{Key: "user", Val: authInfo.SubjectName()})
   186  				return
   187  			}
   188  		}
   189  
   190  		if authInfo != nil {
   191  			klog.V(4).Infof("user %v request %+v is valid", authInfo.SubjectName(), r.URL)
   192  		}
   193  		f(w, r)
   194  	}
   195  }
   196  
   197  // withRateLimiter is used to limit user-requests to protect server
   198  func (h *HTTPHandler) withRateLimiter(f http.HandlerFunc) http.HandlerFunc {
   199  	return func(w http.ResponseWriter, r *http.Request) {
   200  		if r != nil {
   201  			rateLimiterKey := r.RemoteAddr
   202  			authInfo, err := getAuthInfo(r)
   203  			if err != nil {
   204  				klog.Warningf("request %+v has no valid auth info bound to it, using Remote address %v as RateLimiter key, err: %v", r.URL, r.RemoteAddr, err)
   205  			} else {
   206  				rateLimiterKey = authInfo.SubjectName()
   207  			}
   208  
   209  			limiter := h.getHTTPVisitor(rateLimiterKey)
   210  			if !limiter.Allow() {
   211  				klog.Warningf("request %+v has too many requests from %v", r.URL, rateLimiterKey)
   212  				w.Header().Set("Katalyst-Limit", `too many requests`)
   213  				w.WriteHeader(http.StatusTooManyRequests)
   214  				_ = h.emitter.StoreInt64(HTTPThrottled, 1, metrics.MetricTypeNameCount,
   215  					metrics.MetricTag{Key: "path", Val: r.URL.Path},
   216  					metrics.MetricTag{Key: "rateLimiterKey", Val: rateLimiterKey})
   217  				return
   218  			}
   219  		}
   220  
   221  		f(w, r)
   222  	}
   223  }
   224  
   225  func (h *HTTPHandler) withMonitor(f http.HandlerFunc) http.HandlerFunc {
   226  	return func(w http.ResponseWriter, r *http.Request) {
   227  		f(w, r)
   228  
   229  		user := UserUnknown
   230  		if authInfo, err := getAuthInfo(r); err == nil {
   231  			user = authInfo.SubjectName()
   232  		}
   233  		_ = h.emitter.StoreInt64(HTTPRequestCount, 1, metrics.MetricTypeNameCount,
   234  			metrics.MetricTag{Key: "path", Val: r.URL.Path},
   235  			metrics.MetricTag{Key: "user", Val: user})
   236  	}
   237  }
   238  
   239  func (h *HTTPHandler) WithCredential(cred credential.Credential) error {
   240  	if cred == nil {
   241  		return fmt.Errorf("nil Credential is not allowed")
   242  	}
   243  
   244  	h.cred = cred
   245  	return nil
   246  }
   247  
   248  func (h *HTTPHandler) WithAuthorization(auth authorization.AccessControl) error {
   249  	if auth == nil {
   250  		return fmt.Errorf("nil AccessControl is not allowed")
   251  	}
   252  
   253  	h.accessCtl = auth
   254  	return nil
   255  }
   256  
   257  // WithHandleChain builds handler chains for http.Handler
   258  func (h *HTTPHandler) WithHandleChain(f http.Handler) http.Handler {
   259  	// build orders for http chains
   260  	chains := []string{HTTPChainMonitor, HTTPChainRateLimiter, HTTPChainCredential}
   261  	funcs := map[string]func(http.HandlerFunc) http.HandlerFunc{
   262  		HTTPChainRateLimiter: h.withRateLimiter,
   263  		HTTPChainCredential:  h.withCredential,
   264  		HTTPChainMonitor:     h.withMonitor,
   265  	}
   266  
   267  	var handler http.Handler = f
   268  	for _, c := range chains {
   269  		if h.enabled.Has(c) {
   270  			tmpHandler := handler
   271  			handler = funcs[c](func(w http.ResponseWriter, r *http.Request) {
   272  				tmpHandler.ServeHTTP(w, r)
   273  			})
   274  		}
   275  	}
   276  	return handler
   277  }
   278  
   279  // NewDefaultHTTPClient returns a raw HTTP client.
   280  func NewDefaultHTTPClient() *http.Client {
   281  	transport := &http.Transport{
   282  		Proxy: http.ProxyFromEnvironment,
   283  		DialContext: (&net.Dialer{
   284  			Timeout:   httpDefaultConnTimeout,
   285  			KeepAlive: 30 * time.Second,
   286  		}).DialContext,
   287  		MaxIdleConns:          100,
   288  		IdleConnTimeout:       90 * time.Second,
   289  		TLSHandshakeTimeout:   10 * time.Second,
   290  		ExpectContinueTimeout: 1 * time.Second,
   291  	}
   292  
   293  	client := &http.Client{
   294  		Timeout:   httpDefaultTimeout,
   295  		Transport: transport,
   296  	}
   297  	return client
   298  }
   299  
   300  // GetAndUnmarshal gets data from the given url and unmarshal it into the given struct.
   301  func GetAndUnmarshal(url string, v interface{}) error {
   302  	resp, err := http.Get(url)
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	defer resp.Body.Close()
   308  	body, err := ioutil.ReadAll(resp.Body)
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	if resp.StatusCode != http.StatusOK {
   314  		return fmt.Errorf("invalid response status code %d, url: %s", resp.StatusCode, url)
   315  	}
   316  
   317  	err = json.Unmarshal(body, v)
   318  	if err != nil {
   319  		return err
   320  	}
   321  
   322  	return nil
   323  }
   324  
   325  func attachAuthInfo(r *http.Request, authInfo credential.AuthInfo) *http.Request {
   326  	if authInfo == nil {
   327  		return r
   328  	}
   329  	newCtx := context.WithValue(r.Context(), KeyAuthInfo, &authInfo)
   330  	return r.WithContext(newCtx)
   331  }
   332  
   333  func getAuthInfo(r *http.Request) (credential.AuthInfo, error) {
   334  	value := r.Context().Value(KeyAuthInfo)
   335  	if value == nil {
   336  		return nil, fmt.Errorf("no auth info bound to this request")
   337  	}
   338  
   339  	authInfo, ok := value.(*credential.AuthInfo)
   340  	if !ok {
   341  		return nil, fmt.Errorf("invalid auth info bound to this request")
   342  	}
   343  
   344  	return *authInfo, nil
   345  }