Compare commits

...

13 Commits

Author SHA1 Message Date
bakito
1986e87518 correct log fields 2021-04-03 21:04:50 +02:00
bakito
cf0a381f80 log fields 2021-04-03 20:38:41 +02:00
bakito
5e591e04c3 simplifiy
cleanup code
skip arm for darwin and windows
2021-04-03 20:19:34 +02:00
bakito
fcf25538c0 make log available in api 2021-04-03 18:41:28 +02:00
bakito
aa95031136 sync stats and query log config 2021-04-03 17:52:08 +02:00
bakito
6fb2dd12a8 move log attribute to message 2021-03-31 02:34:26 +02:00
bakito
8db9c98644 synch toggles 2021-03-31 02:28:18 +02:00
Marc Brugger
f8578e85b2 add API server to trigger sync remotely 2021-03-29 08:43:18 +02:00
bakito
5dafdf5472 change output paths 2021-03-28 20:28:58 +02:00
bakito
23d5f30eb8 add systemd script 2021-03-28 20:25:25 +02:00
bakito
44609a93e3 correct logger name 2021-03-28 20:00:22 +02:00
bakito
70e60bb7d0 use external semver 2021-03-28 19:31:11 +02:00
bakito
aacffb1518 document config file 2021-03-28 17:04:41 +02:00
16 changed files with 626 additions and 132 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
.idea
coverage.out
dist
adguardhome-sync
main
.adguardhome-sync.yaml

View File

@@ -1,29 +1,38 @@
# This is an example goreleaser.yaml file with some sane defaults.
# Make sure to check the documentation at http://goreleaser.com
builds:
- env:
- CGO_ENABLED=0
ldflags:
- -s -w -X github.com/bakito/adguardhome-sync/version.Version={{.Version}}
goos:
- linux
- windows
- darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
hooks:
post: upx {{ .Path }}
- env:
- CGO_ENABLED=0
ldflags:
- -s -w -X github.com/bakito/adguardhome-sync/version.Version={{.Version}}
goos:
- linux
- windows
- darwin
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 5
- 6
- 7
ignore:
- goos: darwin
goarch: arm
- goos: darwin
goarch: arm64
- goos: windows
goarch: arm
- goos: windows
goarch: arm64
hooks:
post: upx {{ .Path }}
archives:
- replacements:
386: i386
amd64: x86_64
- replacements:
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
@@ -32,5 +41,5 @@ changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
- '^docs:'
- '^test:'

View File

@@ -20,10 +20,15 @@ test: tidy fmt vet
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
release:
@version=$$(go run version/semver/main.go); \
release: semver
@version=$$(semver); \
git tag -s $$version -m"Release $$version"
goreleaser --rm-dist
test-release:
goreleaser --skip-publish --snapshot --rm-dist
goreleaser --skip-publish --snapshot --rm-dist
semver:
ifeq (, $(shell which semver))
$(shell go get -u github.com/bakito/semver)
endif

View File

@@ -6,6 +6,7 @@ Synchronize [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) config to
## Current sync features
- General Settings
- Filters
- Rewrites
- Services
@@ -33,4 +34,47 @@ adguardhome-sync run
# run as daemon
adguardhome-sync run --cron "*/10 * * * *"
```
```
### Config file
location: $HOME/.adguardhome-sync.yaml
```yaml
# cron expression to run in daemon mode. (default; "" = runs only once)
cron: "*/10 * * * *"
origin:
# url of the origin instance
url: https://192.168.1.2:3000
# apiPath: define an api path if other than "/control"
# insecureSkipVerify: true # disable tls check
username: username
password: password
# replica instance (optional, if only one)
replica:
# url of the replica instance
url: http://192.168.1.3
username: username
password: password
# replicas instances (optional, if more than one)
replicas:
# url of the replica instance
- url: http://192.168.1.3
username: username
password: password
- url: http://192.168.1.4
username: username
password: password
# Configure the sync API server, disabled if api port is 0
api:
# Port, default 8080
port: 8080
# if username and password are defined, basic auth is applied to the sync API
username: username
password: password
```

View File

@@ -16,6 +16,10 @@ import (
const (
configCron = "cron"
configAPIPort = "api.port"
configAPIUsername = "api.username"
configAPIPassword = "api.password"
configOriginURL = "origin.url"
configOriginAPIPath = "origin.apiPath"
configOriginUsername = "origin.username"

View File

@@ -15,8 +15,7 @@ var doCmd = &cobra.Command{
Short: "Start a synchronisation from origin to replica",
Long: `Synchronizes the configuration form an origin instance to a replica`,
Run: func(cmd *cobra.Command, args []string) {
logger = log.GetLogger("root")
logger = log.GetLogger("run")
cfg := &types.Config{}
if err := viper.Unmarshal(cfg); err != nil {
logger.Error(err)
@@ -31,6 +30,12 @@ func init() {
rootCmd.AddCommand(doCmd)
doCmd.PersistentFlags().String("cron", "", "The cron expression to run in daemon mode")
_ = viper.BindPFlag(configCron, doCmd.PersistentFlags().Lookup("cron"))
doCmd.PersistentFlags().Int("api-port", 8080, "Sync API Port, the API endpoint will be started to enable remote triggering; if 0 port API is disabled.")
_ = viper.BindPFlag(configAPIPort, doCmd.PersistentFlags().Lookup("api-port"))
doCmd.PersistentFlags().String("api-username", "", "Sync API username")
_ = viper.BindPFlag(configAPIUsername, doCmd.PersistentFlags().Lookup("api-username"))
doCmd.PersistentFlags().String("api-password", "", "Sync API password")
_ = viper.BindPFlag(configAPIPassword, doCmd.PersistentFlags().Lookup("api-password"))
doCmd.PersistentFlags().String("origin-url", "", "Origin instance url")
_ = viper.BindPFlag(configOriginURL, doCmd.PersistentFlags().Lookup("origin-url"))

1
go.mod
View File

@@ -3,7 +3,6 @@ module github.com/bakito/adguardhome-sync
go 1.16
require (
github.com/coreos/go-semver v0.3.0
github.com/go-resty/resty/v2 v2.5.0
github.com/mitchellh/go-homedir v1.1.0
github.com/robfig/cron/v3 v3.0.1

1
go.sum
View File

@@ -28,7 +28,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=

View File

@@ -16,6 +16,7 @@ var (
l = log.GetLogger("client")
)
// New create a new client
func New(config types.AdGuardInstance) (Client, error) {
var apiURL string
@@ -46,10 +47,12 @@ func New(config types.AdGuardInstance) (Client, error) {
}, nil
}
// Client AdGuard Home API client interface
type Client interface {
Host() string
Status() (*types.Status, error)
ToggleProtection(enable bool) error
RewriteList() (*types.RewriteEntries, error)
AddRewriteEntries(e ...types.RewriteEntry) error
DeleteRewriteEntries(e ...types.RewriteEntry) error
@@ -61,8 +64,11 @@ type Client interface {
RefreshFilters(whitelist bool) error
SetCustomRules(rules types.UserRules) error
ToggleSaveBrowsing(enable bool) error
SafeBrowsing() (bool, error)
ToggleSafeBrowsing(enable bool) error
Parental() (bool, error)
ToggleParental(enable bool) error
SafeSearch() (bool, error)
ToggleSafeSearch(enable bool) error
Services() (*types.Services, error)
@@ -72,6 +78,11 @@ type Client interface {
AddClients(client ...types.Client) error
UpdateClients(client ...types.Client) error
DeleteClients(client ...types.Client) error
QueryLogConfig() (*types.QueryLogConfig, error)
SetQueryLogConfig(enabled bool, interval int, anonymizeClientIP bool) error
StatsConfig() (*types.IntervalConfig, error)
SetStatsConfig(interval int) error
}
type client struct {
@@ -118,20 +129,38 @@ func (cl *client) DeleteRewriteEntries(entries ...types.RewriteEntry) error {
return nil
}
func (cl *client) ToggleSaveBrowsing(enable bool) error {
return cl.toggle("safebrowsing", enable)
func (cl *client) SafeBrowsing() (bool, error) {
return cl.toggleStatus("safebrowsing")
}
func (cl *client) ToggleSafeBrowsing(enable bool) error {
return cl.toggleBool("safebrowsing", enable)
}
func (cl *client) Parental() (bool, error) {
return cl.toggleStatus("parental")
}
func (cl *client) ToggleParental(enable bool) error {
return cl.toggle("parental", enable)
return cl.toggleBool("parental", enable)
}
func (cl *client) SafeSearch() (bool, error) {
return cl.toggleStatus("safesearch")
}
func (cl *client) ToggleSafeSearch(enable bool) error {
return cl.toggle("safesearch", enable)
return cl.toggleBool("safesearch", enable)
}
func (cl *client) toggle(mode string, enable bool) error {
cl.log.With("mode", mode, "enable", enable).Info("Toggle")
func (cl *client) toggleStatus(mode string) (bool, error) {
fs := &types.EnableConfig{}
_, err := cl.client.R().EnableTrace().SetResult(fs).Get(fmt.Sprintf("/%s/status", mode))
return fs.Enabled, err
}
func (cl *client) toggleBool(mode string, enable bool) error {
cl.log.With("enable", enable).Info(fmt.Sprintf("Toggle %s", mode))
var target string
if enable {
target = "enable"
@@ -178,6 +207,12 @@ func (cl *client) RefreshFilters(whitelist bool) error {
return err
}
func (cl *client) ToggleProtection(enable bool) error {
cl.log.With("enable", enable).Info("Toggle protection")
_, err := cl.client.R().EnableTrace().SetBody(&types.Protection{ProtectionEnabled: enable}).Post("/dns_config")
return err
}
func (cl *client) SetCustomRules(rules types.UserRules) error {
cl.log.With("rules", len(rules)).Info("Set user rules")
_, err := cl.client.R().EnableTrace().SetBody(rules.String()).Post("/filtering/set_rules")
@@ -186,7 +221,10 @@ func (cl *client) SetCustomRules(rules types.UserRules) error {
func (cl *client) ToggleFiltering(enabled bool, interval int) error {
cl.log.With("enabled", enabled, "interval", interval).Info("Toggle filtering")
_, err := cl.client.R().EnableTrace().SetBody(&types.FilteringConfig{Enabled: enabled, Interval: interval}).Post("/filtering/config")
_, err := cl.client.R().EnableTrace().SetBody(&types.FilteringConfig{
EnableConfig: types.EnableConfig{Enabled: enabled},
IntervalConfig: types.IntervalConfig{Interval: interval},
}).Post("/filtering/config")
return err
}
@@ -240,3 +278,31 @@ func (cl *client) DeleteClients(clients ...types.Client) error {
}
return nil
}
func (cl *client) QueryLogConfig() (*types.QueryLogConfig, error) {
qlc := &types.QueryLogConfig{}
_, err := cl.client.R().EnableTrace().SetResult(qlc).Get("/querylog_info")
return qlc, err
}
func (cl *client) SetQueryLogConfig(enabled bool, interval int, anonymizeClientIP bool) error {
cl.log.With("enabled", enabled, "interval", interval, "anonymizeClientIP", anonymizeClientIP).Info("Set query log config")
_, err := cl.client.R().EnableTrace().SetBody(&types.QueryLogConfig{
EnableConfig: types.EnableConfig{Enabled: enabled},
IntervalConfig: types.IntervalConfig{Interval: interval},
AnonymizeClientIP: anonymizeClientIP,
}).Post("/querylog_config")
return err
}
func (cl *client) StatsConfig() (*types.IntervalConfig, error) {
stats := &types.IntervalConfig{}
_, err := cl.client.R().EnableTrace().SetResult(stats).Get("/stats_info")
return stats, err
}
func (cl *client) SetStatsConfig(interval int) error {
cl.log.With("interval", interval).Info("Set stats config")
_, err := cl.client.R().EnableTrace().SetBody(&types.IntervalConfig{Interval: interval}).Post("/stats_config")
return err
}

View File

@@ -2,9 +2,15 @@ package log
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var rootLogger *zap.Logger
const logHistorySize = 50
var (
rootLogger *zap.Logger
logs []string
)
// GetLogger returns a named logger
func GetLogger(name string) *zap.SugaredLogger {
@@ -19,10 +25,68 @@ func init() {
Development: false,
Encoding: "console",
EncoderConfig: zap.NewDevelopmentEncoderConfig(),
OutputPaths: []string{"stderr"},
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
}
opt := zap.WrapCore(func(c zapcore.Core) zapcore.Core {
return zapcore.NewTee(c, &logList{
enc: zapcore.NewConsoleEncoder(cfg.EncoderConfig),
LevelEnabler: cfg.Level,
})
})
rootLogger, _ = cfg.Build()
rootLogger.Sugar()
rootLogger, _ = cfg.Build(opt)
}
type logList struct {
zapcore.LevelEnabler
enc zapcore.Encoder
}
func (l *logList) clone() *logList {
return &logList{
LevelEnabler: l.LevelEnabler,
enc: l.enc.Clone(),
}
}
func (l *logList) With(fields []zapcore.Field) zapcore.Core {
clone := l.clone()
addFields(clone.enc, fields)
return clone
}
func (l *logList) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if l.Enabled(ent.Level) {
return ce.AddCore(ent, l)
}
return ce
}
func (l *logList) Write(ent zapcore.Entry, fields []zapcore.Field) error {
buf, err := l.enc.EncodeEntry(ent, fields)
if err != nil {
return err
}
logs = append(logs, buf.String())
if len(logs) > logHistorySize {
logs = logs[len(logs)-logHistorySize:]
}
return nil
}
func (l *logList) Sync() error {
return nil
}
// Logs get the current logs
func Logs() []string {
return logs
}
func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) {
for i := range fields {
fields[i].AddTo(enc)
}
}

125
pkg/sync/http.go Normal file
View File

@@ -0,0 +1,125 @@
package sync
import (
"context"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/bakito/adguardhome-sync/pkg/log"
)
func (w *worker) handleSync(rw http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodPost:
l.With("remote-addr", req.RemoteAddr).Info("Starting sync from API")
w.sync()
default:
http.Error(rw, "only POST allowed", http.StatusBadRequest)
}
}
func (w *worker) handleRoot(rw http.ResponseWriter, _ *http.Request) {
_, _ = rw.Write([]byte("adguardhome-sync"))
}
func (w *worker) handleLogs(rw http.ResponseWriter, _ *http.Request) {
_, _ = rw.Write([]byte(strings.Join(log.Logs(), "")))
}
func (w *worker) basicAuth(h http.HandlerFunc) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
username, password, authOK := r.BasicAuth()
if !authOK {
http.Error(rw, "Not authorized", 401)
return
}
if username != w.cfg.API.Username || password != w.cfg.API.Password {
http.Error(rw, "Not authorized", 401)
return
}
h.ServeHTTP(rw, r)
}
}
func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
for _, m := range middleware {
h = m(h)
}
return h
}
func (w *worker) listenAndServe() {
l.With("port", w.cfg.API.Port).Info("Starting API server")
ctx, cancel := context.WithCancel(context.Background())
mux := http.NewServeMux()
httpServer := &http.Server{
Addr: fmt.Sprintf(":%d", w.cfg.API.Port),
Handler: mux,
BaseContext: func(_ net.Listener) context.Context { return ctx },
}
var mw []func(http.HandlerFunc) http.HandlerFunc
if w.cfg.API.Username != "" && w.cfg.API.Password != "" {
mw = append(mw, w.basicAuth)
}
mux.HandleFunc("/api/v1/sync", use(w.handleSync, mw...))
mux.HandleFunc("/api/v1/logs", use(w.handleLogs, mw...))
mux.HandleFunc("/", use(w.handleRoot, mw...))
go func() {
if err := httpServer.ListenAndServe(); err != http.ErrServerClosed {
l.With("error", err).Fatalf("HTTP server ListenAndServe")
}
}()
signalChan := make(chan os.Signal, 1)
signal.Notify(
signalChan,
syscall.SIGHUP, // kill -SIGHUP XXXX
syscall.SIGINT, // kill -SIGINT XXXX or Ctrl+c
syscall.SIGQUIT, // kill -SIGQUIT XXXX
)
<-signalChan
l.Info("os.Interrupt - shutting down...")
go func() {
<-signalChan
l.Fatal("os.Kill - terminating...")
}()
gracefullCtx, cancelShutdown := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelShutdown()
if w.cron != nil {
l.Info("Stopping cron")
w.cron.Stop()
}
if err := httpServer.Shutdown(gracefullCtx); err != nil {
l.With("error", err).Error("Shutdown error")
defer os.Exit(1)
} else {
l.Info("API server stopped")
}
// manually cancel context if not using httpServer.RegisterOnShutdown(cancel)
cancel()
defer os.Exit(0)
}

View File

@@ -14,27 +14,50 @@ var (
// Sync config from origin to replica
func Sync(cfg *types.Config) {
w := &worker{
cfg: cfg,
}
if cfg.Cron != "" {
c := cron.New()
w.cron = cron.New()
cl := l.With("cron", cfg.Cron)
_, err := c.AddFunc(cfg.Cron, func() {
sync(cfg)
_, err := w.cron.AddFunc(cfg.Cron, func() {
w.sync()
})
if err != nil {
cl.With("error", err).Error("Error creating cron job")
cl.With("error", err).Error("Error during cron job setup")
return
}
cl.Info("Starting cronjob")
c.Run()
cl.Info("Setup cronjob")
if cfg.API.Port != 0 {
w.cron.Start()
} else {
w.cron.Run()
}
} else {
sync(cfg)
w.sync()
}
if cfg.API.Port != 0 {
w.listenAndServe()
}
}
func sync(cfg *types.Config) {
oc, err := client.New(cfg.Origin)
type worker struct {
cfg *types.Config
running bool
cron *cron.Cron
}
func (w *worker) sync() {
if w.running {
l.Info("Sync already running")
return
}
w.running = true
defer func() { w.running = false }()
oc, err := client.New(w.cfg.Origin)
if err != nil {
l.With("error", err, "url", cfg.Origin.URL).Error("Error creating origin client")
l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client")
return
}
@@ -48,6 +71,22 @@ func sync(cfg *types.Config) {
return
}
o.parental, err = oc.Parental()
if err != nil {
sl.With("error", err).Error("Error getting parental status")
return
}
o.safeSearch, err = oc.SafeSearch()
if err != nil {
sl.With("error", err).Error("Error getting safe search status")
return
}
o.safeBrowsing, err = oc.SafeBrowsing()
if err != nil {
sl.With("error", err).Error("Error getting safe browsing status")
return
}
o.rewrites, err = oc.RewriteList()
if err != nil {
sl.With("error", err).Error("Error getting origin rewrites")
@@ -70,14 +109,24 @@ func sync(cfg *types.Config) {
sl.With("error", err).Error("Error getting origin clients")
return
}
o.queryLogConfig, err = oc.QueryLogConfig()
if err != nil {
sl.With("error", err).Error("Error getting query log config")
return
}
o.statsConfig, err = oc.StatsConfig()
if err != nil {
sl.With("error", err).Error("Error getting stats config")
return
}
replicas := cfg.UniqueReplicas()
replicas := w.cfg.UniqueReplicas()
for _, replica := range replicas {
syncTo(sl, o, replica)
w.syncTo(sl, o, replica)
}
}
func syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardInstance) {
func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardInstance) {
rc, err := client.New(replica)
if err != nil {
@@ -97,24 +146,36 @@ func syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardInstance) {
l.With("originVersion", o.status.Version, "replicaVersion", rs.Version).Warn("Versions do not match")
}
err = syncRewrites(o.rewrites, rc)
err = w.syncGeneralSettings(o, rs, rc)
if err != nil {
l.With("error", err).Error("Error syncing general settings")
return
}
err = w.syncConfigs(o, rs, rc)
if err != nil {
l.With("error", err).Error("Error syncing configs")
return
}
err = w.syncRewrites(o.rewrites, rc)
if err != nil {
l.With("error", err).Error("Error syncing rewrites")
return
}
err = syncFilters(o.filters, rc)
err = w.syncFilters(o.filters, rc)
if err != nil {
l.With("error", err).Error("Error syncing filters")
return
}
err = syncServices(o.services, rc)
err = w.syncServices(o.services, rc)
if err != nil {
l.With("error", err).Error("Error syncing services")
return
}
if err = syncClients(o.clients, rc); err != nil {
if err = w.syncClients(o.clients, rc); err != nil {
l.With("error", err).Error("Error syncing clients")
return
}
@@ -122,7 +183,7 @@ func syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardInstance) {
rl.Info("Sync done")
}
func syncServices(os *types.Services, replica client.Client) error {
func (w *worker) syncServices(os *types.Services, replica client.Client) error {
rs, err := replica.Services()
if err != nil {
return err
@@ -136,7 +197,7 @@ func syncServices(os *types.Services, replica client.Client) error {
return nil
}
func syncFilters(of *types.FilteringStatus, replica client.Client) error {
func (w *worker) syncFilters(of *types.FilteringStatus, replica client.Client) error {
rf, err := replica.Filtering()
if err != nil {
return err
@@ -185,7 +246,7 @@ func syncFilters(of *types.FilteringStatus, replica client.Client) error {
return nil
}
func syncRewrites(or *types.RewriteEntries, replica client.Client) error {
func (w *worker) syncRewrites(or *types.RewriteEntries, replica client.Client) error {
replicaRewrites, err := replica.RewriteList()
if err != nil {
@@ -203,7 +264,7 @@ func syncRewrites(or *types.RewriteEntries, replica client.Client) error {
return nil
}
func syncClients(oc *types.Clients, replica client.Client) error {
func (w *worker) syncClients(oc *types.Clients, replica client.Client) error {
rc, err := replica.Clients()
if err != nil {
return err
@@ -223,10 +284,69 @@ func syncClients(oc *types.Clients, replica client.Client) error {
return nil
}
type origin struct {
status *types.Status
rewrites *types.RewriteEntries
services *types.Services
filters *types.FilteringStatus
clients *types.Clients
func (w *worker) syncGeneralSettings(o *origin, rs *types.Status, replica client.Client) error {
if o.status.ProtectionEnabled != rs.ProtectionEnabled {
if err := replica.ToggleProtection(o.status.ProtectionEnabled); err != nil {
return err
}
}
if rp, err := replica.Parental(); err != nil {
return err
} else if o.parental != rp {
if err = replica.ToggleParental(o.parental); err != nil {
return err
}
}
if rs, err := replica.SafeSearch(); err != nil {
return err
} else if o.safeSearch != rs {
if err = replica.ToggleSafeSearch(o.safeSearch); err != nil {
return err
}
}
if rs, err := replica.SafeBrowsing(); err != nil {
return err
} else if o.safeBrowsing != rs {
if err = replica.ToggleSafeBrowsing(o.safeBrowsing); err != nil {
return err
}
}
return nil
}
func (w *worker) syncConfigs(o *origin, rs *types.Status, replica client.Client) error {
qlc, err := replica.QueryLogConfig()
if err != nil {
return err
}
if !o.queryLogConfig.Equals(qlc) {
if err = replica.SetQueryLogConfig(o.queryLogConfig.Enabled, o.queryLogConfig.Interval, o.queryLogConfig.AnonymizeClientIP); err != nil {
return err
}
}
sc, err := replica.StatsConfig()
if err != nil {
return err
}
if o.statsConfig.Interval != sc.Interval {
if err = replica.SetStatsConfig(o.statsConfig.Interval); err != nil {
return err
}
}
return nil
}
type origin struct {
status *types.Status
rewrites *types.RewriteEntries
services *types.Services
filters *types.FilteringStatus
clients *types.Clients
queryLogConfig *types.QueryLogConfig
statsConfig *types.IntervalConfig
parental bool
safeSearch bool
safeBrowsing bool
}

View File

@@ -8,13 +8,23 @@ import (
"time"
)
// Config application configuration struct
type Config struct {
Origin AdGuardInstance `json:"origin" yaml:"origin"`
Replica *AdGuardInstance `json:"replica,omitempty" yaml:"replica,omitempty"`
Replicas []AdGuardInstance `json:"replicas,omitempty" yaml:"replicas,omitempty"`
Cron string `json:"cron,omitempty" yaml:"cron,omitempty"`
API API `json:"api,omitempty" yaml:"api,omitempty"`
}
// API configuration
type API struct {
Port int `json:"port,omitempty" yaml:"port,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"`
}
// UniqueReplicas get unique replication instances
func (cfg *Config) UniqueReplicas() []AdGuardInstance {
dedup := make(map[string]AdGuardInstance)
if cfg.Replica != nil {
@@ -31,6 +41,7 @@ func (cfg *Config) UniqueReplicas() []AdGuardInstance {
return r
}
// AdGuardInstance adguard home config instance
type AdGuardInstance struct {
URL string `json:"url" yaml:"url"`
APIPath string `json:"apiPath,omitempty" yaml:"apiPath,omitempty"`
@@ -39,23 +50,32 @@ type AdGuardInstance struct {
InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"`
}
// Key AdGuardInstance key
func (i *AdGuardInstance) Key() string {
return fmt.Sprintf("%s%s", i.URL, i.APIPath)
}
type Status struct {
DNSAddresses []string `json:"dns_addresses"`
DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"`
ProtectionEnabled bool `json:"protection_enabled"`
DhcpAvailable bool `json:"dhcp_available"`
Running bool `json:"running"`
Version string `json:"version"`
Language string `json:"language"`
// Protection API struct
type Protection struct {
ProtectionEnabled bool `json:"protection_enabled"`
}
// Status API struct
type Status struct {
Protection
DNSAddresses []string `json:"dns_addresses"`
DNSPort int `json:"dns_port"`
HTTPPort int `json:"http_port"`
DhcpAvailable bool `json:"dhcp_available"`
Running bool `json:"running"`
Version string `json:"version"`
Language string `json:"language"`
}
// RewriteEntries list of RewriteEntry
type RewriteEntries []RewriteEntry
// Merge RewriteEntries
func (rwe *RewriteEntries) Merge(other *RewriteEntries) (RewriteEntries, RewriteEntries) {
current := make(map[string]RewriteEntry)
@@ -80,17 +100,21 @@ func (rwe *RewriteEntries) Merge(other *RewriteEntries) (RewriteEntries, Rewrite
return adds, removes
}
// RewriteEntry API struct
type RewriteEntry struct {
Domain string `json:"domain"`
Answer string `json:"answer"`
}
// Key RewriteEntry key
func (re *RewriteEntry) Key() string {
return fmt.Sprintf("%s#%s", re.Domain, re.Answer)
}
// Filters list of Filter
type Filters []Filter
// Filter API struct
type Filter struct {
ID int `json:"id"`
Enabled bool `json:"enabled"`
@@ -101,6 +125,7 @@ type Filter struct {
Whitelist bool `json:"whitelist"` // needed for add
}
// FilteringStatus API struct
type FilteringStatus struct {
FilteringConfig
Filters Filters `json:"filters"`
@@ -108,21 +133,48 @@ type FilteringStatus struct {
UserRules UserRules `json:"user_rules"`
}
// UserRules API struct
type UserRules []string
// String toString of Users
func (ur UserRules) String() string {
return strings.Join(ur, "\n")
}
type FilteringConfig struct {
Enabled bool `json:"enabled"`
Interval int `json:"interval"`
// EnableConfig API struct
type EnableConfig struct {
Enabled bool `json:"enabled"`
}
// IntervalConfig API struct
type IntervalConfig struct {
Interval int `json:"interval"`
}
// FilteringConfig API struct
type FilteringConfig struct {
EnableConfig
IntervalConfig
}
// QueryLogConfig API struct
type QueryLogConfig struct {
EnableConfig
IntervalConfig
AnonymizeClientIP bool `json:"anonymize_client_ip"`
}
// Equals QueryLogConfig equal check
func (qlc *QueryLogConfig) Equals(o *QueryLogConfig) bool {
return qlc.Enabled == o.Enabled && qlc.AnonymizeClientIP == o.AnonymizeClientIP && qlc.Interval == o.Interval
}
// RefreshFilter API struct
type RefreshFilter struct {
Whitelist bool `json:"whitelist"`
}
// Merge merge RefreshFilters
func (fs *Filters) Merge(other Filters) (Filters, Filters) {
current := make(map[string]Filter)
@@ -147,18 +199,22 @@ func (fs *Filters) Merge(other Filters) (Filters, Filters) {
return adds, removes
}
// Services API struct
type Services []string
// Sort sort Services
func (s Services) Sort() {
sort.Strings(s)
}
// Equals Services equal check
func (s *Services) Equals(o *Services) bool {
s.Sort()
o.Sort()
return equals(*s, *o)
}
// Clients API struct
type Clients struct {
Clients []Client `json:"clients"`
AutoClients []struct {
@@ -171,6 +227,7 @@ type Clients struct {
SupportedTags []string `json:"supported_tags"`
}
// Client API struct
type Client struct {
Ids []string `json:"ids"`
Tags []string `json:"tags"`
@@ -188,6 +245,7 @@ type Client struct {
DisallowedRule string `json:"disallowed_rule"`
}
// Sort sort clients
func (cl *Client) Sort() {
sort.Strings(cl.Ids)
sort.Strings(cl.Tags)
@@ -195,6 +253,7 @@ func (cl *Client) Sort() {
sort.Strings(cl.Upstreams)
}
// Equal Clients equal check
func (cl *Client) Equal(o *Client) bool {
cl.Sort()
o.Sort()
@@ -204,6 +263,7 @@ func (cl *Client) Equal(o *Client) bool {
return string(a) == string(b)
}
// Merge merge Clients
func (clients *Clients) Merge(other *Clients) ([]Client, []Client, []Client) {
current := make(map[string]Client)
for _, client := range clients.Clients {
@@ -237,6 +297,7 @@ func (clients *Clients) Merge(other *Clients) ([]Client, []Client, []Client) {
return adds, updates, removes
}
// ClientUpdate API struct
type ClientUpdate struct {
Name string `json:"name"`
Data Client `json:"data"`

View File

@@ -0,0 +1,28 @@
[Unit]
Description=AdGuard Home Sync service
ConditionFileIsExecutable=/opt/AdGuardHomeSync/adguardhome-sync
Requires=network.target
After=network-online.target syslog.target
[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/opt/AdGuardHomeSync/adguardhome-sync "run" "--config" "/opt/AdGuardHomeSync/adguardhome-sync.yaml"
WorkingDirectory=/opt/AdGuardHome
Restart=on-success
SuccessExitStatus=1 2 8 SIGKILL
RestartSec=120
EnvironmentFile=-/etc/sysconfig/GoServiceExampleLogging
StandardOutput=file:/var/log/AdGuardHomeSync.out
StandardError=file:/var/log/AdGuardHomeSync.err
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target

11
systemd/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Install
```bash
mkdir -p /opt/AdGuardHomeSync/
sudo cp adguardhome-sync /opt/AdGuardHomeSync/adguardhome-sync
sudo cp adguardhome-sync.yaml /opt/AdGuardHomeSync/adguardhome-sync.yaml
sudo cp AdGuardHomeSync.service /etc/systemd/system/AdGuardHomeSync.service
sudo systemctl enable AdGuardHomeSync
```

View File

@@ -1,49 +0,0 @@
package main
import (
"bufio"
"fmt"
"os"
"os/exec"
"strings"
"github.com/coreos/go-semver/semver"
)
func main() {
out, err := exec.Command("git", "branch", "--show-current").Output()
if err != nil {
panic(err)
}
branch := strings.TrimSpace(string(out))
if branch != "main" {
panic(fmt.Errorf(`error: must be in "master" branch, current branch: %q`, branch))
}
out, err = exec.Command("git", "describe").Output()
if err != nil {
panic(err)
}
version := strings.TrimPrefix(strings.TrimSpace(string(out)), "v")
v := semver.New(version)
v.BumpPatch()
reader := bufio.NewReader(os.Stdin)
if _, err = fmt.Fprintf(os.Stderr, "Enter Release Version: [v%v] ", v); err != nil {
panic(err)
}
text, err := reader.ReadString('\n')
if err != nil {
panic(err)
}
if strings.HasPrefix(text, "v") {
text = text[1:]
v = semver.New(strings.TrimSpace(text))
}
if _, err = fmt.Fprintf(os.Stderr, "Using Version: v%v\n", v); err != nil {
panic(err)
}
fmt.Printf("v%v", v)
}