AGH-31 Use-confs-http

This commit is contained in:
Dimitry Kolyshev
2026-10-08 12:16:36 +03:00
committed by GitHub
parent 07afcdafc6
commit 8dedd32785
4 changed files with 135 additions and 73 deletions
+41 -17
View File
@@ -2,8 +2,8 @@ package configmgr
import (
"fmt"
"net/http"
"net/netip"
"regexp"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
@@ -12,10 +12,10 @@ import (
// HTTPConfig is the on-disk web API configuration.
type HTTPConfig struct {
// DoH contains DNS-over-HTTPS configuration.
// DoH contains DNS-over-HTTPS configuration. It must not be nil.
DoH *DoHConfig `yaml:"doh"`
// Pprof defines the profiling HTTP handler.
// Pprof defines the profiling HTTP handler. It must not be nil.
Pprof *HTTPPprofConfig `yaml:"pprof"`
// Address is the addresses on which to serve web API.
@@ -28,7 +28,8 @@ type HTTPConfig struct {
// DoHConfig is the block with DNS-over-HTTPS configuration.
type DoHConfig struct {
// Routes is the list of HTTP route patterns for DoH requests. Each route
// should be in the format "METHOD /path" or "METHOD /path/{param}".
// must be a valid [http.ServeMux] pattern, and the routes must not conflict
// with each other.
Routes []string `yaml:"routes"`
// InsecureEnabled allows DoH queries via unencrypted HTTP.
@@ -60,38 +61,61 @@ func (c *HTTPConfig) Validate() (err error) {
return errors.Join(errs...)
}
// doHRoutePatternRegexp is a regular expression for validating DoH route
// patterns.
var doHRoutePatternRegexp = regexp.MustCompile(
`^[A-Z]+ /[^\s{}]+(?:/[^\s{}]+)*(?:/\{[_A-Za-z][_A-Za-z0-9]*\})?$`,
)
// type check
var _ validate.Interface = (*DoHConfig)(nil)
// Validate implements the [validate.Interface] interface for *DoHConfig.
func (c *DoHConfig) Validate() (err error) {
if c == nil {
return nil
return errors.ErrNoValue
}
var errs []error
for i, route := range c.Routes {
if doHRoutePatternRegexp.MatchString(route) {
continue
}
if err = validateRoutes(c.Routes); err != nil {
return fmt.Errorf("routes: %w", err)
}
errs = append(errs, fmt.Errorf(`route %q at index %d: incorrect format`, route, i))
return nil
}
// validateRoutes registers the given routes on a temporary [http.ServeMux] and
// returns an error if any of them cannot be registered.
func validateRoutes(routes []string) (err error) {
mux := http.NewServeMux()
var errs []error
for i, route := range routes {
if err = validateRoute(mux, route); err != nil {
errs = append(errs, fmt.Errorf("route at index %d: %q: %w", i, route, err))
}
}
return errors.Join(errs...)
}
// validateRoute tries registering route on mux, converting the panic that
// [http.ServeMux.Handle] raises for an invalid or conflicting pattern into an
// error. mux must not be nil.
//
// TODO(a.garipov): Find out better solution.
func validateRoute(mux *http.ServeMux, route string) (err error) {
defer func() {
err = errors.FromRecovered(recover())
}()
mux.Handle(route, http.NotFoundHandler())
return nil
}
// type check
var _ validate.Interface = (*HTTPPprofConfig)(nil)
// Validate implements the [validate.Interface] interface for *HTTPPprofConfig.
func (c *HTTPPprofConfig) Validate() (err error) {
if c == nil {
return errors.ErrNoValue
}
// TODO(d.kolyshev): Validate.
return nil
+79
View File
@@ -0,0 +1,79 @@
package configmgr_test
import (
"fmt"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/configmgr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDoHConfig_Validate_routes(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
wantMessage string
routes []string
}{{
name: "valid",
wantMessage: "",
routes: []string{
"GET /dns-query",
"POST /dns-query",
"GET /dns-query/{ClientID}",
"POST /dns-query/{ClientID}",
"GET /dns-query/",
"GET /doh/{a}/{b}",
"/dns-query",
},
}, {
name: "empty",
wantMessage: `routes: route at index 0: "": http: invalid pattern`,
routes: []string{""},
}, {
name: "bad_wildcard",
wantMessage: `routes: route at index 0: "GET /dns-query/{ClientID": parsing ` +
`"GET /dns-query/{ClientID"`,
routes: []string{
"GET /dns-query/{ClientID",
},
}, {
name: "duplicate",
wantMessage: `routes: route at index 1: "GET /dns-query": pattern "GET /dns-query"`,
routes: []string{
"GET /dns-query",
"GET /dns-query",
},
}, {
name: "conflict",
wantMessage: `routes: route at index 1: "GET /dns-query/{Other}": pattern ` +
`"GET /dns-query/{Other}"`,
routes: []string{
"GET /dns-query/{ClientID}",
"GET /dns-query/{Other}",
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
conf := &configmgr.DoHConfig{
Routes: tc.routes,
}
err := conf.Validate()
if tc.wantMessage == "" {
assert.NoError(t, err)
return
}
require.Error(t, err)
fmt.Println(err)
assert.Contains(t, err.Error(), tc.wantMessage)
})
}
}
+14 -55
View File
@@ -88,7 +88,8 @@ type configuration struct {
fileData []byte
// HTTPConfig is the block with http conf.
HTTPConfig httpConfig `yaml:"http"`
HTTPConfig configmgr.HTTPConfig `yaml:"http"`
// Users are the clients capable for accessing the web interface.
Users []webUser `yaml:"users"`
// AuthAttempts is the maximum number of failed login attempts a user
@@ -153,52 +154,6 @@ type configuration struct {
UnsafeUseCustomUpdateIndexURL bool `yaml:"unsafe_use_custom_update_index_url,omitempty"`
}
// httpConfig is a block with HTTP configuration params.
//
// Field ordering is important, YAML fields better not to be reordered, if it's
// not absolutely necessary.
type httpConfig struct {
// Pprof defines the profiling HTTP handler. It is never nil.
Pprof *httpPprofConfig `yaml:"pprof"`
// DoH contains DNS-over-HTTPS configuration. It is never nil.
DoH *doHConfig `yaml:"doh"`
// Address is the address to serve the web UI on.
Address netip.AddrPort
// SessionTTL for a web session.
// An active session is automatically refreshed once a day.
SessionTTL timeutil.Duration `yaml:"session_ttl"`
}
// httpPprofConfig is the block with pprof HTTP configuration.
type httpPprofConfig struct {
// Port for the profiling handler.
Port uint16 `yaml:"port"`
// Enabled defines if the profiling handler is enabled.
Enabled bool `yaml:"enabled"`
}
// doHConfig is the block with DNS-over-HTTPS configuration.
type doHConfig struct {
// Routes is the list of HTTP route patterns for DoH requests. Default
// routes are:
// - "GET /dns-query"
// - "POST /dns-query"
// - "GET /dns-query/{ClientID}"
// - "POST /dns-query/{ClientID}"
//
// TODO(d.kolyshev): Validate.
// TODO(d.kolyshev): Since we have multiple muxes now serving on one
// address, the user can register e.g. GET /control/status.
Routes []string `yaml:"routes"`
// InsecureEnabled allows DoH queries via unencrypted HTTP.
InsecureEnabled bool `yaml:"insecure_enabled"`
}
// tlsConfigSettings is the TLS configuration for DNS-over-TLS, DNS-over-QUIC,
// and HTTPS. When adding new properties, update the conversion functions
// [confFromTLSSettings] and [confToTLSSettings] as necessary.
@@ -309,14 +264,8 @@ const (
var config = &configuration{
AuthAttempts: 5,
AuthBlockMin: 15,
HTTPConfig: httpConfig{
Address: netip.AddrPortFrom(netip.IPv4Unspecified(), 3000),
SessionTTL: timeutil.Duration(30 * timeutil.Day),
Pprof: &httpPprofConfig{
Enabled: false,
Port: 6060,
},
DoH: &doHConfig{
HTTPConfig: configmgr.HTTPConfig{
DoH: &configmgr.DoHConfig{
Routes: []string{
"GET /dns-query",
"POST /dns-query",
@@ -325,6 +274,12 @@ var config = &configuration{
},
InsecureEnabled: false,
},
Pprof: &configmgr.HTTPPprofConfig{
Port: 6060,
Enabled: false,
},
Address: netip.AddrPortFrom(netip.IPv4Unspecified(), 3000),
SessionTTL: timeutil.Duration(30 * timeutil.Day),
},
DNS: configmgr.DNSConfig{
EDNSClientSubnet: &configmgr.EDNSClientSubnet{
@@ -658,6 +613,10 @@ func validateConfig(ctx context.Context, l *slog.Logger, fileData []byte) (err e
config.Filtering.FiltersUpdateIntervalHours = 24
}
if err = config.HTTPConfig.Validate(); err != nil {
return fmt.Errorf("validating http config: %w", err)
}
if len(config.Users) == 0 {
l.WarnContext(ctx, "no users in the configuration file; authentication is disabled")
}
+1 -1
View File
@@ -241,7 +241,7 @@ func ipsToUDPAddrs(ips []netip.Addr, port uint16) (udpAddrs []*net.UDPAddr) {
func newServerConfig(
dnsConf *configmgr.DNSConfig,
clientSrcConf *clientSourcesConfig,
dohConf *doHConfig,
dohConf *configmgr.DoHConfig,
tlsManager aghtls.Manager,
dohAddr netip.AddrPort,
httpReg aghhttp.Registrar,