mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-17 08:36:55 +03:00
app/vmauth: allow to serve internal API and different address
vmauth uses 'lib/httpserver' for serving HTTP requests. This server unconditionally defines built-in routes (such as '/metrics', '/health', etc). It makes impossible to proxy `HTTP` requests to backends with the same routes. Since vmauth's httpserver matches built-in route and return local response. This commit adds new flag `httpInternalListenAddr` with default empty value. Which removes internal API routes from public router and exposes it at separate http server. For example given configuration disables private routes at `0.0.0.0:8427` address and serves it at `0.0.0.0:8426`: `./bin/vmauth --auth.config=config.yaml --httpListenAddr=:8427 --httpInternalListenAddr=127.0.0.1:8426` Related issues: - https://github.com/VictoriaMetrics/VictoriaMetrics/issues/6468 - https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7345
This commit is contained in:
@@ -83,6 +83,19 @@ type server struct {
|
||||
// In such cases the caller must serve the request.
|
||||
type RequestHandler func(w http.ResponseWriter, r *http.Request) bool
|
||||
|
||||
// ServeOptions defiens optional parameters for http server
|
||||
type ServeOptions struct {
|
||||
// UseProxyProtocol if is set to true for the corresponding addr, then the incoming connections are accepted via proxy protocol.
|
||||
// See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
|
||||
UseProxyProtocol *flagutil.ArrayBool
|
||||
// DisableBuiltinRoutes whether not to serve built-in routes for the given server, such as:
|
||||
// /health, /debug/pprof and few others
|
||||
// In addition basic auth check and authKey checks will be disabled for the given addr
|
||||
//
|
||||
// Mostly required by http proxy servers, which peforms own authorization and requests routing
|
||||
DisableBuiltinRoutes bool
|
||||
}
|
||||
|
||||
// Serve starts an http server on the given addrs with the given optional rh.
|
||||
//
|
||||
// By default all the responses are transparently compressed, since egress traffic is usually expensive.
|
||||
@@ -97,23 +110,47 @@ func Serve(addrs []string, useProxyProtocol *flagutil.ArrayBool, rh RequestHandl
|
||||
return false
|
||||
}
|
||||
}
|
||||
opts := ServeOptions{
|
||||
UseProxyProtocol: useProxyProtocol,
|
||||
}
|
||||
for idx, addr := range addrs {
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
useProxyProto := false
|
||||
if useProxyProtocol != nil {
|
||||
useProxyProto = useProxyProtocol.GetOptionalArg(idx)
|
||||
}
|
||||
go serve(addr, useProxyProto, rh, idx)
|
||||
go serve(addr, rh, idx, opts)
|
||||
}
|
||||
}
|
||||
|
||||
func serve(addr string, useProxyProtocol bool, rh RequestHandler, idx int) {
|
||||
// ServeWithOpts starts an http server on the given addrs with the given optional request handlers.
|
||||
//
|
||||
// By default all the responses are transparently compressed, since egress traffic is usually expensive.
|
||||
//
|
||||
// The compression can be disabled by specifying -http.disableResponseCompression command-line flag.
|
||||
// TODO: rename to Serve and update Serve usage with new signature
|
||||
func ServeWithOpts(addrs []string, rh RequestHandler, opts ServeOptions) {
|
||||
if rh == nil {
|
||||
rh = func(_ http.ResponseWriter, _ *http.Request) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for idx, addr := range addrs {
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
go serve(addr, rh, idx, opts)
|
||||
}
|
||||
}
|
||||
|
||||
func serve(addr string, rh RequestHandler, idx int, opts ServeOptions) {
|
||||
scheme := "http"
|
||||
if tlsEnable.GetOptionalArg(idx) {
|
||||
scheme = "https"
|
||||
}
|
||||
useProxyProto := false
|
||||
if opts.UseProxyProtocol != nil {
|
||||
useProxyProto = opts.UseProxyProtocol.GetOptionalArg(idx)
|
||||
}
|
||||
|
||||
var tlsConfig *tls.Config
|
||||
if tlsEnable.GetOptionalArg(idx) {
|
||||
certFile := tlsCertFile.GetOptionalArg(idx)
|
||||
@@ -125,20 +162,22 @@ func serve(addr string, useProxyProtocol bool, rh RequestHandler, idx int) {
|
||||
}
|
||||
tlsConfig = tc
|
||||
}
|
||||
ln, err := netutil.NewTCPListener(scheme, addr, useProxyProtocol, tlsConfig)
|
||||
ln, err := netutil.NewTCPListener(scheme, addr, useProxyProto, tlsConfig)
|
||||
if err != nil {
|
||||
logger.Fatalf("cannot start http server at %s: %s", addr, err)
|
||||
} else {
|
||||
logger.Infof("started server at %s://%s/", scheme, ln.Addr())
|
||||
}
|
||||
logger.Infof("started server at %s://%s/", scheme, ln.Addr())
|
||||
if !opts.DisableBuiltinRoutes {
|
||||
logger.Infof("pprof handlers are exposed at %s://%s/debug/pprof/", scheme, ln.Addr())
|
||||
}
|
||||
serveWithListener(addr, ln, rh)
|
||||
|
||||
serveWithListener(addr, ln, rh, opts.DisableBuiltinRoutes)
|
||||
}
|
||||
|
||||
func serveWithListener(addr string, ln net.Listener, rh RequestHandler) {
|
||||
func serveWithListener(addr string, ln net.Listener, rh RequestHandler, disableBuiltinRoutes bool) {
|
||||
var s server
|
||||
|
||||
s.s = &http.Server{
|
||||
Handler: gzipHandler(&s, rh),
|
||||
|
||||
// Disable http/2, since it doesn't give any advantages for VictoriaMetrics services.
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
@@ -162,6 +201,19 @@ func serveWithListener(addr string, ln net.Listener, rh RequestHandler) {
|
||||
return context.WithValue(ctx, connDeadlineTimeKey, &deadline)
|
||||
}
|
||||
}
|
||||
rhw := rh
|
||||
if !disableBuiltinRoutes {
|
||||
rhw = func(w http.ResponseWriter, r *http.Request) bool {
|
||||
return builtinRoutesHandler(&s, r, w, rh)
|
||||
}
|
||||
}
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerWrapper(w, r, rhw)
|
||||
})
|
||||
if !*disableResponseCompression {
|
||||
h = gzipHandlerWrapper(h)
|
||||
}
|
||||
s.s.Handler = h
|
||||
|
||||
serversLock.Lock()
|
||||
servers[addr] = &s
|
||||
@@ -244,16 +296,6 @@ func stop(addr string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func gzipHandler(s *server, rh RequestHandler) http.HandlerFunc {
|
||||
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
handlerWrapper(s, w, r, rh)
|
||||
})
|
||||
if *disableResponseCompression {
|
||||
return h
|
||||
}
|
||||
return gzipHandlerWrapper(h)
|
||||
}
|
||||
|
||||
var gzipHandlerWrapper = func() func(http.Handler) http.HandlerFunc {
|
||||
hw, err := gzhttp.NewWrapper(gzhttp.CompressionLevel(1))
|
||||
if err != nil {
|
||||
@@ -278,7 +320,7 @@ var hostname = func() string {
|
||||
return h
|
||||
}()
|
||||
|
||||
func handlerWrapper(s *server, w http.ResponseWriter, r *http.Request, rh RequestHandler) {
|
||||
func handlerWrapper(w http.ResponseWriter, r *http.Request, rh RequestHandler) {
|
||||
// All the VictoriaMetrics code assumes that panic stops the process.
|
||||
// Unfortunately, the standard net/http.Server recovers from panics in request handlers,
|
||||
// so VictoriaMetrics state can become inconsistent after the recovered panic.
|
||||
@@ -310,12 +352,7 @@ func handlerWrapper(s *server, w http.ResponseWriter, r *http.Request, rh Reques
|
||||
h.Set("Connection", "close")
|
||||
}
|
||||
path := r.URL.Path
|
||||
if strings.HasSuffix(path, "/favicon.ico") {
|
||||
w.Header().Set("Cache-Control", "max-age=3600")
|
||||
faviconRequests.Inc()
|
||||
w.Write(faviconData)
|
||||
return
|
||||
}
|
||||
|
||||
prefix := GetPathPrefix()
|
||||
if prefix != "" {
|
||||
// Trim -http.pathPrefix from path
|
||||
@@ -336,13 +373,40 @@ func handlerWrapper(s *server, w http.ResponseWriter, r *http.Request, rh Reques
|
||||
path = path[len(prefix)-1:]
|
||||
r.URL.Path = path
|
||||
}
|
||||
|
||||
w = &responseWriterWithAbort{
|
||||
ResponseWriter: w,
|
||||
}
|
||||
if rh(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
Errorf(w, r, "unsupported path requested: %q", r.URL.Path)
|
||||
unsupportedRequestErrors.Inc()
|
||||
}
|
||||
|
||||
func builtinRoutesHandler(s *server, r *http.Request, w http.ResponseWriter, rh RequestHandler) bool {
|
||||
if !isProtectedByAuthFlag(r.URL.Path) && !CheckBasicAuth(w, r) {
|
||||
return true
|
||||
}
|
||||
|
||||
h := w.Header()
|
||||
|
||||
path := r.URL.Path
|
||||
if strings.HasSuffix(path, "/favicon.ico") {
|
||||
w.Header().Set("Cache-Control", "max-age=3600")
|
||||
faviconRequests.Inc()
|
||||
w.Write(faviconData)
|
||||
return true
|
||||
}
|
||||
|
||||
switch r.URL.Path {
|
||||
case "/health":
|
||||
h.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
deadline := s.shutdownDelayDeadline.Load()
|
||||
if deadline <= 0 {
|
||||
w.Write([]byte("OK"))
|
||||
return
|
||||
return true
|
||||
}
|
||||
// Return non-OK response during grace period before shutting down the server.
|
||||
// Load balancers must notify these responses and re-route new requests to other servers.
|
||||
@@ -353,7 +417,7 @@ func handlerWrapper(s *server, w http.ResponseWriter, r *http.Request, rh Reques
|
||||
}
|
||||
errMsg := fmt.Sprintf("The server is in delayed shutdown mode, which will end in %.3fs", d.Seconds())
|
||||
http.Error(w, errMsg, http.StatusServiceUnavailable)
|
||||
return
|
||||
return true
|
||||
case "/ping":
|
||||
// This is needed for compatibility with InfluxDB agents.
|
||||
// See https://docs.influxdata.com/influxdb/v1.7/tools/api/#ping-http-endpoint
|
||||
@@ -362,64 +426,54 @@ func handlerWrapper(s *server, w http.ResponseWriter, r *http.Request, rh Reques
|
||||
status = http.StatusOK
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
return
|
||||
return true
|
||||
case "/metrics":
|
||||
metricsRequests.Inc()
|
||||
if !CheckAuthFlag(w, r, metricsAuthKey) {
|
||||
return
|
||||
return true
|
||||
}
|
||||
startTime := time.Now()
|
||||
h.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
appmetrics.WritePrometheusMetrics(w)
|
||||
metricsHandlerDuration.UpdateDuration(startTime)
|
||||
return
|
||||
return true
|
||||
case "/flags":
|
||||
if !CheckAuthFlag(w, r, flagsAuthKey) {
|
||||
return
|
||||
return true
|
||||
}
|
||||
h.Set("Content-Type", "text/plain; charset=utf-8")
|
||||
flagutil.WriteFlags(w)
|
||||
return
|
||||
return true
|
||||
case "/-/healthy":
|
||||
// This is needed for Prometheus compatibility
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1833
|
||||
fmt.Fprintf(w, "VictoriaMetrics is Healthy.\n")
|
||||
return
|
||||
return true
|
||||
case "/-/ready":
|
||||
// This is needed for Prometheus compatibility
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/1833
|
||||
fmt.Fprintf(w, "VictoriaMetrics is Ready.\n")
|
||||
return
|
||||
return true
|
||||
case "/robots.txt":
|
||||
// This prevents search engines from indexing contents
|
||||
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4128
|
||||
fmt.Fprintf(w, "User-agent: *\nDisallow: /\n")
|
||||
return
|
||||
return true
|
||||
default:
|
||||
if strings.HasPrefix(r.URL.Path, "/debug/pprof/") {
|
||||
pprofRequests.Inc()
|
||||
if !CheckAuthFlag(w, r, pprofAuthKey) {
|
||||
return
|
||||
return true
|
||||
}
|
||||
pprofHandler(r.URL.Path[len("/debug/pprof/"):], w, r)
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
if !isProtectedByAuthFlag(r.URL.Path) && !CheckBasicAuth(w, r) {
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
w = &responseWriterWithAbort{
|
||||
ResponseWriter: w,
|
||||
}
|
||||
if rh(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
Errorf(w, r, "unsupported path requested: %q", r.URL.Path)
|
||||
unsupportedRequestErrors.Inc()
|
||||
return
|
||||
}
|
||||
return rh(w, r)
|
||||
}
|
||||
|
||||
func isProtectedByAuthFlag(path string) bool {
|
||||
|
||||
@@ -162,8 +162,11 @@ func TestHandlerWrapper(t *testing.T) {
|
||||
|
||||
srv := &server{s: &http.Server{}}
|
||||
w := &httptest.ResponseRecorder{}
|
||||
handlerWrapper(srv, w, req, func(_ http.ResponseWriter, _ *http.Request) bool {
|
||||
return true
|
||||
|
||||
handlerWrapper(w, req, func(w http.ResponseWriter, r *http.Request) bool {
|
||||
return builtinRoutesHandler(srv, r, w, func(_ http.ResponseWriter, _ *http.Request) bool {
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
h := w.Header()
|
||||
|
||||
Reference in New Issue
Block a user