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 }