websvc: add dns and http apis

This commit is contained in:
Ainar Garipov
2022-08-31 17:53:45 +03:00
parent c098960b39
commit 27bd8bc58b
11 changed files with 547 additions and 42 deletions

View File

@@ -1,4 +1,4 @@
// Package websvc contains the AdGuard Home web service.
// Package websvc contains the AdGuard Home HTTP API service.
//
// TODO(a.garipov): Add tests.
package websvc
@@ -15,17 +15,36 @@ import (
"time"
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
"github.com/AdguardTeam/AdGuardHome/internal/v1/dnssvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
httptreemux "github.com/dimfeld/httptreemux/v5"
)
// ConfigManager is the configuration manager interface.
//
// See internal/v1/svc/ for the main implementation.
type ConfigManager interface {
DNS() (svc *dnssvc.Service)
Web() (svc *Service)
UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)
UpdateWeb(ctx context.Context, c *Config) (err error)
}
// Config is the AdGuard Home web service configuration structure.
type Config struct {
// ConfigManager is used to show information about services as well as
// dynamically reconfigure them.
ConfigManager ConfigManager
// TLS is the optional TLS configuration. If TLS is not nil,
// SecureAddresses must not be empty.
TLS *tls.Config
// Start is the time of start of AdGuard Home.
Start time.Time
// Addresses are the addresses on which to serve the plain HTTP API.
Addresses []netip.AddrPort
@@ -33,40 +52,48 @@ type Config struct {
// SecureAddresses is not empty, TLS must not be nil.
SecureAddresses []netip.AddrPort
// Start is the time of start of AdGuard Home.
Start time.Time
// Timeout is the timeout for all server operations.
Timeout time.Duration
// ForceHTTPS tells if all requests to Addresses should be redirected to a
// secure address instead.
//
// TODO(a.garipov): Use; define rules, which address to redirect to.
ForceHTTPS bool
}
// Service is the AdGuard Home web service. A nil *Service is a valid
// [agh.Service] that does nothing.
type Service struct {
tls *tls.Config
servers []*http.Server
start time.Time
timeout time.Duration
confMgr ConfigManager
tls *tls.Config
start time.Time
servers []*http.Server
timeout time.Duration
forceHTTPS bool
}
// New returns a new properly initialized *Service. If c is nil, svc is a nil
// *Service that does nothing.
// *Service that does nothing. The fields of c must not be modified after
// calling New.
func New(c *Config) (svc *Service) {
if c == nil {
return nil
}
svc = &Service{
tls: c.TLS,
start: c.Start,
timeout: c.Timeout,
confMgr: c.ConfigManager,
tls: c.TLS,
start: c.Start,
timeout: c.Timeout,
forceHTTPS: c.ForceHTTPS,
}
mux := newMux(svc)
for _, a := range c.Addresses {
addr := a.String()
errLog := log.StdLog("websvc: http: "+addr, log.ERROR)
errLog := log.StdLog("websvc: plain http: "+addr, log.ERROR)
svc.servers = append(svc.servers, &http.Server{
Addr: addr,
Handler: mux,
@@ -111,6 +138,21 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
method: http.MethodGet,
path: PathHealthCheck,
isJSON: false,
}, {
handler: svc.handleGetSettingsAll,
method: http.MethodGet,
path: PathV1SettingsAll,
isJSON: true,
}, {
handler: svc.handlePatchSettingsDNS,
method: http.MethodPatch,
path: PathV1SettingsDNS,
isJSON: true,
}, {
handler: svc.handlePatchSettingsHTTP,
method: http.MethodPatch,
path: PathV1SettingsHTTP,
isJSON: true,
}, {
handler: svc.handleGetV1SystemInfo,
method: http.MethodGet,
@@ -119,29 +161,39 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
}}
for _, r := range routes {
var h http.HandlerFunc
if r.isJSON {
// TODO(a.garipov): Consider using httptreemux's MiddlewareFunc.
h = jsonMw(r.handler)
mux.Handle(r.method, r.path, jsonMw(r.handler))
} else {
h = r.handler
mux.Handle(r.method, r.path, r.handler)
}
mux.Handle(r.method, r.path, h)
}
return mux
}
// Addrs returns all addresses on which this server serves the HTTP API. Addrs
// addrs returns all addresses on which this server serves the HTTP API. addrs
// must not be called until Start returns.
func (svc *Service) Addrs() (addrs []string) {
addrs = make([]string, 0, len(svc.servers))
func (svc *Service) addrs() (addrs, secAddrs []netip.AddrPort) {
for _, srv := range svc.servers {
addrs = append(addrs, srv.Addr)
ipp, err := netip.ParseAddrPort(srv.Addr)
if err != nil {
// Technically shouldn't happen, since all servers must have a valid
// address.
panic(fmt.Errorf("websvc: server %q: bad address: %w", srv.Addr, err))
}
// srv.Serve will set TLSConfig to an almost empty value, so, instead of
// relying only on the nilness of TLSConfig, check the length of the
// certificates field as well.
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
addrs = append(addrs, ipp)
} else {
secAddrs = append(secAddrs, ipp)
}
}
return addrs
return addrs, secAddrs
}
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
@@ -149,9 +201,6 @@ func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request)
_, _ = io.WriteString(w, "OK")
}
// unit is a convenient alias for struct{}.
type unit = struct{}
// type check
var _ agh.Service = (*Service)(nil)
@@ -163,11 +212,9 @@ func (svc *Service) Start() (err error) {
return nil
}
srvs := svc.servers
wg := &sync.WaitGroup{}
wg.Add(len(srvs))
for _, srv := range srvs {
wg.Add(len(svc.servers))
for _, srv := range svc.servers {
go serve(srv, wg)
}
@@ -181,11 +228,14 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
addr := srv.Addr
defer log.OnPanic(addr)
var proto string
var l net.Listener
var err error
if srv.TLSConfig == nil {
proto = "http"
l, err = net.Listen("tcp", addr)
} else {
proto = "https"
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
}
if err != nil {
@@ -196,8 +246,12 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
// would mean that a random available port was automatically chosen.
srv.Addr = l.Addr().String()
log.Info("websvc: starting srv http://%s", srv.Addr)
wg.Done()
log.Info("websvc: starting srv %s://%s", proto, srv.Addr)
l = &waitListener{
Listener: l,
firstAcceptWG: wg,
}
err = srv.Serve(l)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
@@ -221,8 +275,26 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
}
if len(errs) > 0 {
return errors.List("shutting down")
return errors.List("shutting down", errs...)
}
return nil
}
// Config returns the current configuration of the web service. Currently, only
// the Addresses and SecureAddresses fields are filled in c.
func (svc *Service) Config() (c *Config) {
c = &Config{
ConfigManager: svc.confMgr,
TLS: svc.tls,
// Leave Addresses and SecureAddresses empty and get the actual
// addresses that include the :0 ones later.
Start: svc.start,
Timeout: svc.timeout,
ForceHTTPS: svc.forceHTTPS,
}
c.Addresses, c.SecureAddresses = svc.addrs()
return c
}