From 27bd8bc58b84660095900346fc337b35d460b540 Mon Sep 17 00:00:00 2001 From: Ainar Garipov Date: Wed, 31 Aug 2022 17:53:45 +0300 Subject: [PATCH] websvc: add dns and http apis --- internal/v1/websvc/dns.go | 75 ++++++++++ internal/v1/websvc/http.go | 92 ++++++++++++ internal/v1/websvc/json.go | 15 +- internal/v1/websvc/json_internal_test.go | 113 ++++++++++++++ internal/v1/websvc/path.go | 5 +- internal/v1/websvc/settings.go | 63 ++++++++ internal/v1/websvc/system_test.go | 2 +- internal/v1/websvc/waitlistener.go | 31 ++++ .../v1/websvc/waitlistener_internal_test.go | 42 ++++++ internal/v1/websvc/websvc.go | 140 +++++++++++++----- internal/v1/websvc/websvc_test.go | 11 +- 11 files changed, 547 insertions(+), 42 deletions(-) create mode 100644 internal/v1/websvc/dns.go create mode 100644 internal/v1/websvc/http.go create mode 100644 internal/v1/websvc/json_internal_test.go create mode 100644 internal/v1/websvc/settings.go create mode 100644 internal/v1/websvc/waitlistener.go create mode 100644 internal/v1/websvc/waitlistener_internal_test.go diff --git a/internal/v1/websvc/dns.go b/internal/v1/websvc/dns.go new file mode 100644 index 00000000..419167c8 --- /dev/null +++ b/internal/v1/websvc/dns.go @@ -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}, + }) +} diff --git a/internal/v1/websvc/http.go b/internal/v1/websvc/http.go new file mode 100644 index 00000000..ec881d4d --- /dev/null +++ b/internal/v1/websvc/http.go @@ -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) + } + }() +} diff --git a/internal/v1/websvc/json.go b/internal/v1/websvc/json.go index ef84211b..e08d7e2e 100644 --- a/internal/v1/websvc/json.go +++ b/internal/v1/websvc/json.go @@ -27,7 +27,7 @@ const nsecPerMsec = float64(time.Millisecond / time.Nanosecond) // always nil. func (t jsonTime) MarshalJSON() (b []byte, err error) { 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 } @@ -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) } } + +// 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) + } +} diff --git a/internal/v1/websvc/json_internal_test.go b/internal/v1/websvc/json_internal_test.go new file mode 100644 index 00000000..69810736 --- /dev/null +++ b/internal/v1/websvc/json_internal_test.go @@ -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) + }) +} diff --git a/internal/v1/websvc/path.go b/internal/v1/websvc/path.go index cfd67fd9..e38a1d60 100644 --- a/internal/v1/websvc/path.go +++ b/internal/v1/websvc/path.go @@ -4,5 +4,8 @@ package websvc const ( 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" ) diff --git a/internal/v1/websvc/settings.go b/internal/v1/websvc/settings.go new file mode 100644 index 00000000..e3c133c8 --- /dev/null +++ b/internal/v1/websvc/settings.go @@ -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, + }, + }) +} diff --git a/internal/v1/websvc/system_test.go b/internal/v1/websvc/system_test.go index 49579ca5..c267e5ac 100644 --- a/internal/v1/websvc/system_test.go +++ b/internal/v1/websvc/system_test.go @@ -17,7 +17,7 @@ func TestService_handleGetV1SystemInfo(t *testing.T) { _, addr := newTestServer(t) u := &url.URL{ Scheme: "http", - Host: addr, + Host: addr.String(), Path: websvc.PathV1SystemInfo, } diff --git a/internal/v1/websvc/waitlistener.go b/internal/v1/websvc/waitlistener.go new file mode 100644 index 00000000..8ab56269 --- /dev/null +++ b/internal/v1/websvc/waitlistener.go @@ -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() +} diff --git a/internal/v1/websvc/waitlistener_internal_test.go b/internal/v1/websvc/waitlistener_internal_test.go new file mode 100644 index 00000000..ad710481 --- /dev/null +++ b/internal/v1/websvc/waitlistener_internal_test.go @@ -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() +} diff --git a/internal/v1/websvc/websvc.go b/internal/v1/websvc/websvc.go index bbaac005..8f385950 100644 --- a/internal/v1/websvc/websvc.go +++ b/internal/v1/websvc/websvc.go @@ -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 +} diff --git a/internal/v1/websvc/websvc_test.go b/internal/v1/websvc/websvc_test.go index de4a9f5d..a1961348 100644 --- a/internal/v1/websvc/websvc_test.go +++ b/internal/v1/websvc/websvc_test.go @@ -25,7 +25,7 @@ var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC) // instance down. // // 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() c := &websvc.Config{ @@ -48,10 +48,11 @@ func newTestServer(t testing.TB) (svc *websvc.Service, addr string) { require.NoError(t, err) }) - addrs := svc.Addrs() - require.Len(t, addrs, 1) + c = svc.Config() + 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 @@ -83,7 +84,7 @@ func TestService_Start_getHealthCheck(t *testing.T) { _, addr := newTestServer(t) u := &url.URL{ Scheme: "http", - Host: addr, + Host: addr.String(), Path: websvc.PathHealthCheck, }