Compare commits
377 Commits
v0.107.17
...
v0.108.0-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7d02fa935 | ||
|
|
af8f64ac00 | ||
|
|
c139287787 | ||
|
|
fa0fd90ddd | ||
|
|
c5565a9e4e | ||
|
|
ac7634da37 | ||
|
|
746e9df727 | ||
|
|
3dd7393b3f | ||
|
|
9c9d6b48e3 | ||
|
|
9951d861d1 | ||
|
|
8a935d4ffb | ||
|
|
bf10f157ab | ||
|
|
15f5876e33 | ||
|
|
04c8e3b288 | ||
|
|
cebbb69a4c | ||
|
|
a272b61ed6 | ||
|
|
b86250737e | ||
|
|
a149d816d9 | ||
|
|
67d89660ca | ||
|
|
2a85d7dd7e | ||
|
|
68d13fcc2b | ||
|
|
2de42284a5 | ||
|
|
d2a09e49ff | ||
|
|
e0080ffa3a | ||
|
|
8dba4ecd01 | ||
|
|
aaaa56fce3 | ||
|
|
ab79168b13 | ||
|
|
5ae826d8a9 | ||
|
|
a736f67205 | ||
|
|
fee81b31ec | ||
|
|
a1acfbbae4 | ||
|
|
4582b1c919 | ||
|
|
893358ea71 | ||
|
|
f109fb17a4 | ||
|
|
5604e33574 | ||
|
|
67da002391 | ||
|
|
d42d1a7ea4 | ||
|
|
e4a42bf233 | ||
|
|
0eba31ca03 | ||
|
|
f5602d9c46 | ||
|
|
f1dd33346a | ||
|
|
960a7a75ed | ||
|
|
a126f514ff | ||
|
|
c0c9d8adb0 | ||
|
|
7cac010573 | ||
|
|
51f426736c | ||
|
|
0c0340d63e | ||
|
|
330ac30324 | ||
|
|
2e0f6e5468 | ||
|
|
b7e815483e | ||
|
|
15b19ff726 | ||
|
|
f557339ca0 | ||
|
|
fe8be3701f | ||
|
|
c26ab190e7 | ||
|
|
6a62f704e2 | ||
|
|
24eb3476db | ||
|
|
8a924cb4ed | ||
|
|
6e7964c9e7 | ||
|
|
9d59be4269 | ||
|
|
bf792b83f6 | ||
|
|
0cce420261 | ||
|
|
61bd217eb3 | ||
|
|
739e0098ec | ||
|
|
27032ef79e | ||
|
|
5e626306d1 | ||
|
|
2ffea605cf | ||
|
|
4d404b887f | ||
|
|
7b48863041 | ||
|
|
756b14a61d | ||
|
|
b71a5d86de | ||
|
|
d45fa5801e | ||
|
|
47c9c946a3 | ||
|
|
690deb1c05 | ||
|
|
59d18c6598 | ||
|
|
91bbb744dc | ||
|
|
11e4f09165 | ||
|
|
c45c02de29 | ||
|
|
fe0c53ec43 | ||
|
|
4fc045de11 | ||
|
|
cc2388e0c8 | ||
|
|
ab6da05b51 | ||
|
|
8e89cc129c | ||
|
|
9ffe078703 | ||
|
|
27b0251b5b | ||
|
|
ed209daf8a | ||
|
|
95771c7aba | ||
|
|
42bd0615c2 | ||
|
|
3a88ef3be2 | ||
|
|
572fed9f35 | ||
|
|
663f0643f2 | ||
|
|
fc62796e2d | ||
|
|
b9e39c8cca | ||
|
|
fffa656758 | ||
|
|
b74b92fc27 | ||
|
|
bc1503af57 | ||
|
|
b79c08316f | ||
|
|
08799e9d0a | ||
|
|
bedfb47a9f | ||
|
|
53e2c1f7cd | ||
|
|
88812f05f5 | ||
|
|
10a8f79644 | ||
|
|
ccc4f1a2da | ||
|
|
451fd7c445 | ||
|
|
782de99a0a | ||
|
|
d4afd60b08 | ||
|
|
c8ace868d4 | ||
|
|
2b4158e5c9 | ||
|
|
53209bc42c | ||
|
|
da1ae33805 | ||
|
|
ab02c829ea | ||
|
|
3c0d2a9253 | ||
|
|
58512c3af9 | ||
|
|
78389e518e | ||
|
|
9c9169ac12 | ||
|
|
e545f3bdb7 | ||
|
|
c000d9f232 | ||
|
|
1fb043768e | ||
|
|
3660b4810e | ||
|
|
a9127c4a45 | ||
|
|
c098960b39 | ||
|
|
5cc2a2cd0c | ||
|
|
8733f55c2c | ||
|
|
a3750ffff1 | ||
|
|
9e0d3eb6e7 | ||
|
|
e0a57d2912 | ||
|
|
53e77cb2c0 | ||
|
|
8ecfef16eb | ||
|
|
d51110acb5 | ||
|
|
2348b8fafa | ||
|
|
7f0b16d074 | ||
|
|
a0c8aee3f7 | ||
|
|
d519929988 | ||
|
|
cb83f8b531 | ||
|
|
45bcc2c09a | ||
|
|
2410639123 | ||
|
|
d1525cf09d | ||
|
|
35c1d84b42 | ||
|
|
986124948a | ||
|
|
fa76ad2a3c | ||
|
|
57c0b1203e | ||
|
|
be1bc76cfa | ||
|
|
6913ebb29f | ||
|
|
e35eeacd74 | ||
|
|
bdcf345155 | ||
|
|
307654f648 | ||
|
|
970b6cf698 | ||
|
|
eccfbf6a6d | ||
|
|
1a1a48482a | ||
|
|
1afd73ad0b | ||
|
|
6856a80380 | ||
|
|
cf3a8991ea | ||
|
|
e3624ec588 | ||
|
|
64df882c5e | ||
|
|
06e4658da9 | ||
|
|
4a7b4d03a1 | ||
|
|
257d167002 | ||
|
|
e6ebb8efef | ||
|
|
7e80980ae4 | ||
|
|
50476cda31 | ||
|
|
ea5d165a70 | ||
|
|
2830f396c6 | ||
|
|
620ad13490 | ||
|
|
f54a2dc1da | ||
|
|
63f6844318 | ||
|
|
12edc05ab0 | ||
|
|
71b8e75138 | ||
|
|
0bcc6699e1 | ||
|
|
385a873b0f | ||
|
|
0daa6a107b | ||
|
|
72098d2255 | ||
|
|
572d2794e2 | ||
|
|
d4c3a43bcb | ||
|
|
6e63757fc7 | ||
|
|
721397cee3 | ||
|
|
fd1c841810 | ||
|
|
f58265ec98 | ||
|
|
14fd995ae9 | ||
|
|
50565bed3b | ||
|
|
70f85fca21 | ||
|
|
4293cf5945 | ||
|
|
4c6377c5cb | ||
|
|
9b3adac145 | ||
|
|
73f935f3f3 | ||
|
|
a481ff4c51 | ||
|
|
bbccd61614 | ||
|
|
8a3d5f046c | ||
|
|
eb8e8166c8 | ||
|
|
3420becce3 | ||
|
|
9ed8699c75 | ||
|
|
b59b82474a | ||
|
|
cce0e593c5 | ||
|
|
da32079516 | ||
|
|
ccf268baf4 | ||
|
|
053bb72a00 | ||
|
|
41f081d8da | ||
|
|
e0f2c3d170 | ||
|
|
f32da12a86 | ||
|
|
f5959a0dc6 | ||
|
|
0a5888f27a | ||
|
|
07d48af10c | ||
|
|
e58a415d10 | ||
|
|
ae43ca0605 | ||
|
|
9acb1f364b | ||
|
|
84cd528103 | ||
|
|
56519548f1 | ||
|
|
bdcd17a41a | ||
|
|
1eafb4e7cf | ||
|
|
bf024fb985 | ||
|
|
a832987f7c | ||
|
|
77e5e27d75 | ||
|
|
3505ce8739 | ||
|
|
14d8f58592 | ||
|
|
006cd98869 | ||
|
|
ce1b2bc4f1 | ||
|
|
8f4acce44a | ||
|
|
b04d1ed6c8 | ||
|
|
f987c25598 | ||
|
|
b9b93f1286 | ||
|
|
a7a5e50620 | ||
|
|
0edf71a4af | ||
|
|
5956b97e7f | ||
|
|
d3f39b0aa1 | ||
|
|
e738508d7a | ||
|
|
302faca32f | ||
|
|
1c1ca1c6e3 | ||
|
|
a497dc09ca | ||
|
|
3ce04f48ca | ||
|
|
368a98fb29 | ||
|
|
cbe32c5a73 | ||
|
|
f46c9f74d5 | ||
|
|
4b884ace62 | ||
|
|
7ce7e90865 | ||
|
|
756c932e37 | ||
|
|
c3d5fcc669 | ||
|
|
65a33a1215 | ||
|
|
1a49d2f0c9 | ||
|
|
549b20bdea | ||
|
|
75f01d51f7 | ||
|
|
a82ec09afd | ||
|
|
c0ac82be6a | ||
|
|
24d7dc8e8a | ||
|
|
79d85a24e9 | ||
|
|
f289f4b1b6 | ||
|
|
b7eedb3feb | ||
|
|
58515fce43 | ||
|
|
21905d9869 | ||
|
|
56f78edb97 | ||
|
|
a580149ad6 | ||
|
|
6dc9e73ce4 | ||
|
|
5d52e68d26 | ||
|
|
c4ff80fd3a | ||
|
|
ed449c6186 | ||
|
|
1c89394aef | ||
|
|
235316e050 | ||
|
|
0a1ff65b4a | ||
|
|
2a1ad532f4 | ||
|
|
9d144ecb0a | ||
|
|
9b7fe74086 | ||
|
|
0f2a9f262e | ||
|
|
82af43039c | ||
|
|
12ee287d0b | ||
|
|
57171f0a61 | ||
|
|
21a1187ed2 | ||
|
|
2c2c0d445b | ||
|
|
9f0fdc5e78 | ||
|
|
96594a3433 | ||
|
|
4c5b38a447 | ||
|
|
0e608fda13 | ||
|
|
8bb95469d9 | ||
|
|
e9e0b7c4f9 | ||
|
|
c70f941bf8 | ||
|
|
a79b61aac3 | ||
|
|
5e71f5df6a | ||
|
|
047970e5ee | ||
|
|
f31ffcc5d1 | ||
|
|
0d562a7b1f | ||
|
|
3603b1fcab | ||
|
|
82505566f8 | ||
|
|
9ce2a0fb34 | ||
|
|
5cba78a8d5 | ||
|
|
2c33ab6a92 | ||
|
|
beb674ecbc | ||
|
|
b16b1d1d24 | ||
|
|
f8e45c13f3 | ||
|
|
b9790f663a | ||
|
|
778585865e | ||
|
|
cd8206ad9b | ||
|
|
573cbafe3f | ||
|
|
c346216424 | ||
|
|
e7b3c9969b | ||
|
|
dc0d081b47 | ||
|
|
ded9842cd7 | ||
|
|
89d9b03dfe | ||
|
|
f1d05a49f0 | ||
|
|
9a764b9b82 | ||
|
|
e0b557eda2 | ||
|
|
ea6e033dae | ||
|
|
3afe7c3daf | ||
|
|
afbc7a72e3 | ||
|
|
ff1e108bfe | ||
|
|
b29f320fd4 | ||
|
|
773b80a969 | ||
|
|
975995a9c7 | ||
|
|
f131067278 | ||
|
|
b43aa86cae | ||
|
|
6824eec308 | ||
|
|
18079ca1bb | ||
|
|
a1f29c31b9 | ||
|
|
0ef8344178 | ||
|
|
f53f48cc33 | ||
|
|
2a5b5f1927 | ||
|
|
b290eddc70 | ||
|
|
6d0a43aad6 | ||
|
|
1bc2186c2d | ||
|
|
6584c300b8 | ||
|
|
dc480ae70f | ||
|
|
e783564084 | ||
|
|
0ee34534c6 | ||
|
|
9146df5493 | ||
|
|
76fa60498e | ||
|
|
8455940b59 | ||
|
|
2d46aa7121 | ||
|
|
bf9b35b9c6 | ||
|
|
f9aa5ae86a | ||
|
|
642d68c482 | ||
|
|
5ff7cdbac8 | ||
|
|
504c54ab0e | ||
|
|
90c17c79de | ||
|
|
0b72bcc5a1 | ||
|
|
dc14f89c9f | ||
|
|
2263adbbe0 | ||
|
|
e29261516f | ||
|
|
f12eaf29a2 | ||
|
|
3e2ab87293 | ||
|
|
41e8db4221 | ||
|
|
3f5605c42e | ||
|
|
f7ff02f3b1 | ||
|
|
5ec4a4dab8 | ||
|
|
13871977f9 | ||
|
|
2fdda8a22c | ||
|
|
d82b290251 | ||
|
|
eb15304ff4 | ||
|
|
1a3bf5ebda | ||
|
|
15956f4511 | ||
|
|
09d0ce4578 | ||
|
|
9735a35123 | ||
|
|
813a06d09a | ||
|
|
061136508e | ||
|
|
008f58c863 | ||
|
|
0e4ffd339f | ||
|
|
1458600c37 | ||
|
|
34c95f99f8 | ||
|
|
e9c59b098e | ||
|
|
a0bb5ce8a4 | ||
|
|
01947bedb4 | ||
|
|
a6ca824064 | ||
|
|
380cff07f2 | ||
|
|
d2ce06e1ca | ||
|
|
2ed1f939b5 | ||
|
|
dea8a585f8 | ||
|
|
313555b10c | ||
|
|
661f4ece48 | ||
|
|
52f36f201e | ||
|
|
46cd974e2a | ||
|
|
201ef10de6 | ||
|
|
d9df7c13be | ||
|
|
64e751e579 | ||
|
|
e6e5958595 | ||
|
|
ff3df0ec33 | ||
|
|
ebe86ce00e | ||
|
|
d317e19291 | ||
|
|
39c4999d2d | ||
|
|
7f55bd8461 | ||
|
|
2968a65f14 | ||
|
|
779fbe79b8 | ||
|
|
da0d1cb754 | ||
|
|
ef80c07075 |
3
Makefile
3
Makefile
@@ -34,6 +34,8 @@ YARN_INSTALL_FLAGS = $(YARN_FLAGS) --network-timeout 120000 --silent\
|
||||
--ignore-engines --ignore-optional --ignore-platform\
|
||||
--ignore-scripts
|
||||
|
||||
NEXTAPI = 0
|
||||
|
||||
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
||||
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
||||
# into BUILD_RELEASE_DEPS_0, and so both frontend and backend
|
||||
@@ -61,6 +63,7 @@ ENV = env\
|
||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||
RACE='$(RACE)'\
|
||||
SIGN='$(SIGN)'\
|
||||
NEXTAPI='$(NEXTAPI)'\
|
||||
VERBOSE='$(VERBOSE)'\
|
||||
VERSION='$(VERSION)'\
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package aghtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -116,6 +118,36 @@ func (w *FSWatcher) Close() (err error) {
|
||||
return w.OnClose()
|
||||
}
|
||||
|
||||
// Package agh
|
||||
|
||||
// type check
|
||||
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
|
||||
|
||||
// ServiceWithConfig is a mock [agh.ServiceWithConfig] implementation for tests.
|
||||
type ServiceWithConfig[ConfigType any] struct {
|
||||
OnStart func() (err error)
|
||||
OnShutdown func(ctx context.Context) (err error)
|
||||
OnConfig func() (c ConfigType)
|
||||
}
|
||||
|
||||
// Start implements the [agh.ServiceWithConfig] interface for
|
||||
// *ServiceWithConfig.
|
||||
func (s *ServiceWithConfig[_]) Start() (err error) {
|
||||
return s.OnStart()
|
||||
}
|
||||
|
||||
// Shutdown implements the [agh.ServiceWithConfig] interface for
|
||||
// *ServiceWithConfig.
|
||||
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
|
||||
return s.OnShutdown(ctx)
|
||||
}
|
||||
|
||||
// Config implements the [agh.ServiceWithConfig] interface for
|
||||
// *ServiceWithConfig.
|
||||
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
||||
return s.OnConfig()
|
||||
}
|
||||
|
||||
// Module dnsproxy
|
||||
|
||||
// Package upstream
|
||||
|
||||
63
internal/next/agh/agh.go
Normal file
63
internal/next/agh/agh.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Package agh contains common entities and interfaces of AdGuard Home.
|
||||
package agh
|
||||
|
||||
import "context"
|
||||
|
||||
// Service is the interface for API servers.
|
||||
//
|
||||
// TODO(a.garipov): Consider adding a context to Start.
|
||||
//
|
||||
// TODO(a.garipov): Consider adding a Wait method or making an extension
|
||||
// interface for that.
|
||||
type Service interface {
|
||||
// Start starts the service. It does not block.
|
||||
Start() (err error)
|
||||
|
||||
// Shutdown gracefully stops the service. ctx is used to determine
|
||||
// a timeout before trying to stop the service less gracefully.
|
||||
Shutdown(ctx context.Context) (err error)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ Service = EmptyService{}
|
||||
|
||||
// EmptyService is a [Service] that does nothing.
|
||||
//
|
||||
// TODO(a.garipov): Remove if unnecessary.
|
||||
type EmptyService struct{}
|
||||
|
||||
// Start implements the [Service] interface for EmptyService.
|
||||
func (EmptyService) Start() (err error) { return nil }
|
||||
|
||||
// Shutdown implements the [Service] interface for EmptyService.
|
||||
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
||||
|
||||
// ServiceWithConfig is an extension of the [Service] interface for services
|
||||
// that can return their configuration.
|
||||
//
|
||||
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
||||
// how to make it testable in a better way.
|
||||
type ServiceWithConfig[ConfigType any] interface {
|
||||
Service
|
||||
|
||||
Config() (c ConfigType)
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)
|
||||
|
||||
// EmptyServiceWithConfig is a ServiceWithConfig that does nothing. Its Config
|
||||
// method returns Conf.
|
||||
//
|
||||
// TODO(a.garipov): Remove if unnecessary.
|
||||
type EmptyServiceWithConfig[ConfigType any] struct {
|
||||
EmptyService
|
||||
|
||||
Conf ConfigType
|
||||
}
|
||||
|
||||
// Config implements the [ServiceWithConfig] interface for
|
||||
// *EmptyServiceWithConfig.
|
||||
func (s *EmptyServiceWithConfig[ConfigType]) Config() (conf ConfigType) {
|
||||
return s.Conf
|
||||
}
|
||||
77
internal/next/cmd/cmd.go
Normal file
77
internal/next/cmd/cmd.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Package cmd is the AdGuard Home entry point. It contains the on-disk
|
||||
// configuration file utilities, signal processing logic, and so on.
|
||||
//
|
||||
// TODO(a.garipov): Move to the upper-level internal/.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Main is the entry point of application.
|
||||
func Main(clientBuildFS fs.FS) {
|
||||
// Initial Configuration
|
||||
|
||||
start := time.Now()
|
||||
rand.Seed(start.UnixNano())
|
||||
|
||||
// TODO(a.garipov): Set up logging.
|
||||
|
||||
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
|
||||
|
||||
// Web Service
|
||||
|
||||
// TODO(a.garipov): Use in the Web service.
|
||||
_ = clientBuildFS
|
||||
|
||||
// TODO(a.garipov): Set up configuration file name.
|
||||
const confFile = "AdGuardHome.1.yaml"
|
||||
|
||||
confMgr, err := configmgr.New(confFile, start)
|
||||
fatalOnError(err)
|
||||
|
||||
web := confMgr.Web()
|
||||
err = web.Start()
|
||||
fatalOnError(err)
|
||||
|
||||
dns := confMgr.DNS()
|
||||
err = dns.Start()
|
||||
fatalOnError(err)
|
||||
|
||||
sigHdlr := newSignalHandler(
|
||||
confFile,
|
||||
start,
|
||||
web,
|
||||
dns,
|
||||
)
|
||||
|
||||
go sigHdlr.handle()
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
// defaultTimeout is the timeout used for some operations where another timeout
|
||||
// hasn't been defined yet.
|
||||
const defaultTimeout = 15 * time.Second
|
||||
|
||||
// ctxWithDefaultTimeout is a helper function that returns a context with
|
||||
// timeout set to defaultTimeout.
|
||||
func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), defaultTimeout)
|
||||
}
|
||||
|
||||
// fatalOnError is a helper that exits the program with an error code if err is
|
||||
// not nil. It must only be used within Main.
|
||||
func fatalOnError(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
118
internal/next/cmd/signal.go
Normal file
118
internal/next/cmd/signal.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// signalHandler processes incoming signals and shuts services down.
|
||||
type signalHandler struct {
|
||||
// signal is the channel to which OS signals are sent.
|
||||
signal chan os.Signal
|
||||
|
||||
// confFile is the path to the configuration file.
|
||||
confFile string
|
||||
|
||||
// start is the time at which AdGuard Home has been started.
|
||||
start time.Time
|
||||
|
||||
// services are the services that are shut down before application exiting.
|
||||
services []agh.Service
|
||||
}
|
||||
|
||||
// handle processes OS signals.
|
||||
func (h *signalHandler) handle() {
|
||||
defer log.OnPanic("signalHandler.handle")
|
||||
|
||||
for sig := range h.signal {
|
||||
log.Info("sighdlr: received signal %q", sig)
|
||||
|
||||
if aghos.IsReconfigureSignal(sig) {
|
||||
h.reconfigure()
|
||||
} else if aghos.IsShutdownSignal(sig) {
|
||||
status := h.shutdown()
|
||||
log.Info("sighdlr: exiting with status %d", status)
|
||||
|
||||
os.Exit(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reconfigure rereads the configuration file and updates and restarts services.
|
||||
func (h *signalHandler) reconfigure() {
|
||||
log.Info("sighdlr: reconfiguring adguard home")
|
||||
|
||||
status := h.shutdown()
|
||||
if status != statusSuccess {
|
||||
log.Info("sighdlr: reconfiruging: exiting with status %d", status)
|
||||
|
||||
os.Exit(status)
|
||||
}
|
||||
|
||||
// TODO(a.garipov): This is a very rough way to do it. Some services can be
|
||||
// reconfigured without the full shutdown, and the error handling is
|
||||
// currently not the best.
|
||||
|
||||
confMgr, err := configmgr.New(h.confFile, h.start)
|
||||
fatalOnError(err)
|
||||
|
||||
web := confMgr.Web()
|
||||
err = web.Start()
|
||||
fatalOnError(err)
|
||||
|
||||
dns := confMgr.DNS()
|
||||
err = dns.Start()
|
||||
fatalOnError(err)
|
||||
|
||||
h.services = []agh.Service{
|
||||
dns,
|
||||
web,
|
||||
}
|
||||
|
||||
log.Info("sighdlr: successfully reconfigured adguard home")
|
||||
}
|
||||
|
||||
// Exit status constants.
|
||||
const (
|
||||
statusSuccess = 0
|
||||
statusError = 1
|
||||
)
|
||||
|
||||
// shutdown gracefully shuts down all services.
|
||||
func (h *signalHandler) shutdown() (status int) {
|
||||
ctx, cancel := ctxWithDefaultTimeout()
|
||||
defer cancel()
|
||||
|
||||
status = statusSuccess
|
||||
|
||||
log.Info("sighdlr: shutting down services")
|
||||
for i, service := range h.services {
|
||||
err := service.Shutdown(ctx)
|
||||
if err != nil {
|
||||
log.Error("sighdlr: shutting down service at index %d: %s", i, err)
|
||||
status = statusError
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
||||
func newSignalHandler(confFile string, start time.Time, svcs ...agh.Service) (h *signalHandler) {
|
||||
h = &signalHandler{
|
||||
signal: make(chan os.Signal, 1),
|
||||
confFile: confFile,
|
||||
start: start,
|
||||
services: svcs,
|
||||
}
|
||||
|
||||
aghos.NotifyShutdownSignal(h.signal)
|
||||
aghos.NotifyReconfigureSignal(h.signal)
|
||||
|
||||
return h
|
||||
}
|
||||
40
internal/next/configmgr/config.go
Normal file
40
internal/next/configmgr/config.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package configmgr
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
)
|
||||
|
||||
// Configuration Structures
|
||||
|
||||
// config is the top-level on-disk configuration structure.
|
||||
type config struct {
|
||||
DNS *dnsConfig `yaml:"dns"`
|
||||
HTTP *httpConfig `yaml:"http"`
|
||||
// TODO(a.garipov): Use.
|
||||
SchemaVersion int `yaml:"schema_version"`
|
||||
// TODO(a.garipov): Use.
|
||||
DebugPprof bool `yaml:"debug_pprof"`
|
||||
Verbose bool `yaml:"verbose"`
|
||||
}
|
||||
|
||||
// dnsConfig is the on-disk DNS configuration.
|
||||
//
|
||||
// TODO(a.garipov): Validate.
|
||||
type dnsConfig struct {
|
||||
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||
BootstrapDNS []string `yaml:"bootstrap_dns"`
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
|
||||
}
|
||||
|
||||
// httpConfig is the on-disk web API configuration.
|
||||
//
|
||||
// TODO(a.garipov): Validate.
|
||||
type httpConfig struct {
|
||||
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
|
||||
Timeout timeutil.Duration `yaml:"timeout"`
|
||||
ForceHTTPS bool `yaml:"force_https"`
|
||||
}
|
||||
205
internal/next/configmgr/configmgr.go
Normal file
205
internal/next/configmgr/configmgr.go
Normal file
@@ -0,0 +1,205 @@
|
||||
// Package configmgr defines the AdGuard Home on-disk configuration entities and
|
||||
// configuration manager.
|
||||
package configmgr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Configuration Manager
|
||||
|
||||
// Manager handles full and partial changes in the configuration, persisting
|
||||
// them to disk if necessary.
|
||||
type Manager struct {
|
||||
// updMu makes sure that at most one reconfiguration is performed at a time.
|
||||
// updMu protects all fields below.
|
||||
updMu *sync.RWMutex
|
||||
|
||||
// dns is the DNS service.
|
||||
dns *dnssvc.Service
|
||||
|
||||
// Web is the Web API service.
|
||||
web *websvc.Service
|
||||
|
||||
// current is the current configuration.
|
||||
current *config
|
||||
|
||||
// fileName is the name of the configuration file.
|
||||
fileName string
|
||||
}
|
||||
|
||||
// New creates a new *Manager that persists changes to the file pointed to by
|
||||
// fileName. It reads the configuration file and populates the service fields.
|
||||
// start is the startup time of AdGuard Home.
|
||||
func New(fileName string, start time.Time) (m *Manager, err error) {
|
||||
defer func() { err = errors.Annotate(err, "reading config") }()
|
||||
|
||||
conf := &config{}
|
||||
f, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
defer func() { err = errors.WithDeferred(err, f.Close()) }()
|
||||
|
||||
err = yaml.NewDecoder(f).Decode(conf)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Move into a separate function and add other logging
|
||||
// settings.
|
||||
if conf.Verbose {
|
||||
log.SetLevel(log.DEBUG)
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Validate the configuration structure. Return an error
|
||||
// if it's incorrect.
|
||||
|
||||
m = &Manager{
|
||||
updMu: &sync.RWMutex{},
|
||||
current: conf,
|
||||
fileName: fileName,
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Get the context with the timeout from the arguments?
|
||||
const assemblyTimeout = 5 * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = m.assemble(ctx, conf, start)
|
||||
if err != nil {
|
||||
// Don't wrap the error, because it's informative enough as is.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// assemble creates all services and puts them into the corresponding fields.
|
||||
// The fields of conf must not be modified after calling assemble.
|
||||
func (m *Manager) assemble(ctx context.Context, conf *config, start time.Time) (err error) {
|
||||
dnsConf := &dnssvc.Config{
|
||||
Addresses: conf.DNS.Addresses,
|
||||
BootstrapServers: conf.DNS.BootstrapDNS,
|
||||
UpstreamServers: conf.DNS.UpstreamDNS,
|
||||
UpstreamTimeout: conf.DNS.UpstreamTimeout.Duration,
|
||||
}
|
||||
err = m.updateDNS(ctx, dnsConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("assembling dnssvc: %w", err)
|
||||
}
|
||||
|
||||
webSvcConf := &websvc.Config{
|
||||
ConfigManager: m,
|
||||
// TODO(a.garipov): Fill from config file.
|
||||
TLS: nil,
|
||||
Start: start,
|
||||
Addresses: conf.HTTP.Addresses,
|
||||
SecureAddresses: conf.HTTP.SecureAddresses,
|
||||
Timeout: conf.HTTP.Timeout.Duration,
|
||||
ForceHTTPS: conf.HTTP.ForceHTTPS,
|
||||
}
|
||||
|
||||
err = m.updateWeb(ctx, webSvcConf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("assembling websvc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DNS returns the current DNS service. It is safe for concurrent use.
|
||||
func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
m.updMu.RLock()
|
||||
defer m.updMu.RUnlock()
|
||||
|
||||
return m.dns
|
||||
}
|
||||
|
||||
// UpdateDNS implements the [websvc.ConfigManager] interface for *Manager. The
|
||||
// fields of c must not be modified after calling UpdateDNS.
|
||||
func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
m.updMu.Lock()
|
||||
defer m.updMu.Unlock()
|
||||
|
||||
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||
// error if something went wrong.
|
||||
|
||||
err = m.updateDNS(ctx, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reassembling dnssvc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
|
||||
func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
if prev := m.dns; prev != nil {
|
||||
err = prev.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down dns svc: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
svc, err := dnssvc.New(c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating dns svc: %w", err)
|
||||
}
|
||||
|
||||
m.dns = svc
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Web returns the current web service. It is safe for concurrent use.
|
||||
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
|
||||
m.updMu.RLock()
|
||||
defer m.updMu.RUnlock()
|
||||
|
||||
return m.web
|
||||
}
|
||||
|
||||
// UpdateWeb implements the [websvc.ConfigManager] interface for *Manager. The
|
||||
// fields of c must not be modified after calling UpdateWeb.
|
||||
func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||
m.updMu.Lock()
|
||||
defer m.updMu.Unlock()
|
||||
|
||||
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||
// error if something went wrong.
|
||||
|
||||
err = m.updateWeb(ctx, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reassembling websvc: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateWeb recreates the web service. m.upd is expected to be locked.
|
||||
func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||
if prev := m.web; prev != nil {
|
||||
err = prev.Shutdown(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("shutting down web svc: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.web = websvc.New(c)
|
||||
|
||||
return nil
|
||||
}
|
||||
227
internal/next/dnssvc/dnssvc.go
Normal file
227
internal/next/dnssvc/dnssvc.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Package dnssvc contains the AdGuard Home DNS service.
|
||||
//
|
||||
// TODO(a.garipov): Define, if all methods of a *Service should work with a nil
|
||||
// receiver.
|
||||
package dnssvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
// TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes
|
||||
// and replacement of module dnsproxy.
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
)
|
||||
|
||||
// Config is the AdGuard Home DNS service configuration structure.
|
||||
//
|
||||
// TODO(a.garipov): Add timeout for incoming requests.
|
||||
type Config struct {
|
||||
// Addresses are the addresses on which to serve plain DNS queries.
|
||||
Addresses []netip.AddrPort
|
||||
|
||||
// Upstreams are the DNS upstreams to use. If not set, upstreams are
|
||||
// created using data from BootstrapServers, UpstreamServers, and
|
||||
// UpstreamTimeout.
|
||||
//
|
||||
// TODO(a.garipov): Think of a better scheme. Those other three parameters
|
||||
// are here only to make Config work properly.
|
||||
Upstreams []upstream.Upstream
|
||||
|
||||
// BootstrapServers are the addresses for bootstrapping the upstream DNS
|
||||
// server addresses.
|
||||
BootstrapServers []string
|
||||
|
||||
// UpstreamServers are the upstream DNS server addresses to use.
|
||||
UpstreamServers []string
|
||||
|
||||
// UpstreamTimeout is the timeout for upstream requests.
|
||||
UpstreamTimeout time.Duration
|
||||
}
|
||||
|
||||
// Service is the AdGuard Home DNS service. A nil *Service is a valid
|
||||
// [agh.Service] that does nothing.
|
||||
type Service struct {
|
||||
// running is an atomic boolean value. Keep it the first value in the
|
||||
// struct to ensure atomic alignment. 0 means that the service is not
|
||||
// running, 1 means that it is running.
|
||||
//
|
||||
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19 or get rid of it
|
||||
// completely.
|
||||
running uint64
|
||||
|
||||
proxy *proxy.Proxy
|
||||
bootstraps []string
|
||||
upstreams []string
|
||||
upsTimeout time.Duration
|
||||
}
|
||||
|
||||
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
||||
// *Service that does nothing. The fields of c must not be modified after
|
||||
// calling New.
|
||||
func New(c *Config) (svc *Service, err error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
svc = &Service{
|
||||
bootstraps: c.BootstrapServers,
|
||||
upstreams: c.UpstreamServers,
|
||||
upsTimeout: c.UpstreamTimeout,
|
||||
}
|
||||
|
||||
var upstreams []upstream.Upstream
|
||||
if len(c.Upstreams) > 0 {
|
||||
upstreams = c.Upstreams
|
||||
} else {
|
||||
upstreams, err = addressesToUpstreams(
|
||||
c.UpstreamServers,
|
||||
c.BootstrapServers,
|
||||
c.UpstreamTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting upstreams: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
svc.proxy = &proxy.Proxy{
|
||||
Config: proxy.Config{
|
||||
UDPListenAddr: udpAddrs(c.Addresses),
|
||||
TCPListenAddr: tcpAddrs(c.Addresses),
|
||||
UpstreamConfig: &proxy.UpstreamConfig{
|
||||
Upstreams: upstreams,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = svc.proxy.Init()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proxy: %w", err)
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// addressesToUpstreams is a wrapper around [upstream.AddressToUpstream]. It
|
||||
// accepts a slice of addresses and other upstream parameters, and returns a
|
||||
// slice of upstreams.
|
||||
func addressesToUpstreams(
|
||||
upsStrs []string,
|
||||
bootstraps []string,
|
||||
timeout time.Duration,
|
||||
) (upstreams []upstream.Upstream, err error) {
|
||||
upstreams = make([]upstream.Upstream, len(upsStrs))
|
||||
for i, upsStr := range upsStrs {
|
||||
upstreams[i], err = upstream.AddressToUpstream(upsStr, &upstream.Options{
|
||||
Bootstrap: bootstraps,
|
||||
Timeout: timeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upstream at index %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return upstreams, nil
|
||||
}
|
||||
|
||||
// tcpAddrs converts []netip.AddrPort into []*net.TCPAddr.
|
||||
func tcpAddrs(addrPorts []netip.AddrPort) (tcpAddrs []*net.TCPAddr) {
|
||||
if addrPorts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tcpAddrs = make([]*net.TCPAddr, len(addrPorts))
|
||||
for i, a := range addrPorts {
|
||||
tcpAddrs[i] = net.TCPAddrFromAddrPort(a)
|
||||
}
|
||||
|
||||
return tcpAddrs
|
||||
}
|
||||
|
||||
// udpAddrs converts []netip.AddrPort into []*net.UDPAddr.
|
||||
func udpAddrs(addrPorts []netip.AddrPort) (udpAddrs []*net.UDPAddr) {
|
||||
if addrPorts == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
udpAddrs = make([]*net.UDPAddr, len(addrPorts))
|
||||
for i, a := range addrPorts {
|
||||
udpAddrs[i] = net.UDPAddrFromAddrPort(a)
|
||||
}
|
||||
|
||||
return udpAddrs
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ agh.Service = (*Service)(nil)
|
||||
|
||||
// Start implements the [agh.Service] interface for *Service. svc may be nil.
|
||||
// After Start exits, all DNS servers have tried to start, but there is no
|
||||
// guarantee that they did. Errors from the servers are written to the log.
|
||||
func (svc *Service) Start() (err error) {
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// TODO(a.garipov): [proxy.Proxy.Start] doesn't actually have any way to
|
||||
// tell when all servers are actually up, so at best this is merely an
|
||||
// assumption.
|
||||
if err != nil {
|
||||
atomic.StoreUint64(&svc.running, 0)
|
||||
} else {
|
||||
atomic.StoreUint64(&svc.running, 1)
|
||||
}
|
||||
}()
|
||||
|
||||
return svc.proxy.Start()
|
||||
}
|
||||
|
||||
// Shutdown implements the [agh.Service] interface for *Service. svc may be
|
||||
// nil.
|
||||
func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return svc.proxy.Stop()
|
||||
}
|
||||
|
||||
// Config returns the current configuration of the web service. Config must not
|
||||
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||
// addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
func (svc *Service) Config() (c *Config) {
|
||||
// TODO(a.garipov): Do we need to get the TCP addresses separately?
|
||||
|
||||
var addrs []netip.AddrPort
|
||||
if atomic.LoadUint64(&svc.running) == 1 {
|
||||
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
|
||||
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||
for i, a := range udpAddrs {
|
||||
addrs[i] = a.(*net.UDPAddr).AddrPort()
|
||||
}
|
||||
} else {
|
||||
conf := svc.proxy.Config
|
||||
udpAddrs := conf.UDPListenAddr
|
||||
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||
for i, a := range udpAddrs {
|
||||
addrs[i] = a.AddrPort()
|
||||
}
|
||||
}
|
||||
|
||||
c = &Config{
|
||||
Addresses: addrs,
|
||||
BootstrapServers: svc.bootstraps,
|
||||
UpstreamServers: svc.upstreams,
|
||||
UpstreamTimeout: svc.upsTimeout,
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
96
internal/next/dnssvc/dnssvc_test.go
Normal file
96
internal/next/dnssvc/dnssvc_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package dnssvc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testutil.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 100 * time.Millisecond
|
||||
|
||||
func TestService(t *testing.T) {
|
||||
const (
|
||||
bootstrapAddr = "bootstrap.example"
|
||||
upstreamAddr = "upstream.example"
|
||||
|
||||
closeErr errors.Error = "closing failed"
|
||||
)
|
||||
|
||||
ups := &aghtest.UpstreamMock{
|
||||
OnAddress: func() (addr string) {
|
||||
return upstreamAddr
|
||||
},
|
||||
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
resp = (&dns.Msg{}).SetReply(req)
|
||||
|
||||
return resp, nil
|
||||
},
|
||||
OnClose: func() (err error) {
|
||||
return closeErr
|
||||
},
|
||||
}
|
||||
|
||||
c := &dnssvc.Config{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||
Upstreams: []upstream.Upstream{ups},
|
||||
BootstrapServers: []string{bootstrapAddr},
|
||||
UpstreamServers: []string{upstreamAddr},
|
||||
UpstreamTimeout: testTimeout,
|
||||
}
|
||||
|
||||
svc, err := dnssvc.New(c)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = svc.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
gotConf := svc.Config()
|
||||
require.NotNil(t, gotConf)
|
||||
require.Len(t, gotConf.Addresses, 1)
|
||||
|
||||
addr := gotConf.Addresses[0]
|
||||
|
||||
t.Run("dns", func(t *testing.T) {
|
||||
req := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: dns.Id(),
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []dns.Question{{
|
||||
Name: "example.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
}},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
cli := &dns.Client{}
|
||||
resp, _, excErr := cli.ExchangeContext(ctx, req, addr.String())
|
||||
require.NoError(t, excErr)
|
||||
|
||||
assert.NotNil(t, resp)
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
defer cancel()
|
||||
|
||||
err = svc.Shutdown(ctx)
|
||||
require.ErrorIs(t, err, closeErr)
|
||||
}
|
||||
84
internal/next/websvc/dns.go
Normal file
84
internal/next/websvc/dns.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
)
|
||||
|
||||
// DNS Settings Handlers
|
||||
|
||||
// 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 JSONDuration `json:"upstream_timeout"`
|
||||
}
|
||||
|
||||
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
|
||||
// DnsSettings object in the OpenAPI specification.
|
||||
type HTTPAPIDNSSettings 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 JSONDuration `json:"upstream_timeout"`
|
||||
}
|
||||
|
||||
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns 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 {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newConf := &dnssvc.Config{
|
||||
Addresses: req.Addresses,
|
||||
BootstrapServers: req.BootstrapServers,
|
||||
UpstreamServers: req.UpstreamServers,
|
||||
UpstreamTimeout: time.Duration(req.UpstreamTimeout),
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newSvc := svc.confMgr.DNS()
|
||||
err = newSvc.Start()
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
|
||||
Addresses: newConf.Addresses,
|
||||
BootstrapServers: newConf.BootstrapServers,
|
||||
UpstreamServers: newConf.UpstreamServers,
|
||||
UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
|
||||
})
|
||||
}
|
||||
69
internal/next/websvc/dns_test.go
Normal file
69
internal/next/websvc/dns_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:53")},
|
||||
BootstrapServers: []string{"1.0.0.1"},
|
||||
UpstreamServers: []string{"1.1.1.1"},
|
||||
UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
|
||||
var numStarted uint64
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
||||
OnStart: func() (err error) {
|
||||
atomic.AddUint64(&numStarted, 1)
|
||||
|
||||
return nil
|
||||
},
|
||||
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") },
|
||||
OnConfig: func() (c *dnssvc.Config) { panic("not implemented") },
|
||||
}
|
||||
}
|
||||
confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SettingsDNS,
|
||||
}
|
||||
|
||||
req := jobj{
|
||||
"addresses": wantDNS.Addresses,
|
||||
"bootstrap_servers": wantDNS.BootstrapServers,
|
||||
"upstream_servers": wantDNS.UpstreamServers,
|
||||
"upstream_timeout": wantDNS.UpstreamTimeout,
|
||||
}
|
||||
|
||||
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||
resp := &websvc.HTTPAPIDNSSettings{}
|
||||
err := json.Unmarshal(respBody, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, uint64(1), numStarted)
|
||||
assert.Equal(t, wantDNS, resp)
|
||||
assert.Equal(t, wantDNS, resp)
|
||||
}
|
||||
110
internal/next/websvc/http.go
Normal file
110
internal/next/websvc/http.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// HTTP Settings Handlers
|
||||
|
||||
// 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"`
|
||||
Timeout JSONDuration `json:"timeout"`
|
||||
}
|
||||
|
||||
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
|
||||
// HttpSettings object in the OpenAPI specification.
|
||||
type HTTPAPIHTTPSettings struct {
|
||||
// TODO(a.garipov): Add more as we go.
|
||||
|
||||
Addresses []netip.AddrPort `json:"addresses"`
|
||||
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||
Timeout JSONDuration `json:"timeout"`
|
||||
ForceHTTPS bool `json:"force_https"`
|
||||
}
|
||||
|
||||
// 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{}
|
||||
|
||||
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
newConf := &Config{
|
||||
ConfigManager: svc.confMgr,
|
||||
TLS: svc.tls,
|
||||
Addresses: req.Addresses,
|
||||
SecureAddresses: req.SecureAddresses,
|
||||
Timeout: time.Duration(req.Timeout),
|
||||
ForceHTTPS: svc.forceHTTPS,
|
||||
}
|
||||
|
||||
writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
|
||||
Addresses: newConf.Addresses,
|
||||
SecureAddresses: newConf.SecureAddresses,
|
||||
Timeout: JSONDuration(newConf.Timeout),
|
||||
ForceHTTPS: newConf.ForceHTTPS,
|
||||
})
|
||||
|
||||
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 {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Consider better ways to do this.
|
||||
const maxUpdDur = 10 * time.Second
|
||||
updStart := time.Now()
|
||||
var newSvc agh.ServiceWithConfig[*Config]
|
||||
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||
if time.Since(updStart) >= maxUpdDur {
|
||||
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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 with error: %s", updErr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
63
internal/next/websvc/http_test.go
Normal file
63
internal/next/websvc/http_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
|
||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
|
||||
Timeout: websvc.JSONDuration(10 * time.Second),
|
||||
ForceHTTPS: false,
|
||||
}
|
||||
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||
return websvc.New(&websvc.Config{
|
||||
TLS: &tls.Config{
|
||||
Certificates: []tls.Certificate{{}},
|
||||
},
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||
Timeout: 5 * time.Second,
|
||||
ForceHTTPS: true,
|
||||
})
|
||||
}
|
||||
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SettingsHTTP,
|
||||
}
|
||||
|
||||
req := jobj{
|
||||
"addresses": wantWeb.Addresses,
|
||||
"secure_addresses": wantWeb.SecureAddresses,
|
||||
"timeout": wantWeb.Timeout,
|
||||
"force_https": wantWeb.ForceHTTPS,
|
||||
}
|
||||
|
||||
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||
resp := &websvc.HTTPAPIHTTPSettings{}
|
||||
err := json.Unmarshal(respBody, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, wantWeb, resp)
|
||||
}
|
||||
143
internal/next/websvc/json.go
Normal file
143
internal/next/websvc/json.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// JSON Utilities
|
||||
|
||||
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||
|
||||
// JSONDuration is a time.Duration that can be decoded from JSON and encoded
|
||||
// into JSON according to our API conventions.
|
||||
type JSONDuration time.Duration
|
||||
|
||||
// type check
|
||||
var _ json.Marshaler = JSONDuration(0)
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for JSONDuration. err is
|
||||
// always nil.
|
||||
func (d JSONDuration) MarshalJSON() (b []byte, err error) {
|
||||
msec := float64(time.Duration(d)) / nsecPerMsec
|
||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ json.Unmarshaler = (*JSONDuration)(nil)
|
||||
|
||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.
|
||||
func (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {
|
||||
if d == nil {
|
||||
return fmt.Errorf("json duration is nil")
|
||||
}
|
||||
|
||||
msec, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing json time: %w", err)
|
||||
}
|
||||
|
||||
*d = JSONDuration(int64(msec * nsecPerMsec))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON
|
||||
// according to our API conventions.
|
||||
type JSONTime time.Time
|
||||
|
||||
// type check
|
||||
var _ json.Marshaler = JSONTime{}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface for JSONTime. err is
|
||||
// always nil.
|
||||
func (t JSONTime) MarshalJSON() (b []byte, err error) {
|
||||
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ json.Unmarshaler = (*JSONTime)(nil)
|
||||
|
||||
// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.
|
||||
func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
|
||||
if t == nil {
|
||||
return fmt.Errorf("json time is nil")
|
||||
}
|
||||
|
||||
msec, err := strconv.ParseFloat(string(b), 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing json time: %w", err)
|
||||
}
|
||||
|
||||
*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
|
||||
// and logs any errors it encounters. r is used to get additional information
|
||||
// from the request.
|
||||
func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
|
||||
writeJSONResponse(w, r, v, http.StatusOK)
|
||||
}
|
||||
|
||||
// writeJSONResponse writes headers with code, encodes v into w, and logs any
|
||||
// errors it encounters. r is used to get additional information from the
|
||||
// request.
|
||||
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
||||
// TODO(a.garipov): Put some of these to a middleware.
|
||||
h := w.Header()
|
||||
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON)
|
||||
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent())
|
||||
|
||||
w.WriteHeader(code)
|
||||
|
||||
err := json.NewEncoder(w).Encode(v)
|
||||
if err != nil {
|
||||
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
||||
// definition in the OpenAPI specification.
|
||||
type ErrorCode string
|
||||
|
||||
// ErrorCode constants.
|
||||
//
|
||||
// TODO(a.garipov): Expand and document codes.
|
||||
const (
|
||||
// ErrorCodeTMP000 is the temporary error code used for all errors.
|
||||
ErrorCodeTMP000 = ""
|
||||
)
|
||||
|
||||
// HTTPAPIErrorResp is the error response as used by the HTTP API. See the
|
||||
// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI
|
||||
// specification.
|
||||
type HTTPAPIErrorResp struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
|
||||
// errors it encounters. r is used to get additional information from the
|
||||
// request.
|
||||
func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
|
||||
|
||||
writeJSONResponse(w, r, &HTTPAPIErrorResp{
|
||||
Code: ErrorCodeTMP000,
|
||||
Msg: err.Error(),
|
||||
}, http.StatusUnprocessableEntity)
|
||||
}
|
||||
114
internal/next/websvc/json_test.go
Normal file
114
internal/next/websvc/json_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testJSONTime is the JSON time for tests.
|
||||
var testJSONTime = websvc.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 websvc.JSONTime
|
||||
want []byte
|
||||
}{{
|
||||
name: "unix_zero",
|
||||
wantErrMsg: "",
|
||||
in: websvc.JSONTime(time.Unix(0, 0)),
|
||||
want: []byte("0"),
|
||||
}, {
|
||||
name: "empty",
|
||||
wantErrMsg: "",
|
||||
in: websvc.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 websvc.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 websvc.JSONTime
|
||||
data []byte
|
||||
}{{
|
||||
name: "time",
|
||||
wantErrMsg: "",
|
||||
want: testJSONTime,
|
||||
data: []byte(testJSONTimeStr),
|
||||
}, {
|
||||
name: "bad",
|
||||
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
||||
`invalid syntax`,
|
||||
want: websvc.JSONTime{},
|
||||
data: []byte(`{}`),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var got websvc.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 := (*websvc.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 websvc.JSONTime
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, want, got.A)
|
||||
})
|
||||
}
|
||||
16
internal/next/websvc/middleware.go
Normal file
16
internal/next/websvc/middleware.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package websvc
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Middlewares
|
||||
|
||||
// jsonMw sets the content type of the response to application/json.
|
||||
func jsonMw(h http.Handler) (wrapped http.HandlerFunc) {
|
||||
f := func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(f)
|
||||
}
|
||||
11
internal/next/websvc/path.go
Normal file
11
internal/next/websvc/path.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package websvc
|
||||
|
||||
// Path constants
|
||||
const (
|
||||
PathHealthCheck = "/health-check"
|
||||
|
||||
PathV1SettingsAll = "/api/v1/settings/all"
|
||||
PathV1SettingsDNS = "/api/v1/settings/dns"
|
||||
PathV1SettingsHTTP = "/api/v1/settings/http"
|
||||
PathV1SystemInfo = "/api/v1/system/info"
|
||||
)
|
||||
42
internal/next/websvc/settings.go
Normal file
42
internal/next/websvc/settings.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// All Settings Handlers
|
||||
|
||||
// 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 *HTTPAPIDNSSettings `json:"dns"`
|
||||
HTTP *HTTPAPIHTTPSettings `json:"http"`
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
webSvc := svc.confMgr.Web()
|
||||
httpConf := webSvc.Config()
|
||||
|
||||
// TODO(a.garipov): Add all currently supported parameters.
|
||||
writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
|
||||
DNS: &HTTPAPIDNSSettings{
|
||||
Addresses: dnsConf.Addresses,
|
||||
BootstrapServers: dnsConf.BootstrapServers,
|
||||
UpstreamServers: dnsConf.UpstreamServers,
|
||||
UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
|
||||
},
|
||||
HTTP: &HTTPAPIHTTPSettings{
|
||||
Addresses: httpConf.Addresses,
|
||||
SecureAddresses: httpConf.SecureAddresses,
|
||||
Timeout: JSONDuration(httpConf.Timeout),
|
||||
ForceHTTPS: httpConf.ForceHTTPS,
|
||||
},
|
||||
})
|
||||
}
|
||||
75
internal/next/websvc/settings_test.go
Normal file
75
internal/next/websvc/settings_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||
// TODO(a.garipov): Add all currently supported parameters.
|
||||
|
||||
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
|
||||
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
|
||||
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
|
||||
UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
|
||||
}
|
||||
|
||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||
Timeout: websvc.JSONDuration(5 * time.Second),
|
||||
ForceHTTPS: true,
|
||||
}
|
||||
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
c, err := dnssvc.New(&dnssvc.Config{
|
||||
Addresses: wantDNS.Addresses,
|
||||
UpstreamServers: wantDNS.UpstreamServers,
|
||||
BootstrapServers: wantDNS.BootstrapServers,
|
||||
UpstreamTimeout: time.Duration(wantDNS.UpstreamTimeout),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||
return websvc.New(&websvc.Config{
|
||||
TLS: &tls.Config{
|
||||
Certificates: []tls.Certificate{{}},
|
||||
},
|
||||
Addresses: wantWeb.Addresses,
|
||||
SecureAddresses: wantWeb.SecureAddresses,
|
||||
Timeout: time.Duration(wantWeb.Timeout),
|
||||
ForceHTTPS: true,
|
||||
})
|
||||
}
|
||||
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SettingsAll,
|
||||
}
|
||||
|
||||
body := httpGet(t, u, http.StatusOK)
|
||||
resp := &websvc.RespGetV1SettingsAll{}
|
||||
err := json.Unmarshal(body, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, wantDNS, resp.DNS)
|
||||
assert.Equal(t, wantWeb, resp.HTTP)
|
||||
}
|
||||
35
internal/next/websvc/system.go
Normal file
35
internal/next/websvc/system.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||
)
|
||||
|
||||
// System Handlers
|
||||
|
||||
// RespGetV1SystemInfo describes the response of the GET /api/v1/system/info
|
||||
// HTTP API.
|
||||
type RespGetV1SystemInfo struct {
|
||||
Arch string `json:"arch"`
|
||||
Channel string `json:"channel"`
|
||||
OS string `json:"os"`
|
||||
NewVersion string `json:"new_version,omitempty"`
|
||||
Start JSONTime `json:"start"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
||||
// API.
|
||||
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSONOKResponse(w, r, &RespGetV1SystemInfo{
|
||||
Arch: runtime.GOARCH,
|
||||
Channel: version.Channel(),
|
||||
OS: runtime.GOOS,
|
||||
// TODO(a.garipov): Fill this when we have an updater.
|
||||
NewVersion: "",
|
||||
Start: JSONTime(svc.start),
|
||||
Version: version.Version(),
|
||||
})
|
||||
}
|
||||
37
internal/next/websvc/system_test.go
Normal file
37
internal/next/websvc/system_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestService_handleGetV1SystemInfo(t *testing.T) {
|
||||
confMgr := newConfigManager()
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathV1SystemInfo,
|
||||
}
|
||||
|
||||
body := httpGet(t, u, http.StatusOK)
|
||||
resp := &websvc.RespGetV1SystemInfo{}
|
||||
err := json.Unmarshal(body, resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
// TODO(a.garipov): Consider making version.Channel and version.Version
|
||||
// testable and test these better.
|
||||
assert.NotEmpty(t, resp.Channel)
|
||||
|
||||
assert.Equal(t, resp.Arch, runtime.GOARCH)
|
||||
assert.Equal(t, resp.OS, runtime.GOOS)
|
||||
assert.Equal(t, testStart, time.Time(resp.Start))
|
||||
}
|
||||
31
internal/next/websvc/waitlistener.go
Normal file
31
internal/next/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()
|
||||
}
|
||||
46
internal/next/websvc/waitlistener_internal_test.go
Normal file
46
internal/next/websvc/waitlistener_internal_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
done := make(chan struct{})
|
||||
go aghchan.MustReceive(done, testTimeout)
|
||||
|
||||
go func() {
|
||||
var wrapper net.Listener = &waitListener{
|
||||
Listener: l,
|
||||
firstAcceptWG: wg,
|
||||
}
|
||||
|
||||
_, _ = wrapper.Accept()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(done)
|
||||
|
||||
assert.Equal(t, uint32(1), atomic.LoadUint32(&numAcceptCalls))
|
||||
}
|
||||
305
internal/next/websvc/websvc.go
Normal file
305
internal/next/websvc/websvc.go
Normal file
@@ -0,0 +1,305 @@
|
||||
// Package websvc contains the AdGuard Home HTTP API service.
|
||||
//
|
||||
// NOTE: Packages other than cmd must not import this package, as it imports
|
||||
// most other packages.
|
||||
//
|
||||
// TODO(a.garipov): Add tests.
|
||||
package websvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
httptreemux "github.com/dimfeld/httptreemux/v5"
|
||||
)
|
||||
|
||||
// ConfigManager is the configuration manager interface.
|
||||
type ConfigManager interface {
|
||||
DNS() (svc agh.ServiceWithConfig[*dnssvc.Config])
|
||||
Web() (svc agh.ServiceWithConfig[*Config])
|
||||
|
||||
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
|
||||
|
||||
// SecureAddresses are the addresses on which to serve the HTTPS API. If
|
||||
// SecureAddresses is not empty, TLS must not be nil.
|
||||
SecureAddresses []netip.AddrPort
|
||||
|
||||
// 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 {
|
||||
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. The fields of c must not be modified after
|
||||
// calling New.
|
||||
func New(c *Config) (svc *Service) {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
svc = &Service{
|
||||
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: plain http: "+addr, log.ERROR)
|
||||
svc.servers = append(svc.servers, &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ErrorLog: errLog,
|
||||
ReadTimeout: c.Timeout,
|
||||
WriteTimeout: c.Timeout,
|
||||
IdleTimeout: c.Timeout,
|
||||
ReadHeaderTimeout: c.Timeout,
|
||||
})
|
||||
}
|
||||
|
||||
for _, a := range c.SecureAddresses {
|
||||
addr := a.String()
|
||||
errLog := log.StdLog("websvc: https: "+addr, log.ERROR)
|
||||
svc.servers = append(svc.servers, &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
TLSConfig: c.TLS,
|
||||
ErrorLog: errLog,
|
||||
ReadTimeout: c.Timeout,
|
||||
WriteTimeout: c.Timeout,
|
||||
IdleTimeout: c.Timeout,
|
||||
ReadHeaderTimeout: c.Timeout,
|
||||
})
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
// newMux returns a new HTTP request multiplexor for the AdGuard Home web
|
||||
// service.
|
||||
func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||
mux = httptreemux.NewContextMux()
|
||||
|
||||
routes := []struct {
|
||||
handler http.HandlerFunc
|
||||
method string
|
||||
path string
|
||||
isJSON bool
|
||||
}{{
|
||||
handler: svc.handleGetHealthCheck,
|
||||
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,
|
||||
path: PathV1SystemInfo,
|
||||
isJSON: true,
|
||||
}}
|
||||
|
||||
for _, r := range routes {
|
||||
if r.isJSON {
|
||||
mux.Handle(r.method, r.path, jsonMw(r.handler))
|
||||
} else {
|
||||
mux.Handle(r.method, r.path, r.handler)
|
||||
}
|
||||
}
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
// addrs returns all addresses on which this server serves the HTTP API. addrs
|
||||
// must not be called simultaneously with Start. If svc was initialized with
|
||||
// ":0" addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
|
||||
for _, srv := range svc.servers {
|
||||
addrPort, 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, addrPort)
|
||||
} else {
|
||||
secureAddrs = append(secureAddrs, addrPort)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return addrs, secureAddrs
|
||||
}
|
||||
|
||||
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
|
||||
func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, "OK")
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ agh.Service = (*Service)(nil)
|
||||
|
||||
// Start implements the [agh.Service] interface for *Service. svc may be nil.
|
||||
// After Start exits, all HTTP servers have tried to start, possibly failing and
|
||||
// writing error messages to the log.
|
||||
func (svc *Service) Start() (err error) {
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(len(svc.servers))
|
||||
for _, srv := range svc.servers {
|
||||
go serve(srv, wg)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// serve starts and runs srv and writes all errors into its log.
|
||||
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 {
|
||||
srv.ErrorLog.Printf("starting srv %s: binding: %s", addr, err)
|
||||
}
|
||||
|
||||
// Update the server's address in case the address had the port zero, which
|
||||
// would mean that a random available port was automatically chosen.
|
||||
srv.Addr = l.Addr().String()
|
||||
|
||||
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) {
|
||||
srv.ErrorLog.Printf("starting srv %s: %s", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown implements the [agh.Service] interface for *Service. svc may be
|
||||
// nil.
|
||||
func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||
if svc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, srv := range svc.servers {
|
||||
serr := srv.Shutdown(ctx)
|
||||
if serr != nil {
|
||||
errs = append(errs, fmt.Errorf("shutting down srv %s: %w", srv.Addr, serr))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.List("shutting down", errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config returns the current configuration of the web service. Config must not
|
||||
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||
// addresses, addrs will not return the actual bound ports until Start is
|
||||
// finished.
|
||||
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
|
||||
}
|
||||
6
internal/next/websvc/websvc_internal_test.go
Normal file
6
internal/next/websvc/websvc_internal_test.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package websvc
|
||||
|
||||
import "time"
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
187
internal/next/websvc/websvc_test.go
Normal file
187
internal/next/websvc/websvc_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package websvc_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||
"github.com/AdguardTeam/golibs/testutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testutil.DiscardLogOutput(m)
|
||||
}
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
|
||||
// testStart is the server start value for tests.
|
||||
var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// type check
|
||||
var _ websvc.ConfigManager = (*configManager)(nil)
|
||||
|
||||
// configManager is a [websvc.ConfigManager] for tests.
|
||||
type configManager struct {
|
||||
onDNS func() (svc agh.ServiceWithConfig[*dnssvc.Config])
|
||||
onWeb func() (svc agh.ServiceWithConfig[*websvc.Config])
|
||||
|
||||
onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)
|
||||
onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)
|
||||
}
|
||||
|
||||
// DNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) DNS() (svc agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
return m.onDNS()
|
||||
}
|
||||
|
||||
// Web implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) Web() (svc agh.ServiceWithConfig[*websvc.Config]) {
|
||||
return m.onWeb()
|
||||
}
|
||||
|
||||
// UpdateDNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||
return m.onUpdateDNS(ctx, c)
|
||||
}
|
||||
|
||||
// UpdateWeb implements the [websvc.ConfigManager] interface for *configManager.
|
||||
func (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||
return m.onUpdateWeb(ctx, c)
|
||||
}
|
||||
|
||||
// newConfigManager returns a *configManager all methods of which panic.
|
||||
func newConfigManager() (m *configManager) {
|
||||
return &configManager{
|
||||
onDNS: func() (svc agh.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") },
|
||||
onWeb: func() (svc agh.ServiceWithConfig[*websvc.Config]) { panic("not implemented") },
|
||||
onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) {
|
||||
panic("not implemented")
|
||||
},
|
||||
onUpdateWeb: func(_ context.Context, _ *websvc.Config) (err error) {
|
||||
panic("not implemented")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// newTestServer creates and starts a new web service instance as well as its
|
||||
// sole address. It also registers a cleanup procedure, which shuts the
|
||||
// instance down.
|
||||
//
|
||||
// TODO(a.garipov): Use svc or remove it.
|
||||
func newTestServer(
|
||||
t testing.TB,
|
||||
confMgr websvc.ConfigManager,
|
||||
) (svc *websvc.Service, addr netip.AddrPort) {
|
||||
t.Helper()
|
||||
|
||||
c := &websvc.Config{
|
||||
ConfigManager: confMgr,
|
||||
TLS: nil,
|
||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||
SecureAddresses: nil,
|
||||
Timeout: testTimeout,
|
||||
Start: testStart,
|
||||
ForceHTTPS: false,
|
||||
}
|
||||
|
||||
svc = websvc.New(c)
|
||||
|
||||
err := svc.Start()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
err = svc.Shutdown(ctx)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
c = svc.Config()
|
||||
require.NotNil(t, c)
|
||||
require.Len(t, c.Addresses, 1)
|
||||
|
||||
return svc, c.Addresses[0]
|
||||
}
|
||||
|
||||
// jobj is a utility alias for JSON objects.
|
||||
type jobj map[string]any
|
||||
|
||||
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
||||
// the response as well as checks that the status code is correct.
|
||||
//
|
||||
// TODO(a.garipov): Add helpers for other methods.
|
||||
func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) {
|
||||
t.Helper()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoErrorf(t, err, "creating req")
|
||||
|
||||
httpCli := &http.Client{
|
||||
Timeout: testTimeout,
|
||||
}
|
||||
resp, err := httpCli.Do(req)
|
||||
require.NoErrorf(t, err, "performing req")
|
||||
require.Equal(t, wantCode, resp.StatusCode)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoErrorf(t, err, "reading body")
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
// httpPatch is a helper that performs an HTTP PATCH request with JSON-encoded
|
||||
// reqBody as the request body and returns the body of the response as well as
|
||||
// checks that the status code is correct.
|
||||
//
|
||||
// TODO(a.garipov): Add helpers for other methods.
|
||||
func httpPatch(t testing.TB, u *url.URL, reqBody any, wantCode int) (body []byte) {
|
||||
t.Helper()
|
||||
|
||||
b, err := json.Marshal(reqBody)
|
||||
require.NoErrorf(t, err, "marshaling reqBody")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPatch, u.String(), bytes.NewReader(b))
|
||||
require.NoErrorf(t, err, "creating req")
|
||||
|
||||
httpCli := &http.Client{
|
||||
Timeout: testTimeout,
|
||||
}
|
||||
resp, err := httpCli.Do(req)
|
||||
require.NoErrorf(t, err, "performing req")
|
||||
require.Equal(t, wantCode, resp.StatusCode)
|
||||
|
||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
require.NoErrorf(t, err, "reading body")
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
func TestService_Start_getHealthCheck(t *testing.T) {
|
||||
confMgr := newConfigManager()
|
||||
_, addr := newTestServer(t, confMgr)
|
||||
u := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: addr.String(),
|
||||
Path: websvc.PathHealthCheck,
|
||||
}
|
||||
|
||||
body := httpGet(t, u, http.StatusOK)
|
||||
|
||||
assert.Equal(t, []byte("OK"), body)
|
||||
}
|
||||
21
main_next.go
Normal file
21
main_next.go
Normal file
@@ -0,0 +1,21 @@
|
||||
//go:build next
|
||||
// +build next
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/cmd"
|
||||
)
|
||||
|
||||
// Embed the prebuilt client here since we strive to keep .go files inside the
|
||||
// internal directory and the embed package is unable to embed files located
|
||||
// outside of the same or underlying directory.
|
||||
|
||||
//go:embed build2
|
||||
var clientBuildFS embed.FS
|
||||
|
||||
func main() {
|
||||
cmd.Main(clientBuildFS)
|
||||
}
|
||||
5041
openapi/v1.yaml
Normal file
5041
openapi/v1.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -123,4 +123,14 @@ CGO_ENABLED="$cgo_enabled"
|
||||
GO111MODULE='on'
|
||||
export CGO_ENABLED GO111MODULE
|
||||
|
||||
"$go" build --ldflags "$ldflags" "$race_flags" --trimpath "$o_flags" "$v_flags" "$x_flags"
|
||||
# Build the new binary if requested.
|
||||
if [ "${NEXTAPI:-0}" -eq '0' ]
|
||||
then
|
||||
tags_flags='--tags='
|
||||
else
|
||||
tags_flags='--tags=next'
|
||||
fi
|
||||
readonly tags_flags
|
||||
|
||||
"$go" build --ldflags "$ldflags" "$race_flags" "$tags_flags" --trimpath "$o_flags" "$v_flags"\
|
||||
"$x_flags"
|
||||
|
||||
@@ -136,6 +136,7 @@ underscores() {
|
||||
-e '_freebsd.go'\
|
||||
-e '_linux.go'\
|
||||
-e '_little.go'\
|
||||
-e '_next.go'\
|
||||
-e '_openbsd.go'\
|
||||
-e '_others.go'\
|
||||
-e '_test.go'\
|
||||
@@ -227,7 +228,7 @@ gocyclo --over 13 ./internal/dhcpd ./internal/filtering/ ./internal/home/
|
||||
# Apply stricter standards to new or somewhat refactored code.
|
||||
gocyclo --over 10 ./internal/aghio/ ./internal/aghnet/ ./internal/aghos/\
|
||||
./internal/aghtest/ ./internal/dnsforward/ ./internal/stats/\
|
||||
./internal/tools/ ./internal/updater/ ./internal/version/\
|
||||
./internal/tools/ ./internal/updater/ ./internal/next/ ./internal/version/\
|
||||
./scripts/vetted-filters/ ./main.go
|
||||
|
||||
ineffassign ./...
|
||||
|
||||
Reference in New Issue
Block a user