websvc: add dns and http apis
This commit is contained in:
75
internal/v1/websvc/dns.go
Normal file
75
internal/v1/websvc/dns.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/v1/dnssvc"
|
||||||
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS Settings Handlers
|
||||||
|
|
||||||
|
// TODO(a.garipov): !! Write tests!
|
||||||
|
|
||||||
|
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
|
||||||
|
// HTTP API.
|
||||||
|
type ReqPatchSettingsDNS struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
|
UpstreamTimeout timeutil.Duration `json:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/http
|
||||||
|
// HTTP API.
|
||||||
|
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &ReqPatchSettingsDNS{
|
||||||
|
Addresses: []netip.AddrPort{},
|
||||||
|
BootstrapServers: []string{},
|
||||||
|
UpstreamServers: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeHTTPError(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConf := &dnssvc.Config{
|
||||||
|
Addresses: req.Addresses,
|
||||||
|
BootstrapServers: req.BootstrapServers,
|
||||||
|
UpstreamServers: req.UpstreamServers,
|
||||||
|
UpstreamTimeout: req.UpstreamTimeout.Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
||||||
|
if err != nil {
|
||||||
|
writeHTTPError(w, r, fmt.Errorf("updating: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newSvc := svc.confMgr.DNS()
|
||||||
|
err = newSvc.Start()
|
||||||
|
if err != nil {
|
||||||
|
writeHTTPError(w, r, fmt.Errorf("starting new service: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONResponse(w, r, &respGetV1SettingsAllDNS{
|
||||||
|
Addresses: newConf.Addresses,
|
||||||
|
BootstrapServers: newConf.BootstrapServers,
|
||||||
|
UpstreamServers: newConf.UpstreamServers,
|
||||||
|
UpstreamTimeout: timeutil.Duration{Duration: newConf.UpstreamTimeout},
|
||||||
|
})
|
||||||
|
}
|
||||||
92
internal/v1/websvc/http.go
Normal file
92
internal/v1/websvc/http.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP Settings Handlers
|
||||||
|
|
||||||
|
// TODO(a.garipov): !! Write tests!
|
||||||
|
|
||||||
|
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
|
||||||
|
// HTTP API.
|
||||||
|
type ReqPatchSettingsHTTP struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add wait time.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http
|
||||||
|
// HTTP API.
|
||||||
|
func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &ReqPatchSettingsHTTP{
|
||||||
|
Addresses: []netip.AddrPort{},
|
||||||
|
SecureAddresses: []netip.AddrPort{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeHTTPError(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConf := &Config{
|
||||||
|
ConfigManager: svc.confMgr,
|
||||||
|
TLS: svc.tls,
|
||||||
|
Addresses: req.Addresses,
|
||||||
|
SecureAddresses: req.SecureAddresses,
|
||||||
|
Timeout: svc.timeout,
|
||||||
|
ForceHTTPS: svc.forceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONResponse(w, r, &respGetV1SettingsAllHTTP{
|
||||||
|
Addresses: newConf.Addresses,
|
||||||
|
SecureAddresses: newConf.SecureAddresses,
|
||||||
|
})
|
||||||
|
|
||||||
|
cancelUpd := func() {}
|
||||||
|
updCtx := context.Background()
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
updCtx, cancelUpd = context.WithDeadline(updCtx, deadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch the new HTTP service in a separate goroutine to let this handler
|
||||||
|
// finish and thus, this server to shutdown.
|
||||||
|
go func() {
|
||||||
|
defer cancelUpd()
|
||||||
|
|
||||||
|
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
|
||||||
|
if updErr != nil {
|
||||||
|
writeHTTPError(w, r, fmt.Errorf("updating: %w", updErr))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): !! Add some kind of timeout? Context?
|
||||||
|
var newSvc *Service
|
||||||
|
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||||
|
log.Debug("websvc: waiting for new websvc to be configured")
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
updErr = newSvc.Start()
|
||||||
|
if updErr != nil {
|
||||||
|
log.Error("websvc: new svc failed to start: %s", updErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
|||||||
// always nil.
|
// always nil.
|
||||||
func (t jsonTime) MarshalJSON() (b []byte, err error) {
|
func (t jsonTime) MarshalJSON() (b []byte, err error) {
|
||||||
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||||
b = strconv.AppendFloat(nil, msec, 'f', 3, 64)
|
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||||
|
|
||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
@@ -59,3 +59,16 @@ func writeJSONResponse(w io.Writer, r *http.Request, v any) {
|
|||||||
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeHTTPError is a helper for logging and writing HTTP errors.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Improve codes, and add JSON error codes.
|
||||||
|
func writeHTTPError(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
_, werr := io.WriteString(w, err.Error())
|
||||||
|
if werr != nil {
|
||||||
|
log.Debug("websvc: writing error resp to %s %s: %s", r.Method, r.URL.Path, werr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
113
internal/v1/websvc/json_internal_test.go
Normal file
113
internal/v1/websvc/json_internal_test.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testJSONTime is the JSON time for tests.
|
||||||
|
var testJSONTime = jsonTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
||||||
|
|
||||||
|
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
||||||
|
const testJSONTimeStr = "1234567890123.456"
|
||||||
|
|
||||||
|
func TestJSONTime_MarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
in jsonTime
|
||||||
|
want []byte
|
||||||
|
}{{
|
||||||
|
name: "unix_zero",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: jsonTime(time.Unix(0, 0)),
|
||||||
|
want: []byte("0"),
|
||||||
|
}, {
|
||||||
|
name: "empty",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: jsonTime{},
|
||||||
|
want: []byte("-6795364578871.345"),
|
||||||
|
}, {
|
||||||
|
name: "time",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: testJSONTime,
|
||||||
|
want: []byte(testJSONTimeStr),
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := tc.in.MarshalJSON()
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
in := &struct {
|
||||||
|
A jsonTime
|
||||||
|
}{
|
||||||
|
A: testJSONTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := json.Marshal(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []byte(`{"A":`+testJSONTimeStr+`}`), got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
want jsonTime
|
||||||
|
data []byte
|
||||||
|
}{{
|
||||||
|
name: "time",
|
||||||
|
wantErrMsg: "",
|
||||||
|
want: testJSONTime,
|
||||||
|
data: []byte(testJSONTimeStr),
|
||||||
|
}, {
|
||||||
|
name: "bad",
|
||||||
|
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
||||||
|
`invalid syntax`,
|
||||||
|
want: jsonTime{},
|
||||||
|
data: []byte(`{}`),
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var got jsonTime
|
||||||
|
err := got.UnmarshalJSON(tc.data)
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
err := (*jsonTime)(nil).UnmarshalJSON([]byte("0"))
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
msg := err.Error()
|
||||||
|
assert.Equal(t, "json time is nil", msg)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
want := testJSONTime
|
||||||
|
var got struct {
|
||||||
|
A jsonTime
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, want, got.A)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,5 +4,8 @@ package websvc
|
|||||||
const (
|
const (
|
||||||
PathHealthCheck = "/health-check"
|
PathHealthCheck = "/health-check"
|
||||||
|
|
||||||
PathV1SystemInfo = "/api/v1/system/info"
|
PathV1SettingsAll = "/api/v1/settings/all"
|
||||||
|
PathV1SettingsDNS = "/api/v1/settings/dns"
|
||||||
|
PathV1SettingsHTTP = "/api/v1/settings/http"
|
||||||
|
PathV1SystemInfo = "/api/v1/system/info"
|
||||||
)
|
)
|
||||||
|
|||||||
63
internal/v1/websvc/settings.go
Normal file
63
internal/v1/websvc/settings.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// All Settings Handlers
|
||||||
|
|
||||||
|
// TODO(a.garipov): !! Write tests!
|
||||||
|
|
||||||
|
// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all
|
||||||
|
// HTTP API.
|
||||||
|
type RespGetV1SettingsAll struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
DNS *respGetV1SettingsAllDNS `json:"dns"`
|
||||||
|
HTTP *respGetV1SettingsAllHTTP `json:"http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// respGetV1SettingsAllDNS describes the DNS part of the response of the GET
|
||||||
|
// /api/v1/settings/all HTTP API.
|
||||||
|
type respGetV1SettingsAllDNS struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
|
UpstreamTimeout timeutil.Duration `json:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// respGetV1SettingsAllHTTP describes the HTTP part of the response of the GET
|
||||||
|
// /api/v1/settings/all HTTP API.
|
||||||
|
type respGetV1SettingsAllHTTP struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP
|
||||||
|
// API.
|
||||||
|
func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dnsSvc := svc.confMgr.DNS()
|
||||||
|
dnsConf := dnsSvc.Config()
|
||||||
|
|
||||||
|
httpConf := svc.Config()
|
||||||
|
|
||||||
|
writeJSONResponse(w, r, &RespGetV1SettingsAll{
|
||||||
|
DNS: &respGetV1SettingsAllDNS{
|
||||||
|
Addresses: dnsConf.Addresses,
|
||||||
|
BootstrapServers: dnsConf.BootstrapServers,
|
||||||
|
UpstreamServers: dnsConf.UpstreamServers,
|
||||||
|
UpstreamTimeout: timeutil.Duration{Duration: dnsConf.UpstreamTimeout},
|
||||||
|
},
|
||||||
|
HTTP: &respGetV1SettingsAllHTTP{
|
||||||
|
Addresses: httpConf.Addresses,
|
||||||
|
SecureAddresses: httpConf.SecureAddresses,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ func TestService_handleGetV1SystemInfo(t *testing.T) {
|
|||||||
_, addr := newTestServer(t)
|
_, addr := newTestServer(t)
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
Host: addr,
|
Host: addr.String(),
|
||||||
Path: websvc.PathV1SystemInfo,
|
Path: websvc.PathV1SystemInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
internal/v1/websvc/waitlistener.go
Normal file
31
internal/v1/websvc/waitlistener.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait Listener
|
||||||
|
|
||||||
|
// waitListener is a wrapper around a listener that also calls wg.Done() on the
|
||||||
|
// first call to Accept. It is useful in situations where it is important to
|
||||||
|
// catch the precise moment of the first call to Accept, for example when
|
||||||
|
// starting an HTTP server.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Move to aghnet?
|
||||||
|
type waitListener struct {
|
||||||
|
net.Listener
|
||||||
|
|
||||||
|
firstAcceptWG *sync.WaitGroup
|
||||||
|
firstAcceptOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ net.Listener = (*waitListener)(nil)
|
||||||
|
|
||||||
|
// Accept implements the [net.Listener] interface for *waitListener.
|
||||||
|
func (l *waitListener) Accept() (conn net.Conn, err error) {
|
||||||
|
l.firstAcceptOnce.Do(l.firstAcceptWG.Done)
|
||||||
|
|
||||||
|
return l.Listener.Accept()
|
||||||
|
}
|
||||||
42
internal/v1/websvc/waitlistener_internal_test.go
Normal file
42
internal/v1/websvc/waitlistener_internal_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWaitListener_Accept(t *testing.T) {
|
||||||
|
// TODO(a.garipov): use atomic.Bool in Go 1.19.
|
||||||
|
var numAcceptCalls uint32
|
||||||
|
var l net.Listener = &aghtest.Listener{
|
||||||
|
OnAccept: func() (conn net.Conn, err error) {
|
||||||
|
atomic.AddUint32(&numAcceptCalls, 1)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
OnAddr: func() (addr net.Addr) {
|
||||||
|
panic("not implemented")
|
||||||
|
},
|
||||||
|
OnClose: func() (err error) {
|
||||||
|
panic("not implemented")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var wrapper net.Listener = &waitListener{
|
||||||
|
Listener: l,
|
||||||
|
firstAcceptWG: wg,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = wrapper.Accept()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
@@ -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.
|
// TODO(a.garipov): Add tests.
|
||||||
package websvc
|
package websvc
|
||||||
@@ -15,17 +15,36 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/v1/dnssvc"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
httptreemux "github.com/dimfeld/httptreemux/v5"
|
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.
|
// Config is the AdGuard Home web service configuration structure.
|
||||||
type Config struct {
|
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,
|
// TLS is the optional TLS configuration. If TLS is not nil,
|
||||||
// SecureAddresses must not be empty.
|
// SecureAddresses must not be empty.
|
||||||
TLS *tls.Config
|
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 are the addresses on which to serve the plain HTTP API.
|
||||||
Addresses []netip.AddrPort
|
Addresses []netip.AddrPort
|
||||||
|
|
||||||
@@ -33,40 +52,48 @@ type Config struct {
|
|||||||
// SecureAddresses is not empty, TLS must not be nil.
|
// SecureAddresses is not empty, TLS must not be nil.
|
||||||
SecureAddresses []netip.AddrPort
|
SecureAddresses []netip.AddrPort
|
||||||
|
|
||||||
// Start is the time of start of AdGuard Home.
|
|
||||||
Start time.Time
|
|
||||||
|
|
||||||
// Timeout is the timeout for all server operations.
|
// Timeout is the timeout for all server operations.
|
||||||
Timeout time.Duration
|
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
|
// Service is the AdGuard Home web service. A nil *Service is a valid
|
||||||
// [agh.Service] that does nothing.
|
// [agh.Service] that does nothing.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
tls *tls.Config
|
confMgr ConfigManager
|
||||||
servers []*http.Server
|
tls *tls.Config
|
||||||
start time.Time
|
start time.Time
|
||||||
timeout time.Duration
|
servers []*http.Server
|
||||||
|
timeout time.Duration
|
||||||
|
forceHTTPS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
// 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) {
|
func New(c *Config) (svc *Service) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
svc = &Service{
|
svc = &Service{
|
||||||
tls: c.TLS,
|
confMgr: c.ConfigManager,
|
||||||
start: c.Start,
|
tls: c.TLS,
|
||||||
timeout: c.Timeout,
|
start: c.Start,
|
||||||
|
timeout: c.Timeout,
|
||||||
|
forceHTTPS: c.ForceHTTPS,
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := newMux(svc)
|
mux := newMux(svc)
|
||||||
|
|
||||||
for _, a := range c.Addresses {
|
for _, a := range c.Addresses {
|
||||||
addr := a.String()
|
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{
|
svc.servers = append(svc.servers, &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
@@ -111,6 +138,21 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: PathHealthCheck,
|
path: PathHealthCheck,
|
||||||
isJSON: false,
|
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,
|
handler: svc.handleGetV1SystemInfo,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
@@ -119,29 +161,39 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
var h http.HandlerFunc
|
|
||||||
if r.isJSON {
|
if r.isJSON {
|
||||||
// TODO(a.garipov): Consider using httptreemux's MiddlewareFunc.
|
mux.Handle(r.method, r.path, jsonMw(r.handler))
|
||||||
h = jsonMw(r.handler)
|
|
||||||
} else {
|
} else {
|
||||||
h = r.handler
|
mux.Handle(r.method, r.path, r.handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
mux.Handle(r.method, r.path, h)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mux
|
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.
|
// must not be called until Start returns.
|
||||||
func (svc *Service) Addrs() (addrs []string) {
|
func (svc *Service) addrs() (addrs, secAddrs []netip.AddrPort) {
|
||||||
addrs = make([]string, 0, len(svc.servers))
|
|
||||||
for _, srv := range svc.servers {
|
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.
|
// 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")
|
_, _ = io.WriteString(w, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
// unit is a convenient alias for struct{}.
|
|
||||||
type unit = struct{}
|
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ agh.Service = (*Service)(nil)
|
var _ agh.Service = (*Service)(nil)
|
||||||
|
|
||||||
@@ -163,11 +212,9 @@ func (svc *Service) Start() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
srvs := svc.servers
|
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(len(srvs))
|
wg.Add(len(svc.servers))
|
||||||
for _, srv := range srvs {
|
for _, srv := range svc.servers {
|
||||||
go serve(srv, wg)
|
go serve(srv, wg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,11 +228,14 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
|
|||||||
addr := srv.Addr
|
addr := srv.Addr
|
||||||
defer log.OnPanic(addr)
|
defer log.OnPanic(addr)
|
||||||
|
|
||||||
|
var proto string
|
||||||
var l net.Listener
|
var l net.Listener
|
||||||
var err error
|
var err error
|
||||||
if srv.TLSConfig == nil {
|
if srv.TLSConfig == nil {
|
||||||
|
proto = "http"
|
||||||
l, err = net.Listen("tcp", addr)
|
l, err = net.Listen("tcp", addr)
|
||||||
} else {
|
} else {
|
||||||
|
proto = "https"
|
||||||
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
||||||
}
|
}
|
||||||
if err != nil {
|
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.
|
// would mean that a random available port was automatically chosen.
|
||||||
srv.Addr = l.Addr().String()
|
srv.Addr = l.Addr().String()
|
||||||
|
|
||||||
log.Info("websvc: starting srv http://%s", srv.Addr)
|
log.Info("websvc: starting srv %s://%s", proto, srv.Addr)
|
||||||
wg.Done()
|
|
||||||
|
l = &waitListener{
|
||||||
|
Listener: l,
|
||||||
|
firstAcceptWG: wg,
|
||||||
|
}
|
||||||
|
|
||||||
err = srv.Serve(l)
|
err = srv.Serve(l)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
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 {
|
if len(errs) > 0 {
|
||||||
return errors.List("shutting down")
|
return errors.List("shutting down", errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
|||||||
// instance down.
|
// instance down.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Use svc or remove it.
|
// TODO(a.garipov): Use svc or remove it.
|
||||||
func newTestServer(t testing.TB) (svc *websvc.Service, addr string) {
|
func newTestServer(t testing.TB) (svc *websvc.Service, addr netip.AddrPort) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
c := &websvc.Config{
|
c := &websvc.Config{
|
||||||
@@ -48,10 +48,11 @@ func newTestServer(t testing.TB) (svc *websvc.Service, addr string) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
addrs := svc.Addrs()
|
c = svc.Config()
|
||||||
require.Len(t, addrs, 1)
|
require.NotNil(t, c)
|
||||||
|
require.Len(t, c.Addresses, 1)
|
||||||
|
|
||||||
return svc, addrs[0]
|
return svc, c.Addresses[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
||||||
@@ -83,7 +84,7 @@ func TestService_Start_getHealthCheck(t *testing.T) {
|
|||||||
_, addr := newTestServer(t)
|
_, addr := newTestServer(t)
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
Host: addr,
|
Host: addr.String(),
|
||||||
Path: websvc.PathHealthCheck,
|
Path: websvc.PathHealthCheck,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user