1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
|
// Copyright 2020 The Prometheus Authors
// This code is partly borrowed from Caddy:
// Copyright 2015 Matthew Holt and The Caddy Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package web
import (
"encoding/hex"
"fmt"
"net/http"
"strings"
"sync"
"github.com/go-kit/log"
"golang.org/x/crypto/bcrypt"
)
// extraHTTPHeaders is a map of HTTP headers that can be added to HTTP
// responses.
// This is private on purpose to ensure consistency in the Prometheus ecosystem.
var extraHTTPHeaders = map[string][]string{
"Strict-Transport-Security": nil,
"X-Content-Type-Options": {"nosniff"},
"X-Frame-Options": {"deny", "sameorigin"},
"X-XSS-Protection": nil,
"Content-Security-Policy": nil,
}
func validateUsers(configPath string) error {
c, err := getConfig(configPath)
if err != nil {
return err
}
for _, p := range c.Users {
_, err = bcrypt.Cost([]byte(p))
if err != nil {
return err
}
}
return nil
}
// validateHeaderConfig checks that the provided header configuration is correct.
// It does not check the validity of all the values, only the ones which are
// well-defined enumerations.
func validateHeaderConfig(headers map[string]string) error {
HeadersLoop:
for k, v := range headers {
values, ok := extraHTTPHeaders[k]
if !ok {
return fmt.Errorf("HTTP header %q can not be configured", k)
}
for _, allowedValue := range values {
if v == allowedValue {
continue HeadersLoop
}
}
if len(values) > 0 {
return fmt.Errorf("invalid value for %s. Expected one of: %q, but got: %q", k, values, v)
}
}
return nil
}
type webHandler struct {
tlsConfigPath string
handler http.Handler
logger log.Logger
cache *cache
// bcryptMtx is there to ensure that bcrypt.CompareHashAndPassword is run
// only once in parallel as this is CPU intensive.
bcryptMtx sync.Mutex
}
func (u *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c, err := getConfig(u.tlsConfigPath)
if err != nil {
u.logger.Log("msg", "Unable to parse configuration", "err", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
// Configure http headers.
for k, v := range c.HTTPConfig.Header {
w.Header().Set(k, v)
}
if len(c.Users) == 0 {
u.handler.ServeHTTP(w, r)
return
}
user, pass, auth := r.BasicAuth()
if auth {
hashedPassword, validUser := c.Users[user]
if !validUser {
// The user is not found. Use a fixed password hash to
// prevent user enumeration by timing requests.
// This is a bcrypt-hashed version of "fakepassword".
hashedPassword = "$2y$10$QOauhQNbBCuQDKes6eFzPeMqBSjb7Mr5DUmpZ/VcEd00UAV/LDeSi"
}
cacheKey := strings.Join(
[]string{
hex.EncodeToString([]byte(user)),
hex.EncodeToString([]byte(hashedPassword)),
hex.EncodeToString([]byte(pass)),
}, ":")
authOk, ok := u.cache.get(cacheKey)
if !ok {
// This user, hashedPassword, password is not cached.
u.bcryptMtx.Lock()
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(pass))
u.bcryptMtx.Unlock()
authOk = validUser && err == nil
u.cache.set(cacheKey, authOk)
}
if authOk && validUser {
u.handler.ServeHTTP(w, r)
return
}
}
w.Header().Set("WWW-Authenticate", "Basic")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
|