mirror of
https://github.com/fatedier/frp.git
synced 2026-10-20 09:03:26 +08:00
feat: add metricsServer for health checks, metrics and pprof (#5578)
Add a separate HTTP listener for observability, configured through metricsServer in both frps and frpc. It serves /healthz and, when pprofEnable is set, the pprof handlers except /debug/pprof/cmdline. It never serves the dashboard, static assets or the admin API, and it does not touch the global assets filesystem. frps also serves /metrics on this listener with both runtime and business metrics, enabled by metricsServer.port alone. The in-memory collector still starts only with the dashboard, and the Prometheus collector starts either with metricsServer or with enablePrometheus plus webServer, so a configuration that collected nothing before still collects nothing. The /healthz, /metrics and pprof endpoints on webServer, and enablePrometheus, keep working but are deprecated and log a hint at startup. frpc does not serve /metrics yet.
This commit is contained in:
@@ -60,10 +60,11 @@ type ClientCommonConfig struct {
|
||||
// set.
|
||||
Start []string `json:"start,omitempty"`
|
||||
|
||||
Log LogConfig `json:"log,omitempty"`
|
||||
WebServer WebServerConfig `json:"webServer,omitempty"`
|
||||
Transport ClientTransportConfig `json:"transport,omitempty"`
|
||||
VirtualNet VirtualNetConfig `json:"virtualNet,omitempty"`
|
||||
Log LogConfig `json:"log,omitempty"`
|
||||
WebServer WebServerConfig `json:"webServer,omitempty"`
|
||||
MetricsServer MetricsServerConfig `json:"metricsServer,omitempty"`
|
||||
Transport ClientTransportConfig `json:"transport,omitempty"`
|
||||
VirtualNet VirtualNetConfig `json:"virtualNet,omitempty"`
|
||||
|
||||
// FeatureGates specifies a set of feature gates to enable or disable.
|
||||
// This can be used to enable alpha/beta features or disable default features.
|
||||
@@ -94,6 +95,7 @@ func (c *ClientCommonConfig) Complete() error {
|
||||
c.Log.Complete()
|
||||
c.Transport.Complete()
|
||||
c.WebServer.Complete()
|
||||
c.MetricsServer.Complete()
|
||||
|
||||
c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500)
|
||||
return nil
|
||||
|
||||
@@ -72,6 +72,26 @@ func (c *WebServerConfig) Complete() {
|
||||
c.Addr = util.EmptyOr(c.Addr, "127.0.0.1")
|
||||
}
|
||||
|
||||
// MetricsServerConfig configures a separate listener for health checks, metrics
|
||||
// (frps only), and optional pprof handlers. It does not serve the web interface.
|
||||
type MetricsServerConfig struct {
|
||||
// Addr defaults to "127.0.0.1".
|
||||
Addr string `json:"addr,omitempty"`
|
||||
// Port disables the listener when set to 0.
|
||||
Port int `json:"port,omitempty"`
|
||||
// User and Password protect metrics and pprof; health checks remain anonymous.
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
// PprofEnable explicitly enables profiling, excluding cmdline.
|
||||
PprofEnable bool `json:"pprofEnable,omitempty"`
|
||||
// Enable TLS if TLS is not nil.
|
||||
TLS *TLSConfig `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
func (c *MetricsServerConfig) Complete() {
|
||||
c.Addr = util.EmptyOr(c.Addr, "127.0.0.1")
|
||||
}
|
||||
|
||||
type TLSConfig struct {
|
||||
// CertFile specifies the path of the cert file that client will load.
|
||||
CertFile string `json:"certFile,omitempty"`
|
||||
|
||||
@@ -69,7 +69,8 @@ type ServerConfig struct {
|
||||
|
||||
SSHTunnelGateway SSHTunnelGateway `json:"sshTunnelGateway,omitempty"`
|
||||
|
||||
WebServer WebServerConfig `json:"webServer,omitempty"`
|
||||
WebServer WebServerConfig `json:"webServer,omitempty"`
|
||||
MetricsServer MetricsServerConfig `json:"metricsServer,omitempty"`
|
||||
// EnablePrometheus will export prometheus metrics on webserver address
|
||||
// in /metrics api.
|
||||
EnablePrometheus bool `json:"enablePrometheus,omitempty"`
|
||||
@@ -105,6 +106,7 @@ func (c *ServerConfig) Complete() error {
|
||||
c.Log.Complete()
|
||||
c.Transport.Complete()
|
||||
c.WebServer.Complete()
|
||||
c.MetricsServer.Complete()
|
||||
c.SSHTunnelGateway.Complete()
|
||||
|
||||
c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0")
|
||||
|
||||
@@ -38,6 +38,7 @@ func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (
|
||||
func() (Warning, error) { return v.validateAuthConfig(&c.Auth) },
|
||||
func() (Warning, error) { return nil, validateLogConfig(&c.Log) },
|
||||
func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) },
|
||||
func() (Warning, error) { return nil, validateMetricsServerConfig(&c.MetricsServer, &c.WebServer) },
|
||||
func() (Warning, error) { return validateTransportConfig(&c.Transport) },
|
||||
func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) },
|
||||
}
|
||||
|
||||
@@ -37,6 +37,27 @@ func validateWebServerConfig(c *v1.WebServerConfig) error {
|
||||
return ValidatePort(c.Port, "webServer.port")
|
||||
}
|
||||
|
||||
func validateMetricsServerConfig(c *v1.MetricsServerConfig, web *v1.WebServerConfig) error {
|
||||
if err := ValidatePort(c.Port, "metricsServer.port"); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.TLS != nil {
|
||||
if c.TLS.UseSystemCa {
|
||||
return fmt.Errorf("metricsServer.tls.useSystemCa is only supported in frpc transport.tls")
|
||||
}
|
||||
if c.TLS.CertFile == "" || c.TLS.KeyFile == "" {
|
||||
return fmt.Errorf("metricsServer.tls.certFile and metricsServer.tls.keyFile must both be specified when tls is enabled")
|
||||
}
|
||||
}
|
||||
// Only reject an exact duplicate. Whether two different addresses can share
|
||||
// a port depends on the address family and the platform, so leave it to
|
||||
// net.Listen to decide.
|
||||
if c.Port > 0 && web.Port > 0 && c.Port == web.Port && c.Addr == web.Addr {
|
||||
return fmt.Errorf("metricsServer.addr:port conflicts with webServer.addr:port")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePort checks that the network port is in range
|
||||
func ValidatePort(port int, fieldPath string) error {
|
||||
if 0 <= port && port <= 65535 {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2026 The frp 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 validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestValidateMetricsServerConfig(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
metrics v1.MetricsServerConfig
|
||||
web v1.WebServerConfig
|
||||
wantErr string
|
||||
}{
|
||||
{name: "disabled"},
|
||||
{name: "negative port", metrics: v1.MetricsServerConfig{Port: -1}, wantErr: "metricsServer.port"},
|
||||
{name: "overflow port", metrics: v1.MetricsServerConfig{Port: 65536}, wantErr: "metricsServer.port"},
|
||||
{name: "maximum port", metrics: v1.MetricsServerConfig{Port: 65535}},
|
||||
{name: "empty TLS", metrics: v1.MetricsServerConfig{TLS: &v1.TLSConfig{}}, wantErr: "metricsServer.tls.certFile"},
|
||||
{name: "only certificate", metrics: v1.MetricsServerConfig{TLS: &v1.TLSConfig{CertFile: "cert"}}, wantErr: "metricsServer.tls.keyFile"},
|
||||
{name: "only key", metrics: v1.MetricsServerConfig{TLS: &v1.TLSConfig{KeyFile: "key"}}, wantErr: "metricsServer.tls.certFile"},
|
||||
{name: "TLS pair", metrics: v1.MetricsServerConfig{TLS: &v1.TLSConfig{CertFile: "cert", KeyFile: "key"}}},
|
||||
{name: "system CA", metrics: v1.MetricsServerConfig{TLS: &v1.TLSConfig{UseSystemCa: true}}, wantErr: "metricsServer.tls.useSystemCa"},
|
||||
{
|
||||
name: "same address", metrics: v1.MetricsServerConfig{Addr: "127.0.0.1", Port: 7501},
|
||||
web: v1.WebServerConfig{Addr: "127.0.0.1", Port: 7501}, wantErr: "conflicts with webServer",
|
||||
},
|
||||
{
|
||||
name: "different addresses", metrics: v1.MetricsServerConfig{Addr: "127.0.0.1", Port: 7501},
|
||||
web: v1.WebServerConfig{Addr: "127.0.0.2", Port: 7501},
|
||||
},
|
||||
{
|
||||
name: "different ports", metrics: v1.MetricsServerConfig{Port: 7501},
|
||||
web: v1.WebServerConfig{Port: 7500},
|
||||
},
|
||||
{
|
||||
name: "web disabled", metrics: v1.MetricsServerConfig{Port: 7501},
|
||||
},
|
||||
{
|
||||
name: "metrics disabled", web: v1.WebServerConfig{Port: 7501},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
checkMetricsConfigBoth(t, tc.metrics, tc.web, tc.wantErr)
|
||||
})
|
||||
}
|
||||
// Two different addresses on the same port may or may not coexist depending
|
||||
// on the address family and the platform, so only an exact duplicate is a
|
||||
// configuration error here.
|
||||
for _, wildcard := range []string{"", "0.0.0.0", "::"} {
|
||||
t.Run("wildcard "+wildcard+" with loopback", func(t *testing.T) {
|
||||
checkMetricsConfigBoth(t, v1.MetricsServerConfig{Addr: wildcard, Port: 7501},
|
||||
v1.WebServerConfig{Addr: "127.0.0.1", Port: 7501}, "")
|
||||
checkMetricsConfigBoth(t, v1.MetricsServerConfig{Addr: "127.0.0.1", Port: 7501},
|
||||
v1.WebServerConfig{Addr: wildcard, Port: 7501}, "")
|
||||
})
|
||||
t.Run("same wildcard "+wildcard, func(t *testing.T) {
|
||||
checkMetricsConfigBoth(t, v1.MetricsServerConfig{Addr: wildcard, Port: 7501},
|
||||
v1.WebServerConfig{Addr: wildcard, Port: 7501}, "conflicts with webServer")
|
||||
})
|
||||
}
|
||||
t.Run("different wildcards on the same port", func(t *testing.T) {
|
||||
checkMetricsConfigBoth(t, v1.MetricsServerConfig{Addr: "0.0.0.0", Port: 7501},
|
||||
v1.WebServerConfig{Addr: "::", Port: 7501}, "")
|
||||
})
|
||||
}
|
||||
|
||||
func checkMetricsConfigBoth(t *testing.T, metrics v1.MetricsServerConfig, web v1.WebServerConfig, wantErr string) {
|
||||
t.Helper()
|
||||
server := &v1.ServerConfig{}
|
||||
client := &v1.ClientCommonConfig{}
|
||||
require.NoError(t, server.Complete())
|
||||
require.NoError(t, client.Complete())
|
||||
// Assign after completion to also check raw empty (wildcard) addresses.
|
||||
server.MetricsServer, server.WebServer = metrics, web
|
||||
client.MetricsServer, client.WebServer = metrics, web
|
||||
validator := NewConfigValidator(nil)
|
||||
_, serverErr := validator.ValidateServerConfig(server)
|
||||
_, clientErr := validator.ValidateClientCommonConfig(client)
|
||||
for _, err := range []error{serverErr, clientErr} {
|
||||
if wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, wantErr)
|
||||
require.ErrorContains(t, err, "metricsServer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsServerDefaults(t *testing.T) {
|
||||
for _, port := range []int{0, 7501} {
|
||||
server := &v1.ServerConfig{MetricsServer: v1.MetricsServerConfig{Port: port}}
|
||||
client := &v1.ClientCommonConfig{MetricsServer: v1.MetricsServerConfig{Port: port}}
|
||||
require.NoError(t, server.Complete())
|
||||
require.NoError(t, client.Complete())
|
||||
for _, cfg := range []v1.MetricsServerConfig{server.MetricsServer, client.MetricsServer} {
|
||||
require.Equal(t, "127.0.0.1", cfg.Addr)
|
||||
require.Equal(t, port, cfg.Port)
|
||||
require.False(t, cfg.PprofEnable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,9 @@ func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, err
|
||||
if err := validateWebServerConfig(&c.WebServer); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
if err := validateMetricsServerConfig(&c.MetricsServer, &c.WebServer); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
if c.Transport.TLS.UseSystemCa {
|
||||
errs = AppendError(errs, fmt.Errorf("transport.tls.useSystemCa is only supported in frpc transport.tls"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2026 The frp 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 http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
// NewMetricsServer creates a listener with only health, optional metrics, and
|
||||
// opt-in profiling endpoints. A zero port disables it and returns a nil server.
|
||||
func NewMetricsServer(cfg v1.MetricsServerConfig, metricsHandler http.Handler) (*Server, error) {
|
||||
if cfg.Port == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
s, err := newServer(cfg.Addr, cfg.Port, cfg.User, cfg.Password, cfg.TLS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.router.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
authed := s.router.NewRoute().Subrouter()
|
||||
authed.Use(s.authMiddleware)
|
||||
if metricsHandler != nil {
|
||||
authed.Handle("/metrics", metricsHandler)
|
||||
}
|
||||
if cfg.PprofEnable {
|
||||
// Use exact routes rather than a prefix so cmdline and unknown endpoints
|
||||
// cannot reach pprof.Index's fallback dispatch.
|
||||
authed.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
authed.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
authed.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
authed.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
for _, name := range []string{"allocs", "block", "goroutine", "heap", "mutex", "threadcreate"} {
|
||||
authed.Handle("/debug/pprof/"+name, pprof.Handler(name))
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright 2026 The frp 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 http
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/fatedier/frp/assets"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func newTestMetricsServer(t *testing.T, pprofEnabled bool, handler http.Handler) *Server {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
require.NoError(t, ln.Close())
|
||||
s, err := NewMetricsServer(v1.MetricsServerConfig{
|
||||
Addr: "127.0.0.1", Port: port, User: "monitor", Password: "secret", PprofEnable: pprofEnabled,
|
||||
}, handler)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, s.Close()) })
|
||||
return s
|
||||
}
|
||||
|
||||
func TestMetricsServerRoutes(t *testing.T) {
|
||||
for _, enabled := range []bool{false, true} {
|
||||
name := "pprof disabled"
|
||||
if enabled {
|
||||
name = "pprof enabled"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
calls := 0
|
||||
s := newTestMetricsServer(t, enabled, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
calls++
|
||||
_, _ = w.Write([]byte("business metrics"))
|
||||
}))
|
||||
for _, authed := range []bool{false, true} {
|
||||
for _, path := range []string{
|
||||
"/healthz", "/metrics", "/debug/pprof/", "/debug/pprof/goroutine",
|
||||
"/debug/pprof/cmdline", "/debug/pprof/unknown", "/", "/static/app.js", "/api/serverinfo", "/api/config",
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
if authed {
|
||||
req.SetBasicAuth("monitor", "secret")
|
||||
}
|
||||
resp := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(resp, req)
|
||||
expected := http.StatusNotFound
|
||||
switch path {
|
||||
case "/healthz":
|
||||
expected = http.StatusOK
|
||||
require.Empty(t, resp.Body.String())
|
||||
case "/metrics":
|
||||
expected = http.StatusUnauthorized
|
||||
if authed {
|
||||
expected = http.StatusOK
|
||||
require.Equal(t, "business metrics", resp.Body.String())
|
||||
}
|
||||
case "/debug/pprof/", "/debug/pprof/goroutine":
|
||||
if enabled {
|
||||
expected = http.StatusUnauthorized
|
||||
if authed {
|
||||
expected = http.StatusOK
|
||||
}
|
||||
}
|
||||
}
|
||||
require.Equal(t, expected, resp.Code, "path=%s authenticated=%v", path, authed)
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, calls, "unauthenticated requests must not reach the metrics handler")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsServerWithoutMetricsPreservesAssets(t *testing.T) {
|
||||
assets.Load(t.TempDir())
|
||||
before := assets.FileSystem
|
||||
t.Cleanup(func() { assets.Load("") })
|
||||
s := newTestMetricsServer(t, false, nil)
|
||||
// assets.Load uses http.Dir for local assets, a value rather than a pointer.
|
||||
require.Equal(t, before, assets.FileSystem)
|
||||
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
|
||||
req.SetBasicAuth("monitor", "secret")
|
||||
resp := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusNotFound, resp.Code)
|
||||
}
|
||||
|
||||
func TestMetricsServerDisabled(t *testing.T) {
|
||||
s, err := NewMetricsServer(v1.MetricsServerConfig{TLS: &v1.TLSConfig{CertFile: "nonexistent"}}, nil)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, s)
|
||||
}
|
||||
+15
-7
@@ -48,7 +48,18 @@ type Server struct {
|
||||
func NewServer(cfg v1.WebServerConfig) (*Server, error) {
|
||||
assets.Load(cfg.AssetsDir)
|
||||
|
||||
addr := net.JoinHostPort(cfg.Addr, strconv.Itoa(cfg.Port))
|
||||
s, err := newServer(cfg.Addr, cfg.Port, cfg.User, cfg.Password, cfg.TLS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.PprofEnable {
|
||||
s.registerPprofHandlers()
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func newServer(bindAddr string, port int, user, password string, tlsConfig *v1.TLSConfig) (*Server, error) {
|
||||
addr := net.JoinHostPort(bindAddr, strconv.Itoa(port))
|
||||
if addr == ":" {
|
||||
addr = ":http"
|
||||
}
|
||||
@@ -65,11 +76,8 @@ func NewServer(cfg v1.WebServerConfig) (*Server, error) {
|
||||
hs: hs,
|
||||
router: router,
|
||||
}
|
||||
if cfg.PprofEnable {
|
||||
s.registerPprofHandlers()
|
||||
}
|
||||
if cfg.TLS != nil {
|
||||
cert, err := tls.LoadX509KeyPair(cfg.TLS.CertFile, cfg.TLS.KeyFile)
|
||||
if tlsConfig != nil {
|
||||
cert, err := tls.LoadX509KeyPair(tlsConfig.CertFile, tlsConfig.KeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -77,7 +85,7 @@ func NewServer(cfg v1.WebServerConfig) (*Server, error) {
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
}
|
||||
s.authMiddleware = netpkg.NewHTTPAuthMiddleware(cfg.User, cfg.Password).SetAuthFailDelay(200 * time.Millisecond).Middleware
|
||||
s.authMiddleware = netpkg.NewHTTPAuthMiddleware(user, password).SetAuthFailDelay(200 * time.Millisecond).Middleware
|
||||
|
||||
// Bind only after all fallible preparation so an error cannot leak the listener.
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
|
||||
Reference in New Issue
Block a user