Compare commits

..

27 Commits

Author SHA1 Message Date
bakito
a73f696ef6 update readme #9 2021-04-18 22:30:25 +02:00
bakito
2e93920931 run on startup #10 2021-04-18 22:20:08 +02:00
Marc Brugger
3edb5222d6 Initial setup (#11)
automatically setup new AdGuardHome instances #9
2021-04-18 22:03:57 +02:00
bakito
d58c8f115e log status 2021-04-18 19:32:03 +02:00
Marc Brugger
f8dd7e6136 Update issue templates 2021-04-18 18:40:43 +02:00
bakito
9fd3694237 add support debug messages 2021-04-18 18:28:35 +02:00
bakito
258ecae016 fix filter synch error 2021-04-18 18:04:53 +02:00
Marc Brugger
04a912fb56 Update issue templates 2021-04-18 11:03:44 +02:00
Marc Brugger
cd75a5f46e Merge pull request #8 from bakito/dependabot/go_modules/github.com/golang/mock-1.5.0
Bump github.com/golang/mock from 1.3.1 to 1.5.0
2021-04-12 12:28:55 +02:00
dependabot[bot]
87c578a07b Bump github.com/golang/mock from 1.3.1 to 1.5.0
Bumps [github.com/golang/mock](https://github.com/golang/mock) from 1.3.1 to 1.5.0.
- [Release notes](https://github.com/golang/mock/releases)
- [Changelog](https://github.com/golang/mock/blob/master/.goreleaser.yml)
- [Commits](https://github.com/golang/mock/compare/1.3.1...v1.5.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-12 10:16:48 +00:00
Marc Brugger
28782fc364 Merge pull request #7 from bakito/dependabot/go_modules/github.com/onsi/ginkgo-1.16.1
Bump github.com/onsi/ginkgo from 1.16.0 to 1.16.1
2021-04-12 10:15:17 +02:00
Marc Brugger
7470a11547 Merge pull request #6 from bakito/dependabot/go_modules/github.com/go-resty/resty/v2-2.6.0
Bump github.com/go-resty/resty/v2 from 2.4.0 to 2.6.0
2021-04-12 10:12:59 +02:00
dependabot[bot]
0d35a77e1b Bump github.com/onsi/ginkgo from 1.16.0 to 1.16.1
Bumps [github.com/onsi/ginkgo](https://github.com/onsi/ginkgo) from 1.16.0 to 1.16.1.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v1.16.0...v1.16.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-12 08:11:52 +00:00
dependabot[bot]
bc8fc796ec Bump github.com/go-resty/resty/v2 from 2.4.0 to 2.6.0
Bumps [github.com/go-resty/resty/v2](https://github.com/go-resty/resty) from 2.4.0 to 2.6.0.
- [Release notes](https://github.com/go-resty/resty/releases)
- [Commits](https://github.com/go-resty/resty/compare/v2.4.0...v2.6.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-12 08:11:21 +00:00
bakito
da3b037009 add sync tests 2021-04-11 18:35:03 +02:00
bakito
4921af09a5 add client tests 2021-04-11 16:13:37 +02:00
bakito
e7a2604268 prepare sync and client tests 2021-04-11 11:56:55 +02:00
bakito
cb624ea52b extend tests 2021-04-11 10:51:24 +02:00
bakito
57612bae1f log enable for filters 2021-04-10 14:48:02 +02:00
bakito
680729580e add merge tests 2021-04-10 13:30:26 +02:00
bakito
97fc7be19a update changed filters #5 2021-04-10 13:15:56 +02:00
bakito
59a55db582 use helper methods for error handling 2021-04-10 11:44:12 +02:00
bakito
d984d66883 abort on http status code != 200 2021-04-10 00:18:29 +02:00
bakito
a78f3f00dc start writing tests 2021-04-06 21:31:26 +02:00
bakito
933a3d8066 add missing command 2021-04-06 20:45:36 +02:00
bakito
64463b6842 add multi replica env support #4 2021-04-05 21:07:28 +02:00
bakito
9450c09e2a add docker build #4 2021-04-05 20:13:13 +02:00
32 changed files with 2309 additions and 169 deletions

27
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. What version of AdGuardHome sync used?
2. What version of AdGuardHome us used?
3. How does the configuration look?
4. What is the error message?
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add log files or json responses from AdGuardHome to help explain your problem.
**Additional context**
Add any other context about the problem here.

10
.github/ISSUE_TEMPLATE/custom.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: Custom issue template
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -32,7 +32,7 @@ jobs:
uses: actions/checkout@v2
- name: Test
run: go test ./... -coverprofile=coverage.out
run: make test
- name: Send coverage
uses: shogo82148/actions-goveralls@v1

44
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name: quay
on:
push:
branches: main
release:
types:
- published
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Quay
uses: docker/login-action@v1
with:
registry: quay.io
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push ${{github.event.release.tag_name }}
id: docker_build_release
uses: docker/build-push-action@v2
if: ${{ github.event.release.tag_name != '' }}
with:
push: true
tags: quay.io/bakito/adguardhome-sync:latest,quay.io/bakito/adguardhome-sync:${{ github.event.release.tag_name }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: VERSION=${{ github.event.release.tag_name }}
- name: Build and push main
id: docker_build_main
uses: docker/build-push-action@v2
if: ${{ github.event.release.tag_name == '' }}
with:
push: true
tags: quay.io/bakito/adguardhome-sync:main
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: VERSION=main
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

27
Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
FROM docker.io/library/golang:1.16 as builder
WORKDIR /go/src/app
RUN apt-get update && apt-get install -y upx
ARG VERSION=main
ENV GOPROXY=https://goproxy.io \
GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux
ADD . /go/src/app/
RUN go build -a -installsuffix cgo -ldflags="-w -s -X github.com/bakito/adguardhome-sync/version.Version=${VERSION}" -o adguardhome-sync . \
&& upx -q adguardhome-sync
# application image
FROM scratch
WORKDIR /opt/go
LABEL maintainer="bakito <github@bakito.ch>"
EXPOSE 8080
ENTRYPOINT ["/opt/go/adguardhome-sync"]
CMD ["run", "--config", "/config/adguardhome-sync.yaml"]
COPY --from=builder /go/src/app/adguardhome-sync /opt/go/adguardhome-sync
USER 1001

View File

@@ -16,10 +16,13 @@ tidy:
go mod tidy
# Run tests
test: tidy fmt vet
test: mocks tidy fmt vet
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
mocks: mockgen
mockgen -destination pkg/mocks/client/mock.go github.com/bakito/adguardhome-sync/pkg/client Client
release: semver
@version=$$(semver); \
git tag -s $$version -m"Release $$version"
@@ -32,3 +35,8 @@ semver:
ifeq (, $(shell which semver))
$(shell go get -u github.com/bakito/semver)
endif
mockgen:
ifeq (, $(shell which mockgen))
$(shell go get github.com/golang/mock/mockgen@v1.5)
endif

View File

@@ -1,4 +1,6 @@
[![Go](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml/badge.svg)](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/bakito/adguardhome-sync)](https://goreportcard.com/report/github.com/bakito/adguardhome-sync)
[![Go](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml/badge.svg)](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/bakito/adguardhome-sync)](https://goreportcard.com/report/github.com/bakito/adguardhome-sync)
[![Coverage Status](https://coveralls.io/repos/github/bakito/adguardhome-sync/badge.svg?branch=main)](https://coveralls.io/github/bakito/adguardhome-sync?branch=main)
# AdGuardHome sync
@@ -12,6 +14,13 @@ Synchronize [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) config to
- Services
- Clients
### Setup of initial instances
New AdGuardHome replica instances can be automatically installed if enabled via the config autoSetup.
During automatic installation, the admin interface will be listening on port 3000 in runtime.
To skip automatic setup
## Install
```bash
@@ -20,7 +29,7 @@ go get -u github.com/bakito/adguardhome-sync
## Prerequisites
Both the origin and replica mist be initially setup via the Adguard Home installation wizard.
Both the origin instance must be initially setup via the AdguardHome installation wizard.
## Run
@@ -40,6 +49,64 @@ adguardhome-sync run
adguardhome-sync run --cron "*/10 * * * *"
```
## docker cli
```bash
docker run -d \
--name=adguardhome-sync \
-p 8080:8080 \
-v /path/to/appdata/config/adguardhome-sync.yaml:/config/adguardhome-sync.yaml \
--restart unless-stopped \
quay.io/bakito/adguardhome-sync:latest
```
## docker compose
### config file
```yaml
---
version: "2.1"
services:
adguardhome-sync:
image: quay.io/bakito/adguardhome-sync
container_name: adguardhome-sync
volumes:
- /path/to/appdata/config/adguardhome-sync.yaml:/config/adguardhome-sync.yaml
ports:
- 8080:8080
restart: unless-stopped
```
### env
```yaml
---
version: "2.1"
services:
adguardhome-sync:
image: quay.io/bakito/adguardhome-sync
container_name: adguardhome-sync
command: run
environment:
- ORIGIN_URL=https://192.168.1.2:3000
- ORIGIN_USERNAME=username
- ORIGIN_PASSWORD=password
- REPLICA_URL=http://192.168.1.3
- REPLICA_USERNAME=username
- REPLICA_PASSWORD=password
- REPLICA1_URL=http://192.168.1.4
- REPLICA1_USERNAME=username
- REPLICA1_PASSWORD=password
- REPLICA1_APIPATH=/some/path/control
# - REPLICA1_AUTOSETUP=true # if true, AdGuardHome is automatically initialized.
- CRON=*/10 * * * * # run every 10 minutes
- RUNONSTART=true
ports:
- 8080:8080
restart: unless-stopped
```
### Config file
location: $HOME/.adguardhome-sync.yaml
@@ -48,6 +115,9 @@ location: $HOME/.adguardhome-sync.yaml
# cron expression to run in daemon mode. (default; "" = runs only once)
cron: "*/10 * * * *"
# runs the synchronisation on startup
runOnStart: true
origin:
# url of the origin instance
url: https://192.168.1.2:3000
@@ -72,6 +142,7 @@ replicas:
- url: http://192.168.1.4
username: username
password: password
# autoSetup: true # if true, AdGuardHome is automatically initialized.
# Configure the sync API server, disabled if api port is 0
api:
@@ -82,3 +153,14 @@ api:
password: password
```
## Log Level
The log level can be set with the environment variable: LOG_LEVEL
The following log levels are supported (default: info)
- debug
- info
- warn
- error

View File

@@ -3,18 +3,19 @@ package cmd
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
)
const (
configCron = "cron"
configCron = "cron"
configRunOnStart = "runOnStart"
configAPIPort = "api.port"
configAPIUsername = "api.username"
@@ -31,11 +32,19 @@ const (
configReplicaUsername = "replica.username"
configReplicaPassword = "replica.password"
configReplicaInsecureSkipVerify = "replica.insecureSkipVerify"
configReplicaAutoSetup = "replica.autoSetup"
envReplicasUsernameFormat = "REPLICA%s_USERNAME"
envReplicasPasswordFormat = "REPLICA%s_PASSWORD"
envReplicasAPIPathFormat = "REPLICA%s_APIPATH"
envReplicasInsecureSkipVerifyFormat = "REPLICA%s_INSECURESKIPVERIFY"
envReplicasAutoSetup = "REPLICA%s_AUTOSETUP"
)
var (
cfgFile string
logger = log.GetLogger("root")
cfgFile string
logger = log.GetLogger("root")
envReplicasURLPattern = regexp.MustCompile(`^REPLICA(\d+)_URL=(.*)`)
)
// rootCmd represents the base command when called without any subcommands
@@ -83,13 +92,48 @@ func initConfig() {
// Search config in home directory with name ".adguardhome-sync" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".adguardhome-sync")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
}
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
logger.Info("Using config file:", viper.ConfigFileUsed())
} else if cfgFile != "" {
fmt.Println(err)
os.Exit(1)
}
}
func getConfig() (*types.Config, error) {
cfg := &types.Config{}
if err := viper.Unmarshal(cfg); err != nil {
return nil, err
}
if len(cfg.Replicas) == 0 {
cfg.Replicas = append(cfg.Replicas, collectEnvReplicas()...)
}
return cfg, nil
}
// Manually collect replicas from env.
func collectEnvReplicas() []types.AdGuardInstance {
var replicas []types.AdGuardInstance
for _, v := range os.Environ() {
if envReplicasURLPattern.MatchString(v) {
sm := envReplicasURLPattern.FindStringSubmatch(v)
re := types.AdGuardInstance{
URL: sm[2],
Username: os.Getenv(fmt.Sprintf(envReplicasUsernameFormat, sm[1])),
Password: os.Getenv(fmt.Sprintf(envReplicasPasswordFormat, sm[1])),
APIPath: os.Getenv(fmt.Sprintf(envReplicasAPIPathFormat, sm[1])),
InsecureSkipVerify: strings.EqualFold(os.Getenv(fmt.Sprintf(envReplicasInsecureSkipVerifyFormat, sm[1])), "true"),
AutoSetup: strings.EqualFold(os.Getenv(fmt.Sprintf(envReplicasAutoSetup, sm[1])), "true"),
}
replicas = append(replicas, re)
}
}
return replicas
}

View File

@@ -3,10 +3,8 @@ package cmd
import (
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/sync"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/spf13/viper"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// runCmd represents the run command
@@ -14,15 +12,15 @@ var doCmd = &cobra.Command{
Use: "run",
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) {
RunE: func(cmd *cobra.Command, args []string) error {
logger = log.GetLogger("run")
cfg := &types.Config{}
if err := viper.Unmarshal(cfg); err != nil {
cfg, err := getConfig()
if err != nil {
logger.Error(err)
return
return err
}
sync.Sync(cfg)
return sync.Sync(cfg)
},
}
@@ -30,6 +28,8 @@ 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().Bool("runOnStart", true, "Run the sync job on start.")
_ = viper.BindPFlag(configRunOnStart, doCmd.PersistentFlags().Lookup("runOnStart"))
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")
@@ -56,6 +56,8 @@ func init() {
_ = viper.BindPFlag(configReplicaUsername, doCmd.PersistentFlags().Lookup("replica-username"))
doCmd.PersistentFlags().String("replica-password", "", "Replica instance password")
_ = viper.BindPFlag(configReplicaPassword, doCmd.PersistentFlags().Lookup("replica-password"))
doCmd.PersistentFlags().String("replica-insecure-skip-verify", "", "Enable Replica instance InsecureSkipVerify")
doCmd.PersistentFlags().Bool("replica-insecure-skip-verify", false, "Enable Replica instance InsecureSkipVerify")
_ = viper.BindPFlag(configReplicaInsecureSkipVerify, doCmd.PersistentFlags().Lookup("replica-insecure-skip-verify"))
doCmd.PersistentFlags().Bool("replica-auto-setup", false, "Enable automatic setup of new AdguardHome instances. This replaces the setup wizard.")
_ = viper.BindPFlag(configReplicaAutoSetup, doCmd.PersistentFlags().Lookup("replica-auto-setup"))
}

7
go.mod
View File

@@ -3,13 +3,14 @@ module github.com/bakito/adguardhome-sync
go 1.16
require (
github.com/go-resty/resty/v2 v2.5.0
github.com/go-resty/resty/v2 v2.6.0
github.com/golang/mock v1.5.0
github.com/google/uuid v1.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/onsi/ginkgo v1.16.0
github.com/onsi/ginkgo v1.16.1
github.com/onsi/gomega v1.11.0
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.1.3
github.com/spf13/viper v1.7.1
go.uber.org/zap v1.16.0
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
)

20
go.sum
View File

@@ -46,8 +46,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-resty/resty/v2 v2.5.0 h1:WFb5bD49/85PO7WgAjZ+/TJQ+Ty1XOcWEfD1zIFCM1c=
github.com/go-resty/resty/v2 v2.5.0/go.mod h1:B88+xCTEwvfD94NOuE6GS1wMlnoKNY8eEiNizfNwOwA=
github.com/go-resty/resty/v2 v2.6.0 h1:joIR5PNLM2EFqqESUjCMGXrWmXNHEU9CEiK813oKYS4=
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -57,6 +57,8 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -78,6 +80,8 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
@@ -150,8 +154,8 @@ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.0 h1:NBrNLB37exjJLxXtFOktx6CISBdS1aF8+7MwKlTV8U4=
github.com/onsi/ginkgo v1.16.0/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/ginkgo v1.16.1 h1:foqVmeWDD6yYpK+Yz3fHyNIxFYNxswxqNFjSKe+vI54=
github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug=
@@ -275,9 +279,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -307,8 +310,9 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -2,7 +2,9 @@ package client
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/url"
"path"
@@ -13,12 +15,12 @@ import (
)
var (
l = log.GetLogger("client")
l = log.GetLogger("client")
SetupNeededError = errors.New("setup needed")
)
// New create a new client
func New(config types.AdGuardInstance) (Client, error) {
var apiURL string
if config.APIPath == "" {
apiURL = fmt.Sprintf("%s/control", config.URL)
@@ -40,6 +42,9 @@ func New(config types.AdGuardInstance) (Client, error) {
cl = cl.SetBasicAuth(config.Username, config.Password)
}
// no redirect
cl.SetRedirectPolicy(resty.NoRedirectPolicy())
return &client{
host: u.Host,
client: cl,
@@ -47,7 +52,7 @@ func New(config types.AdGuardInstance) (Client, error) {
}, nil
}
// Client AdGuard Home API client interface
// Client AdguardHome API client interface
type Client interface {
Host() string
@@ -61,6 +66,7 @@ type Client interface {
ToggleFiltering(enabled bool, interval int) error
AddFilters(whitelist bool, e ...types.Filter) error
DeleteFilters(whitelist bool, e ...types.Filter) error
UpdateFilters(whitelist bool, e ...types.Filter) error
RefreshFilters(whitelist bool) error
SetCustomRules(rules types.UserRules) error
@@ -71,7 +77,7 @@ type Client interface {
SafeSearch() (bool, error)
ToggleSafeSearch(enable bool) error
Services() (*types.Services, error)
Services() (types.Services, error)
SetServices(services types.Services) error
Clients() (*types.Clients, error)
@@ -83,6 +89,7 @@ type Client interface {
SetQueryLogConfig(enabled bool, interval int, anonymizeClientIP bool) error
StatsConfig() (*types.IntervalConfig, error)
SetStatsConfig(interval int) error
Setup() error
}
type client struct {
@@ -94,23 +101,63 @@ type client struct {
func (cl *client) Host() string {
return cl.host
}
func (cl *client) doGet(req *resty.Request, url string) error {
rl := cl.log.With("method", "GET", "path", url)
if cl.client.UserInfo != nil {
rl = rl.With("username", cl.client.UserInfo.Username)
}
rl.Debug("do get")
resp, err := req.Get(url)
if err != nil {
if resp != nil && resp.StatusCode() == http.StatusFound {
loc := resp.Header().Get("Location")
if loc == "/install.html" {
return SetupNeededError
}
}
return err
}
rl.With("status", resp.StatusCode(), "body", string(resp.Body())).Debug("got response")
if resp.StatusCode() != http.StatusOK {
return errors.New(resp.Status())
}
return nil
}
func (cl *client) doPost(req *resty.Request, url string) error {
rl := cl.log.With("method", "POST", "path", url)
if cl.client.UserInfo != nil {
rl = rl.With("username", cl.client.UserInfo.Username)
}
rl.Debug("do post")
resp, err := req.Post(url)
if err != nil {
return err
}
rl.With("status", resp.StatusCode(), "body", string(resp.Body())).Debug("got response")
if resp.StatusCode() != http.StatusOK {
return errors.New(resp.Status())
}
return nil
}
func (cl *client) Status() (*types.Status, error) {
status := &types.Status{}
_, err := cl.client.R().EnableTrace().SetResult(status).Get("status")
err := cl.doGet(cl.client.R().EnableTrace().SetResult(status), "status")
return status, err
}
func (cl *client) RewriteList() (*types.RewriteEntries, error) {
rewrites := &types.RewriteEntries{}
_, err := cl.client.R().EnableTrace().SetResult(&rewrites).Get("/rewrite/list")
err := cl.doGet(cl.client.R().EnableTrace().SetResult(&rewrites), "/rewrite/list")
return rewrites, err
}
func (cl *client) AddRewriteEntries(entries ...types.RewriteEntry) error {
for _, e := range entries {
cl.log.With("domain", e.Domain, "answer", e.Answer).Info("Add rewrite entry")
_, err := cl.client.R().EnableTrace().SetBody(&e).Post("/rewrite/add")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(&e), "/rewrite/add")
if err != nil {
return err
}
@@ -121,7 +168,7 @@ func (cl *client) AddRewriteEntries(entries ...types.RewriteEntry) error {
func (cl *client) DeleteRewriteEntries(entries ...types.RewriteEntry) error {
for _, e := range entries {
cl.log.With("domain", e.Domain, "answer", e.Answer).Info("Delete rewrite entry")
_, err := cl.client.R().EnableTrace().SetBody(&e).Post("/rewrite/delete")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(&e), "/rewrite/delete")
if err != nil {
return err
}
@@ -155,7 +202,7 @@ func (cl *client) ToggleSafeSearch(enable bool) error {
func (cl *client) toggleStatus(mode string) (bool, error) {
fs := &types.EnableConfig{}
_, err := cl.client.R().EnableTrace().SetResult(fs).Get(fmt.Sprintf("/%s/status", mode))
err := cl.doGet(cl.client.R().EnableTrace().SetResult(fs), fmt.Sprintf("/%s/status", mode))
return fs.Enabled, err
}
@@ -167,21 +214,20 @@ func (cl *client) toggleBool(mode string, enable bool) error {
} else {
target = "disable"
}
_, err := cl.client.R().EnableTrace().Post(fmt.Sprintf("/%s/%s", mode, target))
return err
return cl.doPost(cl.client.R().EnableTrace(), fmt.Sprintf("/%s/%s", mode, target))
}
func (cl *client) Filtering() (*types.FilteringStatus, error) {
f := &types.FilteringStatus{}
_, err := cl.client.R().EnableTrace().SetResult(f).Get("/filtering/status")
err := cl.doGet(cl.client.R().EnableTrace().SetResult(f), "/filtering/status")
return f, err
}
func (cl *client) AddFilters(whitelist bool, filters ...types.Filter) error {
for _, f := range filters {
cl.log.With("url", f.URL, "whitelist", whitelist).Info("Add filter")
cl.log.With("url", f.URL, "whitelist", whitelist, "enabled", f.Enabled).Info("Add filter")
ff := &types.Filter{Name: f.Name, URL: f.URL, Whitelist: whitelist}
_, err := cl.client.R().EnableTrace().SetBody(ff).Post("/filtering/add_url")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/add_url")
if err != nil {
return err
}
@@ -191,9 +237,21 @@ func (cl *client) AddFilters(whitelist bool, filters ...types.Filter) error {
func (cl *client) DeleteFilters(whitelist bool, filters ...types.Filter) error {
for _, f := range filters {
cl.log.With("url", f.URL, "whitelist", whitelist).Info("Delete filter")
cl.log.With("url", f.URL, "whitelist", whitelist, "enabled", f.Enabled).Info("Delete filter")
ff := &types.Filter{URL: f.URL, Whitelist: whitelist}
_, err := cl.client.R().EnableTrace().SetBody(ff).Post("/filtering/remove_url")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/remove_url")
if err != nil {
return err
}
}
return nil
}
func (cl *client) UpdateFilters(whitelist bool, filters ...types.Filter) error {
for _, f := range filters {
cl.log.With("url", f.URL, "whitelist", whitelist, "enabled", f.Enabled).Info("Update filter")
fu := &types.FilterUpdate{Whitelist: whitelist, URL: f.URL, Data: types.Filter{ID: f.ID, Name: f.Name, URL: f.URL, Whitelist: whitelist, Enabled: f.Enabled}}
err := cl.doPost(cl.client.R().EnableTrace().SetBody(fu), "/filtering/set_url")
if err != nil {
return err
}
@@ -203,53 +261,48 @@ func (cl *client) DeleteFilters(whitelist bool, filters ...types.Filter) error {
func (cl *client) RefreshFilters(whitelist bool) error {
cl.log.With("whitelist", whitelist).Info("Refresh filter")
_, err := cl.client.R().EnableTrace().SetBody(&types.RefreshFilter{Whitelist: whitelist}).Post("/filtering/refresh")
return err
return cl.doPost(cl.client.R().EnableTrace().SetBody(&types.RefreshFilter{Whitelist: whitelist}), "/filtering/refresh")
}
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
return cl.doPost(cl.client.R().EnableTrace().SetBody(&types.Protection{ProtectionEnabled: enable}), "/dns_config")
}
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")
return err
return cl.doPost(cl.client.R().EnableTrace().SetBody(rules.String()), "/filtering/set_rules")
}
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{
return cl.doPost(cl.client.R().EnableTrace().SetBody(&types.FilteringConfig{
EnableConfig: types.EnableConfig{Enabled: enabled},
IntervalConfig: types.IntervalConfig{Interval: interval},
}).Post("/filtering/config")
return err
}), "/filtering/config")
}
func (cl *client) Services() (*types.Services, error) {
svcs := &types.Services{}
_, err := cl.client.R().EnableTrace().SetResult(svcs).Get("/blocked_services/list")
func (cl *client) Services() (types.Services, error) {
svcs := types.Services{}
err := cl.doGet(cl.client.R().EnableTrace().SetResult(&svcs), "/blocked_services/list")
return svcs, err
}
func (cl *client) SetServices(services types.Services) error {
cl.log.With("services", len(services)).Info("Set services")
_, err := cl.client.R().EnableTrace().SetBody(&services).Post("/blocked_services/set")
return err
return cl.doPost(cl.client.R().EnableTrace().SetBody(&services), "/blocked_services/set")
}
func (cl *client) Clients() (*types.Clients, error) {
clients := &types.Clients{}
_, err := cl.client.R().EnableTrace().SetResult(clients).Get("/clients")
err := cl.doGet(cl.client.R().EnableTrace().SetResult(clients), "/clients")
return clients, err
}
func (cl *client) AddClients(clients ...types.Client) error {
for _, client := range clients {
cl.log.With("name", client.Name).Info("Add client")
_, err := cl.client.R().EnableTrace().SetBody(&client).Post("/clients/add")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(&client), "/clients/add")
if err != nil {
return err
}
@@ -260,7 +313,7 @@ func (cl *client) AddClients(clients ...types.Client) error {
func (cl *client) UpdateClients(clients ...types.Client) error {
for _, client := range clients {
cl.log.With("name", client.Name).Info("Update client")
_, err := cl.client.R().EnableTrace().SetBody(&types.ClientUpdate{Name: client.Name, Data: client}).Post("/clients/update")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(&types.ClientUpdate{Name: client.Name, Data: client}), "/clients/update")
if err != nil {
return err
}
@@ -271,7 +324,7 @@ func (cl *client) UpdateClients(clients ...types.Client) error {
func (cl *client) DeleteClients(clients ...types.Client) error {
for _, client := range clients {
cl.log.With("name", client.Name).Info("Delete client")
_, err := cl.client.R().EnableTrace().SetBody(&client).Post("/clients/delete")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(&client), "/clients/delete")
if err != nil {
return err
}
@@ -281,28 +334,52 @@ func (cl *client) DeleteClients(clients ...types.Client) error {
func (cl *client) QueryLogConfig() (*types.QueryLogConfig, error) {
qlc := &types.QueryLogConfig{}
_, err := cl.client.R().EnableTrace().SetResult(qlc).Get("/querylog_info")
err := cl.doGet(cl.client.R().EnableTrace().SetResult(qlc), "/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{
return cl.doPost(cl.client.R().EnableTrace().SetBody(&types.QueryLogConfig{
EnableConfig: types.EnableConfig{Enabled: enabled},
IntervalConfig: types.IntervalConfig{Interval: interval},
AnonymizeClientIP: anonymizeClientIP,
}).Post("/querylog_config")
return err
}), "/querylog_config")
}
func (cl *client) StatsConfig() (*types.IntervalConfig, error) {
stats := &types.IntervalConfig{}
_, err := cl.client.R().EnableTrace().SetResult(stats).Get("/stats_info")
err := cl.doGet(cl.client.R().EnableTrace().SetResult(stats), "/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
return cl.doPost(cl.client.R().EnableTrace().SetBody(&types.IntervalConfig{Interval: interval}), "/stats_config")
}
func (cl *client) Setup() error {
cl.log.Info("Setup new AdguardHome instance")
cfg := &types.InstallConfig{
Web: types.InstallPort{
IP: "0.0.0.0",
Port: 3000,
Status: "",
CanAutofix: false,
},
DNS: types.InstallPort{
IP: "0.0.0.0",
Port: 53,
Status: "",
CanAutofix: false,
},
}
if cl.client.UserInfo != nil {
cfg.Username = cl.client.UserInfo.Username
cfg.Password = cl.client.UserInfo.Password
}
req := cl.client.R().EnableTrace().SetBody(cfg)
req.UserInfo = nil
return cl.doPost(req, "/install/configure")
}

View File

@@ -0,0 +1,13 @@
package client_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestClient(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Client Suite")
}

349
pkg/client/client_test.go Normal file
View File

@@ -0,0 +1,349 @@
package client_test
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"path/filepath"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/bakito/adguardhome-sync/pkg/client"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/google/uuid"
)
var (
username = uuid.NewString()
password = uuid.NewString()
)
var _ = Describe("Client", func() {
var (
cl client.Client
ts *httptest.Server
)
AfterEach(func() {
if ts != nil {
ts.Close()
}
})
Context("Host", func() {
It("should read the current host", func() {
cl, _ := client.New(types.AdGuardInstance{URL: "https://foo.bar:3000"})
host := cl.Host()
Ω(host).Should(Equal("foo.bar:3000"))
})
})
Context("Filtering", func() {
It("should read filtering status", func() {
ts, cl = ClientGet("filtering-status.json", "/filtering/status")
fs, err := cl.Filtering()
Ω(err).ShouldNot(HaveOccurred())
Ω(fs.Enabled).Should(BeTrue())
Ω(fs.Filters).Should(HaveLen(2))
})
It("should enable protection", func() {
ts, cl = ClientPost("/filtering/config", `{"enabled":true,"interval":123}`)
err := cl.ToggleFiltering(true, 123)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable protection", func() {
ts, cl = ClientPost("/filtering/config", `{"enabled":false,"interval":123}`)
err := cl.ToggleFiltering(false, 123)
Ω(err).ShouldNot(HaveOccurred())
})
It("should call RefreshFilters", func() {
ts, cl = ClientPost("/filtering/refresh", `{"whitelist":true}`)
err := cl.RefreshFilters(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should add Filters", func() {
ts, cl = ClientPost("/filtering/add_url",
`{"id":0,"enabled":false,"url":"foo","name":"","rules_count":0,"whitelist":true}`,
`{"id":0,"enabled":false,"url":"bar","name":"","rules_count":0,"whitelist":true}`,
)
err := cl.AddFilters(true, types.Filter{URL: "foo"}, types.Filter{URL: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
It("should update Filters", func() {
ts, cl = ClientPost("/filtering/set_url",
`{"url":"foo","data":{"id":0,"enabled":false,"url":"foo","name":"","rules_count":0,"whitelist":true},"whitelist":true}`,
`{"url":"bar","data":{"id":0,"enabled":false,"url":"bar","name":"","rules_count":0,"whitelist":true},"whitelist":true}`,
)
err := cl.UpdateFilters(true, types.Filter{URL: "foo"}, types.Filter{URL: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
It("should delete Filters", func() {
ts, cl = ClientPost("/filtering/remove_url",
`{"id":0,"enabled":false,"url":"foo","name":"","rules_count":0,"whitelist":true}`,
`{"id":0,"enabled":false,"url":"bar","name":"","rules_count":0,"whitelist":true}`,
)
err := cl.DeleteFilters(true, types.Filter{URL: "foo"}, types.Filter{URL: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("CustomRules", func() {
It("should set SetCustomRules", func() {
ts, cl = ClientPost("/filtering/set_rules", `foo
bar`)
err := cl.SetCustomRules([]string{"foo", "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Status", func() {
It("should read status", func() {
ts, cl = ClientGet("status.json", "/status")
fs, err := cl.Status()
Ω(err).ShouldNot(HaveOccurred())
Ω(fs.DNSAddresses).Should(HaveLen(1))
Ω(fs.DNSAddresses[0]).Should(Equal("192.168.1.2"))
Ω(fs.Version).Should(Equal("v0.105.2"))
})
It("should return SetupNeededError", func() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/install.html")
w.WriteHeader(http.StatusFound)
}))
cl, err := client.New(types.AdGuardInstance{URL: ts.URL})
Ω(err).ShouldNot(HaveOccurred())
_, err = cl.Status()
Ω(err).Should(HaveOccurred())
Ω(err).Should(Equal(client.SetupNeededError))
})
})
Context("Setup", func() {
It("should add setup the instance", func() {
ts, cl = ClientPost("/install/configure", fmt.Sprintf(`{"web":{"ip":"0.0.0.0","port":3000,"status":"","can_autofix":false},"dns":{"ip":"0.0.0.0","port":53,"status":"","can_autofix":false},"username":"%s","password":"%s"}`, username, password))
err := cl.Setup()
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("RewriteList", func() {
It("should read RewriteList", func() {
ts, cl = ClientGet("rewrite-list.json", "/rewrite/list")
rwl, err := cl.RewriteList()
Ω(err).ShouldNot(HaveOccurred())
Ω(*rwl).Should(HaveLen(2))
})
It("should add RewriteList", func() {
ts, cl = ClientPost("/rewrite/add", `{"domain":"foo","answer":"foo"}`, `{"domain":"bar","answer":"bar"}`)
err := cl.AddRewriteEntries(types.RewriteEntry{Answer: "foo", Domain: "foo"}, types.RewriteEntry{Answer: "bar", Domain: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
It("should delete RewriteList", func() {
ts, cl = ClientPost("/rewrite/delete", `{"domain":"foo","answer":"foo"}`, `{"domain":"bar","answer":"bar"}`)
err := cl.DeleteRewriteEntries(types.RewriteEntry{Answer: "foo", Domain: "foo"}, types.RewriteEntry{Answer: "bar", Domain: "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("SafeBrowsing", func() {
It("should read safebrowsing status", func() {
ts, cl = ClientGet("safebrowsing-status.json", "/safebrowsing/status")
sb, err := cl.SafeBrowsing()
Ω(err).ShouldNot(HaveOccurred())
Ω(sb).Should(BeTrue())
})
It("should enable safebrowsing", func() {
ts, cl = ClientPost("/safebrowsing/enable", "")
err := cl.ToggleSafeBrowsing(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable safebrowsing", func() {
ts, cl = ClientPost("/safebrowsing/disable", "")
err := cl.ToggleSafeBrowsing(false)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("SafeSearch", func() {
It("should read safesearch status", func() {
ts, cl = ClientGet("safesearch-status.json", "/safesearch/status")
ss, err := cl.SafeSearch()
Ω(err).ShouldNot(HaveOccurred())
Ω(ss).Should(BeTrue())
})
It("should enable safesearch", func() {
ts, cl = ClientPost("/safesearch/enable", "")
err := cl.ToggleSafeSearch(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable safesearch", func() {
ts, cl = ClientPost("/safesearch/disable", "")
err := cl.ToggleSafeSearch(false)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Parental", func() {
It("should read parental status", func() {
ts, cl = ClientGet("parental-status.json", "/parental/status")
p, err := cl.Parental()
Ω(err).ShouldNot(HaveOccurred())
Ω(p).Should(BeTrue())
})
It("should enable parental", func() {
ts, cl = ClientPost("/parental/enable", "")
err := cl.ToggleParental(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable parental", func() {
ts, cl = ClientPost("/parental/disable", "")
err := cl.ToggleParental(false)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Protection", func() {
It("should enable protection", func() {
ts, cl = ClientPost("/dns_config", `{"protection_enabled":true}`)
err := cl.ToggleProtection(true)
Ω(err).ShouldNot(HaveOccurred())
})
It("should disable protection", func() {
ts, cl = ClientPost("/dns_config", `{"protection_enabled":false}`)
err := cl.ToggleProtection(false)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Services", func() {
It("should read Services", func() {
ts, cl = ClientGet("blockedservices-list.json", "/blocked_services/list")
s, err := cl.Services()
Ω(err).ShouldNot(HaveOccurred())
Ω(s).Should(HaveLen(2))
})
It("should set Services", func() {
ts, cl = ClientPost("/blocked_services/set", `["foo","bar"]`)
err := cl.SetServices([]string{"foo", "bar"})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Clients", func() {
It("should read Clients", func() {
ts, cl = ClientGet("clients.json", "/clients")
c, err := cl.Clients()
Ω(err).ShouldNot(HaveOccurred())
Ω(c.Clients).Should(HaveLen(2))
})
It("should add Clients", func() {
ts, cl = ClientPost("/clients/add",
`{"ids":["id"],"use_global_settings":false,"use_global_blocked_services":false,"name":"foo","filtering_enabled":false,"parental_enabled":false,"safesearch_enabled":false,"safebrowsing_enabled":false,"disallowed":false,"disallowed_rule":""}`,
)
err := cl.AddClients(types.Client{Name: "foo", Ids: []string{"id"}})
Ω(err).ShouldNot(HaveOccurred())
})
It("should update Clients", func() {
ts, cl = ClientPost("/clients/update",
`{"name":"foo","data":{"ids":["id"],"use_global_settings":false,"use_global_blocked_services":false,"name":"foo","filtering_enabled":false,"parental_enabled":false,"safesearch_enabled":false,"safebrowsing_enabled":false,"disallowed":false,"disallowed_rule":""}}`,
)
err := cl.UpdateClients(types.Client{Name: "foo", Ids: []string{"id"}})
Ω(err).ShouldNot(HaveOccurred())
})
It("should delete Clients", func() {
ts, cl = ClientPost("/clients/delete",
`{"ids":["id"],"use_global_settings":false,"use_global_blocked_services":false,"name":"foo","filtering_enabled":false,"parental_enabled":false,"safesearch_enabled":false,"safebrowsing_enabled":false,"disallowed":false,"disallowed_rule":""}`,
)
err := cl.DeleteClients(types.Client{Name: "foo", Ids: []string{"id"}})
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("QueryLogConfig", func() {
It("should read QueryLogConfig", func() {
ts, cl = ClientGet("querylog_info.json", "/querylog_info")
qlc, err := cl.QueryLogConfig()
Ω(err).ShouldNot(HaveOccurred())
Ω(qlc.Enabled).Should(BeTrue())
Ω(qlc.Interval).Should(Equal(90))
})
It("should set QueryLogConfig", func() {
ts, cl = ClientPost("/querylog_config", `{"enabled":true,"interval":123,"anonymize_client_ip":true}`)
err := cl.SetQueryLogConfig(true, 123, true)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("StatsConfig", func() {
It("should read StatsConfig", func() {
ts, cl = ClientGet("stats_info.json", "/stats_info")
sc, err := cl.StatsConfig()
Ω(err).ShouldNot(HaveOccurred())
Ω(sc.Interval).Should(Equal(1))
})
It("should set StatsConfig", func() {
ts, cl = ClientPost("/stats_config", `{"interval":123}`)
err := cl.SetStatsConfig(123)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("helper functions", func() {
var (
cl client.Client
)
BeforeEach(func() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
var err error
cl, err = client.New(types.AdGuardInstance{URL: ts.URL})
Ω(err).ShouldNot(HaveOccurred())
})
Context("doGet", func() {
It("should return an error on status code != 200", func() {
_, err := cl.Status()
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(Equal("401 Unauthorized"))
})
})
Context("doPost", func() {
It("should return an error on status code != 200", func() {
err := cl.SetStatsConfig(123)
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(Equal("401 Unauthorized"))
})
})
})
})
func ClientGet(file string, path string) (*httptest.Server, client.Client) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Ω(r.URL.Path).Should(Equal(types.DefaultAPIPath + path))
b, err := ioutil.ReadFile(filepath.Join("../../testdata", file))
Ω(err).ShouldNot(HaveOccurred())
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(b)
Ω(err).ShouldNot(HaveOccurred())
}))
cl, err := client.New(types.AdGuardInstance{URL: ts.URL})
Ω(err).ShouldNot(HaveOccurred())
return ts, cl
}
func ClientPost(path string, content ...string) (*httptest.Server, client.Client) {
index := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Ω(r.URL.Path).Should(Equal(types.DefaultAPIPath + path))
body, err := ioutil.ReadAll(r.Body)
Ω(err).ShouldNot(HaveOccurred())
Ω(body).Should(Equal([]byte(content[index])))
index++
}))
cl, err := client.New(types.AdGuardInstance{URL: ts.URL, Username: username, Password: password})
Ω(err).ShouldNot(HaveOccurred())
return ts, cl
}

View File

@@ -1,11 +1,16 @@
package log
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const logHistorySize = 50
const (
logHistorySize = 50
envLogLevel = "LOG_LEVEL"
)
var (
rootLogger *zap.Logger
@@ -20,6 +25,12 @@ func GetLogger(name string) *zap.SugaredLogger {
func init() {
level := zap.InfoLevel
if lvl, ok := os.LookupEnv(envLogLevel); ok {
if err := level.Set(lvl); err != nil {
panic(err)
}
}
cfg := zap.Config{
Level: zap.NewAtomicLevelAt(level),
Development: false,

500
pkg/mocks/client/mock.go Normal file
View File

@@ -0,0 +1,500 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/bakito/adguardhome-sync/pkg/client (interfaces: Client)
// Package mock_client is a generated GoMock package.
package mock_client
import (
reflect "reflect"
types "github.com/bakito/adguardhome-sync/pkg/types"
gomock "github.com/golang/mock/gomock"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// AddClients mocks base method.
func (m *MockClient) AddClients(arg0 ...types.Client) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddClients", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AddClients indicates an expected call of AddClients.
func (mr *MockClientMockRecorder) AddClients(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClients", reflect.TypeOf((*MockClient)(nil).AddClients), arg0...)
}
// AddFilters mocks base method.
func (m *MockClient) AddFilters(arg0 bool, arg1 ...types.Filter) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddFilters", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AddFilters indicates an expected call of AddFilters.
func (mr *MockClientMockRecorder) AddFilters(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFilters", reflect.TypeOf((*MockClient)(nil).AddFilters), varargs...)
}
// AddRewriteEntries mocks base method.
func (m *MockClient) AddRewriteEntries(arg0 ...types.RewriteEntry) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddRewriteEntries", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AddRewriteEntries indicates an expected call of AddRewriteEntries.
func (mr *MockClientMockRecorder) AddRewriteEntries(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRewriteEntries", reflect.TypeOf((*MockClient)(nil).AddRewriteEntries), arg0...)
}
// Clients mocks base method.
func (m *MockClient) Clients() (*types.Clients, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Clients")
ret0, _ := ret[0].(*types.Clients)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Clients indicates an expected call of Clients.
func (mr *MockClientMockRecorder) Clients() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clients", reflect.TypeOf((*MockClient)(nil).Clients))
}
// DeleteClients mocks base method.
func (m *MockClient) DeleteClients(arg0 ...types.Client) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteClients", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteClients indicates an expected call of DeleteClients.
func (mr *MockClientMockRecorder) DeleteClients(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteClients", reflect.TypeOf((*MockClient)(nil).DeleteClients), arg0...)
}
// DeleteFilters mocks base method.
func (m *MockClient) DeleteFilters(arg0 bool, arg1 ...types.Filter) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteFilters", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteFilters indicates an expected call of DeleteFilters.
func (mr *MockClientMockRecorder) DeleteFilters(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFilters", reflect.TypeOf((*MockClient)(nil).DeleteFilters), varargs...)
}
// DeleteRewriteEntries mocks base method.
func (m *MockClient) DeleteRewriteEntries(arg0 ...types.RewriteEntry) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteRewriteEntries", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteRewriteEntries indicates an expected call of DeleteRewriteEntries.
func (mr *MockClientMockRecorder) DeleteRewriteEntries(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRewriteEntries", reflect.TypeOf((*MockClient)(nil).DeleteRewriteEntries), arg0...)
}
// Filtering mocks base method.
func (m *MockClient) Filtering() (*types.FilteringStatus, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Filtering")
ret0, _ := ret[0].(*types.FilteringStatus)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Filtering indicates an expected call of Filtering.
func (mr *MockClientMockRecorder) Filtering() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Filtering", reflect.TypeOf((*MockClient)(nil).Filtering))
}
// Host mocks base method.
func (m *MockClient) Host() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Host")
ret0, _ := ret[0].(string)
return ret0
}
// Host indicates an expected call of Host.
func (mr *MockClientMockRecorder) Host() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Host", reflect.TypeOf((*MockClient)(nil).Host))
}
// Parental mocks base method.
func (m *MockClient) Parental() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Parental")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Parental indicates an expected call of Parental.
func (mr *MockClientMockRecorder) Parental() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Parental", reflect.TypeOf((*MockClient)(nil).Parental))
}
// QueryLogConfig mocks base method.
func (m *MockClient) QueryLogConfig() (*types.QueryLogConfig, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryLogConfig")
ret0, _ := ret[0].(*types.QueryLogConfig)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryLogConfig indicates an expected call of QueryLogConfig.
func (mr *MockClientMockRecorder) QueryLogConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryLogConfig", reflect.TypeOf((*MockClient)(nil).QueryLogConfig))
}
// RefreshFilters mocks base method.
func (m *MockClient) RefreshFilters(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RefreshFilters", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// RefreshFilters indicates an expected call of RefreshFilters.
func (mr *MockClientMockRecorder) RefreshFilters(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshFilters", reflect.TypeOf((*MockClient)(nil).RefreshFilters), arg0)
}
// RewriteList mocks base method.
func (m *MockClient) RewriteList() (*types.RewriteEntries, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RewriteList")
ret0, _ := ret[0].(*types.RewriteEntries)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RewriteList indicates an expected call of RewriteList.
func (mr *MockClientMockRecorder) RewriteList() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RewriteList", reflect.TypeOf((*MockClient)(nil).RewriteList))
}
// SafeBrowsing mocks base method.
func (m *MockClient) SafeBrowsing() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SafeBrowsing")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SafeBrowsing indicates an expected call of SafeBrowsing.
func (mr *MockClientMockRecorder) SafeBrowsing() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeBrowsing", reflect.TypeOf((*MockClient)(nil).SafeBrowsing))
}
// SafeSearch mocks base method.
func (m *MockClient) SafeSearch() (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SafeSearch")
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SafeSearch indicates an expected call of SafeSearch.
func (mr *MockClientMockRecorder) SafeSearch() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeSearch", reflect.TypeOf((*MockClient)(nil).SafeSearch))
}
// Services mocks base method.
func (m *MockClient) Services() (types.Services, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Services")
ret0, _ := ret[0].(types.Services)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Services indicates an expected call of Services.
func (mr *MockClientMockRecorder) Services() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Services", reflect.TypeOf((*MockClient)(nil).Services))
}
// SetCustomRules mocks base method.
func (m *MockClient) SetCustomRules(arg0 types.UserRules) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetCustomRules", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetCustomRules indicates an expected call of SetCustomRules.
func (mr *MockClientMockRecorder) SetCustomRules(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCustomRules", reflect.TypeOf((*MockClient)(nil).SetCustomRules), arg0)
}
// SetQueryLogConfig mocks base method.
func (m *MockClient) SetQueryLogConfig(arg0 bool, arg1 int, arg2 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetQueryLogConfig", arg0, arg1, arg2)
ret0, _ := ret[0].(error)
return ret0
}
// SetQueryLogConfig indicates an expected call of SetQueryLogConfig.
func (mr *MockClientMockRecorder) SetQueryLogConfig(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetQueryLogConfig", reflect.TypeOf((*MockClient)(nil).SetQueryLogConfig), arg0, arg1, arg2)
}
// SetServices mocks base method.
func (m *MockClient) SetServices(arg0 types.Services) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetServices", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetServices indicates an expected call of SetServices.
func (mr *MockClientMockRecorder) SetServices(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetServices", reflect.TypeOf((*MockClient)(nil).SetServices), arg0)
}
// SetStatsConfig mocks base method.
func (m *MockClient) SetStatsConfig(arg0 int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetStatsConfig", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetStatsConfig indicates an expected call of SetStatsConfig.
func (mr *MockClientMockRecorder) SetStatsConfig(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStatsConfig", reflect.TypeOf((*MockClient)(nil).SetStatsConfig), arg0)
}
// Setup mocks base method.
func (m *MockClient) Setup() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Setup")
ret0, _ := ret[0].(error)
return ret0
}
// Setup indicates an expected call of Setup.
func (mr *MockClientMockRecorder) Setup() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Setup", reflect.TypeOf((*MockClient)(nil).Setup))
}
// StatsConfig mocks base method.
func (m *MockClient) StatsConfig() (*types.IntervalConfig, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StatsConfig")
ret0, _ := ret[0].(*types.IntervalConfig)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StatsConfig indicates an expected call of StatsConfig.
func (mr *MockClientMockRecorder) StatsConfig() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatsConfig", reflect.TypeOf((*MockClient)(nil).StatsConfig))
}
// Status mocks base method.
func (m *MockClient) Status() (*types.Status, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Status")
ret0, _ := ret[0].(*types.Status)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Status indicates an expected call of Status.
func (mr *MockClientMockRecorder) Status() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockClient)(nil).Status))
}
// ToggleFiltering mocks base method.
func (m *MockClient) ToggleFiltering(arg0 bool, arg1 int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleFiltering", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleFiltering indicates an expected call of ToggleFiltering.
func (mr *MockClientMockRecorder) ToggleFiltering(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleFiltering", reflect.TypeOf((*MockClient)(nil).ToggleFiltering), arg0, arg1)
}
// ToggleParental mocks base method.
func (m *MockClient) ToggleParental(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleParental", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleParental indicates an expected call of ToggleParental.
func (mr *MockClientMockRecorder) ToggleParental(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleParental", reflect.TypeOf((*MockClient)(nil).ToggleParental), arg0)
}
// ToggleProtection mocks base method.
func (m *MockClient) ToggleProtection(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleProtection", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleProtection indicates an expected call of ToggleProtection.
func (mr *MockClientMockRecorder) ToggleProtection(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleProtection", reflect.TypeOf((*MockClient)(nil).ToggleProtection), arg0)
}
// ToggleSafeBrowsing mocks base method.
func (m *MockClient) ToggleSafeBrowsing(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleSafeBrowsing", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleSafeBrowsing indicates an expected call of ToggleSafeBrowsing.
func (mr *MockClientMockRecorder) ToggleSafeBrowsing(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleSafeBrowsing", reflect.TypeOf((*MockClient)(nil).ToggleSafeBrowsing), arg0)
}
// ToggleSafeSearch mocks base method.
func (m *MockClient) ToggleSafeSearch(arg0 bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ToggleSafeSearch", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// ToggleSafeSearch indicates an expected call of ToggleSafeSearch.
func (mr *MockClientMockRecorder) ToggleSafeSearch(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleSafeSearch", reflect.TypeOf((*MockClient)(nil).ToggleSafeSearch), arg0)
}
// UpdateClients mocks base method.
func (m *MockClient) UpdateClients(arg0 ...types.Client) error {
m.ctrl.T.Helper()
varargs := []interface{}{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UpdateClients", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateClients indicates an expected call of UpdateClients.
func (mr *MockClientMockRecorder) UpdateClients(arg0 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateClients", reflect.TypeOf((*MockClient)(nil).UpdateClients), arg0...)
}
// UpdateFilters mocks base method.
func (m *MockClient) UpdateFilters(arg0 bool, arg1 ...types.Filter) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UpdateFilters", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateFilters indicates an expected call of UpdateFilters.
func (mr *MockClientMockRecorder) UpdateFilters(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFilters", reflect.TypeOf((*MockClient)(nil).UpdateFilters), varargs...)
}

View File

@@ -12,6 +12,7 @@ import (
"time"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/version"
)
func (w *worker) handleSync(rw http.ResponseWriter, req *http.Request) {
@@ -61,7 +62,7 @@ func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFu
}
func (w *worker) listenAndServe() {
l.With("port", w.cfg.API.Port).Info("Starting API server")
l.With("version", version.Version, "port", w.cfg.API.Port).Info("Starting API server")
ctx, cancel := context.WithCancel(context.Background())
mux := http.NewServeMux()

View File

@@ -1,9 +1,13 @@
package sync
import (
"errors"
"fmt"
"github.com/bakito/adguardhome-sync/pkg/client"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/version"
"github.com/robfig/cron/v3"
"go.uber.org/zap"
)
@@ -13,19 +17,33 @@ var (
)
// Sync config from origin to replica
func Sync(cfg *types.Config) {
func Sync(cfg *types.Config) error {
if cfg.Origin.URL == "" {
return fmt.Errorf("origin URL is required")
}
if len(cfg.UniqueReplicas()) == 0 {
return fmt.Errorf("no replicas configured")
}
cfg.Origin.AutoSetup = false
w := &worker{
cfg: cfg,
createClient: func(ai types.AdGuardInstance) (client.Client, error) {
return client.New(ai)
},
}
if cfg.Cron != "" {
w.cron = cron.New()
cl := l.With("cron", cfg.Cron)
cl := l.With("version", version.Version, "cron", cfg.Cron)
_, err := w.cron.AddFunc(cfg.Cron, func() {
w.sync()
})
if err != nil {
cl.With("error", err).Error("Error during cron job setup")
return
return err
}
cl.Info("Setup cronjob")
if cfg.API.Port != 0 {
@@ -33,18 +51,22 @@ func Sync(cfg *types.Config) {
} else {
w.cron.Run()
}
} else {
}
if cfg.RunOnStart {
l.With("version", version.Version).Info("Run on startup")
w.sync()
}
if cfg.API.Port != 0 {
w.listenAndServe()
}
return nil
}
type worker struct {
cfg *types.Config
running bool
cron *cron.Cron
cfg *types.Config
running bool
cron *cron.Cron
createClient func(instance types.AdGuardInstance) (client.Client, error)
}
func (w *worker) sync() {
@@ -55,7 +77,7 @@ func (w *worker) sync() {
w.running = true
defer func() { w.running = false }()
oc, err := client.New(w.cfg.Origin)
oc, err := w.createClient(w.cfg.Origin)
if err != nil {
l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client")
return
@@ -64,7 +86,6 @@ func (w *worker) sync() {
sl := l.With("from", oc.Host())
o := &origin{}
o.status, err = oc.Status()
if err != nil {
sl.With("error", err).Error("Error getting origin status")
@@ -128,69 +149,85 @@ func (w *worker) sync() {
func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardInstance) {
rc, err := client.New(replica)
rc, err := w.createClient(replica)
if err != nil {
l.With("error", err, "url", replica.URL).Error("Error creating replica client")
return
}
rl := l.With("to", rc.Host())
rl.Info("Start sync")
rs, err := rc.Status()
rs, err := w.statusWithSetup(rl, replica, rc)
if err != nil {
l.With("error", err).Error("Error getting replica status")
rl.With("error", err).Error("Error getting replica status")
return
}
if o.status.Version != rs.Version {
l.With("originVersion", o.status.Version, "replicaVersion", rs.Version).Warn("Versions do not match")
rl.With("originVersion", o.status.Version, "replicaVersion", rs.Version).Warn("Versions do not match")
}
err = w.syncGeneralSettings(o, rs, rc)
if err != nil {
l.With("error", err).Error("Error syncing general settings")
rl.With("error", err).Error("Error syncing general settings")
return
}
err = w.syncConfigs(o, rs, rc)
err = w.syncConfigs(o, rc)
if err != nil {
l.With("error", err).Error("Error syncing configs")
rl.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")
rl.With("error", err).Error("Error syncing rewrites")
return
}
err = w.syncFilters(o.filters, rc)
if err != nil {
l.With("error", err).Error("Error syncing filters")
rl.With("error", err).Error("Error syncing filters")
return
}
err = w.syncServices(o.services, rc)
if err != nil {
l.With("error", err).Error("Error syncing services")
rl.With("error", err).Error("Error syncing services")
return
}
if err = w.syncClients(o.clients, rc); err != nil {
l.With("error", err).Error("Error syncing clients")
rl.With("error", err).Error("Error syncing clients")
return
}
rl.Info("Sync done")
}
func (w *worker) syncServices(os *types.Services, replica client.Client) error {
func (w *worker) statusWithSetup(rl *zap.SugaredLogger, replica types.AdGuardInstance, rc client.Client) (*types.Status, error) {
rs, err := rc.Status()
if err != nil {
if replica.AutoSetup && errors.Is(err, client.SetupNeededError) {
if serr := rc.Setup(); serr != nil {
rl.With("error", serr).Error("Error setup AdGuardHome")
return nil, err
}
return rc.Status()
}
return nil, err
}
return rs, err
}
func (w *worker) syncServices(os types.Services, replica client.Client) error {
rs, err := replica.Services()
if err != nil {
return err
}
if !os.Equals(rs) {
if err := replica.SetServices(*os); err != nil {
if err := replica.SetServices(os); err != nil {
return err
}
}
@@ -203,34 +240,10 @@ func (w *worker) syncFilters(of *types.FilteringStatus, replica client.Client) e
return err
}
fa, fd := rf.Filters.Merge(of.Filters)
if err = replica.AddFilters(false, fa...); err != nil {
if err = w.syncFilterType(of.Filters, rf.Filters, false, replica); err != nil {
return err
}
if len(fa) > 0 {
if err = replica.RefreshFilters(false); err != nil {
return err
}
}
if err = replica.DeleteFilters(false, fd...); err != nil {
return err
}
fa, fd = rf.WhitelistFilters.Merge(of.WhitelistFilters)
if err = replica.AddFilters(true, fa...); err != nil {
return err
}
if len(fa) > 0 {
if err = replica.RefreshFilters(true); err != nil {
return err
}
}
if err = replica.DeleteFilters(true, fd...); err != nil {
if err = w.syncFilterType(of.WhitelistFilters, rf.WhitelistFilters, true, replica); err != nil {
return err
}
@@ -246,6 +259,28 @@ func (w *worker) syncFilters(of *types.FilteringStatus, replica client.Client) e
return nil
}
func (w *worker) syncFilterType(of types.Filters, rFilters types.Filters, whitelist bool, replica client.Client) error {
fa, fu, fd := rFilters.Merge(of)
if err := replica.AddFilters(whitelist, fa...); err != nil {
return err
}
if err := replica.UpdateFilters(whitelist, fu...); err != nil {
return err
}
if len(fa) > 0 || len(fu) > 0 {
if err := replica.RefreshFilters(whitelist); err != nil {
return err
}
}
if err := replica.DeleteFilters(whitelist, fd...); err != nil {
return err
}
return nil
}
func (w *worker) syncRewrites(or *types.RewriteEntries, replica client.Client) error {
replicaRewrites, err := replica.RewriteList()
@@ -314,7 +349,7 @@ func (w *worker) syncGeneralSettings(o *origin, rs *types.Status, replica client
return nil
}
func (w *worker) syncConfigs(o *origin, rs *types.Status, replica client.Client) error {
func (w *worker) syncConfigs(o *origin, replica client.Client) error {
qlc, err := replica.QueryLogConfig()
if err != nil {
return err
@@ -341,7 +376,7 @@ func (w *worker) syncConfigs(o *origin, rs *types.Status, replica client.Client)
type origin struct {
status *types.Status
rewrites *types.RewriteEntries
services *types.Services
services types.Services
filters *types.FilteringStatus
clients *types.Clients
queryLogConfig *types.QueryLogConfig

View File

@@ -0,0 +1,13 @@
package sync_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestSync(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Sync Suite")
}

419
pkg/sync/sync_test.go Normal file
View File

@@ -0,0 +1,419 @@
package sync
import (
"errors"
"github.com/bakito/adguardhome-sync/pkg/client"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
mc "github.com/bakito/adguardhome-sync/pkg/mocks/client"
"github.com/bakito/adguardhome-sync/pkg/types"
gm "github.com/golang/mock/gomock"
"github.com/google/uuid"
)
var _ = Describe("Sync", func() {
var (
mockCtrl *gm.Controller
cl *mc.MockClient
w *worker
te error
)
BeforeEach(func() {
mockCtrl = gm.NewController(GinkgoT())
cl = mc.NewMockClient(mockCtrl)
w = &worker{
createClient: func(instance types.AdGuardInstance) (client.Client, error) {
return cl, nil
},
}
te = errors.New(uuid.NewString())
})
AfterEach(func() {
defer mockCtrl.Finish()
})
Context("worker", func() {
Context("syncRewrites", func() {
var (
domain string
answer string
reO types.RewriteEntries
reR types.RewriteEntries
)
BeforeEach(func() {
domain = uuid.NewString()
answer = uuid.NewString()
reO = []types.RewriteEntry{{Domain: domain, Answer: answer}}
reR = []types.RewriteEntry{{Domain: domain, Answer: answer}}
})
It("should have no changes (empty slices)", func() {
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries()
err := w.syncRewrites(&reO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should add one rewrite entry", func() {
reR = []types.RewriteEntry{}
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries(reO[0])
cl.EXPECT().DeleteRewriteEntries()
err := w.syncRewrites(&reO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should remove one rewrite entry", func() {
reO = []types.RewriteEntry{}
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries(reR[0])
err := w.syncRewrites(&reO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should remove one rewrite entry", func() {
reO = []types.RewriteEntry{}
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries(reR[0])
err := w.syncRewrites(&reO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should return error when error on RewriteList()", func() {
cl.EXPECT().RewriteList().Return(nil, te)
err := w.syncRewrites(&reO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on AddRewriteEntries()", func() {
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries().Return(te)
err := w.syncRewrites(&reO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on DeleteRewriteEntries()", func() {
cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries().Return(te)
err := w.syncRewrites(&reO, cl)
Ω(err).Should(HaveOccurred())
})
})
Context("syncClients", func() {
var (
clO *types.Clients
clR *types.Clients
name string
)
BeforeEach(func() {
name = uuid.NewString()
clO = &types.Clients{Clients: []types.Client{{Name: name}}}
clR = &types.Clients{Clients: []types.Client{{Name: name}}}
})
It("should have no changes (empty slices)", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients()
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should add one client", func() {
clR.Clients = []types.Client{}
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients(clO.Clients[0])
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients()
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should update one client", func() {
clR.Clients[0].Disallowed = true
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients(clO.Clients[0])
cl.EXPECT().DeleteClients()
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should delete one client", func() {
clO.Clients = []types.Client{}
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients(clR.Clients[0])
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should return error when error on Clients()", func() {
cl.EXPECT().Clients().Return(nil, te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on AddClients()", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients().Return(te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on UpdateClients()", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients().Return(te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on DeleteClients()", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients().Return(te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
})
Context("syncGeneralSettings", func() {
var (
o *origin
rs *types.Status
)
BeforeEach(func() {
o = &origin{
status: &types.Status{},
}
rs = &types.Status{}
})
It("should have no changes", func() {
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have protection enabled changes", func() {
o.status.ProtectionEnabled = true
cl.EXPECT().ToggleProtection(true)
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have parental enabled changes", func() {
o.parental = true
cl.EXPECT().Parental()
cl.EXPECT().ToggleParental(true)
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have safeSearch enabled changes", func() {
o.safeSearch = true
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().ToggleSafeSearch(true)
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have safeBrowsing enabled changes", func() {
o.safeBrowsing = true
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
cl.EXPECT().ToggleSafeBrowsing(true)
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("syncConfigs", func() {
var (
o *origin
qlc *types.QueryLogConfig
sc *types.IntervalConfig
)
BeforeEach(func() {
o = &origin{
queryLogConfig: &types.QueryLogConfig{},
statsConfig: &types.IntervalConfig{},
}
qlc = &types.QueryLogConfig{}
sc = &types.IntervalConfig{}
})
It("should have no changes", func() {
cl.EXPECT().QueryLogConfig().Return(qlc, nil)
cl.EXPECT().StatsConfig().Return(sc, nil)
err := w.syncConfigs(o, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have QueryLogConfig changes", func() {
o.queryLogConfig.Interval = 123
cl.EXPECT().QueryLogConfig().Return(qlc, nil)
cl.EXPECT().SetQueryLogConfig(false, 123, false)
cl.EXPECT().StatsConfig().Return(sc, nil)
err := w.syncConfigs(o, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have StatsConfig changes", func() {
o.statsConfig.Interval = 123
cl.EXPECT().QueryLogConfig().Return(qlc, nil)
cl.EXPECT().StatsConfig().Return(sc, nil)
cl.EXPECT().SetStatsConfig(123)
err := w.syncConfigs(o, cl)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("statusWithSetup", func() {
var (
status *types.Status
inst types.AdGuardInstance
)
BeforeEach(func() {
status = &types.Status{}
inst = types.AdGuardInstance{
AutoSetup: true,
}
})
It("should get the replica status", func() {
cl.EXPECT().Status().Return(status, nil)
st, err := w.statusWithSetup(l, inst, cl)
Ω(err).ShouldNot(HaveOccurred())
Ω(st).Should(Equal(status))
})
It("should runs setup before getting replica status", func() {
cl.EXPECT().Status().Return(nil, client.SetupNeededError)
cl.EXPECT().Setup()
cl.EXPECT().Status().Return(status, nil)
st, err := w.statusWithSetup(l, inst, cl)
Ω(err).ShouldNot(HaveOccurred())
Ω(st).Should(Equal(status))
})
It("should fail on setup", func() {
cl.EXPECT().Status().Return(nil, client.SetupNeededError)
cl.EXPECT().Setup().Return(te)
st, err := w.statusWithSetup(l, inst, cl)
Ω(err).Should(HaveOccurred())
Ω(st).Should(BeNil())
})
})
Context("syncServices", func() {
var (
os types.Services
rs types.Services
)
BeforeEach(func() {
os = []string{"foo"}
rs = []string{"foo"}
})
It("should have no changes", func() {
cl.EXPECT().Services().Return(rs, nil)
err := w.syncServices(os, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have services changes", func() {
os = []string{"bar"}
cl.EXPECT().Services().Return(rs, nil)
cl.EXPECT().SetServices(os)
err := w.syncServices(os, cl)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("syncFilters", func() {
var (
of *types.FilteringStatus
rf *types.FilteringStatus
)
BeforeEach(func() {
of = &types.FilteringStatus{}
rf = &types.FilteringStatus{}
})
It("should have no changes", func() {
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
err := w.syncFilters(of, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have changes user roles", func() {
of.UserRules = []string{"foo"}
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
cl.EXPECT().SetCustomRules(of.UserRules)
err := w.syncFilters(of, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have changed filtering config", func() {
of.Enabled = true
of.Interval = 123
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
cl.EXPECT().ToggleFiltering(of.Enabled, of.Interval)
err := w.syncFilters(of, cl)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("sync", func() {
It("should have no changes", func() {
w.cfg = &types.Config{
Origin: types.AdGuardInstance{},
Replica: types.AdGuardInstance{URL: "foo"},
}
// origin
cl.EXPECT().Host()
cl.EXPECT().Status().Return(&types.Status{}, nil)
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
cl.EXPECT().RewriteList().Return(&types.RewriteEntries{}, nil)
cl.EXPECT().Services()
cl.EXPECT().Filtering().Return(&types.FilteringStatus{}, nil)
cl.EXPECT().Clients().Return(&types.Clients{}, nil)
cl.EXPECT().QueryLogConfig().Return(&types.QueryLogConfig{}, nil)
cl.EXPECT().StatsConfig().Return(&types.IntervalConfig{}, nil)
// replica
cl.EXPECT().Host()
cl.EXPECT().Status().Return(&types.Status{}, nil)
cl.EXPECT().Parental()
cl.EXPECT().SafeSearch()
cl.EXPECT().SafeBrowsing()
cl.EXPECT().QueryLogConfig().Return(&types.QueryLogConfig{}, nil)
cl.EXPECT().StatsConfig().Return(&types.IntervalConfig{}, nil)
cl.EXPECT().RewriteList().Return(&types.RewriteEntries{}, nil)
cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries()
cl.EXPECT().Filtering().Return(&types.FilteringStatus{}, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
cl.EXPECT().Services()
cl.EXPECT().Clients().Return(&types.Clients{}, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients()
w.sync()
})
})
})
})

View File

@@ -7,13 +7,18 @@ import (
"strings"
)
const (
DefaultAPIPath = "/control"
)
// 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"`
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"`
RunOnStart bool `json:"runOnStart,omitempty" yaml:"runOnStart,omitempty"`
API API `json:"api,omitempty" yaml:"api,omitempty"`
}
// API configuration
@@ -26,32 +31,38 @@ type API struct {
// UniqueReplicas get unique replication instances
func (cfg *Config) UniqueReplicas() []AdGuardInstance {
dedup := make(map[string]AdGuardInstance)
if cfg.Replica != nil {
dedup[cfg.Replica.Key()] = *cfg.Replica
if cfg.Replica.URL != "" {
dedup[cfg.Replica.Key()] = cfg.Replica
}
for _, replica := range cfg.Replicas {
dedup[replica.Key()] = replica
if replica.URL != "" {
dedup[replica.Key()] = replica
}
}
var r []AdGuardInstance
for _, replica := range dedup {
if replica.APIPath == "" {
replica.APIPath = DefaultAPIPath
}
r = append(r, replica)
}
return r
}
// AdGuardInstance adguard home config instance
// AdGuardInstance AdguardHome config instance
type AdGuardInstance struct {
URL string `json:"url" yaml:"url"`
APIPath string `json:"apiPath,omitempty" yaml:"apiPath,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
Password string `json:"password,omitempty" yaml:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"`
AutoSetup bool `json:"autoSetup" yaml:"autoSetup"`
}
// Key AdGuardInstance key
func (i *AdGuardInstance) Key() string {
return fmt.Sprintf("%s%s", i.URL, i.APIPath)
return fmt.Sprintf("%s#%s", i.URL, i.APIPath)
}
// Protection API struct
@@ -113,6 +124,35 @@ func (re *RewriteEntry) Key() string {
// Filters list of Filter
type Filters []Filter
// Merge merge Filters
func (f Filters) Merge(other Filters) (Filters, Filters, Filters) {
current := make(map[string]Filter)
var adds Filters
var updates Filters
var removes Filters
for _, f := range f {
current[f.URL] = f
}
for _, rr := range other {
if c, ok := current[rr.URL]; ok {
if !c.Equals(&rr) {
updates = append(updates, rr)
}
delete(current, rr.URL)
} else {
adds = append(adds, rr)
}
}
for _, rr := range current {
removes = append(removes, rr)
}
return adds, updates, removes
}
// Filter API struct
type Filter struct {
ID int `json:"id"`
@@ -123,6 +163,18 @@ type Filter struct {
Whitelist bool `json:"whitelist"` // needed for add
}
// Equals Filter equal check
func (f *Filter) Equals(o *Filter) bool {
return f.Enabled == o.Enabled && f.URL == o.URL && f.Name == o.Name
}
// FilterUpdate API struct
type FilterUpdate struct {
URL string `json:"url"`
Data Filter `json:"data"`
Whitelist bool `json:"whitelist"`
}
// FilteringStatus API struct
type FilteringStatus struct {
FilteringConfig
@@ -172,31 +224,6 @@ type RefreshFilter struct {
Whitelist bool `json:"whitelist"`
}
// Merge merge RefreshFilters
func (fs *Filters) Merge(other Filters) (Filters, Filters) {
current := make(map[string]Filter)
var adds Filters
var removes Filters
for _, f := range *fs {
current[f.URL] = f
}
for _, rr := range other {
if _, ok := current[rr.URL]; ok {
delete(current, rr.URL)
} else {
adds = append(adds, rr)
}
}
for _, rr := range current {
removes = append(removes, rr)
}
return adds, removes
}
// Services API struct
type Services []string
@@ -206,10 +233,10 @@ func (s Services) Sort() {
}
// Equals Services equal check
func (s *Services) Equals(o *Services) bool {
func (s Services) Equals(o Services) bool {
s.Sort()
o.Sort()
return equals(*s, *o)
return equals(s, o)
}
// Clients API struct
@@ -227,10 +254,10 @@ type Clients struct {
// Client API struct
type Client struct {
Ids []string `json:"ids"`
Tags []string `json:"tags"`
BlockedServices []string `json:"blocked_services"`
Upstreams []string `json:"upstreams"`
Ids []string `json:"ids,omitempty"`
Tags []string `json:"tags,omitempty"`
BlockedServices []string `json:"blocked_services,omitempty"`
Upstreams []string `json:"upstreams,omitempty"`
UseGlobalSettings bool `json:"use_global_settings"`
UseGlobalBlockedServices bool `json:"use_global_blocked_services"`
@@ -312,3 +339,19 @@ func equals(a []string, b []string) bool {
}
return true
}
// InstallConfig AdguardHome install config
type InstallConfig struct {
Web InstallPort `json:"web"`
DNS InstallPort `json:"dns"`
Username string `json:"username"`
Password string `json:"password"`
}
// InstallPort AdguardHome install config port
type InstallPort struct {
IP string `json:"ip"`
Port int `json:"port"`
Status string `json:"status"`
CanAutofix bool `json:"can_autofix"`
}

View File

@@ -2,14 +2,25 @@ package types_test
import (
"encoding/json"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"io/ioutil"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/google/uuid"
)
var _ = Describe("Types", func() {
var (
url string
apiPath string
)
BeforeEach(func() {
url = "https://" + uuid.NewString()
apiPath = "/" + uuid.NewString()
})
Context("FilteringStatus", func() {
It("should correctly parse json", func() {
b, err := ioutil.ReadFile("../..//testdata/filtering-status.json")
@@ -19,4 +30,279 @@ var _ = Describe("Types", func() {
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("Filters", func() {
Context("Merge", func() {
var (
originFilters types.Filters
replicaFilters types.Filters
)
BeforeEach(func() {
originFilters = types.Filters{}
replicaFilters = types.Filters{}
})
It("should add a missing filter", func() {
originFilters = append(originFilters, types.Filter{URL: url})
a, u, d := replicaFilters.Merge(originFilters)
Ω(a).Should(HaveLen(1))
Ω(u).Should(BeEmpty())
Ω(d).Should(BeEmpty())
Ω(a[0].URL).Should(Equal(url))
})
It("should remove additional filter", func() {
replicaFilters = append(replicaFilters, types.Filter{URL: url})
a, u, d := replicaFilters.Merge(originFilters)
Ω(a).Should(BeEmpty())
Ω(u).Should(BeEmpty())
Ω(d).Should(HaveLen(1))
Ω(d[0].URL).Should(Equal(url))
})
It("should update existing filter when enabled differs", func() {
enabled := true
originFilters = append(originFilters, types.Filter{URL: url, Enabled: enabled})
replicaFilters = append(replicaFilters, types.Filter{URL: url, Enabled: !enabled})
a, u, d := replicaFilters.Merge(originFilters)
Ω(a).Should(BeEmpty())
Ω(u).Should(HaveLen(1))
Ω(d).Should(BeEmpty())
Ω(u[0].Enabled).Should(Equal(enabled))
})
It("should update existing filter when name differs", func() {
name1 := uuid.NewString()
name2 := uuid.NewString()
originFilters = append(originFilters, types.Filter{URL: url, Name: name1})
replicaFilters = append(replicaFilters, types.Filter{URL: url, Name: name2})
a, u, d := replicaFilters.Merge(originFilters)
Ω(a).Should(BeEmpty())
Ω(u).Should(HaveLen(1))
Ω(d).Should(BeEmpty())
Ω(u[0].Name).Should(Equal(name1))
})
It("should have no changes", func() {
originFilters = append(originFilters, types.Filter{URL: url})
replicaFilters = append(replicaFilters, types.Filter{URL: url})
a, u, d := replicaFilters.Merge(originFilters)
Ω(a).Should(BeEmpty())
Ω(u).Should(BeEmpty())
Ω(d).Should(BeEmpty())
})
})
})
Context("AdGuardInstance", func() {
It("should build a key with url and api apiPath", func() {
i := &types.AdGuardInstance{URL: url, APIPath: apiPath}
Ω(i.Key()).Should(Equal(url + "#" + apiPath))
})
})
Context("RewriteEntry", func() {
It("should build a key with url and api apiPath", func() {
domain := uuid.NewString()
answer := uuid.NewString()
re := &types.RewriteEntry{Domain: domain, Answer: answer}
Ω(re.Key()).Should(Equal(domain + "#" + answer))
})
})
Context("QueryLogConfig", func() {
Context("Equal", func() {
var (
a *types.QueryLogConfig
b *types.QueryLogConfig
)
BeforeEach(func() {
a = &types.QueryLogConfig{}
b = &types.QueryLogConfig{}
})
It("should be equal", func() {
a.Enabled = true
a.Interval = 1
a.AnonymizeClientIP = true
b.Enabled = true
b.Interval = 1
b.AnonymizeClientIP = true
Ω(a.Equals(b)).Should(BeTrue())
})
It("should not be equal when enabled differs", func() {
a.Enabled = true
b.Enabled = false
Ω(a.Equals(b)).ShouldNot(BeTrue())
})
It("should not be equal when interval differs", func() {
a.Interval = 1
b.Interval = 2
Ω(a.Equals(b)).ShouldNot(BeTrue())
})
It("should not be equal when anonymizeClientIP differs", func() {
a.AnonymizeClientIP = true
b.AnonymizeClientIP = false
Ω(a.Equals(b)).ShouldNot(BeTrue())
})
})
})
Context("RewriteEntries", func() {
Context("Merge", func() {
var (
originRE types.RewriteEntries
replicaRE types.RewriteEntries
domain string
)
BeforeEach(func() {
originRE = types.RewriteEntries{}
replicaRE = types.RewriteEntries{}
domain = uuid.NewString()
})
It("should add a missing rewrite entry", func() {
originRE = append(originRE, types.RewriteEntry{Domain: domain})
a, d := replicaRE.Merge(&originRE)
Ω(a).Should(HaveLen(1))
Ω(d).Should(BeEmpty())
Ω(a[0].Domain).Should(Equal(domain))
})
It("should remove additional ewrite entry", func() {
replicaRE = append(replicaRE, types.RewriteEntry{Domain: domain})
a, d := replicaRE.Merge(&originRE)
Ω(a).Should(BeEmpty())
Ω(d).Should(HaveLen(1))
Ω(d[0].Domain).Should(Equal(domain))
})
It("should have no changes", func() {
originRE = append(originRE, types.RewriteEntry{Domain: domain})
replicaRE = append(replicaRE, types.RewriteEntry{Domain: domain})
a, d := replicaRE.Merge(&originRE)
Ω(a).Should(BeEmpty())
Ω(d).Should(BeEmpty())
})
})
})
Context("UserRules", func() {
It("should join the rules correctly", func() {
r1 := uuid.NewString()
r2 := uuid.NewString()
ur := types.UserRules([]string{r1, r2})
Ω(ur.String()).Should(Equal(r1 + "\n" + r2))
})
})
Context("Config", func() {
var (
cfg *types.Config
)
BeforeEach(func() {
cfg = &types.Config{}
})
Context("UniqueReplicas", func() {
It("should be empty if noting defined", func() {
r := cfg.UniqueReplicas()
Ω(r).Should(BeEmpty())
})
It("should be empty if replica url is not set", func() {
cfg.Replica = types.AdGuardInstance{URL: ""}
r := cfg.UniqueReplicas()
Ω(r).Should(BeEmpty())
})
It("should be empty if replicas url is not set", func() {
cfg.Replicas = []types.AdGuardInstance{{URL: ""}}
r := cfg.UniqueReplicas()
Ω(r).Should(BeEmpty())
})
It("should return only one replica if same url and apiPath", func() {
cfg.Replica = types.AdGuardInstance{URL: url, APIPath: apiPath}
cfg.Replicas = []types.AdGuardInstance{{URL: url, APIPath: apiPath}, {URL: url, APIPath: apiPath}}
r := cfg.UniqueReplicas()
Ω(r).Should(HaveLen(1))
})
It("should return 3 one replicas if urls are different", func() {
cfg.Replica = types.AdGuardInstance{URL: url, APIPath: apiPath}
cfg.Replicas = []types.AdGuardInstance{{URL: url + "1", APIPath: apiPath}, {URL: url, APIPath: apiPath + "1"}}
r := cfg.UniqueReplicas()
Ω(r).Should(HaveLen(3))
})
It("should set default api apiPath if not set", func() {
cfg.Replica = types.AdGuardInstance{URL: url}
cfg.Replicas = []types.AdGuardInstance{{URL: url + "1"}}
r := cfg.UniqueReplicas()
Ω(r).Should(HaveLen(2))
Ω(r[0].APIPath).Should(Equal(types.DefaultAPIPath))
Ω(r[1].APIPath).Should(Equal(types.DefaultAPIPath))
})
})
})
Context("Clients", func() {
Context("Merge", func() {
var (
originClients *types.Clients
replicaClients types.Clients
name string
)
BeforeEach(func() {
originClients = &types.Clients{}
replicaClients = types.Clients{}
name = uuid.NewString()
})
It("should add a missing client", func() {
originClients.Clients = append(originClients.Clients, types.Client{Name: name})
a, u, d := replicaClients.Merge(originClients)
Ω(a).Should(HaveLen(1))
Ω(u).Should(BeEmpty())
Ω(d).Should(BeEmpty())
Ω(a[0].Name).Should(Equal(name))
})
It("should remove additional client", func() {
replicaClients.Clients = append(replicaClients.Clients, types.Client{Name: name})
a, u, d := replicaClients.Merge(originClients)
Ω(a).Should(BeEmpty())
Ω(u).Should(BeEmpty())
Ω(d).Should(HaveLen(1))
Ω(d[0].Name).Should(Equal(name))
})
It("should update existing client when name differs", func() {
disallowed := true
originClients.Clients = append(originClients.Clients, types.Client{Name: name, Disallowed: disallowed})
replicaClients.Clients = append(replicaClients.Clients, types.Client{Name: name, Disallowed: !disallowed})
a, u, d := replicaClients.Merge(originClients)
Ω(a).Should(BeEmpty())
Ω(u).Should(HaveLen(1))
Ω(d).Should(BeEmpty())
Ω(u[0].Disallowed).Should(Equal(disallowed))
})
})
})
Context("Services", func() {
Context("Equals", func() {
It("should be equal", func() {
s1 := types.Services([]string{"a", "b"})
s2 := types.Services([]string{"b", "a"})
Ω(s1.Equals(s2)).Should(BeTrue())
})
It("should not be equal different values", func() {
s1 := types.Services([]string{"a", "b"})
s2 := types.Services([]string{"B", "a"})
Ω(s1.Equals(s2)).ShouldNot(BeTrue())
})
It("should not be equal different length", func() {
s1 := types.Services([]string{"a", "b"})
s2 := types.Services([]string{"b", "a", "c"})
Ω(s1.Equals(s2)).ShouldNot(BeTrue())
})
})
})
})

4
testdata/blockedservices-list.json vendored Normal file
View File

@@ -0,0 +1,4 @@
[
"9gag",
"dailymotion"
]

81
testdata/clients.json vendored Normal file
View File

@@ -0,0 +1,81 @@
{
"clients": [
{
"ids": [
"192.168.1.3"
],
"tags": [
"device_pc"
],
"name": "PC",
"use_global_settings": true,
"filtering_enabled": false,
"parental_enabled": false,
"safesearch_enabled": false,
"safebrowsing_enabled": false,
"use_global_blocked_services": true,
"blocked_services": null,
"upstreams": null,
"whois_info": null,
"disallowed": false,
"disallowed_rule": ""
},
{
"ids": [
"192.168.1.2"
],
"tags": [
"device_phone"
],
"name": "Phone LAN",
"use_global_settings": true,
"filtering_enabled": false,
"parental_enabled": false,
"safesearch_enabled": false,
"safebrowsing_enabled": false,
"use_global_blocked_services": false,
"blocked_services": [
"facebook",
"ok",
"vk",
"mail_ru",
"qq"
],
"upstreams": [],
"whois_info": null,
"disallowed": false,
"disallowed_rule": ""
}
],
"auto_clients": [
{
"ip": "127.0.0.1",
"name": "localhost",
"source": "etc/hosts",
"whois_info": {}
}
],
"supported_tags": [
"device_audio",
"device_camera",
"device_gameconsole",
"device_laptop",
"device_nas",
"device_other",
"device_pc",
"device_phone",
"device_printer",
"device_securityalarm",
"device_tablet",
"device_tv",
"os_android",
"os_ios",
"os_linux",
"os_macos",
"os_other",
"os_windows",
"user_admin",
"user_child",
"user_regular"
]
}

3
testdata/parental-status.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"enabled": true
}

5
testdata/querylog_info.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"enabled": true,
"interval": 90,
"anonymize_client_ip": false
}

10
testdata/rewrite-list.json vendored Normal file
View File

@@ -0,0 +1,10 @@
[
{
"domain": "foo.com",
"answer": "192.168.1.10"
},
{
"domain": "bar.com",
"answer": "192.168.1.12"
}
]

3
testdata/safebrowsing-status.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"enabled": true
}

3
testdata/safesearch-status.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"enabled": true
}

3
testdata/stats_info.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"interval": 1
}

12
testdata/status.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"dns_addresses": [
"192.168.1.2"
],
"dns_port": 53,
"http_port": 45158,
"protection_enabled": true,
"dhcp_available": true,
"running": true,
"version": "v0.105.2",
"language": "en"
}