mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2025-03-31 22:43:31 +03:00
all: sync with master
This commit is contained in:
parent
54f3a5f990
commit
3f95db98d3
143 changed files with 3476 additions and 2959 deletions
.github/workflows
.gitignoreCHANGELOG.mdMakefileREADME.mdbamboo-specs
client/src
__locales
components/SetupGuide
helpers
internal
aghhttp
aghos
os.goos_unix.goos_windows.gopermission.gopermission_unix.gopermission_windows.gopermission_windows_internal_test.go
aghrenameio
aghtest
client
dhcpd
dhcpsvc
dnsforward
filtering
home
auth.goclientshttp.goconfig.gocontrol.gocontrolupdate.godns.gohome.gomobileconfig.gooptions.goservice.goweb.go
next
agh
cmd
configmgr
dnssvc
websvc
permcheck
check_unix.gocheck_windows.gomigrate.gomigrate_unix.gomigrate_windows.gopermcheck.gosecurity_unix.gosecurity_windows.go
querylog
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -1,7 +1,7 @@
|
||||||
'name': 'build'
|
'name': 'build'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.23.2'
|
'GO_VERSION': '1.23.4'
|
||||||
'NODE_VERSION': '16'
|
'NODE_VERSION': '16'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
|
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
@ -1,7 +1,7 @@
|
||||||
'name': 'lint'
|
'name': 'lint'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.23.2'
|
'GO_VERSION': '1.23.4'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
'push':
|
'push':
|
||||||
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,3 +1,8 @@
|
||||||
|
# This comment is used to simplify checking local copies of the file. Bump
|
||||||
|
# this number every time a significant change is made to this file.
|
||||||
|
#
|
||||||
|
# AdGuard-Project-Version: 1
|
||||||
|
|
||||||
# Please, DO NOT put your text editors' temporary files here. The more are
|
# Please, DO NOT put your text editors' temporary files here. The more are
|
||||||
# added, the harder it gets to maintain and manage projects' gitignores. Put
|
# added, the harder it gets to maintain and manage projects' gitignores. Put
|
||||||
# them into your global gitignore file instead.
|
# them into your global gitignore file instead.
|
||||||
|
@ -8,6 +13,7 @@
|
||||||
# bottom to make sure they take effect.
|
# bottom to make sure they take effect.
|
||||||
*.db
|
*.db
|
||||||
*.log
|
*.log
|
||||||
|
*.out
|
||||||
*.snap
|
*.snap
|
||||||
*.test
|
*.test
|
||||||
/agh-backup/
|
/agh-backup/
|
||||||
|
@ -21,6 +27,7 @@
|
||||||
/launchpad_credentials
|
/launchpad_credentials
|
||||||
/querylog.json*
|
/querylog.json*
|
||||||
/snapcraft_login
|
/snapcraft_login
|
||||||
|
/test-reports/
|
||||||
AdGuardHome
|
AdGuardHome
|
||||||
AdGuardHome.exe
|
AdGuardHome.exe
|
||||||
AdGuardHome.yaml*
|
AdGuardHome.yaml*
|
||||||
|
|
47
CHANGELOG.md
47
CHANGELOG.md
|
@ -16,15 +16,49 @@ TODO(a.garipov): Use the common markdown formatting tools.
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
## [v0.107.55] - 2024-11-09 (APPROX.)
|
## [v0.108.0] – TBA
|
||||||
|
|
||||||
See also the [v0.107.55 GitHub milestone][ms-v0.107.55].
|
## [v0.107.56] - 2025-01-10 (APPROX.)
|
||||||
|
|
||||||
[ms-v0.107.55]: https://github.com/AdguardTeam/AdGuardHome/milestone/90?closed=1
|
See also the [v0.107.56 GitHub milestone][ms-v0.107.56].
|
||||||
|
|
||||||
|
[ms-v0.107.56]: https://github.com/AdguardTeam/AdGuardHome/milestone/91?closed=1
|
||||||
|
|
||||||
NOTE: Add new changes BELOW THIS COMMENT.
|
NOTE: Add new changes BELOW THIS COMMENT.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## [v0.107.55] - 2024-12-05
|
||||||
|
|
||||||
|
See also the [v0.107.55 GitHub milestone][ms-v0.107.55].
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- The permission check and migration on Windows has been fixed to use the
|
||||||
|
Windows security model more accurately ([#7400]).
|
||||||
|
- Go version has been updated to prevent the possibility of exploiting the Go
|
||||||
|
vulnerabilities fixed in [1.23.4][go-1.23.4].
|
||||||
|
- The release executables are now signed.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- The `--no-permcheck` command-line option to disable checking and migration of
|
||||||
|
permissions for the security-sensitive files and directories, which caused
|
||||||
|
issues on Windows ([#7400]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Setup guide styles in Firefox.
|
||||||
|
- Goroutine leak during the upstream DNS server test ([#7357]).
|
||||||
|
- Goroutine leak during configuration update resulting in increased response
|
||||||
|
time ([#6818]).
|
||||||
|
|
||||||
|
[#6818]: https://github.com/AdguardTeam/AdGuardHome/issues/6818
|
||||||
|
[#7357]: https://github.com/AdguardTeam/AdGuardHome/issues/7357
|
||||||
|
[#7400]: https://github.com/AdguardTeam/AdGuardHome/issues/7400
|
||||||
|
|
||||||
|
[go-1.23.4]: https://groups.google.com/g/golang-announce/c/3DyiMkYx4Fo
|
||||||
|
[ms-v0.107.55]: https://github.com/AdguardTeam/AdGuardHome/milestone/90?closed=1
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||||
-->
|
-->
|
||||||
|
@ -3171,11 +3205,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.55...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.56...HEAD
|
||||||
[v0.107.55]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.54...v0.107.55
|
[v0.107.55]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.55...v0.107.56
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.54...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.55...HEAD
|
||||||
|
[v0.107.55]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.54...v0.107.55
|
||||||
[v0.107.54]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.53...v0.107.54
|
[v0.107.54]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.53...v0.107.54
|
||||||
[v0.107.53]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.52...v0.107.53
|
[v0.107.53]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.52...v0.107.53
|
||||||
[v0.107.52]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.51...v0.107.52
|
[v0.107.52]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.51...v0.107.52
|
||||||
|
|
35
Makefile
35
Makefile
|
@ -1,14 +1,14 @@
|
||||||
# Keep the Makefile POSIX-compliant. We currently allow hyphens in
|
# Keep the Makefile POSIX-compliant. We currently allow hyphens in
|
||||||
# target names, but that may change in the future.
|
# target names, but that may change in the future.
|
||||||
#
|
#
|
||||||
# See https://pubs.opengroup.org/onlinepubs/9699919799/utilities/make.html.
|
# See https://pubs.opengroup.org/onlinepubs/9799919799/utilities/make.html.
|
||||||
.POSIX:
|
.POSIX:
|
||||||
|
|
||||||
# This comment is used to simplify checking local copies of the
|
# This comment is used to simplify checking local copies of the
|
||||||
# Makefile. Bump this number every time a significant change is made to
|
# Makefile. Bump this number every time a significant change is made to
|
||||||
# this Makefile.
|
# this Makefile.
|
||||||
#
|
#
|
||||||
# AdGuard-Project-Version: 6
|
# AdGuard-Project-Version: 9
|
||||||
|
|
||||||
# Don't name these macros "GO" etc., because GNU Make apparently makes
|
# Don't name these macros "GO" etc., because GNU Make apparently makes
|
||||||
# them exported environment variables with the literal value of
|
# them exported environment variables with the literal value of
|
||||||
|
@ -22,13 +22,12 @@ VERBOSE.MACRO = $${VERBOSE:-0}
|
||||||
|
|
||||||
CHANNEL = development
|
CHANNEL = development
|
||||||
CLIENT_DIR = client
|
CLIENT_DIR = client
|
||||||
COMMIT = $$( git rev-parse --short HEAD )
|
|
||||||
DEPLOY_SCRIPT_PATH = not/a/real/path
|
DEPLOY_SCRIPT_PATH = not/a/real/path
|
||||||
DIST_DIR = dist
|
DIST_DIR = dist
|
||||||
GOAMD64 = v1
|
GOAMD64 = v1
|
||||||
GOPROXY = https://proxy.golang.org|direct
|
GOPROXY = https://proxy.golang.org|direct
|
||||||
GOTOOLCHAIN = go1.23.2
|
|
||||||
GOTELEMETRY = off
|
GOTELEMETRY = off
|
||||||
|
GOTOOLCHAIN = go1.23.4
|
||||||
GPG_KEY = devteam@adguard.com
|
GPG_KEY = devteam@adguard.com
|
||||||
GPG_KEY_PASSPHRASE = not-a-real-password
|
GPG_KEY_PASSPHRASE = not-a-real-password
|
||||||
NPM = npm
|
NPM = npm
|
||||||
|
@ -36,6 +35,7 @@ NPM_FLAGS = --prefix $(CLIENT_DIR)
|
||||||
NPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress --ignore-engines\
|
NPM_INSTALL_FLAGS = $(NPM_FLAGS) --quiet --no-progress --ignore-engines\
|
||||||
--ignore-optional --ignore-platform --ignore-scripts
|
--ignore-optional --ignore-platform --ignore-scripts
|
||||||
RACE = 0
|
RACE = 0
|
||||||
|
REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
|
||||||
SIGN = 1
|
SIGN = 1
|
||||||
SIGNER_API_KEY = not-a-real-key
|
SIGNER_API_KEY = not-a-real-key
|
||||||
VERSION = v0.0.0
|
VERSION = v0.0.0
|
||||||
|
@ -60,7 +60,6 @@ BUILD_RELEASE_DEPS_1 = go-deps
|
||||||
|
|
||||||
ENV = env\
|
ENV = env\
|
||||||
CHANNEL='$(CHANNEL)'\
|
CHANNEL='$(CHANNEL)'\
|
||||||
COMMIT='$(COMMIT)'\
|
|
||||||
DEPLOY_SCRIPT_PATH='$(DEPLOY_SCRIPT_PATH)' \
|
DEPLOY_SCRIPT_PATH='$(DEPLOY_SCRIPT_PATH)' \
|
||||||
DIST_DIR='$(DIST_DIR)'\
|
DIST_DIR='$(DIST_DIR)'\
|
||||||
GO="$(GO.MACRO)"\
|
GO="$(GO.MACRO)"\
|
||||||
|
@ -70,17 +69,19 @@ ENV = env\
|
||||||
GOTOOLCHAIN='$(GOTOOLCHAIN)'\
|
GOTOOLCHAIN='$(GOTOOLCHAIN)'\
|
||||||
GPG_KEY='$(GPG_KEY)'\
|
GPG_KEY='$(GPG_KEY)'\
|
||||||
GPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)'\
|
GPG_KEY_PASSPHRASE='$(GPG_KEY_PASSPHRASE)'\
|
||||||
|
NEXTAPI='$(NEXTAPI)'\
|
||||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||||
RACE='$(RACE)'\
|
RACE='$(RACE)'\
|
||||||
|
REVISION='$(REVISION)'\
|
||||||
SIGN='$(SIGN)'\
|
SIGN='$(SIGN)'\
|
||||||
SIGNER_API_KEY='$(SIGNER_API_KEY)' \
|
SIGNER_API_KEY='$(SIGNER_API_KEY)' \
|
||||||
NEXTAPI='$(NEXTAPI)'\
|
|
||||||
VERBOSE="$(VERBOSE.MACRO)"\
|
VERBOSE="$(VERBOSE.MACRO)"\
|
||||||
VERSION="$(VERSION)"\
|
VERSION="$(VERSION)"\
|
||||||
|
|
||||||
# Keep the line above blank.
|
# Keep the line above blank.
|
||||||
|
|
||||||
ENV_MISC = env\
|
ENV_MISC = env\
|
||||||
|
PATH="$${PWD}/bin:$$("$(GO.MACRO)" env GOPATH)/bin:$${PATH}"\
|
||||||
VERBOSE="$(VERBOSE.MACRO)"\
|
VERBOSE="$(VERBOSE.MACRO)"\
|
||||||
|
|
||||||
# Keep the line above blank.
|
# Keep the line above blank.
|
||||||
|
@ -89,6 +90,8 @@ ENV_MISC = env\
|
||||||
# full build.
|
# full build.
|
||||||
build: deps quick-build
|
build: deps quick-build
|
||||||
|
|
||||||
|
init: ; git config core.hooksPath ./scripts/hooks
|
||||||
|
|
||||||
quick-build: js-build go-build
|
quick-build: js-build go-build
|
||||||
|
|
||||||
deps: js-deps go-deps
|
deps: js-deps go-deps
|
||||||
|
@ -102,9 +105,6 @@ build-docker: ; $(ENV) "$(SHELL)" ./scripts/make/build-docker.sh
|
||||||
build-release: $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT))
|
build-release: $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT))
|
||||||
$(ENV) "$(SHELL)" ./scripts/make/build-release.sh
|
$(ENV) "$(SHELL)" ./scripts/make/build-release.sh
|
||||||
|
|
||||||
clean: ; $(ENV) "$(SHELL)" ./scripts/make/clean.sh
|
|
||||||
init: ; git config core.hooksPath ./scripts/hooks
|
|
||||||
|
|
||||||
js-build: ; $(NPM) $(NPM_FLAGS) run build-prod
|
js-build: ; $(NPM) $(NPM_FLAGS) run build-prod
|
||||||
js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
|
js-deps: ; $(NPM) $(NPM_INSTALL_FLAGS) ci
|
||||||
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
js-lint: ; $(NPM) $(NPM_FLAGS) run lint
|
||||||
|
@ -127,17 +127,16 @@ go-check: go-tools go-lint go-test
|
||||||
# A quick check to make sure that all operating systems relevant to the
|
# A quick check to make sure that all operating systems relevant to the
|
||||||
# development of the project can be typechecked and built successfully.
|
# development of the project can be typechecked and built successfully.
|
||||||
go-os-check:
|
go-os-check:
|
||||||
env GOOS='darwin' "$(GO.MACRO)" vet ./internal/...
|
$(ENV) GOOS='darwin' "$(GO.MACRO)" vet ./internal/...
|
||||||
env GOOS='freebsd' "$(GO.MACRO)" vet ./internal/...
|
$(ENV) GOOS='freebsd' "$(GO.MACRO)" vet ./internal/...
|
||||||
env GOOS='openbsd' "$(GO.MACRO)" vet ./internal/...
|
$(ENV) GOOS='openbsd' "$(GO.MACRO)" vet ./internal/...
|
||||||
env GOOS='linux' "$(GO.MACRO)" vet ./internal/...
|
$(ENV) GOOS='linux' "$(GO.MACRO)" vet ./internal/...
|
||||||
env GOOS='windows' "$(GO.MACRO)" vet ./internal/...
|
$(ENV) GOOS='windows' "$(GO.MACRO)" vet ./internal/...
|
||||||
|
|
||||||
|
|
||||||
openapi-lint: ; cd ./openapi/ && $(YARN) test
|
|
||||||
openapi-show: ; cd ./openapi/ && $(YARN) start
|
|
||||||
|
|
||||||
txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
|
txt-lint: ; $(ENV) "$(SHELL)" ./scripts/make/txt-lint.sh
|
||||||
|
|
||||||
md-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/md-lint.sh
|
md-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/md-lint.sh
|
||||||
sh-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/sh-lint.sh
|
sh-lint: ; $(ENV_MISC) "$(SHELL)" ./scripts/make/sh-lint.sh
|
||||||
|
|
||||||
|
openapi-lint: ; cd ./openapi/ && $(YARN) test
|
||||||
|
openapi-show: ; cd ./openapi/ && $(YARN) start
|
||||||
|
|
|
@ -114,7 +114,7 @@ If you're running **Linux,** there's a secure and easy way to install AdGuard Ho
|
||||||
|
|
||||||
[Docker Hub]: https://hub.docker.com/r/adguard/adguardhome
|
[Docker Hub]: https://hub.docker.com/r/adguard/adguardhome
|
||||||
[Snap Store]: https://snapcraft.io/adguard-home
|
[Snap Store]: https://snapcraft.io/adguard-home
|
||||||
[wiki-start]: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started
|
[wiki-start]: https://adguard-dns.io/kb/adguard-home/getting-started/
|
||||||
|
|
||||||
### <a href="#guides" id="guides" name="guides">Guides</a>
|
### <a href="#guides" id="guides" name="guides">Guides</a>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'edge'
|
'channel': 'edge'
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.2--1'
|
'dockerGo': 'adguard/go-builder:1.23.4--1'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
- 'Build frontend':
|
- 'Build frontend':
|
||||||
|
@ -142,13 +142,15 @@
|
||||||
# Install Qemu, create builder.
|
# Install Qemu, create builder.
|
||||||
docker version -f '{{ .Server.Experimental }}'
|
docker version -f '{{ .Server.Experimental }}'
|
||||||
docker buildx rm buildx-builder || :
|
docker buildx rm buildx-builder || :
|
||||||
docker buildx create --name buildx-builder --driver docker-container\
|
docker buildx create \
|
||||||
--use
|
--name buildx-builder \
|
||||||
|
--driver docker-container \
|
||||||
|
--use
|
||||||
docker buildx inspect --bootstrap
|
docker buildx inspect --bootstrap
|
||||||
|
|
||||||
# Login to DockerHub.
|
# Login to DockerHub.
|
||||||
docker login -u="${bamboo.dockerHubUsername}"\
|
docker login -u="${bamboo.dockerHubUsername}" \
|
||||||
-p="${bamboo.dockerHubPassword}"
|
-p="${bamboo.dockerHubPassword}"
|
||||||
|
|
||||||
# Boot the builder.
|
# Boot the builder.
|
||||||
docker buildx inspect --bootstrap
|
docker buildx inspect --bootstrap
|
||||||
|
@ -157,14 +159,14 @@
|
||||||
docker info
|
docker info
|
||||||
|
|
||||||
# Prepare and push the build.
|
# Prepare and push the build.
|
||||||
env\
|
env \
|
||||||
CHANNEL="${bamboo.channel}"\
|
CHANNEL="${bamboo.channel}" \
|
||||||
COMMIT="${bamboo.repository.revision.number}"\
|
REVISION="${bamboo.repository.revision.number}" \
|
||||||
DIST_DIR='dist'\
|
DIST_DIR='dist' \
|
||||||
DOCKER_IMAGE_NAME='adguard/adguardhome'\
|
DOCKER_IMAGE_NAME='adguard/adguardhome' \
|
||||||
DOCKER_OUTPUT="type=image,name=adguard/adguardhome,push=true"\
|
DOCKER_OUTPUT="type=image,name=adguard/adguardhome,push=true" \
|
||||||
VERBOSE='1'\
|
VERBOSE='1' \
|
||||||
sh ./scripts/make/build-docker.sh
|
sh ./scripts/make/build-docker.sh
|
||||||
'environment':
|
'environment':
|
||||||
DOCKER_CLI_EXPERIMENTAL=enabled
|
DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
'final-tasks':
|
'final-tasks':
|
||||||
|
@ -276,7 +278,7 @@
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'beta'
|
'channel': 'beta'
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.2--1'
|
'dockerGo': 'adguard/go-builder:1.23.4--1'
|
||||||
# release-vX.Y.Z branches are the branches from which the actual final
|
# release-vX.Y.Z branches are the branches from which the actual final
|
||||||
# release is built.
|
# release is built.
|
||||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||||
|
@ -292,4 +294,4 @@
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'release'
|
'channel': 'release'
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.2--1'
|
'dockerGo': 'adguard/go-builder:1.23.4--1'
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
'name': 'AdGuard Home - Build and run tests'
|
'name': 'AdGuard Home - Build and run tests'
|
||||||
'variables':
|
'variables':
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.2--1'
|
'dockerGo': 'adguard/go-builder:1.23.4--1'
|
||||||
'channel': 'development'
|
'channel': 'development'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
|
@ -196,5 +196,5 @@
|
||||||
# may need to build a few of these.
|
# may need to build a few of these.
|
||||||
'variables':
|
'variables':
|
||||||
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
'dockerFrontend': 'adguard/home-js-builder:2.0'
|
||||||
'dockerGo': 'adguard/go-builder:1.23.2--1'
|
'dockerGo': 'adguard/go-builder:1.23.4--1'
|
||||||
'channel': 'candidate'
|
'channel': 'candidate'
|
||||||
|
|
|
@ -542,7 +542,7 @@
|
||||||
"stats_params": "Tilastoinnin määritys",
|
"stats_params": "Tilastoinnin määritys",
|
||||||
"config_successfully_saved": "Asetukset tallennettiin",
|
"config_successfully_saved": "Asetukset tallennettiin",
|
||||||
"interval_6_hour": "6 tuntia",
|
"interval_6_hour": "6 tuntia",
|
||||||
"interval_24_hour": "24 tuntia",
|
"interval_24_hour": "24 tunnilta",
|
||||||
"interval_days": "{{count}} päivä",
|
"interval_days": "{{count}} päivä",
|
||||||
"interval_days_plural": "{{count}} päivää",
|
"interval_days_plural": "{{count}} päivää",
|
||||||
"domain": "Verkkotunnus",
|
"domain": "Verkkotunnus",
|
||||||
|
|
|
@ -122,7 +122,7 @@
|
||||||
"stats_query_domain": "Najczęściej wyszukiwane domeny",
|
"stats_query_domain": "Najczęściej wyszukiwane domeny",
|
||||||
"for_last_hours": "w ciągu ostatniej {{count}} godziny",
|
"for_last_hours": "w ciągu ostatniej {{count}} godziny",
|
||||||
"for_last_hours_plural": "w ciągu ostatnich {{count}} godzin",
|
"for_last_hours_plural": "w ciągu ostatnich {{count}} godzin",
|
||||||
"for_last_days": "za ostatni dzień {{count}}",
|
"for_last_days": "za ostatni {{count}} dzień",
|
||||||
"for_last_days_plural": "z ostatnich {{count}} dni",
|
"for_last_days_plural": "z ostatnich {{count}} dni",
|
||||||
"stats_disabled": "Statystyki zostały wyłączone. Można je włączyć na <0>stronie ustawień</0>.",
|
"stats_disabled": "Statystyki zostały wyłączone. Można je włączyć na <0>stronie ustawień</0>.",
|
||||||
"stats_disabled_short": "Statystyki zostały wyłączone",
|
"stats_disabled_short": "Statystyki zostały wyłączone",
|
||||||
|
@ -130,7 +130,7 @@
|
||||||
"requests_count": "Licznik żądań",
|
"requests_count": "Licznik żądań",
|
||||||
"top_blocked_domains": "Najpopularniejsze zablokowane domeny",
|
"top_blocked_domains": "Najpopularniejsze zablokowane domeny",
|
||||||
"top_clients": "Główni klienci",
|
"top_clients": "Główni klienci",
|
||||||
"no_clients_found": "Nie znaleziono klienta",
|
"no_clients_found": "Nie znaleziono klientów",
|
||||||
"general_statistics": "Ogólne statystyki",
|
"general_statistics": "Ogólne statystyki",
|
||||||
"top_upstreams": "Często żądane serwery nadrzędne",
|
"top_upstreams": "Często żądane serwery nadrzędne",
|
||||||
"no_upstreams_data_found": "Brak danych dotyczących serwerów nadrzędnych",
|
"no_upstreams_data_found": "Brak danych dotyczących serwerów nadrzędnych",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"local_ptr_desc": "ස්ථානීය PTR විමසුම් සඳහා ඇඩ්ගාර්ඩ් හෝම් භාවිතා කරන ව.නා.ප. සේවාදායක. මෙම සේවාදායක පුද්ගලික අ.ජා.කෙ. ලිපින පරාසවල PTR විමසුම් විසඳීමට භාවිතා කරයි, උදාහරණයක් ලෙස ප්රතිවර්ත ව.නා.ප. භාවිතයෙන් \"192.168.12.34\". මෙය සකසා නැති නම්, ඇඩ්ගාර්ඩ් හෝම් හි ලිපින සඳහා හැරුනු විට ඔබගේ මෙහෙයුම් පද්ධතියේ පෙරනිමි ව.නා.ප. විසදුම්වල ලිපින භාවිතා කරයි.",
|
"local_ptr_desc": "ස්ථානීය PTR විමසුම් සඳහා ඇඩ්ගාර්ඩ් හෝම් භාවිතා කරන ව.නා.ප. සේවාදායක. මෙම සේවාදායක පුද්ගලික අ.ජා.කෙ. ලිපින පරාසවල PTR විමසුම් විසඳීමට භාවිතා කරයි, උදාහරණයක් ලෙස ප්රතිවර්ත ව.නා.ප. භාවිතයෙන් \"192.168.12.34\". මෙය සකසා නැති නම්, ඇඩ්ගාර්ඩ් හෝම් හි ලිපින සඳහා හැරුනු විට ඔබගේ මෙහෙයුම් පද්ධතියේ පෙරනිමි ව.නා.ප. විසදුම්වල ලිපින භාවිතා කරයි.",
|
||||||
"local_ptr_default_resolver": "පෙරනිමි පරිදි, ඇඩ්ගාර්ඩ් හෝම් පහත ප්රතිවර්ත ව.නා.ප. පිළිවිසඳු භාවිතා කරයි: {{ip}}.",
|
"local_ptr_default_resolver": "පෙරනිමි පරිදි, ඇඩ්ගාර්ඩ් හෝම් පහත ප්රතිවර්ත ව.නා.ප. පිළිවිසඳු භාවිතා කරයි: {{ip}}.",
|
||||||
"local_ptr_no_default_resolver": "ඇඩ්ගාර්ඩ් හෝම් හට මෙම පද්ධතිය සඳහා සුදුසු පුද්ගලික ප්රතිවර්ත ව.නා.ප. පිළිවිසඳු නිශ්චය කරගත නොහැකි විය.",
|
"local_ptr_no_default_resolver": "ඇඩ්ගාර්ඩ් හෝම් හට මෙම පද්ධතිය සඳහා සුදුසු පුද්ගලික ප්රතිවර්ත ව.නා.ප. පිළිවිසඳු නිශ්චය කරගත නොහැකි විය.",
|
||||||
"local_ptr_placeholder": "පේළියකට එක් සේවාදායක ලිපිනය බැගින් යොදන්න",
|
"local_ptr_placeholder": "පේළියකට අ.ජා.කෙ. ලිපිනය බැගින් ලියන්න",
|
||||||
"resolve_clients_title": "අනුග්රාහකවල අ.ජා.කෙ. ලිපින ප්රතිවර්ත විසඳීම සබල කරන්න",
|
"resolve_clients_title": "අනුග්රාහකවල අ.ජා.කෙ. ලිපින ප්රතිවර්ත විසඳීම සබල කරන්න",
|
||||||
"use_private_ptr_resolvers_title": "පෞද්. ප්රතිවර්ත ව.නා.ප. පිළිවිසඳු භාවිතය",
|
"use_private_ptr_resolvers_title": "පෞද්. ප්රතිවර්ත ව.නා.ප. පිළිවිසඳු භාවිතය",
|
||||||
"check_dhcp_servers": "ග.ධා.වි.කෙ. සේවාදායක පරීක්ෂා කරන්න",
|
"check_dhcp_servers": "ග.ධා.වි.කෙ. සේවාදායක පරීක්ෂා කරන්න",
|
||||||
|
@ -102,7 +102,6 @@
|
||||||
"stats_malware_phishing": "අවහිර කළ ද්වේශාංග/තතුබෑම්",
|
"stats_malware_phishing": "අවහිර කළ ද්වේශාංග/තතුබෑම්",
|
||||||
"stats_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි",
|
"stats_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි",
|
||||||
"stats_query_domain": "ප්රචලිත විමසන ලද වසම්",
|
"stats_query_domain": "ප්රචලිත විමසන ලද වසම්",
|
||||||
"for_last_24_hours": "පසුගිය පැය 24 සඳහා",
|
|
||||||
"for_last_days": "පසුගිය දවස් {{count}} සඳහා",
|
"for_last_days": "පසුගිය දවස් {{count}} සඳහා",
|
||||||
"for_last_days_plural": "පසුගිය දවස් {{count}} සඳහා",
|
"for_last_days_plural": "පසුගිය දවස් {{count}} සඳහා",
|
||||||
"stats_disabled": "සංඛ්යාලේඛන අබල කර ඇත. එය <0>සැකසුම් පිටුවෙන්</0> සබල කළ හැකිය.",
|
"stats_disabled": "සංඛ්යාලේඛන අබල කර ඇත. එය <0>සැකසුම් පිටුවෙන්</0> සබල කළ හැකිය.",
|
||||||
|
@ -115,13 +114,15 @@
|
||||||
"general_statistics": "පොදු සංඛ්යාලේඛන",
|
"general_statistics": "පොදු සංඛ්යාලේඛන",
|
||||||
"number_of_dns_query_days": "පසුගිය දවස් {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන",
|
"number_of_dns_query_days": "පසුගිය දවස් {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන",
|
||||||
"number_of_dns_query_days_plural": "පසුගිය දවස් {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන",
|
"number_of_dns_query_days_plural": "පසුගිය දවස් {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන",
|
||||||
"number_of_dns_query_24_hours": "පසුගිය පැය 24 සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන",
|
"number_of_dns_query_hours": "පසුගිය පැය {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන",
|
||||||
|
"number_of_dns_query_hours_plural": "පසුගිය පැය {{count}} සඳහා සැකසූ ව.නා.ප. විමසුම් ගණන",
|
||||||
"number_of_dns_query_blocked_24_hours": "දැන්වීම් වාරණ පෙරහන් සහ සත්කාරක වාරණ ලැයිස්තු මගින් අවහිර කළ ව.නා.ප. ඉල්ලීම් ගණන",
|
"number_of_dns_query_blocked_24_hours": "දැන්වීම් වාරණ පෙරහන් සහ සත්කාරක වාරණ ලැයිස්තු මගින් අවහිර කළ ව.නා.ප. ඉල්ලීම් ගණන",
|
||||||
"number_of_dns_query_blocked_24_hours_by_sec": "ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්ෂණ ඒකකය මගින් අවහිර කළ ව.නා.ප. ඉල්ලීම් ගණන",
|
"number_of_dns_query_blocked_24_hours_by_sec": "ඇඩ්ගාර්ඩ් පිරික්සුම් ආරක්ෂණ ඒකකය මගින් අවහිර කළ ව.නා.ප. ඉල්ලීම් ගණන",
|
||||||
"number_of_dns_query_blocked_24_hours_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි ගණන",
|
"number_of_dns_query_blocked_24_hours_adult": "අවහිර කළ වැඩිහිටි වියමන අඩවි ගණන",
|
||||||
"enforced_save_search": "ආරක්ෂිත සෙවීම බලාත්මක කළ",
|
"enforced_save_search": "ආරක්ෂිත සෙවීම බලාත්මක කළ",
|
||||||
"number_of_dns_query_to_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කළ සෙවුම් යන්ත්ර සඳහා ව.නා.ප. ඉල්ලීම් ගණන",
|
"number_of_dns_query_to_safe_search": "ආරක්ෂිත සෙවීම බලාත්මක කළ සෙවුම් යන්ත්ර සඳහා ව.නා.ප. ඉල්ලීම් ගණන",
|
||||||
"average_processing_time": "සාමාන්ය සැකසුම් කාලය",
|
"average_processing_time": "සාමාන්ය සැකසුම් කාලය",
|
||||||
|
"response_time": "ප්රතිචාර කාලය",
|
||||||
"average_processing_time_hint": "ව.නා.ප. ඉල්ලීමක් සැකසීමේ සාමාන්ය කාලය මිලි තත්පර වලින්",
|
"average_processing_time_hint": "ව.නා.ප. ඉල්ලීමක් සැකසීමේ සාමාන්ය කාලය මිලි තත්පර වලින්",
|
||||||
"block_domain_use_filters_and_hosts": "පෙරහන් හා සත්කාරක ගොනු භාවිතයෙන් වසම් අවහිර කරන්න",
|
"block_domain_use_filters_and_hosts": "පෙරහන් හා සත්කාරක ගොනු භාවිතයෙන් වසම් අවහිර කරන්න",
|
||||||
"filters_block_toggle_hint": "ඔබට අවහිර කිරීමේ නීති <a>පෙරහන්</a> තුළ පිහිටුවිය හැකිය.",
|
"filters_block_toggle_hint": "ඔබට අවහිර කිරීමේ නීති <a>පෙරහන්</a> තුළ පිහිටුවිය හැකිය.",
|
||||||
|
@ -130,7 +131,7 @@
|
||||||
"use_adguard_parental": "ඇඩ්ගාර්ඩ් දෙමාපිය පාලන වියමන සේවාව භාවිතා කරන්න",
|
"use_adguard_parental": "ඇඩ්ගාර්ඩ් දෙමාපිය පාලන වියමන සේවාව භාවිතා කරන්න",
|
||||||
"use_adguard_parental_hint": "වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි ඇඩ්ගාර්ඩ් හෝම් විසින් පරීක්ෂා කරනු ඇත. එය පිරික්සුම් ආරක්ෂණ වියමන සේවාව මෙන් රහස්යතා හිතකාමී යෙ.ක්ර. අ.මු. (API) භාවිතා කරයි.",
|
"use_adguard_parental_hint": "වසමේ වැඩිහිටියන්ට අදාල කරුණු අඩංගු දැයි ඇඩ්ගාර්ඩ් හෝම් විසින් පරීක්ෂා කරනු ඇත. එය පිරික්සුම් ආරක්ෂණ වියමන සේවාව මෙන් රහස්යතා හිතකාමී යෙ.ක්ර. අ.මු. (API) භාවිතා කරයි.",
|
||||||
"enforce_safe_search": "ආරක්ෂිත සෙවුම භාවිතා කරන්න",
|
"enforce_safe_search": "ආරක්ෂිත සෙවුම භාවිතා කරන්න",
|
||||||
"enforce_save_search_hint": "ඇඩ්ගාර්ඩ් හෝම් පහත සෙවුම් යන්ත්ර තුළ ආරක්ෂිත සෙවුම බලාත්මක කරනු ඇත: ගූගල්, යූටියුබ්, බින්ග්, ඩක්ඩක්ගෝ, යාන්ඩෙක්ස් සහ පික්සාබේ.",
|
"enforce_save_search_hint": "ඇඩ්ගාර්ඩ් හෝම් පහත සෙවුම් යන්ත්ර තුළ ආරක්ෂිත සෙවුම බලාත්මක කරනු ඇත: ගූගල්, යූටියුබ්, බින්ග්, ඩක්ඩක්ගෝ, එකොසියා, යාන්ඩෙක්ස් සහ පික්සාබේ.",
|
||||||
"no_servers_specified": "සේවාදායක කිසිවක් නිශ්චිතව දක්වා නැත",
|
"no_servers_specified": "සේවාදායක කිසිවක් නිශ්චිතව දක්වා නැත",
|
||||||
"general_settings": "පොදු සැකසුම්",
|
"general_settings": "පොදු සැකසුම්",
|
||||||
"dns_settings": "ව.නා.ප. සැකසුම්",
|
"dns_settings": "ව.නා.ප. සැකසුම්",
|
||||||
|
@ -196,12 +197,14 @@
|
||||||
"example_comment_hash": "# එසේම අදහස් දැක්වීමක්.",
|
"example_comment_hash": "# එසේම අදහස් දැක්වීමක්.",
|
||||||
"example_regex_meaning": "නිශ්චිතව දක්වා ඇති නිත්ය වාක්යවිධියට ගැළපෙන වසම් වෙත ප්රවේශය අවහිර කරයි.",
|
"example_regex_meaning": "නිශ්චිතව දක්වා ඇති නිත්ය වාක්යවිධියට ගැළපෙන වසම් වෙත ප්රවේශය අවහිර කරයි.",
|
||||||
"example_upstream_regular": "සාමාන්ය ව.නා.ප. (UDP හරහා);",
|
"example_upstream_regular": "සාමාන්ය ව.නා.ප. (UDP හරහා);",
|
||||||
|
"example_upstream_regular_port": "සාමාන්ය ව.නා.ප. (UDP හරහා, තොට සමඟ);",
|
||||||
"example_upstream_udp": "සාමාන්ය ව.නා.ප. (UDP, සත්කාරක-නම හරහා);",
|
"example_upstream_udp": "සාමාන්ය ව.නා.ප. (UDP, සත්කාරක-නම හරහා);",
|
||||||
"example_upstream_dot": "සංකේතිත <0>TLS-මගින්-ව.නා.ප.</0>;",
|
"example_upstream_dot": "සංකේතිත <0>TLS-මගින්-ව.නා.ප.</0>;",
|
||||||
"example_upstream_doh": "සංකේතිත <0>HTTPS-මගින්-ව.නා.ප.</0>;",
|
"example_upstream_doh": "සංකේතිත <0>HTTPS-මගින්-ව.නා.ප.</0>;",
|
||||||
"example_upstream_doq": "සංකේතිත <0>QUIC-මගින්-ව.නා.ප.</0>;",
|
"example_upstream_doq": "සංකේතිත <0>QUIC-මගින්-ව.නා.ප.</0>;",
|
||||||
"example_upstream_sdns": "<1>DNSCrypt</1> හෝ <2>HTTPS-මගින්-ව.නා.ප.</2> පිළිවිසඳු සඳහා <0>ව.නා.ප. මුද්දර</0>;",
|
"example_upstream_sdns": "<1>DNSCrypt</1> හෝ <2>HTTPS-මගින්-ව.නා.ප.</2> පිළිවිසඳු සඳහා <0>ව.නා.ප. මුද්දර</0>;",
|
||||||
"example_upstream_tcp": "සාමාන්ය ව.නා.ප. (TCP/ස.පා.කෙ. හරහා);",
|
"example_upstream_tcp": "සාමාන්ය ව.නා.ප. (TCP/ස.පා.කෙ. හරහා);",
|
||||||
|
"example_upstream_tcp_port": "සාමාන්ය ව.නා.ප. (TCP හරහා, තොට සමඟ);",
|
||||||
"example_upstream_tcp_hostname": "සාමාන්ය ව.නා.ප. (ස.පා.කෙ., සත්කාරක-නම හරහා);",
|
"example_upstream_tcp_hostname": "සාමාන්ය ව.නා.ප. (ස.පා.කෙ., සත්කාරක-නම හරහා);",
|
||||||
"all_lists_up_to_date_toast": "සියළුම ලැයිස්තු දැනටමත් යාවත්කාලීනයි",
|
"all_lists_up_to_date_toast": "සියළුම ලැයිස්තු දැනටමත් යාවත්කාලීනයි",
|
||||||
"dns_test_ok_toast": "සඳහන් කළ ව.නා.ප. සේවාදායක නිවැරදිව ක්රියා කරයි",
|
"dns_test_ok_toast": "සඳහන් කළ ව.නා.ප. සේවාදායක නිවැරදිව ක්රියා කරයි",
|
||||||
|
@ -275,6 +278,7 @@
|
||||||
"edns_use_custom_ip": "EDNS සඳහා අභිරුචි අ.ජා.කෙ. යොදාගන්න",
|
"edns_use_custom_ip": "EDNS සඳහා අභිරුචි අ.ජා.කෙ. යොදාගන්න",
|
||||||
"edns_use_custom_ip_desc": "EDNS සඳහා අභිරුචි අ.ජා.කෙ. භාවිතයට ඉඩදෙන්න",
|
"edns_use_custom_ip_desc": "EDNS සඳහා අභිරුචි අ.ජා.කෙ. භාවිතයට ඉඩදෙන්න",
|
||||||
"rate_limit_desc": "එක් අනුග්රාහකයකට ඉඩ දී ඇති තත්පරයට ඉල්ලීම් ගණන. එය 0 ලෙස සැකසීම යනුවෙන් අදහස් කරන්නේ සීමාවක් නැති බවයි.",
|
"rate_limit_desc": "එක් අනුග්රාහකයකට ඉඩ දී ඇති තත්පරයට ඉල්ලීම් ගණන. එය 0 ලෙස සැකසීම යනුවෙන් අදහස් කරන්නේ සීමාවක් නැති බවයි.",
|
||||||
|
"rate_limit_whitelist_placeholder": "පේළියකට අ.ජා.කෙ. ලිපිනය බැගින් ලියන්න",
|
||||||
"blocking_ipv4_desc": "අවහිර කළ A ඉල්ලීමක් සඳහා ආපසු එවිය යුතු අ.ජා.කෙ. (IP) ලිපිනය",
|
"blocking_ipv4_desc": "අවහිර කළ A ඉල්ලීමක් සඳහා ආපසු එවිය යුතු අ.ජා.කෙ. (IP) ලිපිනය",
|
||||||
"blocking_ipv6_desc": "අවහිර කළ AAAA ඉල්ලීමක් සඳහා ආපසු එවිය යුතු අ.ජා.කෙ. (IP) ලිපිනය",
|
"blocking_ipv6_desc": "අවහිර කළ AAAA ඉල්ලීමක් සඳහා ආපසු එවිය යුතු අ.ජා.කෙ. (IP) ලිපිනය",
|
||||||
"blocking_mode_default": "පොදු: දැන්වීම් අවහිර කරන ආකාරයේ නීතියක් මගින් අවහිර කළ විට REFUSED සමඟ ප්රතිචාර දක්වයි; /etc/host-style ආකාරයේ නීතියක් මගින් අවහිර කළ විට නීතියේ දක්වා ඇති අ.ජා.කෙ. ලිපිනය සමඟ ප්රතිචාර දක්වයි",
|
"blocking_mode_default": "පොදු: දැන්වීම් අවහිර කරන ආකාරයේ නීතියක් මගින් අවහිර කළ විට REFUSED සමඟ ප්රතිචාර දක්වයි; /etc/host-style ආකාරයේ නීතියක් මගින් අවහිර කළ විට නීතියේ දක්වා ඇති අ.ජා.කෙ. ලිපිනය සමඟ ප්රතිචාර දක්වයි",
|
||||||
|
@ -505,8 +509,8 @@
|
||||||
"statistics_enable": "සංඛ්යාලේඛන සබල කරන්න",
|
"statistics_enable": "සංඛ්යාලේඛන සබල කරන්න",
|
||||||
"ignore_domains": "නොසලකන වසම් (පේළියකට එක බැගින්)",
|
"ignore_domains": "නොසලකන වසම් (පේළියකට එක බැගින්)",
|
||||||
"ignore_domains_title": "නොසලකන වසම්",
|
"ignore_domains_title": "නොසලකන වසම්",
|
||||||
"ignore_domains_desc_stats": "සංඛ්යාලේඛනයෙහි මෙම වසම් සඳහා විමසුම් නොලියැවෙයි",
|
"ignore_domains_desc_stats": "මෙම නීති වලට ගැළපෙන විමසුම් සංඛ්යාලේඛනයට නොලියැවෙයි",
|
||||||
"ignore_domains_desc_query": "විමසුම් සටහනෙහි මෙම වසම් සඳහා විමසුම් නොලියැවෙයි",
|
"ignore_domains_desc_query": "විමසුම් සටහනට මෙම නීති වලට ගැළපෙන විමසුම් නොලියැවෙයි",
|
||||||
"interval_hours": "පැය {{count}}",
|
"interval_hours": "පැය {{count}}",
|
||||||
"interval_hours_plural": "පැය {{count}}",
|
"interval_hours_plural": "පැය {{count}}",
|
||||||
"filters_configuration": "පෙරහන් වින්යාසය",
|
"filters_configuration": "පෙරහන් වින්යාසය",
|
||||||
|
@ -615,8 +619,8 @@
|
||||||
"use_saved_key": "පෙර සුරැකි යතුර භාවිතා කරන්න",
|
"use_saved_key": "පෙර සුරැකි යතුර භාවිතා කරන්න",
|
||||||
"parental_control": "දෙමාපිය පාලනය",
|
"parental_control": "දෙමාපිය පාලනය",
|
||||||
"safe_browsing": "ආරක්ෂිත පිරික්සුම",
|
"safe_browsing": "ආරක්ෂිත පිරික්සුම",
|
||||||
"served_from_cache": "{{value}} <i>(නිහිතයෙන් ගැනිණි)</i>",
|
"served_from_cache_label": "නිහිතයෙන් සැපයිණි",
|
||||||
"form_error_password_length": "මුරපදය අවම වශයෙන් අකුරු {{value}} ක් දිගු විය යුතුමයි",
|
"form_error_password_length": "මුරපදය අකුරු {{min}} සහ {{value}} ක් අතර විය යුතුය",
|
||||||
"anonymizer_notification": "<0>සටහන:</0> අ.ජා.කෙ. නිර්නාමිකකරණය සබලයි. ඔබට එය <1>පොදු සැකසුම්</1> හරහා අබල කිරීමට හැකිය .",
|
"anonymizer_notification": "<0>සටහන:</0> අ.ජා.කෙ. නිර්නාමිකකරණය සබලයි. ඔබට එය <1>පොදු සැකසුම්</1> හරහා අබල කිරීමට හැකිය .",
|
||||||
"confirm_dns_cache_clear": "ඔබට ව.නා.ප. නිහිතය හිස් කිරීමට වුවමනාද?",
|
"confirm_dns_cache_clear": "ඔබට ව.නා.ප. නිහිතය හිස් කිරීමට වුවමනාද?",
|
||||||
"cache_cleared": "ව.නා.ප. නිහිතය හිස් කෙරිණි",
|
"cache_cleared": "ව.නා.ප. නිහිතය හිස් කෙරිණි",
|
||||||
|
@ -646,6 +650,7 @@
|
||||||
"log_and_stats_section_label": "විමසුම් සටහන හා සංඛ්යාලේඛන",
|
"log_and_stats_section_label": "විමසුම් සටහන හා සංඛ්යාලේඛන",
|
||||||
"ignore_query_log": "විමසුම් සටහනට මෙම අනුග්රාහකය යොදන්න එපා",
|
"ignore_query_log": "විමසුම් සටහනට මෙම අනුග්රාහකය යොදන්න එපා",
|
||||||
"ignore_statistics": "සංඛ්යාලේඛනයට මෙම අනුග්රාහකය යොදන්න එපා",
|
"ignore_statistics": "සංඛ්යාලේඛනයට මෙම අනුග්රාහකය යොදන්න එපා",
|
||||||
|
"schedule_services": "සේවා අවහිර විරාමය",
|
||||||
"schedule_invalid_select": "ආරම්භක වේලාව අවසන් වේලාවට කලින් විය යුතුය",
|
"schedule_invalid_select": "ආරම්භක වේලාව අවසන් වේලාවට කලින් විය යුතුය",
|
||||||
"schedule_select_days": "දවස් තෝරන්න",
|
"schedule_select_days": "දවස් තෝරන්න",
|
||||||
"schedule_timezone": "වේලා කලාපයක් තෝරන්න",
|
"schedule_timezone": "වේලා කලාපයක් තෝරන්න",
|
||||||
|
|
|
@ -461,7 +461,7 @@
|
||||||
"form_enter_mac": "Skriv in MAC",
|
"form_enter_mac": "Skriv in MAC",
|
||||||
"form_enter_id": "Ange identifierare",
|
"form_enter_id": "Ange identifierare",
|
||||||
"form_add_id": "Lägg till identifierare",
|
"form_add_id": "Lägg till identifierare",
|
||||||
"form_client_name": "Skriv in klientnamn",
|
"form_client_name": "Ange klientnamn",
|
||||||
"name": "Namn",
|
"name": "Namn",
|
||||||
"client_name": "Klient {{id}}",
|
"client_name": "Klient {{id}}",
|
||||||
"client_global_settings": "Använda globala inställningar",
|
"client_global_settings": "Använda globala inställningar",
|
||||||
|
@ -674,7 +674,6 @@
|
||||||
"use_saved_key": "Använd den tidigare sparade nyckeln",
|
"use_saved_key": "Använd den tidigare sparade nyckeln",
|
||||||
"parental_control": "Föräldrakontroll",
|
"parental_control": "Föräldrakontroll",
|
||||||
"safe_browsing": "Säker surfning",
|
"safe_browsing": "Säker surfning",
|
||||||
"served_from_cache": "{{value}} <i>(levereras från cache)</i>",
|
|
||||||
"form_error_password_length": "Lösenordet måste vara {{min}} till {{max}} tecken långt",
|
"form_error_password_length": "Lösenordet måste vara {{min}} till {{max}} tecken långt",
|
||||||
"anonymizer_notification": "<0>Observera:</0> IP-anonymisering är aktiverad. Du kan inaktivera den i <1>Allmänna inställningar</1>.",
|
"anonymizer_notification": "<0>Observera:</0> IP-anonymisering är aktiverad. Du kan inaktivera den i <1>Allmänna inställningar</1>.",
|
||||||
"confirm_dns_cache_clear": "Är du säker på att du vill rensa DNS-cache?",
|
"confirm_dns_cache_clear": "Är du säker på att du vill rensa DNS-cache?",
|
||||||
|
|
|
@ -14,6 +14,17 @@
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guide__list {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.guide__list {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.guide__address {
|
.guide__address {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 7px;
|
||||||
|
|
|
@ -33,13 +33,13 @@ const SetupGuide = ({
|
||||||
<Trans>install_devices_address</Trans>:
|
<Trans>install_devices_address</Trans>:
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3">
|
<ul className="guide__list">
|
||||||
{dnsAddresses.map((ip: any) => (
|
{dnsAddresses.map((ip: any) => (
|
||||||
<li key={ip} className="guide__address">
|
<li key={ip} className="guide__address">
|
||||||
{ip}
|
{ip}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Guide dnsAddresses={dnsAddresses} />
|
<Guide dnsAddresses={dnsAddresses} />
|
||||||
|
|
|
@ -238,6 +238,12 @@ export default {
|
||||||
"homepage": "https://github.com/hagezi/dns-blocklists",
|
"homepage": "https://github.com/hagezi/dns-blocklists",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_51.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_51.txt"
|
||||||
},
|
},
|
||||||
|
"hagezi_samsung_tracker_blocklist": {
|
||||||
|
"name": "HaGeZi's Samsung Tracker Blocklist",
|
||||||
|
"categoryId": "other",
|
||||||
|
"homepage": "https://github.com/hagezi/dns-blocklists",
|
||||||
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_61.txt"
|
||||||
|
},
|
||||||
"hagezi_the_worlds_most_abused_tlds": {
|
"hagezi_the_worlds_most_abused_tlds": {
|
||||||
"name": "HaGeZi's The World's Most Abused TLDs",
|
"name": "HaGeZi's The World's Most Abused TLDs",
|
||||||
"categoryId": "security",
|
"categoryId": "security",
|
||||||
|
@ -256,6 +262,12 @@ export default {
|
||||||
"homepage": "https://github.com/hagezi/dns-blocklists",
|
"homepage": "https://github.com/hagezi/dns-blocklists",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_49.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_49.txt"
|
||||||
},
|
},
|
||||||
|
"hagezi_windows_office_tracker_blocklist": {
|
||||||
|
"name": "HaGeZi's Windows/Office Tracker Blocklist",
|
||||||
|
"categoryId": "other",
|
||||||
|
"homepage": "https://github.com/hagezi/dns-blocklists",
|
||||||
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_63.txt"
|
||||||
|
},
|
||||||
"hagezi_xiaomi_tracking_blocklist": {
|
"hagezi_xiaomi_tracking_blocklist": {
|
||||||
"name": "HaGeZi's Xiaomi Tracker Blocklist",
|
"name": "HaGeZi's Xiaomi Tracker Blocklist",
|
||||||
"categoryId": "other",
|
"categoryId": "other",
|
||||||
|
@ -346,17 +358,17 @@ export default {
|
||||||
"homepage": "https://github.com/uBlockOrigin/uAssets",
|
"homepage": "https://github.com/uBlockOrigin/uAssets",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_50.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_50.txt"
|
||||||
},
|
},
|
||||||
|
"ukrainian_security_filter": {
|
||||||
|
"name": "Ukrainian Security Filter",
|
||||||
|
"categoryId": "other",
|
||||||
|
"homepage": "https://github.com/braveinnovators/ukrainian-security-filter",
|
||||||
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_62.txt"
|
||||||
|
},
|
||||||
"urlhaus_filter_online": {
|
"urlhaus_filter_online": {
|
||||||
"name": "Malicious URL Blocklist (URLHaus)",
|
"name": "Malicious URL Blocklist (URLHaus)",
|
||||||
"categoryId": "security",
|
"categoryId": "security",
|
||||||
"homepage": "https://urlhaus.abuse.ch/",
|
"homepage": "https://urlhaus.abuse.ch/",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_11.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_11.txt"
|
||||||
},
|
|
||||||
"windowsspyblocker_hosts_spy_rules": {
|
|
||||||
"name": "WindowsSpyBlocker - Hosts spy rules",
|
|
||||||
"categoryId": "other",
|
|
||||||
"homepage": "https://github.com/crazy-max/WindowsSpyBlocker",
|
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_23.txt"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"timeUpdated": "2024-10-28T10:04:59.054Z",
|
"timeUpdated": "2024-12-03T12:12:08.316Z",
|
||||||
"categories": {
|
"categories": {
|
||||||
"0": "audio_video_player",
|
"0": "audio_video_player",
|
||||||
"1": "comments",
|
"1": "comments",
|
||||||
|
@ -2515,6 +2515,13 @@
|
||||||
"url": "http://www.ancoramediasolutions.com/",
|
"url": "http://www.ancoramediasolutions.com/",
|
||||||
"companyId": "ancora"
|
"companyId": "ancora"
|
||||||
},
|
},
|
||||||
|
"android": {
|
||||||
|
"name": "Android",
|
||||||
|
"categoryId": 101,
|
||||||
|
"url": "https://www.android.com/",
|
||||||
|
"companyId": "google",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"anetwork": {
|
"anetwork": {
|
||||||
"name": "Anetwork",
|
"name": "Anetwork",
|
||||||
"categoryId": 4,
|
"categoryId": 4,
|
||||||
|
@ -8195,7 +8202,7 @@
|
||||||
"google_dns": {
|
"google_dns": {
|
||||||
"name": "Google DNS",
|
"name": "Google DNS",
|
||||||
"categoryId": 10,
|
"categoryId": 10,
|
||||||
"url": "hhttps://dns.google/",
|
"url": "https://dns.google/",
|
||||||
"companyId": "google",
|
"companyId": "google",
|
||||||
"source": "AdGuard"
|
"source": "AdGuard"
|
||||||
},
|
},
|
||||||
|
@ -13980,6 +13987,13 @@
|
||||||
"url": "http://prostor-lite.ru/",
|
"url": "http://prostor-lite.ru/",
|
||||||
"companyId": "prostor"
|
"companyId": "prostor"
|
||||||
},
|
},
|
||||||
|
"proton_ag": {
|
||||||
|
"name": "Proton AG",
|
||||||
|
"categoryId": 2,
|
||||||
|
"url": "https://proton.me/",
|
||||||
|
"companyId": "proton_foundation",
|
||||||
|
"source": "AdGuard"
|
||||||
|
},
|
||||||
"provide_support": {
|
"provide_support": {
|
||||||
"name": "Provide Support",
|
"name": "Provide Support",
|
||||||
"categoryId": 2,
|
"categoryId": 2,
|
||||||
|
@ -15654,7 +15668,7 @@
|
||||||
"shareaholic": {
|
"shareaholic": {
|
||||||
"name": "Shareaholic",
|
"name": "Shareaholic",
|
||||||
"categoryId": 6,
|
"categoryId": 6,
|
||||||
"url": "hhttps://www.shareaholic.com/",
|
"url": "https://www.shareaholic.com/",
|
||||||
"companyId": "shareaholic"
|
"companyId": "shareaholic"
|
||||||
},
|
},
|
||||||
"shareasale": {
|
"shareasale": {
|
||||||
|
@ -20827,6 +20841,7 @@
|
||||||
"anametrix.net": "anametrix",
|
"anametrix.net": "anametrix",
|
||||||
"ancestrycdn.com": "ancestry_cdn",
|
"ancestrycdn.com": "ancestry_cdn",
|
||||||
"ancoraplatform.com": "ancora",
|
"ancoraplatform.com": "ancora",
|
||||||
|
"android.com": "android",
|
||||||
"anetwork.ir": "anetwork",
|
"anetwork.ir": "anetwork",
|
||||||
"aniview.com": "aniview.com",
|
"aniview.com": "aniview.com",
|
||||||
"a-ads.com": "anonymousads",
|
"a-ads.com": "anonymousads",
|
||||||
|
@ -23321,6 +23336,7 @@
|
||||||
"mrskincash.com": "mrskincash",
|
"mrskincash.com": "mrskincash",
|
||||||
"a-msedge.net": "msedge",
|
"a-msedge.net": "msedge",
|
||||||
"b-msedge.net": "msedge",
|
"b-msedge.net": "msedge",
|
||||||
|
"dual-s-msedge.net": "msedge",
|
||||||
"e-msedge.net": "msedge",
|
"e-msedge.net": "msedge",
|
||||||
"k-msedge.net": "msedge",
|
"k-msedge.net": "msedge",
|
||||||
"l-msedge.net": "msedge",
|
"l-msedge.net": "msedge",
|
||||||
|
@ -23767,6 +23783,7 @@
|
||||||
"tr.prospecteye.com": "prospecteye",
|
"tr.prospecteye.com": "prospecteye",
|
||||||
"prosperent.com": "prosperent",
|
"prosperent.com": "prosperent",
|
||||||
"prostor-lite.ru": "prostor",
|
"prostor-lite.ru": "prostor",
|
||||||
|
"reports.proton.me": "proton_ag",
|
||||||
"providesupport.com": "provide_support",
|
"providesupport.com": "provide_support",
|
||||||
"proximic.com": "proximic",
|
"proximic.com": "proximic",
|
||||||
"proxistore.com": "proxistore.com",
|
"proxistore.com": "proxistore.com",
|
||||||
|
|
38
go.mod
38
go.mod
|
@ -1,20 +1,20 @@
|
||||||
module github.com/AdguardTeam/AdGuardHome
|
module github.com/AdguardTeam/AdGuardHome
|
||||||
|
|
||||||
go 1.23.2
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
// TODO(a.garipov): Update when v0.73.3 is released.
|
github.com/AdguardTeam/dnsproxy v0.73.4
|
||||||
github.com/AdguardTeam/dnsproxy v0.73.3-0.20241004151328-c7c7b977a2a3
|
github.com/AdguardTeam/golibs v0.30.5
|
||||||
github.com/AdguardTeam/golibs v0.30.0
|
|
||||||
github.com/AdguardTeam/urlfilter v0.20.0
|
github.com/AdguardTeam/urlfilter v0.20.0
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
github.com/ameshkov/dnscrypt/v2 v2.3.0
|
github.com/ameshkov/dnscrypt/v2 v2.3.0
|
||||||
github.com/bluele/gcache v0.0.2
|
github.com/bluele/gcache v0.0.2
|
||||||
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
|
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
|
||||||
github.com/digineo/go-ipset/v2 v2.2.1
|
github.com/digineo/go-ipset/v2 v2.2.1
|
||||||
github.com/dimfeld/httptreemux/v5 v5.5.0
|
github.com/fsnotify/fsnotify v1.8.0
|
||||||
github.com/fsnotify/fsnotify v1.7.0
|
// TODO(e.burkov): This package is deprecated; find a new one or use our
|
||||||
github.com/go-ping/ping v1.1.0
|
// own code for that. Perhaps, use gopacket.
|
||||||
|
github.com/go-ping/ping v1.2.0
|
||||||
github.com/google/go-cmp v0.6.0
|
github.com/google/go-cmp v0.6.0
|
||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/google/renameio/v2 v2.0.0
|
github.com/google/renameio/v2 v2.0.0
|
||||||
|
@ -29,14 +29,14 @@ require (
|
||||||
// own code for that. Perhaps, use gopacket.
|
// own code for that. Perhaps, use gopacket.
|
||||||
github.com/mdlayher/raw v0.1.0
|
github.com/mdlayher/raw v0.1.0
|
||||||
github.com/miekg/dns v1.1.62
|
github.com/miekg/dns v1.1.62
|
||||||
github.com/quic-go/quic-go v0.48.1
|
github.com/quic-go/quic-go v0.48.2
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/ti-mo/netfilter v0.5.2
|
github.com/ti-mo/netfilter v0.5.2
|
||||||
go.etcd.io/bbolt v1.3.11
|
go.etcd.io/bbolt v1.3.11
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.29.0
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||||
golang.org/x/net v0.30.0
|
golang.org/x/net v0.31.0
|
||||||
golang.org/x/sys v0.26.0
|
golang.org/x/sys v0.28.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
|
@ -49,9 +49,9 @@ require (
|
||||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
|
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e // indirect
|
github.com/google/pprof v0.0.0-20241101162523-b92577c0c142 // indirect
|
||||||
github.com/mdlayher/socket v0.5.1 // indirect
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
|
github.com/onsi/ginkgo/v2 v2.21.0 // indirect
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
@ -59,9 +59,9 @@ require (
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
golang.org/x/mod v0.21.0 // indirect
|
golang.org/x/mod v0.22.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.9.0 // indirect
|
||||||
golang.org/x/text v0.19.0 // indirect
|
golang.org/x/text v0.20.0 // indirect
|
||||||
golang.org/x/tools v0.26.0 // indirect
|
golang.org/x/tools v0.27.0 // indirect
|
||||||
gonum.org/v1/gonum v0.15.1 // indirect
|
gonum.org/v1/gonum v0.15.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
70
go.sum
70
go.sum
|
@ -1,7 +1,7 @@
|
||||||
github.com/AdguardTeam/dnsproxy v0.73.3-0.20241004151328-c7c7b977a2a3 h1:IGXwBjdKDzUm007QzZyxSllMnkbdXe7K79x7JWcBW/E=
|
github.com/AdguardTeam/dnsproxy v0.73.4 h1:FTIXX34wQqePjtWUD1I4QfWTq2B2N1gfOW/TzZDdR4o=
|
||||||
github.com/AdguardTeam/dnsproxy v0.73.3-0.20241004151328-c7c7b977a2a3/go.mod h1:356iHROxo+SOdBVifp1MXEh6qHyydtzGCcsQMfx+ZVs=
|
github.com/AdguardTeam/dnsproxy v0.73.4/go.mod h1:18ssqhDgOCiVIwYmmVuXVM05wSwrzkO2yjKhVRWJX/g=
|
||||||
github.com/AdguardTeam/golibs v0.30.0 h1:3pTdW1B9GZgqARrA5BvmYlAaEG1zAHI/ReikCDxrhiE=
|
github.com/AdguardTeam/golibs v0.30.5 h1:xqat/N9o/V/AnakaWpqq+fGU/qJhKtL4A2pj66kC+TE=
|
||||||
github.com/AdguardTeam/golibs v0.30.0/go.mod h1:vjw1OVZG6BYyoqGRY88U4LCJLOMfhBFhU0UJBdaSAuQ=
|
github.com/AdguardTeam/golibs v0.30.5/go.mod h1:2wOvoAsubo/REnBiuu/YWYmkkzyFR52/QljMdQ2R58M=
|
||||||
github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs=
|
github.com/AdguardTeam/urlfilter v0.20.0 h1:X32qiuVCVd8WDYCEsbdZKfXMzwdVqrdulamtUi4rmzs=
|
||||||
github.com/AdguardTeam/urlfilter v0.20.0/go.mod h1:gjrywLTxfJh6JOkwi9SU+frhP7kVVEZ5exFGkR99qpk=
|
github.com/AdguardTeam/urlfilter v0.20.0/go.mod h1:gjrywLTxfJh6JOkwi9SU+frhP7kVVEZ5exFGkR99qpk=
|
||||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||||
|
@ -25,16 +25,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g=
|
github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g=
|
||||||
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
|
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
|
||||||
github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw=
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw=
|
github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ=
|
||||||
github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
@ -44,8 +42,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e h1:v7R0PZoC2p1KWQmv1+GqCXQe59Ab1TkDF8Y9Lg2W6m4=
|
github.com/google/pprof v0.0.0-20241101162523-b92577c0c142 h1:sAGdeJj0bnMgUNVeUpp6AYlVdCt3/GdI3pGRqsNSQLs=
|
||||||
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
github.com/google/pprof v0.0.0-20241101162523-b92577c0c142/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
|
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
|
||||||
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
|
github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4=
|
||||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
@ -82,10 +80,10 @@ github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
|
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||||
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
|
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
|
||||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||||
|
@ -99,8 +97,8 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA=
|
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
|
||||||
github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
|
@ -109,8 +107,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU=
|
github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU=
|
||||||
github.com/ti-mo/netfilter v0.5.2 h1:CTjOwFuNNeZ9QPdRXt1MZFLFUf84cKtiQutNauHWd40=
|
github.com/ti-mo/netfilter v0.5.2 h1:CTjOwFuNNeZ9QPdRXt1MZFLFUf84cKtiQutNauHWd40=
|
||||||
github.com/ti-mo/netfilter v0.5.2/go.mod h1:Btx3AtFiOVdHReTDmP9AE+hlkOcvIy403u7BXXbWZKo=
|
github.com/ti-mo/netfilter v0.5.2/go.mod h1:Btx3AtFiOVdHReTDmP9AE+hlkOcvIy403u7BXXbWZKo=
|
||||||
|
@ -128,26 +126,26 @@ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
@ -158,19 +156,19 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
||||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
|
gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0=
|
||||||
|
|
|
@ -14,12 +14,6 @@ import (
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP scheme constants.
|
|
||||||
const (
|
|
||||||
SchemeHTTP = "http"
|
|
||||||
SchemeHTTPS = "https"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RegisterFunc is the function that sets the handler to handle the URL for the
|
// RegisterFunc is the function that sets the handler to handle the URL for the
|
||||||
// method.
|
// method.
|
||||||
//
|
//
|
||||||
|
|
|
@ -146,16 +146,6 @@ func IsOpenWrt() (ok bool) {
|
||||||
return isOpenWrt()
|
return isOpenWrt()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyReconfigureSignal notifies c on receiving reconfigure signals.
|
|
||||||
func NotifyReconfigureSignal(c chan<- os.Signal) {
|
|
||||||
notifyReconfigureSignal(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsReconfigureSignal returns true if sig is a reconfigure signal.
|
|
||||||
func IsReconfigureSignal(sig os.Signal) (ok bool) {
|
|
||||||
return isReconfigureSignal(sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendShutdownSignal sends the shutdown signal to the channel.
|
// SendShutdownSignal sends the shutdown signal to the channel.
|
||||||
func SendShutdownSignal(c chan<- os.Signal) {
|
func SendShutdownSignal(c chan<- os.Signal) {
|
||||||
sendShutdownSignal(c)
|
sendShutdownSignal(c)
|
||||||
|
|
|
@ -1,22 +1,11 @@
|
||||||
//go:build darwin || freebsd || linux || openbsd
|
//go:build unix
|
||||||
|
|
||||||
package aghos
|
package aghos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func notifyReconfigureSignal(c chan<- os.Signal) {
|
|
||||||
signal.Notify(c, unix.SIGHUP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isReconfigureSignal(sig os.Signal) (ok bool) {
|
|
||||||
return sig == unix.SIGHUP
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendShutdownSignal(_ chan<- os.Signal) {
|
func sendShutdownSignal(_ chan<- os.Signal) {
|
||||||
// On Unix we are already notified by the system.
|
// On Unix we are already notified by the system.
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@ package aghos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setRlimit(val uint64) (err error) {
|
func setRlimit(_ uint64) (err error) {
|
||||||
return Unsupported("setrlimit")
|
return Unsupported("setrlimit")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,14 +37,6 @@ func isOpenWrt() (ok bool) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyReconfigureSignal(c chan<- os.Signal) {
|
|
||||||
signal.Notify(c, windows.SIGHUP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isReconfigureSignal(sig os.Signal) (ok bool) {
|
|
||||||
return sig == windows.SIGHUP
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendShutdownSignal(c chan<- os.Signal) {
|
func sendShutdownSignal(c chan<- os.Signal) {
|
||||||
c <- os.Interrupt
|
c <- os.Interrupt
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
package aghos
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO(e.burkov): Add platform-independent tests.
|
|
||||||
|
|
||||||
// Chmod is an extension for [os.Chmod] that properly handles Windows access
|
|
||||||
// rights.
|
|
||||||
func Chmod(name string, perm fs.FileMode) (err error) {
|
|
||||||
return chmod(name, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mkdir is an extension for [os.Mkdir] that properly handles Windows access
|
|
||||||
// rights.
|
|
||||||
func Mkdir(name string, perm fs.FileMode) (err error) {
|
|
||||||
return mkdir(name, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MkdirAll is an extension for [os.MkdirAll] that properly handles Windows
|
|
||||||
// access rights.
|
|
||||||
func MkdirAll(path string, perm fs.FileMode) (err error) {
|
|
||||||
return mkdirAll(path, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFile is an extension for [os.WriteFile] that properly handles Windows
|
|
||||||
// access rights.
|
|
||||||
func WriteFile(filename string, data []byte, perm fs.FileMode) (err error) {
|
|
||||||
return writeFile(filename, data, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenFile is an extension for [os.OpenFile] that properly handles Windows
|
|
||||||
// access rights.
|
|
||||||
func OpenFile(name string, flag int, perm fs.FileMode) (file *os.File, err error) {
|
|
||||||
return openFile(name, flag, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stat is an extension for [os.Stat] that properly handles Windows access
|
|
||||||
// rights.
|
|
||||||
//
|
|
||||||
// Note that on Windows the "other" permission bits combines the access rights
|
|
||||||
// of any trustee that is neither the owner nor the owning group for the file.
|
|
||||||
//
|
|
||||||
// TODO(e.burkov): Inspect the behavior for the World (everyone) well-known
|
|
||||||
// SID and, perhaps, use it.
|
|
||||||
func Stat(name string) (fi fs.FileInfo, err error) {
|
|
||||||
return stat(name)
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
//go:build unix
|
|
||||||
|
|
||||||
package aghos
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/google/renameio/v2/maybe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// chmod is a Unix implementation of [Chmod].
|
|
||||||
func chmod(name string, perm fs.FileMode) (err error) {
|
|
||||||
return os.Chmod(name, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mkdir is a Unix implementation of [Mkdir].
|
|
||||||
func mkdir(name string, perm fs.FileMode) (err error) {
|
|
||||||
return os.Mkdir(name, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mkdirAll is a Unix implementation of [MkdirAll].
|
|
||||||
func mkdirAll(path string, perm fs.FileMode) (err error) {
|
|
||||||
return os.MkdirAll(path, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeFile is a Unix implementation of [WriteFile].
|
|
||||||
func writeFile(filename string, data []byte, perm fs.FileMode) (err error) {
|
|
||||||
return maybe.WriteFile(filename, data, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// openFile is a Unix implementation of [OpenFile].
|
|
||||||
func openFile(name string, flag int, perm fs.FileMode) (file *os.File, err error) {
|
|
||||||
// #nosec G304 -- This function simply wraps the [os.OpenFile] function, so
|
|
||||||
// the security concerns should be addressed to the [OpenFile] calls.
|
|
||||||
return os.OpenFile(name, flag, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stat is a Unix implementation of [Stat].
|
|
||||||
func stat(name string) (fi os.FileInfo, err error) {
|
|
||||||
return os.Stat(name)
|
|
||||||
}
|
|
|
@ -1,392 +0,0 @@
|
||||||
//go:build windows
|
|
||||||
|
|
||||||
package aghos
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
// fileInfo is a Windows implementation of [fs.FileInfo], that contains the
|
|
||||||
// filemode converted from the security descriptor.
|
|
||||||
type fileInfo struct {
|
|
||||||
// fs.FileInfo is embedded to provide the default implementations and data
|
|
||||||
// successfully retrieved by [os.Stat].
|
|
||||||
fs.FileInfo
|
|
||||||
|
|
||||||
// mode is the file mode converted from the security descriptor.
|
|
||||||
mode fs.FileMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ fs.FileInfo = (*fileInfo)(nil)
|
|
||||||
|
|
||||||
// Mode implements [fs.FileInfo.Mode] for [*fileInfo].
|
|
||||||
func (fi *fileInfo) Mode() (mode fs.FileMode) { return fi.mode }
|
|
||||||
|
|
||||||
// stat is a Windows implementation of [Stat].
|
|
||||||
func stat(name string) (fi os.FileInfo, err error) {
|
|
||||||
absName, err := filepath.Abs(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("computing absolute path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fi, err = os.Stat(absName)
|
|
||||||
if err != nil {
|
|
||||||
// Don't wrap the error, since it's informative enough as is.
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dacl, owner, group, err := retrieveDACL(absName)
|
|
||||||
if err != nil {
|
|
||||||
// Don't wrap the error, since it's informative enough as is.
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownerMask, groupMask, otherMask windows.ACCESS_MASK
|
|
||||||
for i := range uint32(dacl.AceCount) {
|
|
||||||
var ace *windows.ACCESS_ALLOWED_ACE
|
|
||||||
err = windows.GetAce(dacl, i, &ace)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("getting access control entry at index %d: %w", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entrySid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
|
|
||||||
switch {
|
|
||||||
case entrySid.Equals(owner):
|
|
||||||
ownerMask |= ace.Mask
|
|
||||||
case entrySid.Equals(group):
|
|
||||||
groupMask |= ace.Mask
|
|
||||||
default:
|
|
||||||
otherMask |= ace.Mask
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mode := fi.Mode()
|
|
||||||
perm := masksToPerm(ownerMask, groupMask, otherMask, mode.IsDir())
|
|
||||||
|
|
||||||
return &fileInfo{
|
|
||||||
FileInfo: fi,
|
|
||||||
// Use the file mode from the security descriptor, but use the
|
|
||||||
// calculated permission bits.
|
|
||||||
mode: perm | mode&^fs.FileMode(0o777),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieveDACL retrieves the discretionary access control list, owner, and
|
|
||||||
// group from the security descriptor of the file with the specified absolute
|
|
||||||
// name.
|
|
||||||
func retrieveDACL(absName string) (dacl *windows.ACL, owner, group *windows.SID, err error) {
|
|
||||||
// desiredSecInfo defines the parts of a security descriptor to retrieve.
|
|
||||||
const desiredSecInfo windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION |
|
|
||||||
windows.GROUP_SECURITY_INFORMATION |
|
|
||||||
windows.DACL_SECURITY_INFORMATION |
|
|
||||||
windows.PROTECTED_DACL_SECURITY_INFORMATION |
|
|
||||||
windows.UNPROTECTED_DACL_SECURITY_INFORMATION
|
|
||||||
|
|
||||||
sd, err := windows.GetNamedSecurityInfo(absName, windows.SE_FILE_OBJECT, desiredSecInfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, fmt.Errorf("getting security descriptor: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dacl, _, err = sd.DACL()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, fmt.Errorf("getting discretionary access control list: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
owner, _, err = sd.Owner()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, fmt.Errorf("getting owner sid: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
group, _, err = sd.Group()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, fmt.Errorf("getting group sid: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dacl, owner, group, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// chmod is a Windows implementation of [Chmod].
|
|
||||||
func chmod(name string, perm fs.FileMode) (err error) {
|
|
||||||
fi, err := os.Stat(name)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting file info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
entries := make([]windows.EXPLICIT_ACCESS, 0, 3)
|
|
||||||
creatorMask, groupMask, worldMask := permToMasks(perm, fi.IsDir())
|
|
||||||
|
|
||||||
sidMasks := []struct {
|
|
||||||
Key windows.WELL_KNOWN_SID_TYPE
|
|
||||||
Value windows.ACCESS_MASK
|
|
||||||
}{{
|
|
||||||
Key: windows.WinCreatorOwnerSid,
|
|
||||||
Value: creatorMask,
|
|
||||||
}, {
|
|
||||||
Key: windows.WinCreatorGroupSid,
|
|
||||||
Value: groupMask,
|
|
||||||
}, {
|
|
||||||
Key: windows.WinWorldSid,
|
|
||||||
Value: worldMask,
|
|
||||||
}}
|
|
||||||
|
|
||||||
var errs []error
|
|
||||||
for _, sidMask := range sidMasks {
|
|
||||||
if sidMask.Value == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var trustee windows.TRUSTEE
|
|
||||||
trustee, err = newWellKnownTrustee(sidMask.Key)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
entries = append(entries, windows.EXPLICIT_ACCESS{
|
|
||||||
AccessPermissions: sidMask.Value,
|
|
||||||
AccessMode: windows.GRANT_ACCESS,
|
|
||||||
Inheritance: windows.NO_INHERITANCE,
|
|
||||||
Trustee: trustee,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = errors.Join(errs...); err != nil {
|
|
||||||
return fmt.Errorf("creating access control entries: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
acl, err := windows.ACLFromEntries(entries, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating access control list: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// secInfo defines the parts of a security descriptor to set.
|
|
||||||
const secInfo windows.SECURITY_INFORMATION = windows.DACL_SECURITY_INFORMATION |
|
|
||||||
windows.PROTECTED_DACL_SECURITY_INFORMATION
|
|
||||||
|
|
||||||
err = windows.SetNamedSecurityInfo(name, windows.SE_FILE_OBJECT, secInfo, nil, nil, acl, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("setting security descriptor: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// mkdir is a Windows implementation of [Mkdir].
|
|
||||||
//
|
|
||||||
// TODO(e.burkov): Consider using [windows.CreateDirectory] instead of
|
|
||||||
// [os.Mkdir] to reduce the number of syscalls.
|
|
||||||
func mkdir(name string, perm os.FileMode) (err error) {
|
|
||||||
name, err = filepath.Abs(name)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("computing absolute path: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Mkdir(name, perm)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
err = errors.WithDeferred(err, os.Remove(name))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return chmod(name, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// mkdirAll is a Windows implementation of [MkdirAll].
|
|
||||||
func mkdirAll(path string, perm os.FileMode) (err error) {
|
|
||||||
parent, _ := filepath.Split(path)
|
|
||||||
|
|
||||||
if parent != "" {
|
|
||||||
err = os.MkdirAll(parent, perm)
|
|
||||||
if err != nil && !errors.Is(err, os.ErrExist) {
|
|
||||||
return fmt.Errorf("creating parent directories: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mkdir(path, perm)
|
|
||||||
if errors.Is(err, os.ErrExist) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeFile is a Windows implementation of [WriteFile].
|
|
||||||
func writeFile(filename string, data []byte, perm os.FileMode) (err error) {
|
|
||||||
file, err := openFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("opening file: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { err = errors.WithDeferred(err, file.Close()) }()
|
|
||||||
|
|
||||||
_, err = file.Write(data)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("writing data: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// openFile is a Windows implementation of [OpenFile].
|
|
||||||
func openFile(name string, flag int, perm os.FileMode) (file *os.File, err error) {
|
|
||||||
// Only change permissions if the file not yet exists, but should be
|
|
||||||
// created.
|
|
||||||
if flag&os.O_CREATE == 0 {
|
|
||||||
return os.OpenFile(name, flag, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = stat(name)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
defer func() { err = errors.WithDeferred(err, chmod(name, perm)) }()
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("getting file info: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.OpenFile(name, flag, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newWellKnownTrustee returns a trustee for a well-known SID.
|
|
||||||
func newWellKnownTrustee(stype windows.WELL_KNOWN_SID_TYPE) (t windows.TRUSTEE, err error) {
|
|
||||||
sid, err := windows.CreateWellKnownSid(stype)
|
|
||||||
if err != nil {
|
|
||||||
return windows.TRUSTEE{}, fmt.Errorf("creating sid for type %d: %w", stype, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return windows.TRUSTEE{
|
|
||||||
TrusteeForm: windows.TRUSTEE_IS_SID,
|
|
||||||
TrusteeValue: windows.TrusteeValueFromSID(sid),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UNIX file mode permission bits.
|
|
||||||
const (
|
|
||||||
permRead = 0b100
|
|
||||||
permWrite = 0b010
|
|
||||||
permExecute = 0b001
|
|
||||||
)
|
|
||||||
|
|
||||||
// Windows access masks for appropriate UNIX file mode permission bits and
|
|
||||||
// file types.
|
|
||||||
const (
|
|
||||||
fileReadRights windows.ACCESS_MASK = windows.READ_CONTROL |
|
|
||||||
windows.FILE_READ_DATA |
|
|
||||||
windows.FILE_READ_ATTRIBUTES |
|
|
||||||
windows.FILE_READ_EA |
|
|
||||||
windows.SYNCHRONIZE |
|
|
||||||
windows.ACCESS_SYSTEM_SECURITY
|
|
||||||
|
|
||||||
fileWriteRights windows.ACCESS_MASK = windows.WRITE_DAC |
|
|
||||||
windows.WRITE_OWNER |
|
|
||||||
windows.FILE_WRITE_DATA |
|
|
||||||
windows.FILE_WRITE_ATTRIBUTES |
|
|
||||||
windows.FILE_WRITE_EA |
|
|
||||||
windows.DELETE |
|
|
||||||
windows.FILE_APPEND_DATA |
|
|
||||||
windows.SYNCHRONIZE |
|
|
||||||
windows.ACCESS_SYSTEM_SECURITY
|
|
||||||
|
|
||||||
fileExecuteRights windows.ACCESS_MASK = windows.FILE_EXECUTE
|
|
||||||
|
|
||||||
dirReadRights windows.ACCESS_MASK = windows.READ_CONTROL |
|
|
||||||
windows.FILE_LIST_DIRECTORY |
|
|
||||||
windows.FILE_READ_EA |
|
|
||||||
windows.FILE_READ_ATTRIBUTES<<1 |
|
|
||||||
windows.SYNCHRONIZE |
|
|
||||||
windows.ACCESS_SYSTEM_SECURITY
|
|
||||||
|
|
||||||
dirWriteRights windows.ACCESS_MASK = windows.WRITE_DAC |
|
|
||||||
windows.WRITE_OWNER |
|
|
||||||
windows.DELETE |
|
|
||||||
windows.FILE_WRITE_DATA |
|
|
||||||
windows.FILE_APPEND_DATA |
|
|
||||||
windows.FILE_WRITE_EA |
|
|
||||||
windows.FILE_WRITE_ATTRIBUTES<<1 |
|
|
||||||
windows.SYNCHRONIZE |
|
|
||||||
windows.ACCESS_SYSTEM_SECURITY
|
|
||||||
|
|
||||||
dirExecuteRights windows.ACCESS_MASK = windows.FILE_TRAVERSE
|
|
||||||
)
|
|
||||||
|
|
||||||
// permToMasks converts a UNIX file mode permissions to the corresponding
|
|
||||||
// Windows access masks. The [isDir] argument is used to set specific access
|
|
||||||
// bits for directories.
|
|
||||||
func permToMasks(fm os.FileMode, isDir bool) (owner, group, world windows.ACCESS_MASK) {
|
|
||||||
mask := fm.Perm()
|
|
||||||
|
|
||||||
owner = permToMask(byte((mask>>6)&0b111), isDir)
|
|
||||||
group = permToMask(byte((mask>>3)&0b111), isDir)
|
|
||||||
world = permToMask(byte(mask&0b111), isDir)
|
|
||||||
|
|
||||||
return owner, group, world
|
|
||||||
}
|
|
||||||
|
|
||||||
// permToMask converts a UNIX file mode permission bits within p byte to the
|
|
||||||
// corresponding Windows access mask. The [isDir] argument is used to set
|
|
||||||
// specific access bits for directories.
|
|
||||||
func permToMask(p byte, isDir bool) (mask windows.ACCESS_MASK) {
|
|
||||||
readRights, writeRights, executeRights := fileReadRights, fileWriteRights, fileExecuteRights
|
|
||||||
if isDir {
|
|
||||||
readRights, writeRights, executeRights = dirReadRights, dirWriteRights, dirExecuteRights
|
|
||||||
}
|
|
||||||
|
|
||||||
if p&permRead != 0 {
|
|
||||||
mask |= readRights
|
|
||||||
}
|
|
||||||
if p&permWrite != 0 {
|
|
||||||
mask |= writeRights
|
|
||||||
}
|
|
||||||
if p&permExecute != 0 {
|
|
||||||
mask |= executeRights
|
|
||||||
}
|
|
||||||
|
|
||||||
return mask
|
|
||||||
}
|
|
||||||
|
|
||||||
// masksToPerm converts Windows access masks to the corresponding UNIX file
|
|
||||||
// mode permission bits.
|
|
||||||
func masksToPerm(u, g, o windows.ACCESS_MASK, isDir bool) (perm fs.FileMode) {
|
|
||||||
perm |= fs.FileMode(maskToPerm(u, isDir)) << 6
|
|
||||||
perm |= fs.FileMode(maskToPerm(g, isDir)) << 3
|
|
||||||
perm |= fs.FileMode(maskToPerm(o, isDir))
|
|
||||||
|
|
||||||
return perm
|
|
||||||
}
|
|
||||||
|
|
||||||
// maskToPerm converts a Windows access mask to the corresponding UNIX file
|
|
||||||
// mode permission bits.
|
|
||||||
func maskToPerm(mask windows.ACCESS_MASK, isDir bool) (perm byte) {
|
|
||||||
readMask, writeMask, executeMask := fileReadRights, fileWriteRights, fileExecuteRights
|
|
||||||
if isDir {
|
|
||||||
readMask, writeMask, executeMask = dirReadRights, dirWriteRights, dirExecuteRights
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove common bits to avoid false positive detection of unset rights.
|
|
||||||
readMask ^= windows.SYNCHRONIZE | windows.ACCESS_SYSTEM_SECURITY
|
|
||||||
writeMask ^= windows.SYNCHRONIZE | windows.ACCESS_SYSTEM_SECURITY
|
|
||||||
|
|
||||||
if mask&readMask != 0 {
|
|
||||||
perm |= permRead
|
|
||||||
}
|
|
||||||
if mask&writeMask != 0 {
|
|
||||||
perm |= permWrite
|
|
||||||
}
|
|
||||||
if mask&executeMask != 0 {
|
|
||||||
perm |= permExecute
|
|
||||||
}
|
|
||||||
|
|
||||||
return perm
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
//go:build windows
|
|
||||||
|
|
||||||
package aghos
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestPermToMasks(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
perm fs.FileMode
|
|
||||||
wantUser windows.ACCESS_MASK
|
|
||||||
wantGroup windows.ACCESS_MASK
|
|
||||||
wantOther windows.ACCESS_MASK
|
|
||||||
isDir bool
|
|
||||||
}{{
|
|
||||||
name: "all",
|
|
||||||
perm: 0b111_111_111,
|
|
||||||
wantUser: fileReadRights | fileWriteRights | fileExecuteRights,
|
|
||||||
wantGroup: fileReadRights | fileWriteRights | fileExecuteRights,
|
|
||||||
wantOther: fileReadRights | fileWriteRights | fileExecuteRights,
|
|
||||||
isDir: false,
|
|
||||||
}, {
|
|
||||||
name: "user_write",
|
|
||||||
perm: 0b010_000_000,
|
|
||||||
wantUser: fileWriteRights,
|
|
||||||
wantGroup: 0,
|
|
||||||
wantOther: 0,
|
|
||||||
isDir: false,
|
|
||||||
}, {
|
|
||||||
name: "group_read",
|
|
||||||
perm: 0b000_100_000,
|
|
||||||
wantUser: 0,
|
|
||||||
wantGroup: fileReadRights,
|
|
||||||
wantOther: 0,
|
|
||||||
isDir: false,
|
|
||||||
}, {
|
|
||||||
name: "all_dir",
|
|
||||||
perm: 0b111_111_111,
|
|
||||||
wantUser: dirReadRights | dirWriteRights | dirExecuteRights,
|
|
||||||
wantGroup: dirReadRights | dirWriteRights | dirExecuteRights,
|
|
||||||
wantOther: dirReadRights | dirWriteRights | dirExecuteRights,
|
|
||||||
isDir: true,
|
|
||||||
}, {
|
|
||||||
name: "user_write_dir",
|
|
||||||
perm: 0b010_000_000,
|
|
||||||
wantUser: dirWriteRights,
|
|
||||||
wantGroup: 0,
|
|
||||||
wantOther: 0,
|
|
||||||
isDir: true,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
user, group, other := permToMasks(tc.perm, tc.isDir)
|
|
||||||
assert.Equal(t, tc.wantUser, user)
|
|
||||||
assert.Equal(t, tc.wantGroup, group)
|
|
||||||
assert.Equal(t, tc.wantOther, other)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMasksToPerm(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
user windows.ACCESS_MASK
|
|
||||||
group windows.ACCESS_MASK
|
|
||||||
other windows.ACCESS_MASK
|
|
||||||
wantPerm fs.FileMode
|
|
||||||
isDir bool
|
|
||||||
}{{
|
|
||||||
name: "all",
|
|
||||||
user: fileReadRights | fileWriteRights | fileExecuteRights,
|
|
||||||
group: fileReadRights | fileWriteRights | fileExecuteRights,
|
|
||||||
other: fileReadRights | fileWriteRights | fileExecuteRights,
|
|
||||||
wantPerm: 0b111_111_111,
|
|
||||||
isDir: false,
|
|
||||||
}, {
|
|
||||||
name: "user_write",
|
|
||||||
user: fileWriteRights,
|
|
||||||
group: 0,
|
|
||||||
other: 0,
|
|
||||||
wantPerm: 0b010_000_000,
|
|
||||||
isDir: false,
|
|
||||||
}, {
|
|
||||||
name: "group_read",
|
|
||||||
user: 0,
|
|
||||||
group: fileReadRights,
|
|
||||||
other: 0,
|
|
||||||
wantPerm: 0b000_100_000,
|
|
||||||
isDir: false,
|
|
||||||
}, {
|
|
||||||
name: "no_access",
|
|
||||||
user: 0,
|
|
||||||
group: 0,
|
|
||||||
other: 0,
|
|
||||||
wantPerm: 0,
|
|
||||||
isDir: false,
|
|
||||||
}, {
|
|
||||||
name: "all_dir",
|
|
||||||
user: dirReadRights | dirWriteRights | dirExecuteRights,
|
|
||||||
group: dirReadRights | dirWriteRights | dirExecuteRights,
|
|
||||||
other: dirReadRights | dirWriteRights | dirExecuteRights,
|
|
||||||
wantPerm: 0b111_111_111,
|
|
||||||
isDir: true,
|
|
||||||
}, {
|
|
||||||
name: "user_write_dir",
|
|
||||||
user: dirWriteRights,
|
|
||||||
group: 0,
|
|
||||||
other: 0,
|
|
||||||
wantPerm: 0b010_000_000,
|
|
||||||
isDir: true,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Don't call [fs.FileMode.Perm] since the result is expected to
|
|
||||||
// contain only the permission bits.
|
|
||||||
assert.Equal(t, tc.wantPerm, masksToPerm(tc.user, tc.group, tc.other, tc.isDir))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,7 +62,9 @@ func newPendingFile(filePath string, mode fs.FileMode) (f PendingFile, err error
|
||||||
return nil, fmt.Errorf("opening pending file: %w", err)
|
return nil, fmt.Errorf("opening pending file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghos.Chmod(file.Name(), mode)
|
// TODO(e.burkov): The [os.Chmod] implementation is useless on Windows,
|
||||||
|
// investigate if it can be removed.
|
||||||
|
err = os.Chmod(file.Name(), mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("preparing pending file: %w", err)
|
return nil, fmt.Errorf("preparing pending file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ func (w *FSWatcher) Add(name string) (err error) {
|
||||||
|
|
||||||
// ServiceWithConfig is a fake [agh.ServiceWithConfig] implementation for tests.
|
// ServiceWithConfig is a fake [agh.ServiceWithConfig] implementation for tests.
|
||||||
type ServiceWithConfig[ConfigType any] struct {
|
type ServiceWithConfig[ConfigType any] struct {
|
||||||
OnStart func() (err error)
|
OnStart func(ctx context.Context) (err error)
|
||||||
OnShutdown func(ctx context.Context) (err error)
|
OnShutdown func(ctx context.Context) (err error)
|
||||||
OnConfig func() (c ConfigType)
|
OnConfig func() (c ConfigType)
|
||||||
}
|
}
|
||||||
|
@ -68,8 +68,8 @@ var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
|
||||||
|
|
||||||
// Start implements the [agh.ServiceWithConfig] interface for
|
// Start implements the [agh.ServiceWithConfig] interface for
|
||||||
// *ServiceWithConfig.
|
// *ServiceWithConfig.
|
||||||
func (s *ServiceWithConfig[_]) Start() (err error) {
|
func (s *ServiceWithConfig[_]) Start(ctx context.Context) (err error) {
|
||||||
return s.OnStart()
|
return s.OnStart(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown implements the [agh.ServiceWithConfig] interface for
|
// Shutdown implements the [agh.ServiceWithConfig] interface for
|
||||||
|
|
|
@ -2,6 +2,7 @@ package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
|
@ -9,7 +10,6 @@ import (
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// macKey contains MAC as byte array of 6, 8, or 20 bytes.
|
// macKey contains MAC as byte array of 6, 8, or 20 bytes.
|
||||||
|
@ -330,12 +330,14 @@ func (ci *index) size() (n int) {
|
||||||
// rangeByName is like [Index.Range] but sorts the persistent clients by name
|
// rangeByName is like [Index.Range] but sorts the persistent clients by name
|
||||||
// before iterating ensuring a predictable order.
|
// before iterating ensuring a predictable order.
|
||||||
func (ci *index) rangeByName(f func(c *Persistent) (cont bool)) {
|
func (ci *index) rangeByName(f func(c *Persistent) (cont bool)) {
|
||||||
cs := maps.Values(ci.uidToClient)
|
clients := slices.SortedStableFunc(
|
||||||
slices.SortFunc(cs, func(a, b *Persistent) (n int) {
|
maps.Values(ci.uidToClient),
|
||||||
return strings.Compare(a.Name, b.Name)
|
func(a, b *Persistent) (res int) {
|
||||||
})
|
return strings.Compare(a.Name, b.Name)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
for _, c := range cs {
|
for _, c := range clients {
|
||||||
if !f(c) {
|
if !f(c) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/hostsfile"
|
"github.com/AdguardTeam/golibs/hostsfile"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -506,7 +505,7 @@ func (s *Storage) FindByMAC(mac net.HardwareAddr) (p *Persistent, ok bool) {
|
||||||
|
|
||||||
// RemoveByName removes persistent client information. ok is false if no such
|
// RemoveByName removes persistent client information. ok is false if no such
|
||||||
// client exists by that name.
|
// client exists by that name.
|
||||||
func (s *Storage) RemoveByName(name string) (ok bool) {
|
func (s *Storage) RemoveByName(ctx context.Context, name string) (ok bool) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
@ -516,7 +515,7 @@ func (s *Storage) RemoveByName(name string) (ok bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.CloseUpstreams(); err != nil {
|
if err := p.CloseUpstreams(); err != nil {
|
||||||
log.Error("client storage: removing client %q: %s", p.Name, err)
|
s.logger.ErrorContext(ctx, "removing client", "name", p.Name, slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.index.remove(p)
|
s.index.remove(p)
|
||||||
|
|
|
@ -735,7 +735,7 @@ func TestStorage_RemoveByName(t *testing.T) {
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
tc.want(t, s.RemoveByName(tc.cliName))
|
tc.want(t, s.RemoveByName(ctx, tc.cliName))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -744,8 +744,8 @@ func TestStorage_RemoveByName(t *testing.T) {
|
||||||
err = s.Add(ctx, existingClient)
|
err = s.Add(ctx, existingClient)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.True(t, s.RemoveByName(existingName))
|
assert.True(t, s.RemoveByName(ctx, existingName))
|
||||||
assert.False(t, s.RemoveByName(existingName))
|
assert.False(t, s.RemoveByName(ctx, existingName))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,11 @@ import (
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
"github.com/go-ping/ping"
|
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||||
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
"github.com/insomniacslk/dhcp/dhcpv4/server4"
|
||||||
|
|
||||||
|
//lint:ignore SA1019 See the TODO in go.mod.
|
||||||
|
"github.com/go-ping/ping"
|
||||||
)
|
)
|
||||||
|
|
||||||
// v4Server is a DHCPv4 server.
|
// v4Server is a DHCPv4 server.
|
||||||
|
|
|
@ -82,7 +82,7 @@ type Empty struct{}
|
||||||
var _ agh.ServiceWithConfig[*Config] = Empty{}
|
var _ agh.ServiceWithConfig[*Config] = Empty{}
|
||||||
|
|
||||||
// Start implements the [Service] interface for Empty.
|
// Start implements the [Service] interface for Empty.
|
||||||
func (Empty) Start() (err error) { return nil }
|
func (Empty) Start(_ context.Context) (err error) { return nil }
|
||||||
|
|
||||||
// Shutdown implements the [Service] interface for Empty.
|
// Shutdown implements the [Service] interface for Empty.
|
||||||
func (Empty) Shutdown(_ context.Context) (err error) { return nil }
|
func (Empty) Shutdown(_ context.Context) (err error) { return nil }
|
||||||
|
|
|
@ -818,6 +818,8 @@ func (s *Server) proxy() (p *proxy.Proxy) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconfigure applies the new configuration to the DNS server.
|
// Reconfigure applies the new configuration to the DNS server.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): This whole piece of API is weird and needs to be remade.
|
||||||
func (s *Server) Reconfigure(conf *ServerConfig) error {
|
func (s *Server) Reconfigure(conf *ServerConfig) error {
|
||||||
s.serverLock.Lock()
|
s.serverLock.Lock()
|
||||||
defer s.serverLock.Unlock()
|
defer s.serverLock.Unlock()
|
||||||
|
@ -831,14 +833,15 @@ func (s *Server) Reconfigure(conf *ServerConfig) error {
|
||||||
// We wait for some time and hope that this fd will be closed.
|
// We wait for some time and hope that this fd will be closed.
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
// TODO(a.garipov): This whole piece of API is weird and needs to be remade.
|
if s.addrProc != nil {
|
||||||
|
err := s.addrProc.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("dnsforward: closing address processor: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if conf == nil {
|
if conf == nil {
|
||||||
conf = &s.conf
|
conf = &s.conf
|
||||||
} else {
|
|
||||||
closeErr := s.addrProc.Close()
|
|
||||||
if closeErr != nil {
|
|
||||||
log.Error("dnsforward: closing address processor: %s", closeErr)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(e.burkov): It seems an error here brings the server down, which is
|
// TODO(e.burkov): It seems an error here brings the server down, which is
|
||||||
|
|
|
@ -500,6 +500,10 @@ func TestServerRace(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSafeSearch(t *testing.T) {
|
func TestSafeSearch(t *testing.T) {
|
||||||
|
const (
|
||||||
|
googleSafeSearch = "forcesafesearch.google.com."
|
||||||
|
)
|
||||||
|
|
||||||
safeSearchConf := filtering.SafeSearchConfig{
|
safeSearchConf := filtering.SafeSearchConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Google: true,
|
Google: true,
|
||||||
|
@ -536,10 +540,17 @@ func TestSafeSearch(t *testing.T) {
|
||||||
ServePlainDNS: true,
|
ServePlainDNS: true,
|
||||||
}
|
}
|
||||||
s := createTestServer(t, filterConf, forwardConf)
|
s := createTestServer(t, filterConf, forwardConf)
|
||||||
startDeferStop(t, s)
|
|
||||||
|
|
||||||
|
ups := aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||||
|
pt := testutil.PanicT{}
|
||||||
|
assert.Equal(pt, googleSafeSearch, req.Question[0].Name)
|
||||||
|
|
||||||
|
return aghtest.MatchedResponse(req, dns.TypeA, googleSafeSearch, "1.2.3.4"), nil
|
||||||
|
})
|
||||||
|
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ups}
|
||||||
|
|
||||||
|
startDeferStop(t, s)
|
||||||
addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
|
addr := s.dnsProxy.Addr(proxy.ProtoUDP).String()
|
||||||
client := &dns.Client{}
|
|
||||||
|
|
||||||
yandexIP := netip.AddrFrom4([4]byte{213, 180, 193, 56})
|
yandexIP := netip.AddrFrom4([4]byte{213, 180, 193, 56})
|
||||||
|
|
||||||
|
@ -585,15 +596,9 @@ func TestSafeSearch(t *testing.T) {
|
||||||
t.Run(tc.host, func(t *testing.T) {
|
t.Run(tc.host, func(t *testing.T) {
|
||||||
req := createTestMessage(tc.host)
|
req := createTestMessage(tc.host)
|
||||||
|
|
||||||
// TODO(a.garipov): Create our own helper for this.
|
|
||||||
var reply *dns.Msg
|
var reply *dns.Msg
|
||||||
once := &sync.Once{}
|
reply, err = dns.Exchange(req, addr)
|
||||||
require.EventuallyWithT(t, func(c *assert.CollectT) {
|
require.NoError(t, err)
|
||||||
r, _, errExch := client.Exchange(req, addr)
|
|
||||||
if assert.NoError(c, errExch) {
|
|
||||||
once.Do(func() { reply = r })
|
|
||||||
}
|
|
||||||
}, testTimeout*10, testTimeout)
|
|
||||||
|
|
||||||
if tc.wantCNAME != "" {
|
if tc.wantCNAME != "" {
|
||||||
require.Len(t, reply.Answer, 2)
|
require.Len(t, reply.Answer, 2)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -33,7 +33,7 @@ func serveHTTPLocally(t *testing.T, h http.Handler) (urlStr string) {
|
||||||
require.IsType(t, (*net.TCPAddr)(nil), addr)
|
require.IsType(t, (*net.TCPAddr)(nil), addr)
|
||||||
|
|
||||||
return (&url.URL{
|
return (&url.URL{
|
||||||
Scheme: aghhttp.SchemeHTTP,
|
Scheme: urlutil.SchemeHTTP,
|
||||||
Host: addr.String(),
|
Host: addr.String(),
|
||||||
}).String()
|
}).String()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1057,7 +1057,7 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghos.MkdirAll(filepath.Join(d.conf.DataDir, filterDir), aghos.DefaultPermDir)
|
err = os.MkdirAll(filepath.Join(d.conf.DataDir, filterDir), aghos.DefaultPermDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.Close()
|
d.Close()
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ func (d *DNSFilter) validateFilterURL(urlStr string) (err error) {
|
||||||
|
|
||||||
if filepath.IsAbs(urlStr) {
|
if filepath.IsAbs(urlStr) {
|
||||||
urlStr = filepath.Clean(urlStr)
|
urlStr = filepath.Clean(urlStr)
|
||||||
_, err = aghos.Stat(urlStr)
|
_, err = os.Stat(urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error since it's informative enough as is.
|
// Don't wrap the error since it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
|
@ -41,19 +41,14 @@ func (d *DNSFilter) validateFilterURL(urlStr string) (err error) {
|
||||||
|
|
||||||
u, err := url.ParseRequestURI(urlStr)
|
u, err := url.ParseRequestURI(urlStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error since it's informative enough as is.
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s := u.Scheme; s != aghhttp.SchemeHTTP && s != aghhttp.SchemeHTTPS {
|
err = urlutil.ValidateHTTPURL(u)
|
||||||
return &url.Error{
|
if err != nil {
|
||||||
Op: "Check scheme",
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
URL: urlStr,
|
return err
|
||||||
Err: fmt.Errorf("only %v allowed", []string{
|
|
||||||
aghhttp.SchemeHTTP,
|
|
||||||
aghhttp.SchemeHTTPS,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -3,11 +3,12 @@ package rulelist
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/urlfilter"
|
"github.com/AdguardTeam/urlfilter"
|
||||||
"github.com/AdguardTeam/urlfilter/filterlist"
|
"github.com/AdguardTeam/urlfilter/filterlist"
|
||||||
"github.com/c2h5oh/datasize"
|
"github.com/c2h5oh/datasize"
|
||||||
|
@ -18,6 +19,9 @@ import (
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Merge with [TextEngine] in some way?
|
// TODO(a.garipov): Merge with [TextEngine] in some way?
|
||||||
type Engine struct {
|
type Engine struct {
|
||||||
|
// logger is used to log the operation of the engine and its refreshes.
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
// mu protects engine and storage.
|
// mu protects engine and storage.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): See if anything else should be protected.
|
// TODO(a.garipov): See if anything else should be protected.
|
||||||
|
@ -29,8 +33,7 @@ type Engine struct {
|
||||||
// storage is the filtering-rule storage. It is saved here to close it.
|
// storage is the filtering-rule storage. It is saved here to close it.
|
||||||
storage *filterlist.RuleStorage
|
storage *filterlist.RuleStorage
|
||||||
|
|
||||||
// name is the human-readable name of the engine, like "allowed", "blocked",
|
// name is the human-readable name of the engine.
|
||||||
// or "custom".
|
|
||||||
name string
|
name string
|
||||||
|
|
||||||
// filters is the data about rule filters in this engine.
|
// filters is the data about rule filters in this engine.
|
||||||
|
@ -40,12 +43,15 @@ type Engine struct {
|
||||||
// EngineConfig is the configuration for rule-list filtering engines created by
|
// EngineConfig is the configuration for rule-list filtering engines created by
|
||||||
// combining refreshable filters.
|
// combining refreshable filters.
|
||||||
type EngineConfig struct {
|
type EngineConfig struct {
|
||||||
// Name is the human-readable name of this engine, like "allowed",
|
// Logger is used to log the operation of the engine. It must not be nil.
|
||||||
// "blocked", or "custom".
|
Logger *slog.Logger
|
||||||
|
|
||||||
|
// name is the human-readable name of the engine; see [EngineNameAllow] and
|
||||||
|
// similar constants.
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
// Filters is the data about rule lists in this engine. There must be no
|
// Filters is the data about rule lists in this engine. There must be no
|
||||||
// other references to the elements of this slice.
|
// other references to the items of this slice. Each item must not be nil.
|
||||||
Filters []*Filter
|
Filters []*Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +59,7 @@ type EngineConfig struct {
|
||||||
// refreshed, so a refresh should be performed before use.
|
// refreshed, so a refresh should be performed before use.
|
||||||
func NewEngine(c *EngineConfig) (e *Engine) {
|
func NewEngine(c *EngineConfig) (e *Engine) {
|
||||||
return &Engine{
|
return &Engine{
|
||||||
|
logger: c.Logger,
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
name: c.Name,
|
name: c.Name,
|
||||||
filters: c.Filters,
|
filters: c.Filters,
|
||||||
|
@ -85,7 +92,7 @@ func (e *Engine) FilterRequest(
|
||||||
}
|
}
|
||||||
|
|
||||||
// currentEngine returns the current filtering engine.
|
// currentEngine returns the current filtering engine.
|
||||||
func (e *Engine) currentEngine() (enging *urlfilter.DNSEngine) {
|
func (e *Engine) currentEngine() (engine *urlfilter.DNSEngine) {
|
||||||
e.mu.RLock()
|
e.mu.RLock()
|
||||||
defer e.mu.RUnlock()
|
defer e.mu.RUnlock()
|
||||||
|
|
||||||
|
@ -96,7 +103,7 @@ func (e *Engine) currentEngine() (enging *urlfilter.DNSEngine) {
|
||||||
// parseBuf, cli, cacheDir, and maxSize are used for updates of rule-list
|
// parseBuf, cli, cacheDir, and maxSize are used for updates of rule-list
|
||||||
// filters; see [Filter.Refresh].
|
// filters; see [Filter.Refresh].
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Unexport and test in an internal test or through enigne
|
// TODO(a.garipov): Unexport and test in an internal test or through engine
|
||||||
// tests.
|
// tests.
|
||||||
func (e *Engine) Refresh(
|
func (e *Engine) Refresh(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
@ -115,20 +122,20 @@ func (e *Engine) Refresh(
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(filtersToRefresh) == 0 {
|
if len(filtersToRefresh) == 0 {
|
||||||
log.Info("filtering: updating engine %q: no rule-list filters", e.name)
|
e.logger.InfoContext(ctx, "updating: no rule-list filters")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
engRefr := &engineRefresh{
|
engRefr := &engineRefresh{
|
||||||
httpCli: cli,
|
logger: e.logger,
|
||||||
cacheDir: cacheDir,
|
httpCli: cli,
|
||||||
engineName: e.name,
|
cacheDir: cacheDir,
|
||||||
parseBuf: parseBuf,
|
parseBuf: parseBuf,
|
||||||
maxSize: maxSize,
|
maxSize: maxSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleLists, errs := engRefr.process(ctx, e.filters)
|
ruleLists, errs := engRefr.process(ctx, filtersToRefresh)
|
||||||
if isOneTimeoutError(errs) {
|
if isOneTimeoutError(errs) {
|
||||||
// Don't wrap the error since it's informative enough as is.
|
// Don't wrap the error since it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
|
@ -141,14 +148,14 @@ func (e *Engine) Refresh(
|
||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.resetStorage(storage)
|
e.resetStorage(ctx, storage)
|
||||||
|
|
||||||
return errors.Join(errs...)
|
return errors.Join(errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resetStorage sets e.storage and e.engine and closes the previous storage.
|
// resetStorage sets e.storage and e.engine and closes the previous storage.
|
||||||
// Errors from closing the previous storage are logged.
|
// Errors from closing the previous storage are logged.
|
||||||
func (e *Engine) resetStorage(storage *filterlist.RuleStorage) {
|
func (e *Engine) resetStorage(ctx context.Context, storage *filterlist.RuleStorage) {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
defer e.mu.Unlock()
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
@ -161,7 +168,7 @@ func (e *Engine) resetStorage(storage *filterlist.RuleStorage) {
|
||||||
|
|
||||||
err := prevStorage.Close()
|
err := prevStorage.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("filtering: engine %q: closing old storage: %s", e.name, err)
|
e.logger.WarnContext(ctx, "closing old storage", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,11 +186,11 @@ func isOneTimeoutError(errs []error) (ok bool) {
|
||||||
|
|
||||||
// engineRefresh represents a single ongoing engine refresh.
|
// engineRefresh represents a single ongoing engine refresh.
|
||||||
type engineRefresh struct {
|
type engineRefresh struct {
|
||||||
httpCli *http.Client
|
logger *slog.Logger
|
||||||
cacheDir string
|
httpCli *http.Client
|
||||||
engineName string
|
cacheDir string
|
||||||
parseBuf []byte
|
parseBuf []byte
|
||||||
maxSize datasize.ByteSize
|
maxSize datasize.ByteSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// process runs updates of all given rule-list filters. All errors are logged
|
// process runs updates of all given rule-list filters. All errors are logged
|
||||||
|
@ -216,12 +223,12 @@ func (r *engineRefresh) process(
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
|
|
||||||
// Also log immediately, since the update can take a lot of time.
|
// Also log immediately, since the update can take a lot of time.
|
||||||
log.Error(
|
r.logger.ErrorContext(
|
||||||
"filtering: updating engine %q: rule list %s from url %q: %s\n",
|
ctx,
|
||||||
r.engineName,
|
"updating rule list",
|
||||||
f.uid,
|
"uid", f.uid,
|
||||||
f.url,
|
"url", f.url,
|
||||||
err,
|
slogutil.KeyError, err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,17 +244,17 @@ func (r *engineRefresh) processFilter(ctx context.Context, f *Filter) (err error
|
||||||
}
|
}
|
||||||
|
|
||||||
if prevChecksum == parseRes.Checksum {
|
if prevChecksum == parseRes.Checksum {
|
||||||
log.Info("filtering: engine %q: filter %q: no change", r.engineName, f.uid)
|
r.logger.InfoContext(ctx, "no change in filter", "uid", f.uid)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info(
|
r.logger.InfoContext(
|
||||||
"filtering: updated engine %q: filter %q: %d bytes, %d rules",
|
ctx,
|
||||||
r.engineName,
|
"filter updated",
|
||||||
f.uid,
|
"uid", f.uid,
|
||||||
parseRes.BytesWritten,
|
"bytes", parseRes.BytesWritten,
|
||||||
parseRes.RulesCount,
|
"rules", parseRes.RulesCount,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/urlfilter"
|
"github.com/AdguardTeam/urlfilter"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
@ -13,6 +14,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEngine_Refresh(t *testing.T) {
|
func TestEngine_Refresh(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
cacheDir := t.TempDir()
|
cacheDir := t.TempDir()
|
||||||
|
|
||||||
fileURL, srvURL := newFilterLocations(t, cacheDir, testRuleTextBlocked, testRuleTextBlocked2)
|
fileURL, srvURL := newFilterLocations(t, cacheDir, testRuleTextBlocked, testRuleTextBlocked2)
|
||||||
|
@ -21,6 +24,7 @@ func TestEngine_Refresh(t *testing.T) {
|
||||||
httpFlt := newFilter(t, srvURL, "HTTP Filter")
|
httpFlt := newFilter(t, srvURL, "HTTP Filter")
|
||||||
|
|
||||||
eng := rulelist.NewEngine(&rulelist.EngineConfig{
|
eng := rulelist.NewEngine(&rulelist.EngineConfig{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
Name: "Engine",
|
Name: "Engine",
|
||||||
Filters: []*rulelist.Filter{fileFlt, httpFlt},
|
Filters: []*rulelist.Filter{fileFlt, httpFlt},
|
||||||
})
|
})
|
||||||
|
|
|
@ -105,7 +105,7 @@ func NewFilter(c *FilterConfig) (f *Filter, err error) {
|
||||||
// buffer used to parse information from the data. cli and maxSize are only
|
// buffer used to parse information from the data. cli and maxSize are only
|
||||||
// used when f is a URL-based list.
|
// used when f is a URL-based list.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Unexport and test in an internal test or through enigne
|
// TODO(a.garipov): Unexport and test in an internal test or through engine
|
||||||
// tests.
|
// tests.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Consider not returning parseRes.
|
// TODO(a.garipov): Consider not returning parseRes.
|
||||||
|
|
|
@ -8,12 +8,15 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFilter_Refresh(t *testing.T) {
|
func TestFilter_Refresh(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
cacheDir := t.TempDir()
|
cacheDir := t.TempDir()
|
||||||
uid := rulelist.MustNewUID()
|
uid := rulelist.MustNewUID()
|
||||||
|
|
||||||
|
@ -37,7 +40,7 @@ func TestFilter_Refresh(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "file",
|
name: "file",
|
||||||
url: &url.URL{
|
url: &url.URL{
|
||||||
Scheme: "file",
|
Scheme: urlutil.SchemeFile,
|
||||||
Path: fileURL.Path,
|
Path: fileURL.Path,
|
||||||
},
|
},
|
||||||
wantNewErrMsg: "",
|
wantNewErrMsg: "",
|
||||||
|
@ -49,6 +52,8 @@ func TestFilter_Refresh(t *testing.T) {
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
f, err := rulelist.NewFilter(&rulelist.FilterConfig{
|
f, err := rulelist.NewFilter(&rulelist.FilterConfig{
|
||||||
URL: tc.url,
|
URL: tc.url,
|
||||||
Name: tc.name,
|
Name: tc.name,
|
||||||
|
|
|
@ -71,3 +71,10 @@ var _ fmt.Stringer = UID{}
|
||||||
func (id UID) String() (s string) {
|
func (id UID) String() (s string) {
|
||||||
return uuid.UUID(id).String()
|
return uuid.UUID(id).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Common engine names.
|
||||||
|
const (
|
||||||
|
EngineNameAllow = "allow"
|
||||||
|
EngineNameBlock = "block"
|
||||||
|
EngineNameCustom = "custom"
|
||||||
|
)
|
||||||
|
|
|
@ -6,20 +6,16 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
testutil.DiscardLogOutput(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testTimeout is the common timeout for tests.
|
// testTimeout is the common timeout for tests.
|
||||||
const testTimeout = 1 * time.Second
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
|
@ -31,6 +27,7 @@ const testTitle = "Test Title"
|
||||||
|
|
||||||
// Common rule texts for tests.
|
// Common rule texts for tests.
|
||||||
const (
|
const (
|
||||||
|
testRuleTextAllowed = "||allowed.example^\n"
|
||||||
testRuleTextBadTab = "||bad-tab-and-comment.example^\t# A comment.\n"
|
testRuleTextBadTab = "||bad-tab-and-comment.example^\t# A comment.\n"
|
||||||
testRuleTextBlocked = "||blocked.example^\n"
|
testRuleTextBlocked = "||blocked.example^\n"
|
||||||
testRuleTextBlocked2 = "||blocked-2.example^\n"
|
testRuleTextBlocked2 = "||blocked-2.example^\n"
|
||||||
|
@ -79,8 +76,16 @@ func newFilterLocations(
|
||||||
fileData string,
|
fileData string,
|
||||||
httpData string,
|
httpData string,
|
||||||
) (fileURL, srvURL *url.URL) {
|
) (fileURL, srvURL *url.URL) {
|
||||||
filePath := filepath.Join(cacheDir, "initial.txt")
|
t.Helper()
|
||||||
err := os.WriteFile(filePath, []byte(fileData), 0o644)
|
|
||||||
|
f, err := os.CreateTemp(cacheDir, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
filePath := f.Name()
|
||||||
|
err = os.WriteFile(filePath, []byte(fileData), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testutil.CleanupAndRequireSuccess(t, func() (err error) {
|
testutil.CleanupAndRequireSuccess(t, func() (err error) {
|
||||||
|
@ -88,7 +93,7 @@ func newFilterLocations(
|
||||||
})
|
})
|
||||||
|
|
||||||
fileURL = &url.URL{
|
fileURL = &url.URL{
|
||||||
Scheme: "file",
|
Scheme: urlutil.SchemeFile,
|
||||||
Path: filePath,
|
Path: filePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
112
internal/filtering/rulelist/storage.go
Normal file
112
internal/filtering/rulelist/storage.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package rulelist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Storage contains the main filtering engines, including the allowlist, the
|
||||||
|
// blocklist, and the user's custom filtering rules.
|
||||||
|
type Storage struct {
|
||||||
|
// refreshMu makes sure that only one update takes place at a time.
|
||||||
|
refreshMu *sync.Mutex
|
||||||
|
|
||||||
|
allow *Engine
|
||||||
|
block *Engine
|
||||||
|
custom *TextEngine
|
||||||
|
httpCli *http.Client
|
||||||
|
cacheDir string
|
||||||
|
parseBuf []byte
|
||||||
|
maxSize datasize.ByteSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageConfig is the configuration for the filtering-engine storage.
|
||||||
|
type StorageConfig struct {
|
||||||
|
// Logger is used to log the operation of the storage. It must not be nil.
|
||||||
|
Logger *slog.Logger
|
||||||
|
|
||||||
|
// HTTPClient is the HTTP client used to perform updates of rule lists.
|
||||||
|
// It must not be nil.
|
||||||
|
HTTPClient *http.Client
|
||||||
|
|
||||||
|
// CacheDir is the path to the directory used to cache rule-list files.
|
||||||
|
// It must be set.
|
||||||
|
CacheDir string
|
||||||
|
|
||||||
|
// AllowFilters are the filtering-rule lists used to exclude domain names
|
||||||
|
// from the filtering. Each item must not be nil.
|
||||||
|
AllowFilters []*Filter
|
||||||
|
|
||||||
|
// BlockFilters are the filtering-rule lists used to block domain names.
|
||||||
|
// Each item must not be nil.
|
||||||
|
BlockFilters []*Filter
|
||||||
|
|
||||||
|
// CustomRules contains custom rules of the user. They have priority over
|
||||||
|
// both allow- and blacklist rules.
|
||||||
|
CustomRules []string
|
||||||
|
|
||||||
|
// MaxRuleListTextSize is the maximum size of a rule-list file. It must be
|
||||||
|
// greater than zero.
|
||||||
|
MaxRuleListTextSize datasize.ByteSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStorage creates a new filtering-engine storage. The engines are not
|
||||||
|
// refreshed, so a refresh should be performed before use.
|
||||||
|
func NewStorage(c *StorageConfig) (s *Storage, err error) {
|
||||||
|
custom, err := NewTextEngine(&TextEngineConfig{
|
||||||
|
Name: EngineNameCustom,
|
||||||
|
Rules: c.CustomRules,
|
||||||
|
ID: URLFilterIDCustom,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating custom engine: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Storage{
|
||||||
|
refreshMu: &sync.Mutex{},
|
||||||
|
allow: NewEngine(&EngineConfig{
|
||||||
|
Logger: c.Logger.With("engine", EngineNameAllow),
|
||||||
|
Name: EngineNameAllow,
|
||||||
|
Filters: c.AllowFilters,
|
||||||
|
}),
|
||||||
|
block: NewEngine(&EngineConfig{
|
||||||
|
Logger: c.Logger.With("engine", EngineNameBlock),
|
||||||
|
Name: EngineNameBlock,
|
||||||
|
Filters: c.BlockFilters,
|
||||||
|
}),
|
||||||
|
custom: custom,
|
||||||
|
httpCli: c.HTTPClient,
|
||||||
|
cacheDir: c.CacheDir,
|
||||||
|
parseBuf: make([]byte, DefaultRuleBufSize),
|
||||||
|
maxSize: c.MaxRuleListTextSize,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the underlying rule-list engines.
|
||||||
|
func (s *Storage) Close() (err error) {
|
||||||
|
// Don't wrap the errors since they are informative enough as is.
|
||||||
|
return errors.Join(
|
||||||
|
s.allow.Close(),
|
||||||
|
s.block.Close(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh updates all engines in s.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Refresh allow and block separately?
|
||||||
|
func (s *Storage) Refresh(ctx context.Context) (err error) {
|
||||||
|
s.refreshMu.Lock()
|
||||||
|
defer s.refreshMu.Unlock()
|
||||||
|
|
||||||
|
// Don't wrap the errors since they are informative enough as is.
|
||||||
|
return errors.Join(
|
||||||
|
s.allow.Refresh(ctx, s.parseBuf, s.httpCli, s.cacheDir, s.maxSize),
|
||||||
|
s.block.Refresh(ctx, s.parseBuf, s.httpCli, s.cacheDir, s.maxSize),
|
||||||
|
)
|
||||||
|
}
|
49
internal/filtering/rulelist/storage_test.go
Normal file
49
internal/filtering/rulelist/storage_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package rulelist_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/c2h5oh/datasize"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStorage_Refresh(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cacheDir := t.TempDir()
|
||||||
|
|
||||||
|
allowedFileURL, _ := newFilterLocations(t, cacheDir, testRuleTextAllowed, "")
|
||||||
|
allowedFlt := newFilter(t, allowedFileURL, "Allowed 1")
|
||||||
|
|
||||||
|
blockedFileURL, _ := newFilterLocations(t, cacheDir, testRuleTextBlocked, "")
|
||||||
|
blockedFlt := newFilter(t, blockedFileURL, "Blocked 1")
|
||||||
|
|
||||||
|
strg, err := rulelist.NewStorage(&rulelist.StorageConfig{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Timeout: testTimeout,
|
||||||
|
},
|
||||||
|
CacheDir: cacheDir,
|
||||||
|
AllowFilters: []*rulelist.Filter{
|
||||||
|
allowedFlt,
|
||||||
|
},
|
||||||
|
BlockFilters: []*rulelist.Filter{
|
||||||
|
blockedFlt,
|
||||||
|
},
|
||||||
|
CustomRules: []string{
|
||||||
|
testRuleTextBlocked2,
|
||||||
|
},
|
||||||
|
MaxRuleListTextSize: 1 * datasize.KB,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
testutil.CleanupAndRequireSuccess(t, strg.Close)
|
||||||
|
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
err = strg.Refresh(ctx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
|
@ -20,15 +20,15 @@ type TextEngine struct {
|
||||||
// storage is the filtering-rule storage. It is saved here to close it.
|
// storage is the filtering-rule storage. It is saved here to close it.
|
||||||
storage *filterlist.RuleStorage
|
storage *filterlist.RuleStorage
|
||||||
|
|
||||||
// name is the human-readable name of the engine, like "custom".
|
// name is the human-readable name of the engine.
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextEngineConfig is the configuration for a rule-list filtering engine
|
// TextEngineConfig is the configuration for a rule-list filtering engine
|
||||||
// created from a filtering rule text.
|
// created from a filtering rule text.
|
||||||
type TextEngineConfig struct {
|
type TextEngineConfig struct {
|
||||||
// Name is the human-readable name of this engine, like "allowed",
|
// name is the human-readable name of the engine; see [EngineNameAllow] and
|
||||||
// "blocked", or "custom".
|
// similar constants.
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
// Rules is the text of the filtering rules for this engine.
|
// Rules is the text of the filtering rules for this engine.
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewTextEngine(t *testing.T) {
|
func TestNewTextEngine(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
eng, err := rulelist.NewTextEngine(&rulelist.TextEngineConfig{
|
eng, err := rulelist.NewTextEngine(&rulelist.TextEngineConfig{
|
||||||
Name: "RulesEngine",
|
Name: "RulesEngine",
|
||||||
Rules: []string{
|
Rules: []string{
|
||||||
|
|
|
@ -91,10 +91,7 @@ func InitAuth(
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
opts := *bbolt.DefaultOptions
|
a.db, err = bbolt.Open(dbFilename, aghos.DefaultPermFile, nil)
|
||||||
opts.OpenFile = aghos.OpenFile
|
|
||||||
|
|
||||||
a.db, err = bbolt.Open(dbFilename, aghos.DefaultPermFile, &opts)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("auth: open DB: %s: %s", dbFilename, err)
|
log.Error("auth: open DB: %s: %s", dbFilename, err)
|
||||||
if err.Error() == "invalid argument" {
|
if err.Error() == "invalid argument" {
|
||||||
|
|
|
@ -369,7 +369,7 @@ func (clients *clientsContainer) handleDelClient(w http.ResponseWriter, r *http.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !clients.storage.RemoveByName(cj.Name) {
|
if !clients.storage.RemoveByName(r.Context(), cj.Name) {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "Client not found")
|
aghhttp.Error(r, w, http.StatusBadRequest, "Client not found")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
@ -162,6 +162,12 @@ type configuration struct {
|
||||||
// SchemaVersion is the version of the configuration schema. See
|
// SchemaVersion is the version of the configuration schema. See
|
||||||
// [configmigrate.LastSchemaVersion].
|
// [configmigrate.LastSchemaVersion].
|
||||||
SchemaVersion uint `yaml:"schema_version"`
|
SchemaVersion uint `yaml:"schema_version"`
|
||||||
|
|
||||||
|
// UnsafeUseCustomUpdateIndexURL is the URL to the custom update index.
|
||||||
|
//
|
||||||
|
// NOTE: It's only exists for testing purposes and should not be used in
|
||||||
|
// release.
|
||||||
|
UnsafeUseCustomUpdateIndexURL bool `yaml:"unsafe_use_custom_update_index_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// httpConfig is a block with HTTP configuration params.
|
// httpConfig is a block with HTTP configuration params.
|
||||||
|
@ -708,7 +714,7 @@ func (c *configuration) write() (err error) {
|
||||||
return fmt.Errorf("generating config file: %w", err)
|
return fmt.Errorf("generating config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghos.WriteFile(confPath, buf.Bytes(), aghos.DefaultPermFile)
|
err = maybe.WriteFile(confPath, buf.Bytes(), aghos.DefaultPermFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writing config file: %w", err)
|
return fmt.Errorf("writing config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/AdguardTeam/golibs/httphdr"
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/NYTimes/gziphandler"
|
"github.com/NYTimes/gziphandler"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -376,7 +377,7 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (proceed bool)
|
||||||
//
|
//
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin.
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin.
|
||||||
originURL := &url.URL{
|
originURL := &url.URL{
|
||||||
Scheme: aghhttp.SchemeHTTP,
|
Scheme: urlutil.SchemeHTTP,
|
||||||
Host: r.Host,
|
Host: r.Host,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -395,7 +396,7 @@ func httpsURL(u *url.URL, host string, portHTTPS uint16) (redirectURL *url.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &url.URL{
|
return &url.URL{
|
||||||
Scheme: aghhttp.SchemeHTTPS,
|
Scheme: urlutil.SchemeHTTPS,
|
||||||
Host: hostPort,
|
Host: hostPort,
|
||||||
Path: u.Path,
|
Path: u.Path,
|
||||||
RawQuery: u.RawQuery,
|
RawQuery: u.RawQuery,
|
||||||
|
|
|
@ -75,30 +75,31 @@ func (web *webAPI) handleVersionJSON(w http.ResponseWriter, r *http.Request) {
|
||||||
// update server.
|
// update server.
|
||||||
func (web *webAPI) requestVersionInfo(resp *versionResponse, recheck bool) (err error) {
|
func (web *webAPI) requestVersionInfo(resp *versionResponse, recheck bool) (err error) {
|
||||||
updater := web.conf.updater
|
updater := web.conf.updater
|
||||||
for i := 0; i != 3; i++ {
|
for range 3 {
|
||||||
resp.VersionInfo, err = updater.VersionInfo(recheck)
|
resp.VersionInfo, err = updater.VersionInfo(recheck)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
var terr temporaryError
|
return nil
|
||||||
if errors.As(err, &terr) && terr.Temporary() {
|
}
|
||||||
// Temporary network error. This case may happen while we're
|
|
||||||
// restarting our DNS server. Log and sleep for some time.
|
|
||||||
//
|
|
||||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/934.
|
|
||||||
d := time.Duration(i) * time.Second
|
|
||||||
log.Info("update: temp net error: %q; sleeping for %s and retrying", err, d)
|
|
||||||
time.Sleep(d)
|
|
||||||
|
|
||||||
continue
|
var terr temporaryError
|
||||||
}
|
if errors.As(err, &terr) && terr.Temporary() {
|
||||||
|
// Temporary network error. This case may happen while we're
|
||||||
|
// restarting our DNS server. Log and sleep for some time.
|
||||||
|
//
|
||||||
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/934.
|
||||||
|
const sleepTime = 2 * time.Second
|
||||||
|
|
||||||
|
log.Info("update: temp net error: %v; sleeping for %s and retrying", err, sleepTime)
|
||||||
|
time.Sleep(sleepTime)
|
||||||
|
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vcu := updater.VersionCheckURL()
|
return fmt.Errorf("getting version info: %w", err)
|
||||||
|
|
||||||
return fmt.Errorf("getting version info from %s: %w", vcu, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/ameshkov/dnscrypt/v2"
|
"github.com/ameshkov/dnscrypt/v2"
|
||||||
yaml "gopkg.in/yaml.v3"
|
yaml "gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
@ -47,11 +48,11 @@ func onConfigModified() {
|
||||||
// initDNS updates all the fields of the [Context] needed to initialize the DNS
|
// initDNS updates all the fields of the [Context] needed to initialize the DNS
|
||||||
// server and initializes it at last. It also must not be called unless
|
// server and initializes it at last. It also must not be called unless
|
||||||
// [config] and [Context] are initialized. l must not be nil.
|
// [config] and [Context] are initialized. l must not be nil.
|
||||||
func initDNS(l *slog.Logger, statsDir, querylogDir string) (err error) {
|
func initDNS(baseLogger *slog.Logger, statsDir, querylogDir string) (err error) {
|
||||||
anonymizer := config.anonymizer()
|
anonymizer := config.anonymizer()
|
||||||
|
|
||||||
statsConf := stats.Config{
|
statsConf := stats.Config{
|
||||||
Logger: l.With(slogutil.KeyPrefix, "stats"),
|
Logger: baseLogger.With(slogutil.KeyPrefix, "stats"),
|
||||||
Filename: filepath.Join(statsDir, "stats.db"),
|
Filename: filepath.Join(statsDir, "stats.db"),
|
||||||
Limit: config.Stats.Interval.Duration,
|
Limit: config.Stats.Interval.Duration,
|
||||||
ConfigModified: onConfigModified,
|
ConfigModified: onConfigModified,
|
||||||
|
@ -72,6 +73,7 @@ func initDNS(l *slog.Logger, statsDir, querylogDir string) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
conf := querylog.Config{
|
conf := querylog.Config{
|
||||||
|
Logger: baseLogger.With(slogutil.KeyPrefix, "querylog"),
|
||||||
Anonymizer: anonymizer,
|
Anonymizer: anonymizer,
|
||||||
ConfigModified: onConfigModified,
|
ConfigModified: onConfigModified,
|
||||||
HTTPRegister: httpRegister,
|
HTTPRegister: httpRegister,
|
||||||
|
@ -112,7 +114,7 @@ func initDNS(l *slog.Logger, statsDir, querylogDir string) (err error) {
|
||||||
anonymizer,
|
anonymizer,
|
||||||
httpRegister,
|
httpRegister,
|
||||||
tlsConf,
|
tlsConf,
|
||||||
l,
|
baseLogger,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,7 +373,7 @@ func getDNSEncryption() (de dnsEncryption) {
|
||||||
}
|
}
|
||||||
|
|
||||||
de.https = (&url.URL{
|
de.https = (&url.URL{
|
||||||
Scheme: "https",
|
Scheme: urlutil.SchemeHTTPS,
|
||||||
Host: addr,
|
Host: addr,
|
||||||
Path: "/dns-query",
|
Path: "/dns-query",
|
||||||
}).String()
|
}).String()
|
||||||
|
@ -456,7 +458,8 @@ func startDNSServer() error {
|
||||||
Context.filters.EnableFilters(false)
|
Context.filters.EnableFilters(false)
|
||||||
|
|
||||||
// TODO(s.chzhen): Pass context.
|
// TODO(s.chzhen): Pass context.
|
||||||
err := Context.clients.Start(context.TODO())
|
ctx := context.TODO()
|
||||||
|
err := Context.clients.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting clients container: %w", err)
|
return fmt.Errorf("starting clients container: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -468,7 +471,11 @@ func startDNSServer() error {
|
||||||
|
|
||||||
Context.filters.Start()
|
Context.filters.Start()
|
||||||
Context.stats.Start()
|
Context.stats.Start()
|
||||||
Context.queryLog.Start()
|
|
||||||
|
err = Context.queryLog.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("starting query log: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -524,12 +531,16 @@ func closeDNSServer() {
|
||||||
if Context.stats != nil {
|
if Context.stats != nil {
|
||||||
err := Context.stats.Close()
|
err := Context.stats.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("closing stats: %s", err)
|
log.Error("closing stats: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if Context.queryLog != nil {
|
if Context.queryLog != nil {
|
||||||
Context.queryLog.Close()
|
// TODO(s.chzhen): Pass context.
|
||||||
|
err := Context.queryLog.Shutdown(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
log.Error("closing query log: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("all dns modules are closed")
|
log.Debug("all dns modules are closed")
|
||||||
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
|
@ -21,7 +20,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
||||||
|
@ -42,6 +40,7 @@ import (
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -159,7 +158,7 @@ func setupContext(opts options) (err error) {
|
||||||
|
|
||||||
if Context.firstRun {
|
if Context.firstRun {
|
||||||
log.Info("This is the first time AdGuard Home is launched")
|
log.Info("This is the first time AdGuard Home is launched")
|
||||||
checkPermissions()
|
checkNetworkPermissions()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -495,11 +494,42 @@ func checkPorts() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isUpdateEnabled returns true if the update is enabled for current
|
||||||
|
// configuration. It also logs the decision. customURL should be true if the
|
||||||
|
// updater is using a custom URL.
|
||||||
|
func isUpdateEnabled(ctx context.Context, l *slog.Logger, opts *options, customURL bool) (ok bool) {
|
||||||
|
if opts.disableUpdate {
|
||||||
|
l.DebugContext(ctx, "updates are disabled by command-line option")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch version.Channel() {
|
||||||
|
case
|
||||||
|
version.ChannelDevelopment,
|
||||||
|
version.ChannelCandidate:
|
||||||
|
if customURL {
|
||||||
|
l.DebugContext(ctx, "updates are enabled because custom url is used")
|
||||||
|
} else {
|
||||||
|
l.DebugContext(ctx, "updates are disabled for development and candidate builds")
|
||||||
|
}
|
||||||
|
|
||||||
|
return customURL
|
||||||
|
default:
|
||||||
|
l.DebugContext(ctx, "updates are enabled")
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initWeb initializes the web module.
|
||||||
func initWeb(
|
func initWeb(
|
||||||
|
ctx context.Context,
|
||||||
opts options,
|
opts options,
|
||||||
clientBuildFS fs.FS,
|
clientBuildFS fs.FS,
|
||||||
upd *updater.Updater,
|
upd *updater.Updater,
|
||||||
l *slog.Logger,
|
l *slog.Logger,
|
||||||
|
customURL bool,
|
||||||
) (web *webAPI, err error) {
|
) (web *webAPI, err error) {
|
||||||
var clientFS fs.FS
|
var clientFS fs.FS
|
||||||
if opts.localFrontend {
|
if opts.localFrontend {
|
||||||
|
@ -513,17 +543,7 @@ func initWeb(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disableUpdate := opts.disableUpdate
|
disableUpdate := !isUpdateEnabled(ctx, l, &opts, customURL)
|
||||||
switch version.Channel() {
|
|
||||||
case
|
|
||||||
version.ChannelDevelopment,
|
|
||||||
version.ChannelCandidate:
|
|
||||||
disableUpdate = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if disableUpdate {
|
|
||||||
log.Info("AdGuard Home updates are disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
webConf := &webConfig{
|
webConf := &webConfig{
|
||||||
updater: upd,
|
updater: upd,
|
||||||
|
@ -544,7 +564,7 @@ func initWeb(
|
||||||
|
|
||||||
web = newWebAPI(webConf, l)
|
web = newWebAPI(webConf, l)
|
||||||
if web == nil {
|
if web == nil {
|
||||||
return nil, fmt.Errorf("initializing web: %w", err)
|
return nil, errors.Error("can not initialize web")
|
||||||
}
|
}
|
||||||
|
|
||||||
return web, nil
|
return web, nil
|
||||||
|
@ -557,6 +577,8 @@ func fatalOnError(err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// run configures and starts AdGuard Home.
|
// run configures and starts AdGuard Home.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Make opts a pointer.
|
||||||
func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
||||||
// Configure working dir.
|
// Configure working dir.
|
||||||
err := initWorkingDir(opts)
|
err := initWorkingDir(opts)
|
||||||
|
@ -604,33 +626,13 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
||||||
execPath, err := os.Executable()
|
execPath, err := os.Executable()
|
||||||
fatalOnError(errors.Annotate(err, "getting executable path: %w"))
|
fatalOnError(errors.Annotate(err, "getting executable path: %w"))
|
||||||
|
|
||||||
u := &url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
// TODO(a.garipov): Make configurable.
|
|
||||||
Host: "static.adtidy.org",
|
|
||||||
Path: path.Join("adguardhome", version.Channel(), "version.json"),
|
|
||||||
}
|
|
||||||
|
|
||||||
confPath := configFilePath()
|
confPath := configFilePath()
|
||||||
log.Debug("using config path %q for updater", confPath)
|
|
||||||
|
|
||||||
upd := updater.NewUpdater(&updater.Config{
|
upd, customURL := newUpdater(ctx, slogLogger, Context.workDir, confPath, execPath, config)
|
||||||
Client: config.Filtering.HTTPClient,
|
|
||||||
Version: version.Version(),
|
|
||||||
Channel: version.Channel(),
|
|
||||||
GOARCH: runtime.GOARCH,
|
|
||||||
GOOS: runtime.GOOS,
|
|
||||||
GOARM: version.GOARM(),
|
|
||||||
GOMIPS: version.GOMIPS(),
|
|
||||||
WorkDir: Context.workDir,
|
|
||||||
ConfName: confPath,
|
|
||||||
ExecPath: execPath,
|
|
||||||
VersionCheckURL: u.String(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO(e.burkov): This could be made earlier, probably as the option's
|
// TODO(e.burkov): This could be made earlier, probably as the option's
|
||||||
// effect.
|
// effect.
|
||||||
cmdlineUpdate(opts, upd, slogLogger)
|
cmdlineUpdate(ctx, slogLogger, opts, upd)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
// Save the updated config.
|
// Save the updated config.
|
||||||
|
@ -643,7 +645,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
dataDir := Context.getDataDir()
|
dataDir := Context.getDataDir()
|
||||||
err = aghos.MkdirAll(dataDir, aghos.DefaultPermDir)
|
err = os.MkdirAll(dataDir, aghos.DefaultPermDir)
|
||||||
fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dataDir))
|
fatalOnError(errors.Annotate(err, "creating DNS data dir at %s: %w", dataDir))
|
||||||
|
|
||||||
GLMode = opts.glinetMode
|
GLMode = opts.glinetMode
|
||||||
|
@ -658,7 +660,7 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
||||||
onConfigModified()
|
onConfigModified()
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.web, err = initWeb(opts, clientBuildFS, upd, slogLogger)
|
Context.web, err = initWeb(ctx, opts, clientBuildFS, upd, slogLogger, customURL)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
|
statsDir, querylogDir, err := checkStatsAndQuerylogDirs(&Context, config)
|
||||||
|
@ -686,18 +688,87 @@ func run(opts options, clientBuildFS fs.FS, done chan struct{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if permcheck.NeedsMigration(confPath) {
|
if !opts.noPermCheck {
|
||||||
permcheck.Migrate(Context.workDir, dataDir, statsDir, querylogDir, confPath)
|
checkPermissions(ctx, slogLogger, Context.workDir, confPath, dataDir, statsDir, querylogDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
permcheck.Check(Context.workDir, dataDir, statsDir, querylogDir, confPath)
|
|
||||||
|
|
||||||
Context.web.start()
|
Context.web.start()
|
||||||
|
|
||||||
// Wait for other goroutines to complete their job.
|
// Wait for other goroutines to complete their job.
|
||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newUpdater creates a new AdGuard Home updater. customURL is true if the user
|
||||||
|
// has specified a custom version announcement URL.
|
||||||
|
func newUpdater(
|
||||||
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
|
workDir string,
|
||||||
|
confPath string,
|
||||||
|
execPath string,
|
||||||
|
config *configuration,
|
||||||
|
) (upd *updater.Updater, customURL bool) {
|
||||||
|
// envName is the name of the environment variable that can be used to
|
||||||
|
// override the default version check URL.
|
||||||
|
const envName = "ADGUARD_HOME_TEST_UPDATE_VERSION_URL"
|
||||||
|
|
||||||
|
customURLStr := os.Getenv(envName)
|
||||||
|
|
||||||
|
var versionURL *url.URL
|
||||||
|
switch {
|
||||||
|
case version.Channel() == version.ChannelRelease:
|
||||||
|
// Only enable custom version URL for development builds.
|
||||||
|
l.DebugContext(ctx, "custom version url is disabled for release builds")
|
||||||
|
case !config.UnsafeUseCustomUpdateIndexURL:
|
||||||
|
l.DebugContext(ctx, "custom version url is disabled in config")
|
||||||
|
default:
|
||||||
|
versionURL, _ = url.Parse(customURLStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := urlutil.ValidateHTTPURL(versionURL)
|
||||||
|
if customURL = err == nil; !customURL {
|
||||||
|
l.DebugContext(ctx, "parsing custom version url", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
versionURL = updater.DefaultVersionURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
l.DebugContext(ctx, "creating updater", "config_path", confPath)
|
||||||
|
|
||||||
|
return updater.NewUpdater(&updater.Config{
|
||||||
|
Client: config.Filtering.HTTPClient,
|
||||||
|
Version: version.Version(),
|
||||||
|
Channel: version.Channel(),
|
||||||
|
GOARCH: runtime.GOARCH,
|
||||||
|
GOOS: runtime.GOOS,
|
||||||
|
GOARM: version.GOARM(),
|
||||||
|
GOMIPS: version.GOMIPS(),
|
||||||
|
WorkDir: workDir,
|
||||||
|
ConfName: confPath,
|
||||||
|
ExecPath: execPath,
|
||||||
|
VersionCheckURL: versionURL,
|
||||||
|
}), customURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPermissions checks and migrates permissions of the files and directories
|
||||||
|
// used by AdGuard Home, if needed.
|
||||||
|
func checkPermissions(
|
||||||
|
ctx context.Context,
|
||||||
|
baseLogger *slog.Logger,
|
||||||
|
workDir string,
|
||||||
|
confPath string,
|
||||||
|
dataDir string,
|
||||||
|
statsDir string,
|
||||||
|
querylogDir string,
|
||||||
|
) {
|
||||||
|
l := baseLogger.With(slogutil.KeyPrefix, "permcheck")
|
||||||
|
|
||||||
|
if permcheck.NeedsMigration(ctx, l, workDir, confPath) {
|
||||||
|
permcheck.Migrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
permcheck.Check(ctx, l, workDir, dataDir, statsDir, querylogDir, confPath)
|
||||||
|
}
|
||||||
|
|
||||||
// initUsers initializes context auth module. Clears config users field.
|
// initUsers initializes context auth module. Clears config users field.
|
||||||
func initUsers() (auth *Auth, err error) {
|
func initUsers() (auth *Auth, err error) {
|
||||||
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
||||||
|
@ -757,8 +828,9 @@ func startMods(l *slog.Logger) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the current user permissions are enough to run AdGuard Home
|
// checkNetworkPermissions checks if the current user permissions are enough to
|
||||||
func checkPermissions() {
|
// use the required networking functionality.
|
||||||
|
func checkNetworkPermissions() {
|
||||||
log.Info("Checking if AdGuard Home has necessary permissions")
|
log.Info("Checking if AdGuard Home has necessary permissions")
|
||||||
|
|
||||||
if ok, err := aghnet.CanBindPrivilegedPorts(); !ok || err != nil {
|
if ok, err := aghnet.CanBindPrivilegedPorts(); !ok || err != nil {
|
||||||
|
@ -936,12 +1008,12 @@ func printHTTPAddresses(proto string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
port := config.HTTPConfig.Address.Port()
|
port := config.HTTPConfig.Address.Port()
|
||||||
if proto == aghhttp.SchemeHTTPS {
|
if proto == urlutil.SchemeHTTPS {
|
||||||
port = tlsConf.PortHTTPS
|
port = tlsConf.PortHTTPS
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(e.burkov): Inspect and perhaps merge with the previous condition.
|
// TODO(e.burkov): Inspect and perhaps merge with the previous condition.
|
||||||
if proto == aghhttp.SchemeHTTPS && tlsConf.ServerName != "" {
|
if proto == urlutil.SchemeHTTPS && tlsConf.ServerName != "" {
|
||||||
printWebAddrs(proto, tlsConf.ServerName, tlsConf.PortHTTPS)
|
printWebAddrs(proto, tlsConf.ServerName, tlsConf.PortHTTPS)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -1001,7 +1073,7 @@ type jsonError struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// cmdlineUpdate updates current application and exits. l must not be nil.
|
// cmdlineUpdate updates current application and exits. l must not be nil.
|
||||||
func cmdlineUpdate(opts options, upd *updater.Updater, l *slog.Logger) {
|
func cmdlineUpdate(ctx context.Context, l *slog.Logger, opts options, upd *updater.Updater) {
|
||||||
if !opts.performUpdate {
|
if !opts.performUpdate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1014,20 +1086,19 @@ func cmdlineUpdate(opts options, upd *updater.Updater, l *slog.Logger) {
|
||||||
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{}, l)
|
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{}, l)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
log.Info("cmdline update: performing update")
|
l.InfoContext(ctx, "performing update via cli")
|
||||||
|
|
||||||
info, err := upd.VersionInfo(true)
|
info, err := upd.VersionInfo(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vcu := upd.VersionCheckURL()
|
l.ErrorContext(ctx, "getting version info", slogutil.KeyError, err)
|
||||||
log.Error("getting version info from %s: %s", vcu, err)
|
|
||||||
|
|
||||||
os.Exit(1)
|
os.Exit(osutil.ExitCodeFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.NewVersion == version.Version() {
|
if info.NewVersion == version.Version() {
|
||||||
log.Info("no updates available")
|
l.InfoContext(ctx, "no updates available")
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(osutil.ExitCodeSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = upd.Update(Context.firstRun)
|
err = upd.Update(Context.firstRun)
|
||||||
|
@ -1035,10 +1106,10 @@ func cmdlineUpdate(opts options, upd *updater.Updater, l *slog.Logger) {
|
||||||
|
|
||||||
err = restartService()
|
err = restartService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("restarting service: %s", err)
|
l.DebugContext(ctx, "restarting service", slogutil.KeyError, err)
|
||||||
log.Info("AdGuard Home was not installed as a service. " +
|
l.InfoContext(ctx, "AdGuard Home was not installed as a service. "+
|
||||||
"Please restart running instances of AdGuardHome manually.")
|
"Please restart running instances of AdGuardHome manually.")
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(osutil.ExitCodeSuccess)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,11 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/httphdr"
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"howett.net/plist"
|
"howett.net/plist"
|
||||||
)
|
)
|
||||||
|
@ -84,7 +84,7 @@ func encodeMobileConfig(d *dnsSettings, clientID string) ([]byte, error) {
|
||||||
case dnsProtoHTTPS:
|
case dnsProtoHTTPS:
|
||||||
dspName = fmt.Sprintf("%s DoH", d.ServerName)
|
dspName = fmt.Sprintf("%s DoH", d.ServerName)
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: aghhttp.SchemeHTTPS,
|
Scheme: urlutil.SchemeHTTPS,
|
||||||
Host: d.ServerName,
|
Host: d.ServerName,
|
||||||
Path: path.Join("/dns-query", clientID),
|
Path: path.Join("/dns-query", clientID),
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,10 @@ type options struct {
|
||||||
// localFrontend forces AdGuard Home to use the frontend files from disk
|
// localFrontend forces AdGuard Home to use the frontend files from disk
|
||||||
// rather than the ones that have been compiled into the binary.
|
// rather than the ones that have been compiled into the binary.
|
||||||
localFrontend bool
|
localFrontend bool
|
||||||
|
|
||||||
|
// noPermCheck disables checking and migration of permissions for the
|
||||||
|
// security-sensitive files.
|
||||||
|
noPermCheck bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// initCmdLineOpts completes initialization of the global command-line option
|
// initCmdLineOpts completes initialization of the global command-line option
|
||||||
|
@ -305,6 +309,15 @@ var cmdLineOpts = []cmdLineOpt{{
|
||||||
description: "Run in GL-Inet compatibility mode.",
|
description: "Run in GL-Inet compatibility mode.",
|
||||||
longName: "glinet",
|
longName: "glinet",
|
||||||
shortName: "",
|
shortName: "",
|
||||||
|
}, {
|
||||||
|
updateWithValue: nil,
|
||||||
|
updateNoValue: func(o options) (options, error) { o.noPermCheck = true; return o, nil },
|
||||||
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) { return "", o.noPermCheck },
|
||||||
|
description: "Skip checking and migration of permissions " +
|
||||||
|
"of security-sensitive files.",
|
||||||
|
longName: "no-permcheck",
|
||||||
|
shortName: "",
|
||||||
}, {
|
}, {
|
||||||
updateWithValue: nil,
|
updateWithValue: nil,
|
||||||
updateNoValue: nil,
|
updateNoValue: nil,
|
||||||
|
|
|
@ -10,11 +10,11 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ AdGuard Home is successfully installed and will automatically start on boot.
|
||||||
There are a few more things that must be configured before you can use it.
|
There are a few more things that must be configured before you can use it.
|
||||||
Click on the link below and follow the Installation Wizard steps to finish setup.
|
Click on the link below and follow the Installation Wizard steps to finish setup.
|
||||||
AdGuard Home is now available at the following addresses:`)
|
AdGuard Home is now available at the following addresses:`)
|
||||||
printHTTPAddresses(aghhttp.SchemeHTTP)
|
printHTTPAddresses(urlutil.SchemeHTTP)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,13 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
"github.com/AdguardTeam/AdGuardHome/internal/updater"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil/httputil"
|
"github.com/AdguardTeam/golibs/netutil/httputil"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/NYTimes/gziphandler"
|
"github.com/NYTimes/gziphandler"
|
||||||
"github.com/quic-go/quic-go/http3"
|
"github.com/quic-go/quic-go/http3"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
|
@ -101,6 +101,8 @@ type webAPI struct {
|
||||||
|
|
||||||
// newWebAPI creates a new instance of the web UI and API server. l must not be
|
// newWebAPI creates a new instance of the web UI and API server. l must not be
|
||||||
// nil.
|
// nil.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Return a proper error.
|
||||||
func newWebAPI(conf *webConfig, l *slog.Logger) (w *webAPI) {
|
func newWebAPI(conf *webConfig, l *slog.Logger) (w *webAPI) {
|
||||||
log.Info("web: initializing")
|
log.Info("web: initializing")
|
||||||
|
|
||||||
|
@ -192,7 +194,7 @@ func (web *webAPI) start() {
|
||||||
|
|
||||||
// this loop is used as an ability to change listening host and/or port
|
// this loop is used as an ability to change listening host and/or port
|
||||||
for !web.httpsServer.inShutdown {
|
for !web.httpsServer.inShutdown {
|
||||||
printHTTPAddresses(aghhttp.SchemeHTTP)
|
printHTTPAddresses(urlutil.SchemeHTTP)
|
||||||
errs := make(chan error, 2)
|
errs := make(chan error, 2)
|
||||||
|
|
||||||
// Use an h2c handler to support unencrypted HTTP/2, e.g. for proxies.
|
// Use an h2c handler to support unencrypted HTTP/2, e.g. for proxies.
|
||||||
|
@ -286,7 +288,7 @@ func (web *webAPI) tlsServerLoop() {
|
||||||
WriteTimeout: web.conf.WriteTimeout,
|
WriteTimeout: web.conf.WriteTimeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
printHTTPAddresses(aghhttp.SchemeHTTPS)
|
printHTTPAddresses(urlutil.SchemeHTTPS)
|
||||||
|
|
||||||
if web.conf.serveHTTP3 {
|
if web.conf.serveHTTP3 {
|
||||||
go web.mustStartHTTP3(addr)
|
go web.mustStartHTTP3(addr)
|
||||||
|
|
|
@ -1,36 +1,9 @@
|
||||||
// Package agh contains common entities and interfaces of AdGuard Home.
|
// Package agh contains common entities and interfaces of AdGuard Home.
|
||||||
package agh
|
package agh
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"github.com/AdguardTeam/golibs/service"
|
||||||
// Service is the interface for API servers.
|
)
|
||||||
//
|
|
||||||
// TODO(a.garipov): Consider adding a context to Start.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Consider adding a Wait method or making an extension
|
|
||||||
// interface for that.
|
|
||||||
type Service interface {
|
|
||||||
// Start starts the service. It does not block.
|
|
||||||
Start() (err error)
|
|
||||||
|
|
||||||
// Shutdown gracefully stops the service. ctx is used to determine
|
|
||||||
// a timeout before trying to stop the service less gracefully.
|
|
||||||
Shutdown(ctx context.Context) (err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ Service = EmptyService{}
|
|
||||||
|
|
||||||
// EmptyService is a [Service] that does nothing.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Remove if unnecessary.
|
|
||||||
type EmptyService struct{}
|
|
||||||
|
|
||||||
// Start implements the [Service] interface for EmptyService.
|
|
||||||
func (EmptyService) Start() (err error) { return nil }
|
|
||||||
|
|
||||||
// Shutdown implements the [Service] interface for EmptyService.
|
|
||||||
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
|
||||||
|
|
||||||
// ServiceWithConfig is an extension of the [Service] interface for services
|
// ServiceWithConfig is an extension of the [Service] interface for services
|
||||||
// that can return their configuration.
|
// that can return their configuration.
|
||||||
|
@ -38,7 +11,7 @@ func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
||||||
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
||||||
// how to make it testable in a better way.
|
// how to make it testable in a better way.
|
||||||
type ServiceWithConfig[ConfigType any] interface {
|
type ServiceWithConfig[ConfigType any] interface {
|
||||||
Service
|
service.Interface
|
||||||
|
|
||||||
Config() (c ConfigType)
|
Config() (c ConfigType)
|
||||||
}
|
}
|
||||||
|
@ -51,7 +24,7 @@ var _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Remove if unnecessary.
|
// TODO(a.garipov): Remove if unnecessary.
|
||||||
type EmptyServiceWithConfig[ConfigType any] struct {
|
type EmptyServiceWithConfig[ConfigType any] struct {
|
||||||
EmptyService
|
service.Empty
|
||||||
|
|
||||||
Conf ConfigType
|
Conf ConfigType
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,15 @@ import (
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main is the entry point of AdGuard Home.
|
// Main is the entry point of AdGuard Home.
|
||||||
func Main(embeddedFrontend fs.FS) {
|
func Main(embeddedFrontend fs.FS) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
cmdName := os.Args[0]
|
cmdName := os.Args[0]
|
||||||
|
@ -26,70 +30,69 @@ func Main(embeddedFrontend fs.FS) {
|
||||||
os.Exit(exitCode)
|
os.Exit(exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = setLog(opts)
|
baseLogger := newBaseLogger(opts)
|
||||||
check(err)
|
|
||||||
|
|
||||||
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
|
baseLogger.InfoContext(
|
||||||
|
ctx,
|
||||||
|
"starting adguard home",
|
||||||
|
"version", version.Version(),
|
||||||
|
"pid", os.Getpid(),
|
||||||
|
)
|
||||||
|
|
||||||
if opts.workDir != "" {
|
if opts.workDir != "" {
|
||||||
log.Info("changing working directory to %q", opts.workDir)
|
baseLogger.InfoContext(ctx, "changing working directory", "dir", opts.workDir)
|
||||||
|
|
||||||
err = os.Chdir(opts.workDir)
|
err = os.Chdir(opts.workDir)
|
||||||
check(err)
|
errors.Check(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
frontend, err := frontendFromOpts(opts, embeddedFrontend)
|
frontend, err := frontendFromOpts(ctx, baseLogger, opts, embeddedFrontend)
|
||||||
check(err)
|
errors.Check(err)
|
||||||
|
|
||||||
|
startCtx, startCancel := context.WithTimeout(ctx, defaultTimeoutStart)
|
||||||
|
defer startCancel()
|
||||||
|
|
||||||
confMgrConf := &configmgr.Config{
|
confMgrConf := &configmgr.Config{
|
||||||
Frontend: frontend,
|
BaseLogger: baseLogger,
|
||||||
WebAddr: opts.webAddr,
|
Logger: baseLogger.With(slogutil.KeyPrefix, "configmgr"),
|
||||||
Start: start,
|
Frontend: frontend,
|
||||||
FileName: opts.confFile,
|
WebAddr: opts.webAddr,
|
||||||
|
Start: start,
|
||||||
|
FileName: opts.confFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
confMgr, err := newConfigMgr(confMgrConf)
|
confMgr, err := configmgr.New(startCtx, confMgrConf)
|
||||||
check(err)
|
errors.Check(err)
|
||||||
|
|
||||||
web := confMgr.Web()
|
web := confMgr.Web()
|
||||||
err = web.Start()
|
err = web.Start(startCtx)
|
||||||
check(err)
|
errors.Check(err)
|
||||||
|
|
||||||
dns := confMgr.DNS()
|
dns := confMgr.DNS()
|
||||||
err = dns.Start()
|
err = dns.Start(startCtx)
|
||||||
check(err)
|
errors.Check(err)
|
||||||
|
|
||||||
sigHdlr := newSignalHandler(
|
sigHdlr := newSignalHandler(
|
||||||
|
baseLogger.With(slogutil.KeyPrefix, service.SignalHandlerPrefix),
|
||||||
confMgrConf,
|
confMgrConf,
|
||||||
opts.pidFile,
|
opts.pidFile,
|
||||||
web,
|
web,
|
||||||
dns,
|
dns,
|
||||||
)
|
)
|
||||||
|
|
||||||
sigHdlr.handle()
|
os.Exit(sigHdlr.handle(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultTimeout is the timeout used for some operations where another timeout
|
// Default timeouts.
|
||||||
// hasn't been defined yet.
|
//
|
||||||
const defaultTimeout = 5 * time.Second
|
// TODO(a.garipov): Make configurable.
|
||||||
|
const (
|
||||||
// ctxWithDefaultTimeout is a helper function that returns a context with
|
defaultTimeoutStart = 1 * time.Minute
|
||||||
// timeout set to defaultTimeout.
|
defaultTimeoutShutdown = 5 * time.Second
|
||||||
func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) {
|
)
|
||||||
return context.WithTimeout(context.Background(), defaultTimeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newConfigMgr returns a new configuration manager using defaultTimeout as the
|
// newConfigMgr returns a new configuration manager using defaultTimeout as the
|
||||||
// context timeout.
|
// context timeout.
|
||||||
func newConfigMgr(c *configmgr.Config) (m *configmgr.Manager, err error) {
|
func newConfigMgr(ctx context.Context, c *configmgr.Config) (m *configmgr.Manager, err error) {
|
||||||
ctx, cancel := ctxWithDefaultTimeout()
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return configmgr.New(ctx, c)
|
return configmgr.New(ctx, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check is a simple error-checking helper. It must only be used within Main.
|
|
||||||
func check(err error) {
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,39 +1,39 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// syslogServiceName is the name of the AdGuard Home service used for writing
|
// newBaseLogger constructs a base logger based on the command-line options.
|
||||||
// logs to the system log.
|
// opts must not be nil.
|
||||||
const syslogServiceName = "AdGuardHome"
|
func newBaseLogger(opts *options) (baseLogger *slog.Logger) {
|
||||||
|
var output io.Writer
|
||||||
// setLog sets up the text logging.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Add parameters from configuration file.
|
|
||||||
func setLog(opts *options) (err error) {
|
|
||||||
switch opts.confFile {
|
switch opts.confFile {
|
||||||
case "stdout":
|
case "stdout":
|
||||||
log.SetOutput(os.Stdout)
|
output = os.Stdout
|
||||||
case "stderr":
|
case "stderr":
|
||||||
log.SetOutput(os.Stderr)
|
output = os.Stderr
|
||||||
case "syslog":
|
case "syslog":
|
||||||
err = aghos.ConfigureSyslog(syslogServiceName)
|
// TODO(a.garipov): Add a syslog handler to golibs.
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initializing syslog: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
// TODO(a.garipov): Use the path.
|
// TODO(a.garipov): Use the path.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lvl := slog.LevelInfo
|
||||||
if opts.verbose {
|
if opts.verbose {
|
||||||
log.SetLevel(log.DEBUG)
|
lvl = slog.LevelDebug
|
||||||
log.Debug("verbose logging enabled")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return slogutil.New(&slogutil.Config{
|
||||||
|
Output: output,
|
||||||
|
// TODO(a.garipov): Get from config?
|
||||||
|
Format: slogutil.FormatText,
|
||||||
|
Level: lvl,
|
||||||
|
// TODO(a.garipov): Get from config.
|
||||||
|
AddTimestamp: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding"
|
"encoding"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
@ -14,7 +16,7 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/configmigrate"
|
"github.com/AdguardTeam/AdGuardHome/internal/configmigrate"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// options contains all command-line options for the AdGuardHome(.exe) binary.
|
// options contains all command-line options for the AdGuardHome(.exe) binary.
|
||||||
|
@ -87,6 +89,12 @@ type options struct {
|
||||||
// TODO(a.garipov): Use.
|
// TODO(a.garipov): Use.
|
||||||
performUpdate bool
|
performUpdate bool
|
||||||
|
|
||||||
|
// noPermCheck, if true, instructs AdGuard Home to skip checking and
|
||||||
|
// migrating the permissions of its security-sensitive files.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use.
|
||||||
|
noPermCheck bool
|
||||||
|
|
||||||
// verbose, if true, instructs AdGuard Home to enable verbose logging.
|
// verbose, if true, instructs AdGuard Home to enable verbose logging.
|
||||||
verbose bool
|
verbose bool
|
||||||
|
|
||||||
|
@ -108,7 +116,8 @@ const (
|
||||||
disableUpdateIdx
|
disableUpdateIdx
|
||||||
glinetModeIdx
|
glinetModeIdx
|
||||||
helpIdx
|
helpIdx
|
||||||
localFrontend
|
localFrontendIdx
|
||||||
|
noPermCheckIdx
|
||||||
performUpdateIdx
|
performUpdateIdx
|
||||||
verboseIdx
|
verboseIdx
|
||||||
versionIdx
|
versionIdx
|
||||||
|
@ -212,7 +221,7 @@ var commandLineOptions = []*commandLineOption{
|
||||||
valueType: "",
|
valueType: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
localFrontend: {
|
localFrontendIdx: {
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
description: "Use local frontend directories.",
|
description: "Use local frontend directories.",
|
||||||
long: "local-frontend",
|
long: "local-frontend",
|
||||||
|
@ -220,6 +229,14 @@ var commandLineOptions = []*commandLineOption{
|
||||||
valueType: "",
|
valueType: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noPermCheckIdx: {
|
||||||
|
defaultValue: false,
|
||||||
|
description: "Skip checking the permissions of security-sensitive files.",
|
||||||
|
long: "no-permcheck",
|
||||||
|
short: "",
|
||||||
|
valueType: "",
|
||||||
|
},
|
||||||
|
|
||||||
performUpdateIdx: {
|
performUpdateIdx: {
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
description: "Update the current binary and restart the service in case it's installed.",
|
description: "Update the current binary and restart the service in case it's installed.",
|
||||||
|
@ -262,7 +279,8 @@ func parseOptions(cmdName string, args []string) (opts *options, err error) {
|
||||||
disableUpdateIdx: &opts.disableUpdate,
|
disableUpdateIdx: &opts.disableUpdate,
|
||||||
glinetModeIdx: &opts.glinetMode,
|
glinetModeIdx: &opts.glinetMode,
|
||||||
helpIdx: &opts.help,
|
helpIdx: &opts.help,
|
||||||
localFrontend: &opts.localFrontend,
|
localFrontendIdx: &opts.localFrontend,
|
||||||
|
noPermCheckIdx: &opts.noPermCheck,
|
||||||
performUpdateIdx: &opts.performUpdate,
|
performUpdateIdx: &opts.performUpdate,
|
||||||
verboseIdx: &opts.verbose,
|
verboseIdx: &opts.verbose,
|
||||||
versionIdx: &opts.version,
|
versionIdx: &opts.version,
|
||||||
|
@ -372,13 +390,13 @@ func processOptions(
|
||||||
) (exitCode int, needExit bool) {
|
) (exitCode int, needExit bool) {
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
// Assume that usage has already been printed.
|
// Assume that usage has already been printed.
|
||||||
return statusArgumentError, true
|
return osutil.ExitCodeArgumentError, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.help {
|
if opts.help {
|
||||||
usage(cmdName, os.Stdout)
|
usage(cmdName, os.Stdout)
|
||||||
|
|
||||||
return statusSuccess, true
|
return osutil.ExitCodeSuccess, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.version {
|
if opts.version {
|
||||||
|
@ -388,7 +406,7 @@ func processOptions(
|
||||||
fmt.Printf("AdGuard Home %s\n", version.Version())
|
fmt.Printf("AdGuard Home %s\n", version.Version())
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusSuccess, true
|
return osutil.ExitCodeSuccess, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.checkConfig {
|
if opts.checkConfig {
|
||||||
|
@ -396,21 +414,26 @@ func processOptions(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = io.WriteString(os.Stdout, err.Error()+"\n")
|
_, _ = io.WriteString(os.Stdout, err.Error()+"\n")
|
||||||
|
|
||||||
return statusError, true
|
return osutil.ExitCodeFailure, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusSuccess, true
|
return osutil.ExitCodeSuccess, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// frontendFromOpts returns the frontend to use based on the options.
|
// frontendFromOpts returns the frontend to use based on the options.
|
||||||
func frontendFromOpts(opts *options, embeddedFrontend fs.FS) (frontend fs.FS, err error) {
|
func frontendFromOpts(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *slog.Logger,
|
||||||
|
opts *options,
|
||||||
|
embeddedFrontend fs.FS,
|
||||||
|
) (frontend fs.FS, err error) {
|
||||||
const frontendSubdir = "build/static"
|
const frontendSubdir = "build/static"
|
||||||
|
|
||||||
if opts.localFrontend {
|
if opts.localFrontend {
|
||||||
log.Info("warning: using local frontend files")
|
logger.WarnContext(ctx, "using local frontend files")
|
||||||
|
|
||||||
return os.DirFS(frontendSubdir), nil
|
return os.DirFS(frontendSubdir), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/osutil"
|
"github.com/AdguardTeam/golibs/osutil"
|
||||||
|
"github.com/AdguardTeam/golibs/service"
|
||||||
|
"github.com/google/renameio/v2/maybe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// signalHandler processes incoming signals and shuts services down.
|
// signalHandler processes incoming signals and shuts services down.
|
||||||
type signalHandler struct {
|
type signalHandler struct {
|
||||||
|
// logger is used for logging the operation of the signal handler.
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
// confMgrConf contains the configuration parameters for the configuration
|
// confMgrConf contains the configuration parameters for the configuration
|
||||||
// manager.
|
// manager.
|
||||||
confMgrConf *configmgr.Config
|
confMgrConf *configmgr.Config
|
||||||
|
@ -24,145 +32,172 @@ type signalHandler struct {
|
||||||
pidFile string
|
pidFile string
|
||||||
|
|
||||||
// services are the services that are shut down before application exiting.
|
// services are the services that are shut down before application exiting.
|
||||||
services []agh.Service
|
services []service.Interface
|
||||||
|
|
||||||
|
// shutdownTimeout is the timeout for the shutdown operation.
|
||||||
|
shutdownTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle processes OS signals.
|
// handle processes OS signals. It blocks until a termination or a
|
||||||
func (h *signalHandler) handle() {
|
// reconfiguration signal is received, after which it either shuts down all
|
||||||
defer log.OnPanic("signalHandler.handle")
|
// services or reconfigures them. ctx is used for logging and serves as the
|
||||||
|
// base for the shutdown timeout. status is [osutil.ExitCodeSuccess] on success
|
||||||
|
// and [osutil.ExitCodeFailure] on error.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add reconfiguration logic to golibs.
|
||||||
|
func (h *signalHandler) handle(ctx context.Context) (status osutil.ExitCode) {
|
||||||
|
defer slogutil.RecoverAndLog(ctx, h.logger)
|
||||||
|
|
||||||
h.writePID()
|
h.writePID(ctx)
|
||||||
|
|
||||||
for sig := range h.signal {
|
for sig := range h.signal {
|
||||||
log.Info("sighdlr: received signal %q", sig)
|
h.logger.InfoContext(ctx, "received", "signal", sig)
|
||||||
|
|
||||||
if aghos.IsReconfigureSignal(sig) {
|
if osutil.IsReconfigureSignal(sig) {
|
||||||
h.reconfigure()
|
err := h.reconfigure(ctx)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.ErrorContext(ctx, "reconfiguration error", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
return osutil.ExitCodeFailure
|
||||||
|
}
|
||||||
} else if osutil.IsShutdownSignal(sig) {
|
} else if osutil.IsShutdownSignal(sig) {
|
||||||
status := h.shutdown()
|
status = h.shutdown(ctx)
|
||||||
h.removePID()
|
|
||||||
|
|
||||||
log.Info("sighdlr: exiting with status %d", status)
|
h.removePID(ctx)
|
||||||
|
|
||||||
os.Exit(status)
|
return status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shouldn't happen, since h.signal is currently never closed.
|
||||||
|
panic("unexpected close of h.signal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writePID writes the PID to the file, if needed. Any errors are reported to
|
||||||
|
// log.
|
||||||
|
func (h *signalHandler) writePID(ctx context.Context) {
|
||||||
|
if h.pidFile == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pid := os.Getpid()
|
||||||
|
data := strconv.AppendInt(nil, int64(pid), 10)
|
||||||
|
data = append(data, '\n')
|
||||||
|
|
||||||
|
err := maybe.WriteFile(h.pidFile, data, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.ErrorContext(ctx, "writing pidfile", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.logger.DebugContext(ctx, "wrote pid", "file", h.pidFile, "pid", pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// reconfigure rereads the configuration file and updates and restarts services.
|
// reconfigure rereads the configuration file and updates and restarts services.
|
||||||
func (h *signalHandler) reconfigure() {
|
func (h *signalHandler) reconfigure(ctx context.Context) (err error) {
|
||||||
log.Info("sighdlr: reconfiguring adguard home")
|
h.logger.InfoContext(ctx, "reconfiguring started")
|
||||||
|
|
||||||
status := h.shutdown()
|
status := h.shutdown(ctx)
|
||||||
if status != statusSuccess {
|
if status != osutil.ExitCodeSuccess {
|
||||||
log.Info("sighdlr: reconfiguring: exiting with status %d", status)
|
return errors.Error("shutdown failed")
|
||||||
|
|
||||||
os.Exit(status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(a.garipov): This is a very rough way to do it. Some services can be
|
// TODO(a.garipov): This is a very rough way to do it. Some services can
|
||||||
// reconfigured without the full shutdown, and the error handling is
|
// be reconfigured without the full shutdown, and the error handling is
|
||||||
// currently not the best.
|
// currently not the best.
|
||||||
|
|
||||||
confMgr, err := newConfigMgr(h.confMgrConf)
|
var errs []error
|
||||||
check(err)
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, defaultTimeoutStart)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
confMgr, err := newConfigMgr(ctx, h.confMgrConf)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("configuration manager: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
web := confMgr.Web()
|
web := confMgr.Web()
|
||||||
err = web.Start()
|
err = web.Start(ctx)
|
||||||
check(err)
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("starting web: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
dns := confMgr.DNS()
|
dns := confMgr.DNS()
|
||||||
err = dns.Start()
|
err = dns.Start(ctx)
|
||||||
check(err)
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("starting dns: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
h.services = []agh.Service{
|
if len(errs) > 0 {
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.services = []service.Interface{
|
||||||
dns,
|
dns,
|
||||||
web,
|
web,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("sighdlr: successfully reconfigured adguard home")
|
h.logger.InfoContext(ctx, "reconfiguring finished")
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit status constants.
|
|
||||||
const (
|
|
||||||
statusSuccess = 0
|
|
||||||
statusError = 1
|
|
||||||
statusArgumentError = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// shutdown gracefully shuts down all services.
|
// shutdown gracefully shuts down all services.
|
||||||
func (h *signalHandler) shutdown() (status int) {
|
func (h *signalHandler) shutdown(ctx context.Context) (status int) {
|
||||||
ctx, cancel := ctxWithDefaultTimeout()
|
ctx, cancel := context.WithTimeout(ctx, h.shutdownTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
status = statusSuccess
|
status = osutil.ExitCodeSuccess
|
||||||
|
|
||||||
log.Info("sighdlr: shutting down services")
|
h.logger.InfoContext(ctx, "shutting down")
|
||||||
for i, service := range h.services {
|
for i, svc := range h.services {
|
||||||
err := service.Shutdown(ctx)
|
err := svc.Shutdown(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("sighdlr: shutting down service at index %d: %s", i, err)
|
h.logger.ErrorContext(ctx, "shutting down service", "idx", i, slogutil.KeyError, err)
|
||||||
status = statusError
|
status = osutil.ExitCodeFailure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
// newSignalHandler returns a new signalHandler that shuts down svcs. logger
|
||||||
|
// and confMgrConf must not be nil.
|
||||||
func newSignalHandler(
|
func newSignalHandler(
|
||||||
|
logger *slog.Logger,
|
||||||
confMgrConf *configmgr.Config,
|
confMgrConf *configmgr.Config,
|
||||||
pidFile string,
|
pidFile string,
|
||||||
svcs ...agh.Service,
|
svcs ...service.Interface,
|
||||||
) (h *signalHandler) {
|
) (h *signalHandler) {
|
||||||
h = &signalHandler{
|
h = &signalHandler{
|
||||||
confMgrConf: confMgrConf,
|
logger: logger,
|
||||||
signal: make(chan os.Signal, 1),
|
confMgrConf: confMgrConf,
|
||||||
pidFile: pidFile,
|
signal: make(chan os.Signal, 1),
|
||||||
services: svcs,
|
pidFile: pidFile,
|
||||||
|
services: svcs,
|
||||||
|
shutdownTimeout: defaultTimeoutShutdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier := osutil.DefaultSignalNotifier{}
|
notifier := osutil.DefaultSignalNotifier{}
|
||||||
osutil.NotifyShutdownSignal(notifier, h.signal)
|
osutil.NotifyShutdownSignal(notifier, h.signal)
|
||||||
aghos.NotifyReconfigureSignal(h.signal)
|
osutil.NotifyReconfigureSignal(notifier, h.signal)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// writePID writes the PID to the file, if needed. Any errors are reported to
|
|
||||||
// log.
|
|
||||||
func (h *signalHandler) writePID() {
|
|
||||||
if h.pidFile == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use 8, since most PIDs will fit.
|
|
||||||
data := make([]byte, 0, 8)
|
|
||||||
data = strconv.AppendInt(data, int64(os.Getpid()), 10)
|
|
||||||
data = append(data, '\n')
|
|
||||||
|
|
||||||
err := aghos.WriteFile(h.pidFile, data, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("sighdlr: writing pidfile: %s", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("sighdlr: wrote pid to %q", h.pidFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// removePID removes the PID file, if any.
|
// removePID removes the PID file, if any.
|
||||||
func (h *signalHandler) removePID() {
|
func (h *signalHandler) removePID(ctx context.Context) {
|
||||||
if h.pidFile == "" {
|
if h.pidFile == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := os.Remove(h.pidFile)
|
err := os.Remove(h.pidFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("sighdlr: removing pidfile: %s", err)
|
h.logger.ErrorContext(ctx, "removing pidfile", slogutil.KeyError, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("sighdlr: removed pid at %q", h.pidFile)
|
h.logger.DebugContext(ctx, "removed pidfile", "file", h.pidFile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/container"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configuration Structures
|
|
||||||
|
|
||||||
// config is the top-level on-disk configuration structure.
|
// config is the top-level on-disk configuration structure.
|
||||||
type config struct {
|
type config struct {
|
||||||
DNS *dnsConfig `yaml:"dns"`
|
DNS *dnsConfig `yaml:"dns"`
|
||||||
|
@ -19,35 +18,33 @@ type config struct {
|
||||||
SchemaVersion int `yaml:"schema_version"`
|
SchemaVersion int `yaml:"schema_version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const errNoConf errors.Error = "configuration not found"
|
// type check
|
||||||
|
var _ validator = (*config)(nil)
|
||||||
|
|
||||||
// validate returns an error if the configuration structure is invalid.
|
// validate implements the [validator] interface for *config.
|
||||||
func (c *config) validate() (err error) {
|
func (c *config) validate() (err error) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return errNoConf
|
return errors.ErrNoValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(a.garipov): Add more validations.
|
// TODO(a.garipov): Add more validations.
|
||||||
|
|
||||||
// Keep this in the same order as the fields in the config.
|
// Keep this in the same order as the fields in the config.
|
||||||
validators := []struct {
|
validators := container.KeyValues[string, validator]{{
|
||||||
validate func() (err error)
|
Key: "dns",
|
||||||
name string
|
Value: c.DNS,
|
||||||
}{{
|
|
||||||
validate: c.DNS.validate,
|
|
||||||
name: "dns",
|
|
||||||
}, {
|
}, {
|
||||||
validate: c.HTTP.validate,
|
Key: "http",
|
||||||
name: "http",
|
Value: c.HTTP,
|
||||||
}, {
|
}, {
|
||||||
validate: c.Log.validate,
|
Key: "log",
|
||||||
name: "log",
|
Value: c.Log,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, v := range validators {
|
for _, kv := range validators {
|
||||||
err = v.validate()
|
err = kv.Value.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s: %w", v.name, err)
|
return fmt.Errorf("%s: %w", kv.Key, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,16 +62,19 @@ type dnsConfig struct {
|
||||||
UseDNS64 bool `yaml:"use_dns64"`
|
UseDNS64 bool `yaml:"use_dns64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate returns an error if the DNS configuration structure is invalid.
|
// type check
|
||||||
|
var _ validator = (*dnsConfig)(nil)
|
||||||
|
|
||||||
|
// validate implements the [validator] interface for *dnsConfig.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Add more validations.
|
// TODO(a.garipov): Add more validations.
|
||||||
func (c *dnsConfig) validate() (err error) {
|
func (c *dnsConfig) validate() (err error) {
|
||||||
// TODO(a.garipov): Add more validations.
|
// TODO(a.garipov): Add more validations.
|
||||||
switch {
|
switch {
|
||||||
case c == nil:
|
case c == nil:
|
||||||
return errNoConf
|
return errors.ErrNoValue
|
||||||
case c.UpstreamTimeout.Duration <= 0:
|
case c.UpstreamTimeout.Duration <= 0:
|
||||||
return newMustBePositiveError("upstream_timeout", c.UpstreamTimeout)
|
return newErrNotPositive("upstream_timeout", c.UpstreamTimeout)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -91,15 +91,18 @@ type httpConfig struct {
|
||||||
ForceHTTPS bool `yaml:"force_https"`
|
ForceHTTPS bool `yaml:"force_https"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate returns an error if the HTTP configuration structure is invalid.
|
// type check
|
||||||
|
var _ validator = (*httpConfig)(nil)
|
||||||
|
|
||||||
|
// validate implements the [validator] interface for *httpConfig.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Add more validations.
|
// TODO(a.garipov): Add more validations.
|
||||||
func (c *httpConfig) validate() (err error) {
|
func (c *httpConfig) validate() (err error) {
|
||||||
switch {
|
switch {
|
||||||
case c == nil:
|
case c == nil:
|
||||||
return errNoConf
|
return errors.ErrNoValue
|
||||||
case c.Timeout.Duration <= 0:
|
case c.Timeout.Duration <= 0:
|
||||||
return newMustBePositiveError("timeout", c.Timeout)
|
return newErrNotPositive("timeout", c.Timeout)
|
||||||
default:
|
default:
|
||||||
return c.Pprof.validate()
|
return c.Pprof.validate()
|
||||||
}
|
}
|
||||||
|
@ -111,10 +114,13 @@ type httpPprofConfig struct {
|
||||||
Enabled bool `yaml:"enabled"`
|
Enabled bool `yaml:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate returns an error if the pprof configuration structure is invalid.
|
// type check
|
||||||
|
var _ validator = (*httpPprofConfig)(nil)
|
||||||
|
|
||||||
|
// validate implements the [validator] interface for *httpPprofConfig.
|
||||||
func (c *httpPprofConfig) validate() (err error) {
|
func (c *httpPprofConfig) validate() (err error) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return errNoConf
|
return errors.ErrNoValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -126,12 +132,15 @@ type logConfig struct {
|
||||||
Verbose bool `yaml:"verbose"`
|
Verbose bool `yaml:"verbose"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate returns an error if the HTTP configuration structure is invalid.
|
// type check
|
||||||
|
var _ validator = (*logConfig)(nil)
|
||||||
|
|
||||||
|
// validate implements the [validator] interface for *logConfig.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Add more validations.
|
// TODO(a.garipov): Add more validations.
|
||||||
func (c *logConfig) validate() (err error) {
|
func (c *logConfig) validate() (err error) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return errNoConf
|
return errors.ErrNoValue
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
@ -19,18 +20,23 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
|
"github.com/google/renameio/v2/maybe"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configuration Manager
|
|
||||||
|
|
||||||
// Manager handles full and partial changes in the configuration, persisting
|
// Manager handles full and partial changes in the configuration, persisting
|
||||||
// them to disk if necessary.
|
// them to disk if necessary.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Support missing configs and default values.
|
// TODO(a.garipov): Support missing configs and default values.
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
|
// baseLogger is used to create loggers for other entities.
|
||||||
|
baseLogger *slog.Logger
|
||||||
|
|
||||||
|
// logger is used for logging the operation of the configuration manager.
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
// updMu makes sure that at most one reconfiguration is performed at a time.
|
// updMu makes sure that at most one reconfiguration is performed at a time.
|
||||||
// updMu protects all fields below.
|
// updMu protects all fields below.
|
||||||
updMu *sync.RWMutex
|
updMu *sync.RWMutex
|
||||||
|
@ -57,12 +63,24 @@ func Validate(fileName string) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't wrap the error, because it's informative enough as is.
|
err = conf.validate()
|
||||||
return conf.validate()
|
if err != nil {
|
||||||
|
return fmt.Errorf("validating config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config contains the configuration parameters for the configuration manager.
|
// Config contains the configuration parameters for the configuration manager.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// BaseLogger is used to create loggers for other entities. It must not be
|
||||||
|
// nil.
|
||||||
|
BaseLogger *slog.Logger
|
||||||
|
|
||||||
|
// Logger is used for logging the operation of the configuration manager.
|
||||||
|
// It must not be nil.
|
||||||
|
Logger *slog.Logger
|
||||||
|
|
||||||
// Frontend is the filesystem with the frontend files.
|
// Frontend is the filesystem with the frontend files.
|
||||||
Frontend fs.FS
|
Frontend fs.FS
|
||||||
|
|
||||||
|
@ -93,9 +111,11 @@ func New(ctx context.Context, c *Config) (m *Manager, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
m = &Manager{
|
m = &Manager{
|
||||||
updMu: &sync.RWMutex{},
|
baseLogger: c.BaseLogger,
|
||||||
current: conf,
|
logger: c.Logger,
|
||||||
fileName: c.FileName,
|
updMu: &sync.RWMutex{},
|
||||||
|
current: conf,
|
||||||
|
fileName: c.FileName,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.assemble(ctx, conf, c.Frontend, c.WebAddr, c.Start)
|
err = m.assemble(ctx, conf, c.Frontend, c.WebAddr, c.Start)
|
||||||
|
@ -137,6 +157,7 @@ func (m *Manager) assemble(
|
||||||
start time.Time,
|
start time.Time,
|
||||||
) (err error) {
|
) (err error) {
|
||||||
dnsConf := &dnssvc.Config{
|
dnsConf := &dnssvc.Config{
|
||||||
|
Logger: m.baseLogger.With(slogutil.KeyPrefix, "dnssvc"),
|
||||||
Addresses: conf.DNS.Addresses,
|
Addresses: conf.DNS.Addresses,
|
||||||
BootstrapServers: conf.DNS.BootstrapDNS,
|
BootstrapServers: conf.DNS.BootstrapDNS,
|
||||||
UpstreamServers: conf.DNS.UpstreamDNS,
|
UpstreamServers: conf.DNS.UpstreamDNS,
|
||||||
|
@ -151,6 +172,7 @@ func (m *Manager) assemble(
|
||||||
}
|
}
|
||||||
|
|
||||||
webSvcConf := &websvc.Config{
|
webSvcConf := &websvc.Config{
|
||||||
|
Logger: m.baseLogger.With(slogutil.KeyPrefix, "websvc"),
|
||||||
Pprof: &websvc.PprofConfig{
|
Pprof: &websvc.PprofConfig{
|
||||||
Port: conf.HTTP.Pprof.Port,
|
Port: conf.HTTP.Pprof.Port,
|
||||||
Enabled: conf.HTTP.Pprof.Enabled,
|
Enabled: conf.HTTP.Pprof.Enabled,
|
||||||
|
@ -176,18 +198,18 @@ func (m *Manager) assemble(
|
||||||
}
|
}
|
||||||
|
|
||||||
// write writes the current configuration to disk.
|
// write writes the current configuration to disk.
|
||||||
func (m *Manager) write() (err error) {
|
func (m *Manager) write(ctx context.Context) (err error) {
|
||||||
b, err := yaml.Marshal(m.current)
|
b, err := yaml.Marshal(m.current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encoding: %w", err)
|
return fmt.Errorf("encoding: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghos.WriteFile(m.fileName, b, aghos.DefaultPermFile)
|
err = maybe.WriteFile(m.fileName, b, aghos.DefaultPermFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writing: %w", err)
|
return fmt.Errorf("writing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("configmgr: written to %q", m.fileName)
|
m.logger.InfoContext(ctx, "config file written", "path", m.fileName)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -216,7 +238,7 @@ func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
|
||||||
m.updateCurrentDNS(c)
|
m.updateCurrentDNS(c)
|
||||||
|
|
||||||
return m.write()
|
return m.write(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
|
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
|
||||||
|
@ -270,7 +292,7 @@ func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
|
||||||
m.updateCurrentWeb(c)
|
m.updateCurrentWeb(c)
|
||||||
|
|
||||||
return m.write()
|
return m.write(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateWeb recreates the web service. m.upd is expected to be locked.
|
// updateWeb recreates the web service. m.upd is expected to be locked.
|
||||||
|
|
|
@ -3,25 +3,29 @@ package configmgr
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
"golang.org/x/exp/constraints"
|
"golang.org/x/exp/constraints"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// validator is the interface for configuration entities that can validate
|
||||||
|
// themselves.
|
||||||
|
type validator interface {
|
||||||
|
// validate returns an error if the entity isn't valid.
|
||||||
|
validate() (err error)
|
||||||
|
}
|
||||||
|
|
||||||
// numberOrDuration is the constraint for integer types along with
|
// numberOrDuration is the constraint for integer types along with
|
||||||
// timeutil.Duration.
|
// timeutil.Duration.
|
||||||
type numberOrDuration interface {
|
type numberOrDuration interface {
|
||||||
constraints.Integer | timeutil.Duration
|
constraints.Integer | timeutil.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// newMustBePositiveError returns an error about the value that must be positive
|
// newErrNotPositive returns an error about the value that must be positive but
|
||||||
// but isn't. prop is the name of the property to mention in the error message.
|
// isn't. prop is the name of the property to mention in the error message.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Consider moving such helpers to golibs and use in AdGuardDNS
|
// TODO(a.garipov): Consider moving such helpers to golibs and use in AdGuardDNS
|
||||||
// as well.
|
// as well.
|
||||||
func newMustBePositiveError[T numberOrDuration](prop string, v T) (err error) {
|
func newErrNotPositive[T numberOrDuration](prop string, v T) (err error) {
|
||||||
if s, ok := any(v).(fmt.Stringer); ok {
|
return fmt.Errorf("%s: %w, got %v", prop, errors.ErrNotPositive, v)
|
||||||
return fmt.Errorf("%s must be positive, got %s", prop, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%s must be positive, got %d", prop, v)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package dnssvc
|
package dnssvc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -9,6 +10,10 @@ import (
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Add timeout for incoming requests.
|
// TODO(a.garipov): Add timeout for incoming requests.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// Logger is used for logging the operation of the web API service. It must
|
||||||
|
// not be nil.
|
||||||
|
Logger *slog.Logger
|
||||||
|
|
||||||
// Addresses are the addresses on which to serve plain DNS queries.
|
// Addresses are the addresses on which to serve plain DNS queries.
|
||||||
Addresses []netip.AddrPort
|
Addresses []netip.AddrPort
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ package dnssvc
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
@ -28,6 +29,7 @@ import (
|
||||||
// TODO(a.garipov): Consider saving a [*proxy.Config] instance for those
|
// TODO(a.garipov): Consider saving a [*proxy.Config] instance for those
|
||||||
// fields that are only used in [New] and [Service.Config].
|
// fields that are only used in [New] and [Service.Config].
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
logger *slog.Logger
|
||||||
proxy *proxy.Proxy
|
proxy *proxy.Proxy
|
||||||
bootstraps []string
|
bootstraps []string
|
||||||
bootstrapResolvers []*upstream.UpstreamResolver
|
bootstrapResolvers []*upstream.UpstreamResolver
|
||||||
|
@ -48,6 +50,7 @@ func New(c *Config) (svc *Service, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
svc = &Service{
|
svc = &Service{
|
||||||
|
logger: c.Logger,
|
||||||
bootstraps: c.BootstrapServers,
|
bootstraps: c.BootstrapServers,
|
||||||
upstreams: c.UpstreamServers,
|
upstreams: c.UpstreamServers,
|
||||||
dns64Prefixes: c.DNS64Prefixes,
|
dns64Prefixes: c.DNS64Prefixes,
|
||||||
|
@ -68,6 +71,7 @@ func New(c *Config) (svc *Service, err error) {
|
||||||
|
|
||||||
svc.bootstrapResolvers = resolvers
|
svc.bootstrapResolvers = resolvers
|
||||||
svc.proxy, err = proxy.New(&proxy.Config{
|
svc.proxy, err = proxy.New(&proxy.Config{
|
||||||
|
Logger: svc.logger,
|
||||||
UDPListenAddr: udpAddrs(c.Addresses),
|
UDPListenAddr: udpAddrs(c.Addresses),
|
||||||
TCPListenAddr: tcpAddrs(c.Addresses),
|
TCPListenAddr: tcpAddrs(c.Addresses),
|
||||||
UpstreamConfig: &proxy.UpstreamConfig{
|
UpstreamConfig: &proxy.UpstreamConfig{
|
||||||
|
@ -153,12 +157,12 @@ func udpAddrs(addrPorts []netip.AddrPort) (udpAddrs []*net.UDPAddr) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ agh.Service = (*Service)(nil)
|
var _ agh.ServiceWithConfig[*Config] = (*Service)(nil)
|
||||||
|
|
||||||
// Start implements the [agh.Service] interface for *Service. svc may be nil.
|
// Start implements the [agh.Service] interface for *Service. svc may be nil.
|
||||||
// After Start exits, all DNS servers have tried to start, but there is no
|
// After Start exits, all DNS servers have tried to start, but there is no
|
||||||
// guarantee that they did. Errors from the servers are written to the log.
|
// guarantee that they did. Errors from the servers are written to the log.
|
||||||
func (svc *Service) Start() (err error) {
|
func (svc *Service) Start(ctx context.Context) (err error) {
|
||||||
if svc == nil {
|
if svc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -170,7 +174,7 @@ func (svc *Service) Start() (err error) {
|
||||||
svc.running.Store(err == nil)
|
svc.running.Store(err == nil)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return svc.proxy.Start(context.Background())
|
return svc.proxy.Start(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown implements the [agh.Service] interface for *Service. svc may be
|
// Shutdown implements the [agh.Service] interface for *Service. svc may be
|
||||||
|
@ -215,6 +219,7 @@ func (svc *Service) Config() (c *Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c = &Config{
|
c = &Config{
|
||||||
|
Logger: svc.logger,
|
||||||
Addresses: addrs,
|
Addresses: addrs,
|
||||||
BootstrapServers: svc.bootstraps,
|
BootstrapServers: svc.bootstraps,
|
||||||
UpstreamServers: svc.upstreams,
|
UpstreamServers: svc.upstreams,
|
||||||
|
|
|
@ -6,16 +6,13 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
testutil.DiscardLogOutput(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testTimeout is the common timeout for tests.
|
// testTimeout is the common timeout for tests.
|
||||||
const testTimeout = 1 * time.Second
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
|
@ -59,6 +56,7 @@ func TestService(t *testing.T) {
|
||||||
_, _ = testutil.RequireReceive(t, upstreamStartedCh, testTimeout)
|
_, _ = testutil.RequireReceive(t, upstreamStartedCh, testTimeout)
|
||||||
|
|
||||||
c := &dnssvc.Config{
|
c := &dnssvc.Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort(listenAddr)},
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort(listenAddr)},
|
||||||
BootstrapServers: []string{upstreamSrv.PacketConn.LocalAddr().String()},
|
BootstrapServers: []string{upstreamSrv.PacketConn.LocalAddr().String()},
|
||||||
UpstreamServers: []string{upstreamAddr},
|
UpstreamServers: []string{upstreamAddr},
|
||||||
|
@ -71,7 +69,7 @@ func TestService(t *testing.T) {
|
||||||
svc, err := dnssvc.New(c)
|
svc, err := dnssvc.New(c)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = svc.Start()
|
err = svc.Start(testutil.ContextWithTimeout(t, testTimeout))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
gotConf := svc.Config()
|
gotConf := svc.Config()
|
||||||
|
|
|
@ -3,12 +3,17 @@ package websvc
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the AdGuard Home web service configuration structure.
|
// Config is the AdGuard Home web service configuration structure.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// Logger is used for logging the operation of the web API service. It must
|
||||||
|
// not be nil.
|
||||||
|
Logger *slog.Logger
|
||||||
|
|
||||||
// Pprof is the configuration for the pprof debug API. It must not be nil.
|
// Pprof is the configuration for the pprof debug API. It must not be nil.
|
||||||
Pprof *PprofConfig
|
Pprof *PprofConfig
|
||||||
|
|
||||||
|
@ -60,17 +65,20 @@ type PprofConfig struct {
|
||||||
// finished.
|
// finished.
|
||||||
func (svc *Service) Config() (c *Config) {
|
func (svc *Service) Config() (c *Config) {
|
||||||
c = &Config{
|
c = &Config{
|
||||||
|
Logger: svc.logger,
|
||||||
Pprof: &PprofConfig{
|
Pprof: &PprofConfig{
|
||||||
Port: svc.pprofPort,
|
Port: svc.pprofPort,
|
||||||
Enabled: svc.pprof != nil,
|
Enabled: svc.pprof != nil,
|
||||||
},
|
},
|
||||||
ConfigManager: svc.confMgr,
|
ConfigManager: svc.confMgr,
|
||||||
|
Frontend: svc.frontend,
|
||||||
TLS: svc.tls,
|
TLS: svc.tls,
|
||||||
// Leave Addresses and SecureAddresses empty and get the actual
|
// Leave Addresses and SecureAddresses empty and get the actual
|
||||||
// addresses that include the :0 ones later.
|
// addresses that include the :0 ones later.
|
||||||
Start: svc.start,
|
Start: svc.start,
|
||||||
Timeout: svc.timeout,
|
OverrideAddress: svc.overrideAddr,
|
||||||
ForceHTTPS: svc.forceHTTPS,
|
Timeout: svc.timeout,
|
||||||
|
ForceHTTPS: svc.forceHTTPS,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Addresses, c.SecureAddresses = svc.addrs()
|
c.Addresses, c.SecureAddresses = svc.addrs()
|
||||||
|
|
|
@ -11,8 +11,6 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DNS Settings Handlers
|
|
||||||
|
|
||||||
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
|
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
|
||||||
// HTTP API.
|
// HTTP API.
|
||||||
type ReqPatchSettingsDNS struct {
|
type ReqPatchSettingsDNS struct {
|
||||||
|
@ -60,6 +58,7 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
newConf := &dnssvc.Config{
|
newConf := &dnssvc.Config{
|
||||||
|
Logger: svc.logger,
|
||||||
Addresses: req.Addresses,
|
Addresses: req.Addresses,
|
||||||
BootstrapServers: req.BootstrapServers,
|
BootstrapServers: req.BootstrapServers,
|
||||||
UpstreamServers: req.UpstreamServers,
|
UpstreamServers: req.UpstreamServers,
|
||||||
|
@ -78,7 +77,7 @@ func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
newSvc := svc.confMgr.DNS()
|
newSvc := svc.confMgr.DNS()
|
||||||
err = newSvc.Start()
|
err = newSvc.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("starting new service: %w", err))
|
aghhttp.WriteJSONResponseError(w, r, fmt.Errorf("starting new service: %w", err))
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -34,7 +35,7 @@ func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||||
confMgr := newConfigManager()
|
confMgr := newConfigManager()
|
||||||
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
||||||
OnStart: func() (err error) {
|
OnStart: func(_ context.Context) (err error) {
|
||||||
started.Store(true)
|
started.Store(true)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -49,9 +50,9 @@ func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||||
|
|
||||||
_, addr := newTestServer(t, confMgr)
|
_, addr := newTestServer(t, confMgr)
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: urlutil.SchemeHTTP,
|
||||||
Host: addr.String(),
|
Host: addr.String(),
|
||||||
Path: websvc.PathV1SettingsDNS,
|
Path: websvc.PathPatternV1SettingsDNS,
|
||||||
}
|
}
|
||||||
|
|
||||||
req := jobj{
|
req := jobj{
|
||||||
|
|
|
@ -10,11 +10,9 @@ import (
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP Settings Handlers
|
|
||||||
|
|
||||||
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
|
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
|
||||||
// HTTP API.
|
// HTTP API.
|
||||||
type ReqPatchSettingsHTTP struct {
|
type ReqPatchSettingsHTTP struct {
|
||||||
|
@ -53,6 +51,7 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
newConf := &Config{
|
newConf := &Config{
|
||||||
|
Logger: svc.logger,
|
||||||
Pprof: &PprofConfig{
|
Pprof: &PprofConfig{
|
||||||
Port: svc.pprofPort,
|
Port: svc.pprofPort,
|
||||||
Enabled: svc.pprof != nil,
|
Enabled: svc.pprof != nil,
|
||||||
|
@ -89,13 +88,13 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
|
||||||
// relaunch updates the web service in the configuration manager and starts it.
|
// relaunch updates the web service in the configuration manager and starts it.
|
||||||
// It is intended to be used as a goroutine.
|
// It is intended to be used as a goroutine.
|
||||||
func (svc *Service) relaunch(ctx context.Context, cancel context.CancelFunc, newConf *Config) {
|
func (svc *Service) relaunch(ctx context.Context, cancel context.CancelFunc, newConf *Config) {
|
||||||
defer log.OnPanic("websvc: relaunching")
|
defer slogutil.RecoverAndLog(ctx, svc.logger)
|
||||||
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err := svc.confMgr.UpdateWeb(ctx, newConf)
|
err := svc.confMgr.UpdateWeb(ctx, newConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("websvc: updating web: %s", err)
|
svc.logger.ErrorContext(ctx, "updating web", slogutil.KeyError, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -106,18 +105,18 @@ func (svc *Service) relaunch(ctx context.Context, cancel context.CancelFunc, new
|
||||||
var newSvc agh.ServiceWithConfig[*Config]
|
var newSvc agh.ServiceWithConfig[*Config]
|
||||||
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||||
if time.Since(updStart) >= maxUpdDur {
|
if time.Since(updStart) >= maxUpdDur {
|
||||||
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
svc.logger.ErrorContext(ctx, "failed to update service on time", "duration", maxUpdDur)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("websvc: waiting for new websvc to be configured")
|
svc.logger.DebugContext(ctx, "waiting for new service")
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = newSvc.Start()
|
err = newSvc.Start(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("websvc: new svc failed to start with error: %s", err)
|
svc.logger.ErrorContext(ctx, "new service failed", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -26,14 +28,15 @@ func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
svc, err := websvc.New(&websvc.Config{
|
svc, err := websvc.New(&websvc.Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
Pprof: &websvc.PprofConfig{
|
Pprof: &websvc.PprofConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
},
|
},
|
||||||
TLS: &tls.Config{
|
TLS: &tls.Config{
|
||||||
Certificates: []tls.Certificate{{}},
|
Certificates: []tls.Certificate{{}},
|
||||||
},
|
},
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
ForceHTTPS: true,
|
ForceHTTPS: true,
|
||||||
})
|
})
|
||||||
|
@ -45,9 +48,9 @@ func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||||
|
|
||||||
_, addr := newTestServer(t, confMgr)
|
_, addr := newTestServer(t, confMgr)
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: urlutil.SchemeHTTP,
|
||||||
Host: addr.String(),
|
Host: addr.String(),
|
||||||
Path: websvc.PathV1SettingsHTTP,
|
Path: websvc.PathPatternV1SettingsHTTP,
|
||||||
}
|
}
|
||||||
|
|
||||||
req := jobj{
|
req := jobj{
|
||||||
|
|
|
@ -2,15 +2,11 @@ package websvc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/golibs/httphdr"
|
"github.com/AdguardTeam/golibs/httphdr"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Middlewares
|
|
||||||
|
|
||||||
// jsonMw sets the content type of the response to application/json.
|
// jsonMw sets the content type of the response to application/json.
|
||||||
func jsonMw(h http.Handler) (wrapped http.HandlerFunc) {
|
func jsonMw(h http.Handler) (wrapped http.HandlerFunc) {
|
||||||
f := func(w http.ResponseWriter, r *http.Request) {
|
f := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -21,18 +17,3 @@ func jsonMw(h http.Handler) (wrapped http.HandlerFunc) {
|
||||||
|
|
||||||
return http.HandlerFunc(f)
|
return http.HandlerFunc(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// logMw logs the queries with level debug.
|
|
||||||
func logMw(h http.Handler) (wrapped http.HandlerFunc) {
|
|
||||||
f := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
start := time.Now()
|
|
||||||
m, u := r.Method, r.RequestURI
|
|
||||||
|
|
||||||
log.Debug("websvc: %s %s started", m, u)
|
|
||||||
defer func() { log.Debug("websvc: %s %s finished in %s", m, u, time.Since(start)) }()
|
|
||||||
|
|
||||||
h.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return http.HandlerFunc(f)
|
|
||||||
}
|
|
||||||
|
|
73
internal/next/websvc/route.go
Normal file
73
internal/next/websvc/route.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Path pattern constants.
|
||||||
|
const (
|
||||||
|
PathPatternFrontend = "/"
|
||||||
|
PathPatternHealthCheck = "/health-check"
|
||||||
|
PathPatternV1SettingsAll = "/api/v1/settings/all"
|
||||||
|
PathPatternV1SettingsDNS = "/api/v1/settings/dns"
|
||||||
|
PathPatternV1SettingsHTTP = "/api/v1/settings/http"
|
||||||
|
PathPatternV1SystemInfo = "/api/v1/system/info"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Route pattern constants.
|
||||||
|
const (
|
||||||
|
routePatternFrontend = http.MethodGet + " " + PathPatternFrontend
|
||||||
|
routePatternGetV1SettingsAll = http.MethodGet + " " + PathPatternV1SettingsAll
|
||||||
|
routePatternGetV1SystemInfo = http.MethodGet + " " + PathPatternV1SystemInfo
|
||||||
|
routePatternHealthCheck = http.MethodGet + " " + PathPatternHealthCheck
|
||||||
|
routePatternPatchV1SettingsDNS = http.MethodPatch + " " + PathPatternV1SettingsDNS
|
||||||
|
routePatternPatchV1SettingsHTTP = http.MethodPatch + " " + PathPatternV1SettingsHTTP
|
||||||
|
)
|
||||||
|
|
||||||
|
// route registers all necessary handlers in mux.
|
||||||
|
func (svc *Service) route(mux *http.ServeMux) {
|
||||||
|
routes := []struct {
|
||||||
|
handler http.Handler
|
||||||
|
pattern string
|
||||||
|
isJSON bool
|
||||||
|
}{{
|
||||||
|
handler: httputil.HealthCheckHandler,
|
||||||
|
pattern: routePatternHealthCheck,
|
||||||
|
isJSON: false,
|
||||||
|
}, {
|
||||||
|
handler: http.FileServer(http.FS(svc.frontend)),
|
||||||
|
pattern: routePatternFrontend,
|
||||||
|
isJSON: false,
|
||||||
|
}, {
|
||||||
|
handler: http.HandlerFunc(svc.handleGetSettingsAll),
|
||||||
|
pattern: routePatternGetV1SettingsAll,
|
||||||
|
isJSON: true,
|
||||||
|
}, {
|
||||||
|
handler: http.HandlerFunc(svc.handlePatchSettingsDNS),
|
||||||
|
pattern: routePatternPatchV1SettingsDNS,
|
||||||
|
isJSON: true,
|
||||||
|
}, {
|
||||||
|
handler: http.HandlerFunc(svc.handlePatchSettingsHTTP),
|
||||||
|
pattern: routePatternPatchV1SettingsHTTP,
|
||||||
|
isJSON: true,
|
||||||
|
}, {
|
||||||
|
handler: http.HandlerFunc(svc.handleGetV1SystemInfo),
|
||||||
|
pattern: routePatternGetV1SystemInfo,
|
||||||
|
isJSON: true,
|
||||||
|
}}
|
||||||
|
|
||||||
|
logMw := httputil.NewLogMiddleware(svc.logger, slog.LevelDebug)
|
||||||
|
for _, r := range routes {
|
||||||
|
var hdlr http.Handler
|
||||||
|
if r.isJSON {
|
||||||
|
hdlr = jsonMw(r.handler)
|
||||||
|
} else {
|
||||||
|
hdlr = r.handler
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.Handle(r.pattern, logMw.Wrap(hdlr))
|
||||||
|
}
|
||||||
|
}
|
156
internal/next/websvc/server.go
Normal file
156
internal/next/websvc/server.go
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// server contains an *http.Server as well as entities and data associated with
|
||||||
|
// it.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Join with similar structs in other projects and move to
|
||||||
|
// golibs/netutil/httputil.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Once the above standardization is complete, consider
|
||||||
|
// merging debugsvc and websvc into a single httpsvc.
|
||||||
|
type server struct {
|
||||||
|
// mu protects http, logger, tcpListener, and url.
|
||||||
|
mu *sync.Mutex
|
||||||
|
http *http.Server
|
||||||
|
logger *slog.Logger
|
||||||
|
tcpListener *net.TCPListener
|
||||||
|
url *url.URL
|
||||||
|
|
||||||
|
tlsConf *tls.Config
|
||||||
|
initialAddr netip.AddrPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// loggerKeyServer is the key used by [server] to identify itself.
|
||||||
|
const loggerKeyServer = "server"
|
||||||
|
|
||||||
|
// newServer returns a *server that is ready to serve HTTP queries. The TCP
|
||||||
|
// listener is not started. handler must not be nil.
|
||||||
|
func newServer(
|
||||||
|
baseLogger *slog.Logger,
|
||||||
|
initialAddr netip.AddrPort,
|
||||||
|
tlsConf *tls.Config,
|
||||||
|
handler http.Handler,
|
||||||
|
timeout time.Duration,
|
||||||
|
) (s *server) {
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: urlutil.SchemeHTTP,
|
||||||
|
Host: initialAddr.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsConf != nil {
|
||||||
|
u.Scheme = urlutil.SchemeHTTPS
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := baseLogger.With(loggerKeyServer, u)
|
||||||
|
|
||||||
|
return &server{
|
||||||
|
mu: &sync.Mutex{},
|
||||||
|
http: &http.Server{
|
||||||
|
Handler: handler,
|
||||||
|
ReadTimeout: timeout,
|
||||||
|
ReadHeaderTimeout: timeout,
|
||||||
|
WriteTimeout: timeout,
|
||||||
|
IdleTimeout: timeout,
|
||||||
|
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
|
||||||
|
},
|
||||||
|
logger: logger,
|
||||||
|
url: u,
|
||||||
|
|
||||||
|
tlsConf: tlsConf,
|
||||||
|
initialAddr: initialAddr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// localAddr returns the local address of the server if the server has started
|
||||||
|
// listening; otherwise, it returns nil.
|
||||||
|
func (s *server) localAddr() (addr net.Addr) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if l := s.tcpListener; l != nil {
|
||||||
|
return l.Addr()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve starts s. baseLogger is used as a base logger for s. If s fails to
|
||||||
|
// serve with anything other than [http.ErrServerClosed], it causes an unhandled
|
||||||
|
// panic. It is intended to be used as a goroutine.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Improve error handling.
|
||||||
|
func (s *server) serve(ctx context.Context, baseLogger *slog.Logger) {
|
||||||
|
l, err := net.ListenTCP("tcp", net.TCPAddrFromAddrPort(s.initialAddr))
|
||||||
|
if err != nil {
|
||||||
|
s.logger.ErrorContext(ctx, "listening tcp", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
panic(fmt.Errorf("websvc: listening tcp: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func() {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.tcpListener = l
|
||||||
|
|
||||||
|
// Reassign the address in case the port was zero.
|
||||||
|
s.url.Host = l.Addr().String()
|
||||||
|
s.logger = baseLogger.With(loggerKeyServer, s.url)
|
||||||
|
s.http.ErrorLog = slog.NewLogLogger(s.logger.Handler(), slog.LevelError)
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.logger.InfoContext(ctx, "starting")
|
||||||
|
defer s.logger.InfoContext(ctx, "started")
|
||||||
|
|
||||||
|
err = s.http.Serve(l)
|
||||||
|
if err == nil || errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.ErrorContext(ctx, "serving", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
panic(fmt.Errorf("websvc: serving: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown shuts s down.
|
||||||
|
func (s *server) shutdown(ctx context.Context) (err error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
err = s.http.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("shutting down server %s: %w", s.url, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the listener separately, as it might not have been closed if the
|
||||||
|
// context has been canceled.
|
||||||
|
//
|
||||||
|
// NOTE: The listener could remain uninitialized if [net.ListenTCP] failed
|
||||||
|
// in [s.serve].
|
||||||
|
if l := s.tcpListener; l != nil {
|
||||||
|
err = l.Close()
|
||||||
|
if err != nil && !errors.Is(err, net.ErrClosed) {
|
||||||
|
errs = append(errs, fmt.Errorf("closing listener for server %s: %w", s.url, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package websvc_test
|
package websvc_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
@ -13,6 +12,8 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -28,16 +29,10 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||||
BootstrapPreferIPv6: true,
|
BootstrapPreferIPv6: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
|
||||||
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
|
||||||
Timeout: aghhttp.JSONDuration(5 * time.Second),
|
|
||||||
ForceHTTPS: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
confMgr := newConfigManager()
|
confMgr := newConfigManager()
|
||||||
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
c, err := dnssvc.New(&dnssvc.Config{
|
c, err := dnssvc.New(&dnssvc.Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
Addresses: wantDNS.Addresses,
|
Addresses: wantDNS.Addresses,
|
||||||
UpstreamServers: wantDNS.UpstreamServers,
|
UpstreamServers: wantDNS.UpstreamServers,
|
||||||
BootstrapServers: wantDNS.BootstrapServers,
|
BootstrapServers: wantDNS.BootstrapServers,
|
||||||
|
@ -49,34 +44,27 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
svc, err := websvc.New(&websvc.Config{
|
svc, addr := newTestServer(t, confMgr)
|
||||||
Pprof: &websvc.PprofConfig{
|
u := &url.URL{
|
||||||
Enabled: false,
|
Scheme: urlutil.SchemeHTTP,
|
||||||
},
|
Host: addr.String(),
|
||||||
TLS: &tls.Config{
|
Path: websvc.PathPatternV1SettingsAll,
|
||||||
Certificates: []tls.Certificate{{}},
|
}
|
||||||
},
|
|
||||||
Addresses: wantWeb.Addresses,
|
|
||||||
SecureAddresses: wantWeb.SecureAddresses,
|
|
||||||
Timeout: time.Duration(wantWeb.Timeout),
|
|
||||||
ForceHTTPS: true,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
_, addr := newTestServer(t, confMgr)
|
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||||
u := &url.URL{
|
Addresses: []netip.AddrPort{addr},
|
||||||
Scheme: "http",
|
SecureAddresses: nil,
|
||||||
Host: addr.String(),
|
Timeout: aghhttp.JSONDuration(testTimeout),
|
||||||
Path: websvc.PathV1SettingsAll,
|
ForceHTTPS: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
body := httpGet(t, u, http.StatusOK)
|
body := httpGet(t, u, http.StatusOK)
|
||||||
resp := &websvc.RespGetV1SettingsAll{}
|
resp := &websvc.RespGetV1SettingsAll{}
|
||||||
err = json.Unmarshal(body, resp)
|
err := json.Unmarshal(body, resp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, wantDNS, resp.DNS)
|
assert.Equal(t, wantDNS, resp.DNS)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -17,9 +18,9 @@ func TestService_handleGetV1SystemInfo(t *testing.T) {
|
||||||
confMgr := newConfigManager()
|
confMgr := newConfigManager()
|
||||||
_, addr := newTestServer(t, confMgr)
|
_, addr := newTestServer(t, confMgr)
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: urlutil.SchemeHTTP,
|
||||||
Host: addr.String(),
|
Host: addr.String(),
|
||||||
Path: websvc.PathV1SystemInfo,
|
Path: websvc.PathPatternV1SystemInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
body := httpGet(t, u, http.StatusOK)
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
|
|
@ -10,22 +10,18 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/AdguardTeam/golibs/mathutil"
|
|
||||||
"github.com/AdguardTeam/golibs/netutil/httputil"
|
"github.com/AdguardTeam/golibs/netutil/httputil"
|
||||||
httptreemux "github.com/dimfeld/httptreemux/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigManager is the configuration manager interface.
|
// ConfigManager is the configuration manager interface.
|
||||||
|
@ -40,13 +36,14 @@ type ConfigManager interface {
|
||||||
// Service is the AdGuard Home web service. A nil *Service is a valid
|
// Service is the AdGuard Home web service. A nil *Service is a valid
|
||||||
// [agh.Service] that does nothing.
|
// [agh.Service] that does nothing.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
logger *slog.Logger
|
||||||
confMgr ConfigManager
|
confMgr ConfigManager
|
||||||
frontend fs.FS
|
frontend fs.FS
|
||||||
tls *tls.Config
|
tls *tls.Config
|
||||||
pprof *http.Server
|
pprof *server
|
||||||
start time.Time
|
start time.Time
|
||||||
overrideAddr netip.AddrPort
|
overrideAddr netip.AddrPort
|
||||||
servers []*http.Server
|
servers []*server
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
pprofPort uint16
|
pprofPort uint16
|
||||||
forceHTTPS bool
|
forceHTTPS bool
|
||||||
|
@ -64,6 +61,7 @@ func New(c *Config) (svc *Service, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
svc = &Service{
|
svc = &Service{
|
||||||
|
logger: c.Logger,
|
||||||
confMgr: c.ConfigManager,
|
confMgr: c.ConfigManager,
|
||||||
frontend: c.Frontend,
|
frontend: c.Frontend,
|
||||||
tls: c.TLS,
|
tls: c.TLS,
|
||||||
|
@ -73,17 +71,18 @@ func New(c *Config) (svc *Service, err error) {
|
||||||
forceHTTPS: c.ForceHTTPS,
|
forceHTTPS: c.ForceHTTPS,
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := newMux(svc)
|
mux := http.NewServeMux()
|
||||||
|
svc.route(mux)
|
||||||
|
|
||||||
if svc.overrideAddr != (netip.AddrPort{}) {
|
if svc.overrideAddr != (netip.AddrPort{}) {
|
||||||
svc.servers = []*http.Server{newSrv(svc.overrideAddr, nil, mux, c.Timeout)}
|
svc.servers = []*server{newServer(svc.logger, svc.overrideAddr, nil, mux, c.Timeout)}
|
||||||
} else {
|
} else {
|
||||||
for _, a := range c.Addresses {
|
for _, a := range c.Addresses {
|
||||||
svc.servers = append(svc.servers, newSrv(a, nil, mux, c.Timeout))
|
svc.servers = append(svc.servers, newServer(svc.logger, a, nil, mux, c.Timeout))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range c.SecureAddresses {
|
for _, a := range c.SecureAddresses {
|
||||||
svc.servers = append(svc.servers, newSrv(a, c.TLS, mux, c.Timeout))
|
svc.servers = append(svc.servers, newServer(svc.logger, a, c.TLS, mux, c.Timeout))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,96 +111,7 @@ func (svc *Service) setupPprof(c *PprofConfig) {
|
||||||
svc.pprofPort = c.Port
|
svc.pprofPort = c.Port
|
||||||
addr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), c.Port)
|
addr := netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), c.Port)
|
||||||
|
|
||||||
// TODO(a.garipov): Consider making pprof timeout configurable.
|
svc.pprof = newServer(svc.logger, addr, nil, pprofMux, 10*time.Minute)
|
||||||
svc.pprof = newSrv(addr, nil, pprofMux, 10*time.Minute)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newSrv returns a new *http.Server with the given parameters.
|
|
||||||
func newSrv(
|
|
||||||
addr netip.AddrPort,
|
|
||||||
tlsConf *tls.Config,
|
|
||||||
h http.Handler,
|
|
||||||
timeout time.Duration,
|
|
||||||
) (srv *http.Server) {
|
|
||||||
addrStr := addr.String()
|
|
||||||
srv = &http.Server{
|
|
||||||
Addr: addrStr,
|
|
||||||
Handler: h,
|
|
||||||
TLSConfig: tlsConf,
|
|
||||||
ReadTimeout: timeout,
|
|
||||||
WriteTimeout: timeout,
|
|
||||||
IdleTimeout: timeout,
|
|
||||||
ReadHeaderTimeout: timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
if tlsConf == nil {
|
|
||||||
srv.ErrorLog = log.StdLog("websvc: plain http: "+addrStr, log.ERROR)
|
|
||||||
} else {
|
|
||||||
srv.ErrorLog = log.StdLog("websvc: https: "+addrStr, log.ERROR)
|
|
||||||
}
|
|
||||||
|
|
||||||
return srv
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMux returns a new HTTP request multiplexer for the AdGuard Home web
|
|
||||||
// service.
|
|
||||||
func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
|
||||||
mux = httptreemux.NewContextMux()
|
|
||||||
|
|
||||||
routes := []struct {
|
|
||||||
handler http.HandlerFunc
|
|
||||||
method string
|
|
||||||
pattern string
|
|
||||||
isJSON bool
|
|
||||||
}{{
|
|
||||||
handler: svc.handleGetHealthCheck,
|
|
||||||
method: http.MethodGet,
|
|
||||||
pattern: PathHealthCheck,
|
|
||||||
isJSON: false,
|
|
||||||
}, {
|
|
||||||
handler: http.FileServer(http.FS(svc.frontend)).ServeHTTP,
|
|
||||||
method: http.MethodGet,
|
|
||||||
pattern: PathFrontend,
|
|
||||||
isJSON: false,
|
|
||||||
}, {
|
|
||||||
handler: http.FileServer(http.FS(svc.frontend)).ServeHTTP,
|
|
||||||
method: http.MethodGet,
|
|
||||||
pattern: PathRoot,
|
|
||||||
isJSON: false,
|
|
||||||
}, {
|
|
||||||
handler: svc.handleGetSettingsAll,
|
|
||||||
method: http.MethodGet,
|
|
||||||
pattern: PathV1SettingsAll,
|
|
||||||
isJSON: true,
|
|
||||||
}, {
|
|
||||||
handler: svc.handlePatchSettingsDNS,
|
|
||||||
method: http.MethodPatch,
|
|
||||||
pattern: PathV1SettingsDNS,
|
|
||||||
isJSON: true,
|
|
||||||
}, {
|
|
||||||
handler: svc.handlePatchSettingsHTTP,
|
|
||||||
method: http.MethodPatch,
|
|
||||||
pattern: PathV1SettingsHTTP,
|
|
||||||
isJSON: true,
|
|
||||||
}, {
|
|
||||||
handler: svc.handleGetV1SystemInfo,
|
|
||||||
method: http.MethodGet,
|
|
||||||
pattern: PathV1SystemInfo,
|
|
||||||
isJSON: true,
|
|
||||||
}}
|
|
||||||
|
|
||||||
for _, r := range routes {
|
|
||||||
var hdlr http.Handler
|
|
||||||
if r.isJSON {
|
|
||||||
hdlr = jsonMw(r.handler)
|
|
||||||
} else {
|
|
||||||
hdlr = r.handler
|
|
||||||
}
|
|
||||||
|
|
||||||
mux.Handle(r.method, r.pattern, logMw(hdlr))
|
|
||||||
}
|
|
||||||
|
|
||||||
return mux
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// addrs returns all addresses on which this server serves the HTTP API. addrs
|
// addrs returns all addresses on which this server serves the HTTP API. addrs
|
||||||
|
@ -214,14 +124,12 @@ func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, srv := range svc.servers {
|
for _, srv := range svc.servers {
|
||||||
// Use MustParseAddrPort, since no errors should technically happen
|
addrPort := netutil.NetAddrToAddrPort(srv.localAddr())
|
||||||
// here, because all servers must have a valid address.
|
if addrPort == (netip.AddrPort{}) {
|
||||||
addrPort := netip.MustParseAddrPort(srv.Addr)
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// [srv.Serve] will set TLSConfig to an almost empty value, so, instead
|
if srv.tlsConf == nil {
|
||||||
// of relying only on the nilness of TLSConfig, check the length of the
|
|
||||||
// certificates field as well.
|
|
||||||
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
|
|
||||||
addrs = append(addrs, addrPort)
|
addrs = append(addrs, addrPort)
|
||||||
} else {
|
} else {
|
||||||
secureAddrs = append(secureAddrs, addrPort)
|
secureAddrs = append(secureAddrs, addrPort)
|
||||||
|
@ -231,74 +139,60 @@ func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
|
||||||
return addrs, secureAddrs
|
return addrs, secureAddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
|
|
||||||
func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
_, _ = io.WriteString(w, "OK")
|
|
||||||
}
|
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ agh.Service = (*Service)(nil)
|
var _ agh.ServiceWithConfig[*Config] = (*Service)(nil)
|
||||||
|
|
||||||
// Start implements the [agh.Service] interface for *Service. svc may be nil.
|
// Start implements the [agh.Service] interface for *Service. svc may be nil.
|
||||||
// After Start exits, all HTTP servers have tried to start, possibly failing and
|
// After Start exits, all HTTP servers have tried to start, possibly failing and
|
||||||
// writing error messages to the log.
|
// writing error messages to the log.
|
||||||
func (svc *Service) Start() (err error) {
|
//
|
||||||
|
// TODO(a.garipov): Use the context for cancelation as well.
|
||||||
|
func (svc *Service) Start(ctx context.Context) (err error) {
|
||||||
if svc == nil {
|
if svc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pprofEnabled := svc.pprof != nil
|
svc.logger.InfoContext(ctx, "starting")
|
||||||
srvNum := len(svc.servers) + mathutil.BoolToNumber[int](pprofEnabled)
|
defer svc.logger.InfoContext(ctx, "started")
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
wg.Add(srvNum)
|
|
||||||
for _, srv := range svc.servers {
|
for _, srv := range svc.servers {
|
||||||
go serve(srv, wg)
|
go srv.serve(ctx, svc.logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pprofEnabled {
|
if svc.pprof != nil {
|
||||||
go serve(svc.pprof, wg)
|
go svc.pprof.serve(ctx, svc.logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
return svc.wait(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait waits until either the context is canceled or all servers have started.
|
||||||
|
func (svc *Service) wait(ctx context.Context) (err error) {
|
||||||
|
for !svc.serversHaveStarted() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
// Wait and let the other goroutines do their job.
|
||||||
|
runtime.Gosched()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve starts and runs srv and writes all errors into its log.
|
// serversHaveStarted returns true if all servers have started serving.
|
||||||
func serve(srv *http.Server, wg *sync.WaitGroup) {
|
func (svc *Service) serversHaveStarted() (started bool) {
|
||||||
addr := srv.Addr
|
started = len(svc.servers) != 0
|
||||||
defer log.OnPanic(addr)
|
for _, srv := range svc.servers {
|
||||||
|
started = started && srv.localAddr() != nil
|
||||||
var proto string
|
|
||||||
var l net.Listener
|
|
||||||
var err error
|
|
||||||
if srv.TLSConfig == nil {
|
|
||||||
proto = "http"
|
|
||||||
l, err = net.Listen("tcp", addr)
|
|
||||||
} else {
|
|
||||||
proto = "https"
|
|
||||||
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
srv.ErrorLog.Printf("starting srv %s: binding: %s", addr, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the server's address in case the address had the port zero, which
|
if svc.pprof != nil {
|
||||||
// would mean that a random available port was automatically chosen.
|
started = started && svc.pprof.localAddr() != nil
|
||||||
srv.Addr = l.Addr().String()
|
|
||||||
|
|
||||||
log.Info("websvc: starting srv %s://%s", proto, srv.Addr)
|
|
||||||
|
|
||||||
l = &waitListener{
|
|
||||||
Listener: l,
|
|
||||||
firstAcceptWG: wg,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = srv.Serve(l)
|
return started
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
srv.ErrorLog.Printf("starting srv %s: %s", addr, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown implements the [agh.Service] interface for *Service. svc may be
|
// Shutdown implements the [agh.Service] interface for *Service. svc may be
|
||||||
|
@ -308,20 +202,24 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svc.logger.InfoContext(ctx, "shutting down")
|
||||||
|
defer svc.logger.InfoContext(ctx, "shut down")
|
||||||
|
|
||||||
defer func() { err = errors.Annotate(err, "shutting down: %w") }()
|
defer func() { err = errors.Annotate(err, "shutting down: %w") }()
|
||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
for _, srv := range svc.servers {
|
for _, srv := range svc.servers {
|
||||||
shutdownErr := srv.Shutdown(ctx)
|
shutdownErr := srv.shutdown(ctx)
|
||||||
if shutdownErr != nil {
|
if shutdownErr != nil {
|
||||||
errs = append(errs, fmt.Errorf("srv %s: %w", srv.Addr, shutdownErr))
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if svc.pprof != nil {
|
if svc.pprof != nil {
|
||||||
shutdownErr := svc.pprof.Shutdown(ctx)
|
shutdownErr := svc.pprof.shutdown(ctx)
|
||||||
if shutdownErr != nil {
|
if shutdownErr != nil {
|
||||||
errs = append(errs, fmt.Errorf("pprof srv %s: %w", svc.pprof.Addr, shutdownErr))
|
errs = append(errs, fmt.Errorf("pprof: %w", shutdownErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,16 +15,15 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/httputil"
|
||||||
|
"github.com/AdguardTeam/golibs/netutil/urlutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil/fakefs"
|
"github.com/AdguardTeam/golibs/testutil/fakefs"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
testutil.DiscardLogOutput(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testTimeout is the common timeout for tests.
|
// testTimeout is the common timeout for tests.
|
||||||
const testTimeout = 1 * time.Second
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
|
@ -80,8 +79,6 @@ func newConfigManager() (m *configManager) {
|
||||||
// newTestServer creates and starts a new web service instance as well as its
|
// newTestServer creates and starts a new web service instance as well as its
|
||||||
// sole address. It also registers a cleanup procedure, which shuts the
|
// sole address. It also registers a cleanup procedure, which shuts the
|
||||||
// instance down.
|
// instance down.
|
||||||
//
|
|
||||||
// TODO(a.garipov): Use svc or remove it.
|
|
||||||
func newTestServer(
|
func newTestServer(
|
||||||
t testing.TB,
|
t testing.TB,
|
||||||
confMgr websvc.ConfigManager,
|
confMgr websvc.ConfigManager,
|
||||||
|
@ -89,6 +86,7 @@ func newTestServer(
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
c := &websvc.Config{
|
c := &websvc.Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
Pprof: &websvc.PprofConfig{
|
Pprof: &websvc.PprofConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
},
|
},
|
||||||
|
@ -107,7 +105,7 @@ func newTestServer(
|
||||||
svc, err := websvc.New(c)
|
svc, err := websvc.New(c)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = svc.Start()
|
err = svc.Start(testutil.ContextWithTimeout(t, testTimeout))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testutil.CleanupAndRequireSuccess(t, func() (err error) {
|
testutil.CleanupAndRequireSuccess(t, func() (err error) {
|
||||||
return svc.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
|
return svc.Shutdown(testutil.ContextWithTimeout(t, testTimeout))
|
||||||
|
@ -181,12 +179,12 @@ func TestService_Start_getHealthCheck(t *testing.T) {
|
||||||
confMgr := newConfigManager()
|
confMgr := newConfigManager()
|
||||||
_, addr := newTestServer(t, confMgr)
|
_, addr := newTestServer(t, confMgr)
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: urlutil.SchemeHTTP,
|
||||||
Host: addr.String(),
|
Host: addr.String(),
|
||||||
Path: websvc.PathHealthCheck,
|
Path: websvc.PathPatternHealthCheck,
|
||||||
}
|
}
|
||||||
|
|
||||||
body := httpGet(t, u, http.StatusOK)
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
|
||||||
assert.Equal(t, []byte("OK"), body)
|
assert.Equal(t, []byte(httputil.HealthCheckHandler), body)
|
||||||
}
|
}
|
||||||
|
|
43
internal/permcheck/check_unix.go
Normal file
43
internal/permcheck/check_unix.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package permcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
)
|
||||||
|
|
||||||
|
// check is the Unix-specific implementation of [Check].
|
||||||
|
func check(
|
||||||
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
|
workDir string,
|
||||||
|
dataDir string,
|
||||||
|
statsDir string,
|
||||||
|
querylogDir string,
|
||||||
|
confFilePath string,
|
||||||
|
) {
|
||||||
|
dirLoggger, fileLogger := l.With("type", typeDir), l.With("type", typeFile)
|
||||||
|
|
||||||
|
for _, ent := range entities(workDir, dataDir, statsDir, querylogDir, confFilePath) {
|
||||||
|
if ent.Value {
|
||||||
|
checkDir(ctx, dirLoggger, ent.Key)
|
||||||
|
} else {
|
||||||
|
checkFile(ctx, fileLogger, ent.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDir checks the permissions of a single directory. The results are
|
||||||
|
// logged at the appropriate level.
|
||||||
|
func checkDir(ctx context.Context, l *slog.Logger, dirPath string) {
|
||||||
|
checkPath(ctx, l, dirPath, aghos.DefaultPermDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFile checks the permissions of a single file. The results are logged at
|
||||||
|
// the appropriate level.
|
||||||
|
func checkFile(ctx context.Context, l *slog.Logger, filePath string) {
|
||||||
|
checkPath(ctx, l, filePath, aghos.DefaultPermFile)
|
||||||
|
}
|
60
internal/permcheck/check_windows.go
Normal file
60
internal/permcheck/check_windows.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package permcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// check is the Windows-specific implementation of [Check].
|
||||||
|
//
|
||||||
|
// Note, that it only checks the owner and the ACEs of the working directory.
|
||||||
|
// This is due to the assumption that the working directory ACEs are inherited
|
||||||
|
// by the underlying files and directories, since at least [migrate] sets this
|
||||||
|
// inheritance mode.
|
||||||
|
func check(ctx context.Context, l *slog.Logger, workDir, _, _, _, _ string) {
|
||||||
|
l = l.With("type", typeDir, "path", workDir)
|
||||||
|
|
||||||
|
dacl, owner, err := getSecurityInfo(workDir)
|
||||||
|
if err != nil {
|
||||||
|
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
|
||||||
|
l.WarnContext(ctx, "owner is not in administrators group")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rangeACEs(dacl, func(
|
||||||
|
hdr windows.ACE_HEADER,
|
||||||
|
mask windows.ACCESS_MASK,
|
||||||
|
sid *windows.SID,
|
||||||
|
) (cont bool) {
|
||||||
|
l.DebugContext(ctx, "checking access control entry", "mask", mask, "sid", sid)
|
||||||
|
|
||||||
|
warn := false
|
||||||
|
switch {
|
||||||
|
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
|
||||||
|
// Skip non-allowed ACEs.
|
||||||
|
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
|
||||||
|
// Non-administrator ACEs should not have any access rights.
|
||||||
|
warn = mask > 0
|
||||||
|
default:
|
||||||
|
// Administrators should full control access rights.
|
||||||
|
warn = mask&fullControlMask != fullControlMask
|
||||||
|
}
|
||||||
|
if warn {
|
||||||
|
l.WarnContext(ctx, "unexpected access control entry", "mask", mask, "sid", sid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
l.ErrorContext(ctx, "checking access control entries", slogutil.KeyError, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,93 +0,0 @@
|
||||||
package permcheck
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NeedsMigration returns true if AdGuard Home files need permission migration.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Consider ways to detect this better.
|
|
||||||
func NeedsMigration(confFilePath string) (ok bool) {
|
|
||||||
s, err := aghos.Stat(confFilePath)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
// Likely a first run. Don't check.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("permcheck: checking if files need migration: %s", err)
|
|
||||||
|
|
||||||
// Unexpected error. Try to migrate just in case.
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.Mode().Perm() != aghos.DefaultPermFile
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate attempts to change the permissions of AdGuard Home's files. It logs
|
|
||||||
// the results at an appropriate level.
|
|
||||||
func Migrate(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
|
|
||||||
chmodDir(workDir)
|
|
||||||
|
|
||||||
chmodFile(confFilePath)
|
|
||||||
|
|
||||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
|
||||||
chmodDir(dataDir)
|
|
||||||
chmodDir(filepath.Join(dataDir, "filters"))
|
|
||||||
chmodFile(filepath.Join(dataDir, "sessions.db"))
|
|
||||||
chmodFile(filepath.Join(dataDir, "leases.json"))
|
|
||||||
|
|
||||||
if dataDir != querylogDir {
|
|
||||||
chmodDir(querylogDir)
|
|
||||||
}
|
|
||||||
chmodFile(filepath.Join(querylogDir, "querylog.json"))
|
|
||||||
chmodFile(filepath.Join(querylogDir, "querylog.json.1"))
|
|
||||||
|
|
||||||
if dataDir != statsDir {
|
|
||||||
chmodDir(statsDir)
|
|
||||||
}
|
|
||||||
chmodFile(filepath.Join(statsDir, "stats.db"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// chmodDir changes the permissions of a single directory. The results are
|
|
||||||
// logged at the appropriate level.
|
|
||||||
func chmodDir(dirPath string) {
|
|
||||||
chmodPath(dirPath, typeDir, aghos.DefaultPermDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// chmodFile changes the permissions of a single file. The results are logged
|
|
||||||
// at the appropriate level.
|
|
||||||
func chmodFile(filePath string) {
|
|
||||||
chmodPath(filePath, typeFile, aghos.DefaultPermFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// chmodPath changes the permissions of a single filesystem entity. The results
|
|
||||||
// are logged at the appropriate level.
|
|
||||||
func chmodPath(entPath, fileType string, fm fs.FileMode) {
|
|
||||||
err := aghos.Chmod(entPath, fm)
|
|
||||||
if err == nil {
|
|
||||||
log.Info("permcheck: changed permissions for %s %q", fileType, entPath)
|
|
||||||
|
|
||||||
return
|
|
||||||
} else if errors.Is(err, os.ErrNotExist) {
|
|
||||||
log.Debug("permcheck: changing permissions for %s %q: %s", fileType, entPath, err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error(
|
|
||||||
"permcheck: SECURITY WARNING: cannot change permissions for %s %q to %#o: %s; "+
|
|
||||||
"this can leave your system vulnerable, see "+
|
|
||||||
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns",
|
|
||||||
fileType,
|
|
||||||
entPath,
|
|
||||||
fm,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
66
internal/permcheck/migrate_unix.go
Normal file
66
internal/permcheck/migrate_unix.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package permcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// needsMigration is a Unix-specific implementation of [NeedsMigration].
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider ways to detect this better.
|
||||||
|
func needsMigration(ctx context.Context, l *slog.Logger, _, confFilePath string) (ok bool) {
|
||||||
|
s, err := os.Stat(confFilePath)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// Likely a first run. Don't check.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
l.ErrorContext(ctx, "checking a need for permission migration", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
// Unexpected error. Try to migrate just in case.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Mode().Perm() != aghos.DefaultPermFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate is a Unix-specific implementation of [Migrate].
|
||||||
|
func migrate(
|
||||||
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
|
workDir string,
|
||||||
|
dataDir string,
|
||||||
|
statsDir string,
|
||||||
|
querylogDir string,
|
||||||
|
confFilePath string,
|
||||||
|
) {
|
||||||
|
dirLoggger, fileLogger := l.With("type", typeDir), l.With("type", typeFile)
|
||||||
|
|
||||||
|
for _, ent := range entities(workDir, dataDir, statsDir, querylogDir, confFilePath) {
|
||||||
|
if ent.Value {
|
||||||
|
chmodDir(ctx, dirLoggger, ent.Key)
|
||||||
|
} else {
|
||||||
|
chmodFile(ctx, fileLogger, ent.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// chmodDir changes the permissions of a single directory. The results are
|
||||||
|
// logged at the appropriate level.
|
||||||
|
func chmodDir(ctx context.Context, l *slog.Logger, dirPath string) {
|
||||||
|
chmodPath(ctx, l, dirPath, aghos.DefaultPermDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// chmodFile changes the permissions of a single file. The results are logged
|
||||||
|
// at the appropriate level.
|
||||||
|
func chmodFile(ctx context.Context, l *slog.Logger, filePath string) {
|
||||||
|
chmodPath(ctx, l, filePath, aghos.DefaultPermFile)
|
||||||
|
}
|
135
internal/permcheck/migrate_windows.go
Normal file
135
internal/permcheck/migrate_windows.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package permcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// needsMigration is the Windows-specific implementation of [NeedsMigration].
|
||||||
|
func needsMigration(ctx context.Context, l *slog.Logger, workDir, _ string) (ok bool) {
|
||||||
|
l = l.With("type", typeDir, "path", workDir)
|
||||||
|
|
||||||
|
dacl, owner, err := getSecurityInfo(workDir)
|
||||||
|
if err != nil {
|
||||||
|
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rangeACEs(dacl, func(
|
||||||
|
hdr windows.ACE_HEADER,
|
||||||
|
mask windows.ACCESS_MASK,
|
||||||
|
sid *windows.SID,
|
||||||
|
) (cont bool) {
|
||||||
|
switch {
|
||||||
|
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
|
||||||
|
// Skip non-allowed access control entries.
|
||||||
|
l.DebugContext(ctx, "skipping deny access control entry", "sid", sid)
|
||||||
|
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
|
||||||
|
// Non-administrator access control entries should not have any
|
||||||
|
// access rights.
|
||||||
|
ok = mask > 0
|
||||||
|
default:
|
||||||
|
// Administrators should have full control.
|
||||||
|
ok = mask&fullControlMask != fullControlMask
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop ranging if the access control entry is unexpected.
|
||||||
|
return !ok
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
l.ErrorContext(ctx, "checking access control entries", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// migrate is the Windows-specific implementation of [Migrate].
|
||||||
|
//
|
||||||
|
// It sets the owner to administrators and adds a full control access control
|
||||||
|
// entry for the account. It also removes all non-administrator access control
|
||||||
|
// entries, and keeps deny access control entries. For any created or modified
|
||||||
|
// entry it sets the propagation flags to be inherited by child objects.
|
||||||
|
func migrate(ctx context.Context, logger *slog.Logger, workDir, _, _, _, _ string) {
|
||||||
|
l := logger.With("type", typeDir, "path", workDir)
|
||||||
|
|
||||||
|
dacl, owner, err := getSecurityInfo(workDir)
|
||||||
|
if err != nil {
|
||||||
|
l.ErrorContext(ctx, "getting security info", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
admins, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
|
||||||
|
if err != nil {
|
||||||
|
l.ErrorContext(ctx, "creating administrators sid", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(e.burkov): Check for duplicates?
|
||||||
|
var accessEntries []windows.EXPLICIT_ACCESS
|
||||||
|
var setACL bool
|
||||||
|
// Iterate over the access control entries in DACL to determine if its
|
||||||
|
// migration is needed.
|
||||||
|
err = rangeACEs(dacl, func(
|
||||||
|
hdr windows.ACE_HEADER,
|
||||||
|
mask windows.ACCESS_MASK,
|
||||||
|
sid *windows.SID,
|
||||||
|
) (cont bool) {
|
||||||
|
switch {
|
||||||
|
case hdr.AceType != windows.ACCESS_ALLOWED_ACE_TYPE:
|
||||||
|
// Add non-allowed access control entries as is, since they specify
|
||||||
|
// the access restrictions, which shouldn't be lost.
|
||||||
|
l.InfoContext(ctx, "migrating deny access control entry", "sid", sid)
|
||||||
|
accessEntries = append(accessEntries, newDenyExplicitAccess(sid, mask))
|
||||||
|
setACL = true
|
||||||
|
case !sid.IsWellKnown(windows.WinBuiltinAdministratorsSid):
|
||||||
|
// Remove non-administrator ACEs, since such accounts should not
|
||||||
|
// have any access rights.
|
||||||
|
l.InfoContext(ctx, "removing access control entry", "sid", sid)
|
||||||
|
setACL = true
|
||||||
|
default:
|
||||||
|
// Administrators should have full control. Don't add a new entry
|
||||||
|
// here since it will be added later in case there are other
|
||||||
|
// required entries.
|
||||||
|
l.InfoContext(ctx, "migrating access control entry", "sid", sid, "mask", mask)
|
||||||
|
setACL = setACL || mask&fullControlMask != fullControlMask
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
l.ErrorContext(ctx, "ranging through access control entries", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if setACL {
|
||||||
|
accessEntries = append(accessEntries, newFullExplicitAccess(admins))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) {
|
||||||
|
l.InfoContext(ctx, "migrating owner", "sid", owner)
|
||||||
|
owner = admins
|
||||||
|
} else {
|
||||||
|
l.DebugContext(ctx, "owner is already an administrator")
|
||||||
|
owner = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = setSecurityInfo(workDir, owner, accessEntries)
|
||||||
|
if err != nil {
|
||||||
|
l.ErrorContext(ctx, "setting security info", slogutil.KeyError, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,10 @@
|
||||||
// Package permcheck contains code for simplifying permissions checks on files
|
// Package permcheck contains code for simplifying permissions checks on files
|
||||||
// and directories.
|
// and directories.
|
||||||
//
|
|
||||||
// TODO(a.garipov): Improve the approach on Windows.
|
|
||||||
package permcheck
|
package permcheck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/fs"
|
"context"
|
||||||
"os"
|
"log/slog"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// File type constants for logging.
|
// File type constants for logging.
|
||||||
|
@ -22,65 +15,33 @@ const (
|
||||||
|
|
||||||
// Check checks the permissions on important files. It logs the results at
|
// Check checks the permissions on important files. It logs the results at
|
||||||
// appropriate levels.
|
// appropriate levels.
|
||||||
func Check(workDir, dataDir, statsDir, querylogDir, confFilePath string) {
|
func Check(
|
||||||
checkDir(workDir)
|
ctx context.Context,
|
||||||
|
l *slog.Logger,
|
||||||
checkFile(confFilePath)
|
workDir string,
|
||||||
|
dataDir string,
|
||||||
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
statsDir string,
|
||||||
checkDir(dataDir)
|
querylogDir string,
|
||||||
checkDir(filepath.Join(dataDir, "filters"))
|
confFilePath string,
|
||||||
checkFile(filepath.Join(dataDir, "sessions.db"))
|
) {
|
||||||
checkFile(filepath.Join(dataDir, "leases.json"))
|
check(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
|
||||||
|
|
||||||
if dataDir != querylogDir {
|
|
||||||
checkDir(querylogDir)
|
|
||||||
}
|
|
||||||
checkFile(filepath.Join(querylogDir, "querylog.json"))
|
|
||||||
checkFile(filepath.Join(querylogDir, "querylog.json.1"))
|
|
||||||
|
|
||||||
if dataDir != statsDir {
|
|
||||||
checkDir(statsDir)
|
|
||||||
}
|
|
||||||
checkFile(filepath.Join(statsDir, "stats.db"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDir checks the permissions of a single directory. The results are
|
// NeedsMigration returns true if AdGuard Home files need permission migration.
|
||||||
// logged at the appropriate level.
|
func NeedsMigration(ctx context.Context, l *slog.Logger, workDir, confFilePath string) (ok bool) {
|
||||||
func checkDir(dirPath string) {
|
return needsMigration(ctx, l, workDir, confFilePath)
|
||||||
checkPath(dirPath, typeDir, aghos.DefaultPermDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkFile checks the permissions of a single file. The results are logged at
|
// Migrate attempts to change the permissions of AdGuard Home's files. It logs
|
||||||
// the appropriate level.
|
// the results at an appropriate level.
|
||||||
func checkFile(filePath string) {
|
func Migrate(
|
||||||
checkPath(filePath, typeFile, aghos.DefaultPermFile)
|
ctx context.Context,
|
||||||
}
|
l *slog.Logger,
|
||||||
|
workDir string,
|
||||||
// checkPath checks the permissions of a single filesystem entity. The results
|
dataDir string,
|
||||||
// are logged at the appropriate level.
|
statsDir string,
|
||||||
func checkPath(entPath, fileType string, want fs.FileMode) {
|
querylogDir string,
|
||||||
s, err := aghos.Stat(entPath)
|
confFilePath string,
|
||||||
if err != nil {
|
) {
|
||||||
logFunc := log.Error
|
migrate(ctx, l, workDir, dataDir, statsDir, querylogDir, confFilePath)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
logFunc = log.Debug
|
|
||||||
}
|
|
||||||
|
|
||||||
logFunc("permcheck: checking %s %q: %s", fileType, entPath, err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(a.garipov): Add a more fine-grained check and result reporting.
|
|
||||||
perm := s.Mode().Perm()
|
|
||||||
if perm != want {
|
|
||||||
log.Info(
|
|
||||||
"permcheck: SECURITY WARNING: %s %q has unexpected permissions %#o; want %#o",
|
|
||||||
fileType,
|
|
||||||
entPath,
|
|
||||||
perm,
|
|
||||||
want,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
123
internal/permcheck/security_unix.go
Normal file
123
internal/permcheck/security_unix.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package permcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/container"
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// entity is a filesystem entity with a path and a flag indicating whether it is
|
||||||
|
// a directory.
|
||||||
|
type entity = container.KeyValue[string, bool]
|
||||||
|
|
||||||
|
// entities returns a list of filesystem entities that need to be ranged over.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Put all paths in one place and remove this duplication.
|
||||||
|
func entities(workDir, dataDir, statsDir, querylogDir, confFilePath string) (ents []entity) {
|
||||||
|
ents = []entity{{
|
||||||
|
Key: workDir,
|
||||||
|
Value: true,
|
||||||
|
}, {
|
||||||
|
Key: confFilePath,
|
||||||
|
Value: false,
|
||||||
|
}, {
|
||||||
|
Key: dataDir,
|
||||||
|
Value: true,
|
||||||
|
}, {
|
||||||
|
Key: filepath.Join(dataDir, "filters"),
|
||||||
|
Value: true,
|
||||||
|
}, {
|
||||||
|
Key: filepath.Join(dataDir, "sessions.db"),
|
||||||
|
Value: false,
|
||||||
|
}, {
|
||||||
|
Key: filepath.Join(dataDir, "leases.json"),
|
||||||
|
Value: false,
|
||||||
|
}}
|
||||||
|
|
||||||
|
if dataDir != querylogDir {
|
||||||
|
ents = append(ents, entity{
|
||||||
|
Key: querylogDir,
|
||||||
|
Value: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ents = append(ents, entity{
|
||||||
|
Key: filepath.Join(querylogDir, "querylog.json"),
|
||||||
|
Value: false,
|
||||||
|
}, entity{
|
||||||
|
Key: filepath.Join(querylogDir, "querylog.json.1"),
|
||||||
|
Value: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if dataDir != statsDir {
|
||||||
|
ents = append(ents, entity{
|
||||||
|
Key: statsDir,
|
||||||
|
Value: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ents = append(ents, entity{
|
||||||
|
Key: filepath.Join(statsDir, "stats.db"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return ents
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPath checks the permissions of a single filesystem entity. The results
|
||||||
|
// are logged at the appropriate level.
|
||||||
|
func checkPath(ctx context.Context, l *slog.Logger, entPath string, want fs.FileMode) {
|
||||||
|
l = l.With("path", entPath)
|
||||||
|
|
||||||
|
s, err := os.Stat(entPath)
|
||||||
|
if err != nil {
|
||||||
|
lvl := slog.LevelError
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
lvl = slog.LevelDebug
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Log(ctx, lvl, "checking permissions", slogutil.KeyError, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Add a more fine-grained check and result reporting.
|
||||||
|
perm := s.Mode().Perm()
|
||||||
|
if perm == want {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
permOct, wantOct := fmt.Sprintf("%#o", perm), fmt.Sprintf("%#o", want)
|
||||||
|
l.WarnContext(ctx, "found unexpected permissions", "perm", permOct, "want", wantOct)
|
||||||
|
}
|
||||||
|
|
||||||
|
// chmodPath changes the permissions of a single filesystem entity. The results
|
||||||
|
// are logged at the appropriate level.
|
||||||
|
func chmodPath(ctx context.Context, l *slog.Logger, entPath string, fm fs.FileMode) {
|
||||||
|
var lvl slog.Level
|
||||||
|
var msg string
|
||||||
|
args := []any{"path", entPath}
|
||||||
|
|
||||||
|
switch err := os.Chmod(entPath, fm); {
|
||||||
|
case err == nil:
|
||||||
|
lvl = slog.LevelInfo
|
||||||
|
msg = "changed permissions"
|
||||||
|
case errors.Is(err, os.ErrNotExist):
|
||||||
|
lvl = slog.LevelDebug
|
||||||
|
msg = "checking permissions"
|
||||||
|
args = append(args, slogutil.KeyError, err)
|
||||||
|
default:
|
||||||
|
lvl = slog.LevelError
|
||||||
|
msg = "cannot change permissions; this can leave your system vulnerable, see " +
|
||||||
|
"https://adguard-dns.io/kb/adguard-home/running-securely/#os-service-concerns"
|
||||||
|
args = append(args, "target_perm", fmt.Sprintf("%#o", fm), slogutil.KeyError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Log(ctx, lvl, msg, args...)
|
||||||
|
}
|
167
internal/permcheck/security_windows.go
Normal file
167
internal/permcheck/security_windows.go
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package permcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// objectType is the type of the object for directories in context of security
|
||||||
|
// API.
|
||||||
|
const objectType windows.SE_OBJECT_TYPE = windows.SE_FILE_OBJECT
|
||||||
|
|
||||||
|
// fileDeleteChildRight is the mask bit for the right to delete a child object.
|
||||||
|
// It seems to be missing from the [windows] package.
|
||||||
|
//
|
||||||
|
// See https://learn.microsoft.com/en-us/windows-hardware/drivers/ifs/access-mask.
|
||||||
|
const fileDeleteChildRight windows.ACCESS_MASK = 0b0100_0000
|
||||||
|
|
||||||
|
// fullControlMask is the mask for full control access rights.
|
||||||
|
const fullControlMask windows.ACCESS_MASK = windows.FILE_LIST_DIRECTORY |
|
||||||
|
windows.FILE_WRITE_DATA |
|
||||||
|
windows.FILE_APPEND_DATA |
|
||||||
|
windows.FILE_READ_EA |
|
||||||
|
windows.FILE_WRITE_EA |
|
||||||
|
windows.FILE_TRAVERSE |
|
||||||
|
fileDeleteChildRight |
|
||||||
|
windows.FILE_READ_ATTRIBUTES |
|
||||||
|
windows.FILE_WRITE_ATTRIBUTES |
|
||||||
|
windows.DELETE |
|
||||||
|
windows.READ_CONTROL |
|
||||||
|
windows.WRITE_DAC |
|
||||||
|
windows.WRITE_OWNER |
|
||||||
|
windows.SYNCHRONIZE
|
||||||
|
|
||||||
|
// aceFunc is a function that handles access control entries in the
|
||||||
|
// discretionary access control list. It should return true to continue
|
||||||
|
// iterating over the entries, or false to stop.
|
||||||
|
type aceFunc = func(
|
||||||
|
hdr windows.ACE_HEADER,
|
||||||
|
mask windows.ACCESS_MASK,
|
||||||
|
sid *windows.SID,
|
||||||
|
) (cont bool)
|
||||||
|
|
||||||
|
// rangeACEs ranges over the access control entries in the discretionary access
|
||||||
|
// control list of the specified security descriptor and calls f for each one.
|
||||||
|
func rangeACEs(dacl *windows.ACL, f aceFunc) (err error) {
|
||||||
|
var errs []error
|
||||||
|
for i := range uint32(dacl.AceCount) {
|
||||||
|
var ace *windows.ACCESS_ALLOWED_ACE
|
||||||
|
err = windows.GetAce(dacl, i, &ace)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("getting entry at index %d: %w", i, err))
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sid := (*windows.SID)(unsafe.Pointer(&ace.SidStart))
|
||||||
|
if !f(ace.Header, ace.Mask, sid) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = errors.Join(errs...); err != nil {
|
||||||
|
return fmt.Errorf("checking access control entries: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSecurityInfo sets the security information on the specified file, using
|
||||||
|
// ents to create a discretionary access control list. Either owner or ents can
|
||||||
|
// be nil, in which case the corresponding information is not set, but at least
|
||||||
|
// one of them should be specified.
|
||||||
|
func setSecurityInfo(fname string, owner *windows.SID, ents []windows.EXPLICIT_ACCESS) (err error) {
|
||||||
|
var secInfo windows.SECURITY_INFORMATION
|
||||||
|
|
||||||
|
var acl *windows.ACL
|
||||||
|
if len(ents) > 0 {
|
||||||
|
// TODO(e.burkov): Investigate if this whole set is necessary.
|
||||||
|
secInfo |= windows.DACL_SECURITY_INFORMATION |
|
||||||
|
windows.PROTECTED_DACL_SECURITY_INFORMATION |
|
||||||
|
windows.UNPROTECTED_DACL_SECURITY_INFORMATION
|
||||||
|
|
||||||
|
acl, err = windows.ACLFromEntries(ents, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating access control list: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if owner != nil {
|
||||||
|
secInfo |= windows.OWNER_SECURITY_INFORMATION
|
||||||
|
}
|
||||||
|
|
||||||
|
if secInfo == 0 {
|
||||||
|
return errors.Error("no security information to set")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = windows.SetNamedSecurityInfo(fname, objectType, secInfo, owner, nil, acl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setting security info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSecurityInfo retrieves the security information for the specified file.
|
||||||
|
func getSecurityInfo(fname string) (dacl *windows.ACL, owner *windows.SID, err error) {
|
||||||
|
// desiredSecInfo defines the parts of a security descriptor to retrieve.
|
||||||
|
const desiredSecInfo windows.SECURITY_INFORMATION = windows.OWNER_SECURITY_INFORMATION |
|
||||||
|
windows.DACL_SECURITY_INFORMATION |
|
||||||
|
windows.PROTECTED_DACL_SECURITY_INFORMATION |
|
||||||
|
windows.UNPROTECTED_DACL_SECURITY_INFORMATION
|
||||||
|
|
||||||
|
sd, err := windows.GetNamedSecurityInfo(fname, objectType, desiredSecInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("getting security descriptor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
owner, _, err = sd.Owner()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("getting owner sid: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dacl, _, err = sd.DACL()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("getting discretionary access control list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dacl, owner, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFullExplicitAccess creates a new explicit access entry with full control
|
||||||
|
// permissions.
|
||||||
|
func newFullExplicitAccess(sid *windows.SID) (accEnt windows.EXPLICIT_ACCESS) {
|
||||||
|
return windows.EXPLICIT_ACCESS{
|
||||||
|
AccessPermissions: fullControlMask,
|
||||||
|
AccessMode: windows.GRANT_ACCESS,
|
||||||
|
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||||
|
Trustee: windows.TRUSTEE{
|
||||||
|
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||||
|
TrusteeType: windows.TRUSTEE_IS_UNKNOWN,
|
||||||
|
TrusteeValue: windows.TrusteeValueFromSID(sid),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDenyExplicitAccess creates a new explicit access entry with specified deny
|
||||||
|
// permissions.
|
||||||
|
func newDenyExplicitAccess(
|
||||||
|
sid *windows.SID,
|
||||||
|
mask windows.ACCESS_MASK,
|
||||||
|
) (accEnt windows.EXPLICIT_ACCESS) {
|
||||||
|
return windows.EXPLICIT_ACCESS{
|
||||||
|
AccessPermissions: mask,
|
||||||
|
AccessMode: windows.DENY_ACCESS,
|
||||||
|
Inheritance: windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||||
|
Trustee: windows.TRUSTEE{
|
||||||
|
TrusteeForm: windows.TRUSTEE_IS_SID,
|
||||||
|
TrusteeType: windows.TRUSTEE_IS_UNKNOWN,
|
||||||
|
TrusteeValue: windows.TrusteeValueFromSID(sid),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -13,7 +14,7 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rulelist"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/urlfilter/rules"
|
"github.com/AdguardTeam/urlfilter/rules"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
@ -174,26 +175,32 @@ var logEntryHandlers = map[string]logEntryHandler{
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeResultRuleKey decodes the token of "Rules" type to logEntry struct.
|
// decodeResultRuleKey decodes the token of "Rules" type to logEntry struct.
|
||||||
func decodeResultRuleKey(key string, i int, dec *json.Decoder, ent *logEntry) {
|
func (l *queryLog) decodeResultRuleKey(
|
||||||
|
ctx context.Context,
|
||||||
|
key string,
|
||||||
|
i int,
|
||||||
|
dec *json.Decoder,
|
||||||
|
ent *logEntry,
|
||||||
|
) {
|
||||||
var vToken json.Token
|
var vToken json.Token
|
||||||
switch key {
|
switch key {
|
||||||
case "FilterListID":
|
case "FilterListID":
|
||||||
ent.Result.Rules, vToken = decodeVTokenAndAddRule(key, i, dec, ent.Result.Rules)
|
ent.Result.Rules, vToken = l.decodeVTokenAndAddRule(ctx, key, i, dec, ent.Result.Rules)
|
||||||
if n, ok := vToken.(json.Number); ok {
|
if n, ok := vToken.(json.Number); ok {
|
||||||
id, _ := n.Int64()
|
id, _ := n.Int64()
|
||||||
ent.Result.Rules[i].FilterListID = rulelist.URLFilterID(id)
|
ent.Result.Rules[i].FilterListID = rulelist.URLFilterID(id)
|
||||||
}
|
}
|
||||||
case "IP":
|
case "IP":
|
||||||
ent.Result.Rules, vToken = decodeVTokenAndAddRule(key, i, dec, ent.Result.Rules)
|
ent.Result.Rules, vToken = l.decodeVTokenAndAddRule(ctx, key, i, dec, ent.Result.Rules)
|
||||||
if ipStr, ok := vToken.(string); ok {
|
if ipStr, ok := vToken.(string); ok {
|
||||||
if ip, err := netip.ParseAddr(ipStr); err == nil {
|
if ip, err := netip.ParseAddr(ipStr); err == nil {
|
||||||
ent.Result.Rules[i].IP = ip
|
ent.Result.Rules[i].IP = ip
|
||||||
} else {
|
} else {
|
||||||
log.Debug("querylog: decoding ipStr value: %s", err)
|
l.logger.DebugContext(ctx, "decoding ip", "value", ipStr, slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "Text":
|
case "Text":
|
||||||
ent.Result.Rules, vToken = decodeVTokenAndAddRule(key, i, dec, ent.Result.Rules)
|
ent.Result.Rules, vToken = l.decodeVTokenAndAddRule(ctx, key, i, dec, ent.Result.Rules)
|
||||||
if s, ok := vToken.(string); ok {
|
if s, ok := vToken.(string); ok {
|
||||||
ent.Result.Rules[i].Text = s
|
ent.Result.Rules[i].Text = s
|
||||||
}
|
}
|
||||||
|
@ -204,7 +211,8 @@ func decodeResultRuleKey(key string, i int, dec *json.Decoder, ent *logEntry) {
|
||||||
|
|
||||||
// decodeVTokenAndAddRule decodes the "Rules" toke as [filtering.ResultRule]
|
// decodeVTokenAndAddRule decodes the "Rules" toke as [filtering.ResultRule]
|
||||||
// and then adds the decoded object to the slice of result rules.
|
// and then adds the decoded object to the slice of result rules.
|
||||||
func decodeVTokenAndAddRule(
|
func (l *queryLog) decodeVTokenAndAddRule(
|
||||||
|
ctx context.Context,
|
||||||
key string,
|
key string,
|
||||||
i int,
|
i int,
|
||||||
dec *json.Decoder,
|
dec *json.Decoder,
|
||||||
|
@ -215,7 +223,12 @@ func decodeVTokenAndAddRule(
|
||||||
vToken, err := dec.Token()
|
vToken, err := dec.Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Debug("decodeResultRuleKey %s err: %s", key, err)
|
l.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
"decoding result rule key",
|
||||||
|
"key", key,
|
||||||
|
slogutil.KeyError, err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newRules, nil
|
return newRules, nil
|
||||||
|
@ -230,12 +243,14 @@ func decodeVTokenAndAddRule(
|
||||||
|
|
||||||
// decodeResultRules parses the dec's tokens into logEntry ent interpreting it
|
// decodeResultRules parses the dec's tokens into logEntry ent interpreting it
|
||||||
// as a slice of the result rules.
|
// as a slice of the result rules.
|
||||||
func decodeResultRules(dec *json.Decoder, ent *logEntry) {
|
func (l *queryLog) decodeResultRules(ctx context.Context, dec *json.Decoder, ent *logEntry) {
|
||||||
|
const msgPrefix = "decoding result rules"
|
||||||
|
|
||||||
for {
|
for {
|
||||||
delimToken, err := dec.Token()
|
delimToken, err := dec.Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Debug("decodeResultRules err: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; token", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -244,13 +259,17 @@ func decodeResultRules(dec *json.Decoder, ent *logEntry) {
|
||||||
if d, ok := delimToken.(json.Delim); !ok {
|
if d, ok := delimToken.(json.Delim); !ok {
|
||||||
return
|
return
|
||||||
} else if d != '[' {
|
} else if d != '[' {
|
||||||
log.Debug("decodeResultRules: unexpected delim %q", d)
|
l.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
msgPrefix,
|
||||||
|
slogutil.KeyError, newUnexpectedDelimiterError(d),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = decodeResultRuleToken(dec, ent)
|
err = l.decodeResultRuleToken(ctx, dec, ent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF && !errors.Is(err, ErrEndOfToken) {
|
if err != io.EOF && !errors.Is(err, ErrEndOfToken) {
|
||||||
log.Debug("decodeResultRules err: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; rule token", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -259,7 +278,11 @@ func decodeResultRules(dec *json.Decoder, ent *logEntry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeResultRuleToken decodes the tokens of "Rules" type to the logEntry ent.
|
// decodeResultRuleToken decodes the tokens of "Rules" type to the logEntry ent.
|
||||||
func decodeResultRuleToken(dec *json.Decoder, ent *logEntry) (err error) {
|
func (l *queryLog) decodeResultRuleToken(
|
||||||
|
ctx context.Context,
|
||||||
|
dec *json.Decoder,
|
||||||
|
ent *logEntry,
|
||||||
|
) (err error) {
|
||||||
i := 0
|
i := 0
|
||||||
for {
|
for {
|
||||||
var keyToken json.Token
|
var keyToken json.Token
|
||||||
|
@ -287,7 +310,7 @@ func decodeResultRuleToken(dec *json.Decoder, ent *logEntry) (err error) {
|
||||||
return fmt.Errorf("keyToken is %T (%[1]v) and not string", keyToken)
|
return fmt.Errorf("keyToken is %T (%[1]v) and not string", keyToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeResultRuleKey(key, i, dec, ent)
|
l.decodeResultRuleKey(ctx, key, i, dec, ent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,12 +319,14 @@ func decodeResultRuleToken(dec *json.Decoder, ent *logEntry) (err error) {
|
||||||
// other occurrences of DNSRewriteResult in the entry since hosts container's
|
// other occurrences of DNSRewriteResult in the entry since hosts container's
|
||||||
// rewrites currently has the highest priority along the entire filtering
|
// rewrites currently has the highest priority along the entire filtering
|
||||||
// pipeline.
|
// pipeline.
|
||||||
func decodeResultReverseHosts(dec *json.Decoder, ent *logEntry) {
|
func (l *queryLog) decodeResultReverseHosts(ctx context.Context, dec *json.Decoder, ent *logEntry) {
|
||||||
|
const msgPrefix = "decoding result reverse hosts"
|
||||||
|
|
||||||
for {
|
for {
|
||||||
itemToken, err := dec.Token()
|
itemToken, err := dec.Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Debug("decodeResultReverseHosts err: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; token", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -315,7 +340,11 @@ func decodeResultReverseHosts(dec *json.Decoder, ent *logEntry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("decodeResultReverseHosts: unexpected delim %q", v)
|
l.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
msgPrefix,
|
||||||
|
slogutil.KeyError, newUnexpectedDelimiterError(v),
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
case string:
|
case string:
|
||||||
|
@ -346,12 +375,14 @@ func decodeResultReverseHosts(dec *json.Decoder, ent *logEntry) {
|
||||||
|
|
||||||
// decodeResultIPList parses the dec's tokens into logEntry ent interpreting it
|
// decodeResultIPList parses the dec's tokens into logEntry ent interpreting it
|
||||||
// as the result IP addresses list.
|
// as the result IP addresses list.
|
||||||
func decodeResultIPList(dec *json.Decoder, ent *logEntry) {
|
func (l *queryLog) decodeResultIPList(ctx context.Context, dec *json.Decoder, ent *logEntry) {
|
||||||
|
const msgPrefix = "decoding result ip list"
|
||||||
|
|
||||||
for {
|
for {
|
||||||
itemToken, err := dec.Token()
|
itemToken, err := dec.Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Debug("decodeResultIPList err: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; token", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -365,7 +396,11 @@ func decodeResultIPList(dec *json.Decoder, ent *logEntry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("decodeResultIPList: unexpected delim %q", v)
|
l.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
msgPrefix,
|
||||||
|
slogutil.KeyError, newUnexpectedDelimiterError(v),
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
case string:
|
case string:
|
||||||
|
@ -382,7 +417,14 @@ func decodeResultIPList(dec *json.Decoder, ent *logEntry) {
|
||||||
|
|
||||||
// decodeResultDNSRewriteResultKey decodes the token of "DNSRewriteResult" type
|
// decodeResultDNSRewriteResultKey decodes the token of "DNSRewriteResult" type
|
||||||
// to the logEntry struct.
|
// to the logEntry struct.
|
||||||
func decodeResultDNSRewriteResultKey(key string, dec *json.Decoder, ent *logEntry) {
|
func (l *queryLog) decodeResultDNSRewriteResultKey(
|
||||||
|
ctx context.Context,
|
||||||
|
key string,
|
||||||
|
dec *json.Decoder,
|
||||||
|
ent *logEntry,
|
||||||
|
) {
|
||||||
|
const msgPrefix = "decoding result dns rewrite result key"
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
|
@ -391,7 +433,7 @@ func decodeResultDNSRewriteResultKey(key string, dec *json.Decoder, ent *logEntr
|
||||||
vToken, err = dec.Token()
|
vToken, err = dec.Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Debug("decodeResultDNSRewriteResultKey err: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; token", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -419,7 +461,7 @@ func decodeResultDNSRewriteResultKey(key string, dec *json.Decoder, ent *logEntr
|
||||||
// decoding and correct the values.
|
// decoding and correct the values.
|
||||||
err = dec.Decode(&ent.Result.DNSRewriteResult.Response)
|
err = dec.Decode(&ent.Result.DNSRewriteResult.Response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("decodeResultDNSRewriteResultKey response err: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; response", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ent.parseDNSRewriteResultIPs()
|
ent.parseDNSRewriteResultIPs()
|
||||||
|
@ -430,12 +472,18 @@ func decodeResultDNSRewriteResultKey(key string, dec *json.Decoder, ent *logEntr
|
||||||
|
|
||||||
// decodeResultDNSRewriteResult parses the dec's tokens into logEntry ent
|
// decodeResultDNSRewriteResult parses the dec's tokens into logEntry ent
|
||||||
// interpreting it as the result DNSRewriteResult.
|
// interpreting it as the result DNSRewriteResult.
|
||||||
func decodeResultDNSRewriteResult(dec *json.Decoder, ent *logEntry) {
|
func (l *queryLog) decodeResultDNSRewriteResult(
|
||||||
|
ctx context.Context,
|
||||||
|
dec *json.Decoder,
|
||||||
|
ent *logEntry,
|
||||||
|
) {
|
||||||
|
const msgPrefix = "decoding result dns rewrite result"
|
||||||
|
|
||||||
for {
|
for {
|
||||||
key, err := parseKeyToken(dec)
|
key, err := parseKeyToken(dec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF && !errors.Is(err, ErrEndOfToken) {
|
if err != io.EOF && !errors.Is(err, ErrEndOfToken) {
|
||||||
log.Debug("decodeResultDNSRewriteResult: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; token", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -445,7 +493,7 @@ func decodeResultDNSRewriteResult(dec *json.Decoder, ent *logEntry) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeResultDNSRewriteResultKey(key, dec, ent)
|
l.decodeResultDNSRewriteResultKey(ctx, key, dec, ent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -508,14 +556,16 @@ func parseKeyToken(dec *json.Decoder) (key string, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeResult decodes a token of "Result" type to logEntry struct.
|
// decodeResult decodes a token of "Result" type to logEntry struct.
|
||||||
func decodeResult(dec *json.Decoder, ent *logEntry) {
|
func (l *queryLog) decodeResult(ctx context.Context, dec *json.Decoder, ent *logEntry) {
|
||||||
|
const msgPrefix = "decoding result"
|
||||||
|
|
||||||
defer translateResult(ent)
|
defer translateResult(ent)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
key, err := parseKeyToken(dec)
|
key, err := parseKeyToken(dec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF && !errors.Is(err, ErrEndOfToken) {
|
if err != io.EOF && !errors.Is(err, ErrEndOfToken) {
|
||||||
log.Debug("decodeResult: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; token", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -525,10 +575,8 @@ func decodeResult(dec *json.Decoder, ent *logEntry) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
decHandler, ok := resultDecHandlers[key]
|
ok := l.resultDecHandler(ctx, key, dec, ent)
|
||||||
if ok {
|
if ok {
|
||||||
decHandler(dec, ent)
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -543,7 +591,7 @@ func decodeResult(dec *json.Decoder, ent *logEntry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = handler(val, ent); err != nil {
|
if err = handler(val, ent); err != nil {
|
||||||
log.Debug("decodeResult handler err: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; handler", slogutil.KeyError, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -636,16 +684,34 @@ var resultHandlers = map[string]logEntryHandler{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// resultDecHandlers is the map of decode handlers for various keys.
|
// resultDecHandlers calls a decode handler for key if there is one.
|
||||||
var resultDecHandlers = map[string]func(dec *json.Decoder, ent *logEntry){
|
func (l *queryLog) resultDecHandler(
|
||||||
"ReverseHosts": decodeResultReverseHosts,
|
ctx context.Context,
|
||||||
"IPList": decodeResultIPList,
|
name string,
|
||||||
"Rules": decodeResultRules,
|
dec *json.Decoder,
|
||||||
"DNSRewriteResult": decodeResultDNSRewriteResult,
|
ent *logEntry,
|
||||||
|
) (ok bool) {
|
||||||
|
ok = true
|
||||||
|
switch name {
|
||||||
|
case "ReverseHosts":
|
||||||
|
l.decodeResultReverseHosts(ctx, dec, ent)
|
||||||
|
case "IPList":
|
||||||
|
l.decodeResultIPList(ctx, dec, ent)
|
||||||
|
case "Rules":
|
||||||
|
l.decodeResultRules(ctx, dec, ent)
|
||||||
|
case "DNSRewriteResult":
|
||||||
|
l.decodeResultDNSRewriteResult(ctx, dec, ent)
|
||||||
|
default:
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeLogEntry decodes string str to logEntry ent.
|
// decodeLogEntry decodes string str to logEntry ent.
|
||||||
func decodeLogEntry(ent *logEntry, str string) {
|
func (l *queryLog) decodeLogEntry(ctx context.Context, ent *logEntry, str string) {
|
||||||
|
const msgPrefix = "decoding log entry"
|
||||||
|
|
||||||
dec := json.NewDecoder(strings.NewReader(str))
|
dec := json.NewDecoder(strings.NewReader(str))
|
||||||
dec.UseNumber()
|
dec.UseNumber()
|
||||||
|
|
||||||
|
@ -653,7 +719,7 @@ func decodeLogEntry(ent *logEntry, str string) {
|
||||||
keyToken, err := dec.Token()
|
keyToken, err := dec.Token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
log.Debug("decodeLogEntry err: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; token", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -665,13 +731,14 @@ func decodeLogEntry(ent *logEntry, str string) {
|
||||||
|
|
||||||
key, ok := keyToken.(string)
|
key, ok := keyToken.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Debug("decodeLogEntry: keyToken is %T (%[1]v) and not string", keyToken)
|
err = fmt.Errorf("%s: keyToken is %T (%[2]v) and not string", msgPrefix, keyToken)
|
||||||
|
l.logger.DebugContext(ctx, msgPrefix, slogutil.KeyError, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if key == "Result" {
|
if key == "Result" {
|
||||||
decodeResult(dec, ent)
|
l.decodeResult(ctx, dec, ent)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -687,9 +754,14 @@ func decodeLogEntry(ent *logEntry, str string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = handler(val, ent); err != nil {
|
if err = handler(val, ent); err != nil {
|
||||||
log.Debug("decodeLogEntry handler err: %s", err)
|
l.logger.DebugContext(ctx, msgPrefix+"; handler", slogutil.KeyError, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newUnexpectedDelimiterError is a helper for creating informative errors.
|
||||||
|
func newUnexpectedDelimiterError(d json.Delim) (err error) {
|
||||||
|
return fmt.Errorf("unexpected delimiter: %q", d)
|
||||||
|
}
|
||||||
|
|
|
@ -3,27 +3,35 @@ package querylog
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/urlfilter/rules"
|
"github.com/AdguardTeam/urlfilter/rules"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Common constants for tests.
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
func TestDecodeLogEntry(t *testing.T) {
|
func TestDecodeLogEntry(t *testing.T) {
|
||||||
logOutput := &bytes.Buffer{}
|
logOutput := &bytes.Buffer{}
|
||||||
|
l := &queryLog{
|
||||||
|
logger: slog.New(slog.NewTextHandler(logOutput, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
ReplaceAttr: slogutil.RemoveTime,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
aghtest.ReplaceLogWriter(t, logOutput)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
aghtest.ReplaceLogLevel(t, log.DEBUG)
|
|
||||||
|
|
||||||
t.Run("success", func(t *testing.T) {
|
t.Run("success", func(t *testing.T) {
|
||||||
const ansStr = `Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==`
|
const ansStr = `Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==`
|
||||||
|
@ -92,7 +100,7 @@ func TestDecodeLogEntry(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
got := &logEntry{}
|
got := &logEntry{}
|
||||||
decodeLogEntry(got, data)
|
l.decodeLogEntry(ctx, got, data)
|
||||||
|
|
||||||
s := logOutput.String()
|
s := logOutput.String()
|
||||||
assert.Empty(t, s)
|
assert.Empty(t, s)
|
||||||
|
@ -113,11 +121,11 @@ func TestDecodeLogEntry(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "bad_filter_id_old_rule",
|
name: "bad_filter_id_old_rule",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"FilterID":1.5},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"FilterID":1.5},"Elapsed":837429}`,
|
||||||
want: "decodeResult handler err: strconv.ParseInt: parsing \"1.5\": invalid syntax\n",
|
want: `level=DEBUG msg="decoding result; handler" err="strconv.ParseInt: parsing \"1.5\": invalid syntax"`,
|
||||||
}, {
|
}, {
|
||||||
name: "bad_is_filtered",
|
name: "bad_is_filtered",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":trooe,"Reason":3},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":trooe,"Reason":3},"Elapsed":837429}`,
|
||||||
want: "decodeLogEntry err: invalid character 'o' in literal true (expecting 'u')\n",
|
want: `level=DEBUG msg="decoding log entry; token" err="invalid character 'o' in literal true (expecting 'u')"`,
|
||||||
}, {
|
}, {
|
||||||
name: "bad_elapsed",
|
name: "bad_elapsed",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":-1}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":-1}`,
|
||||||
|
@ -129,7 +137,7 @@ func TestDecodeLogEntry(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "bad_time",
|
name: "bad_time",
|
||||||
log: `{"IP":"127.0.0.1","T":"12/09/1998T15:00:00.000000+05:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"12/09/1998T15:00:00.000000+05:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
||||||
want: "decodeLogEntry handler err: parsing time \"12/09/1998T15:00:00.000000+05:00\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"12/09/1998T15:00:00.000000+05:00\" as \"2006\"\n",
|
want: `level=DEBUG msg="decoding log entry; handler" err="parsing time \"12/09/1998T15:00:00.000000+05:00\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"12/09/1998T15:00:00.000000+05:00\" as \"2006\""`,
|
||||||
}, {
|
}, {
|
||||||
name: "bad_host",
|
name: "bad_host",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":6,"QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":6,"QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
||||||
|
@ -149,7 +157,7 @@ func TestDecodeLogEntry(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "very_bad_client_proto",
|
name: "very_bad_client_proto",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"dog","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"dog","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
||||||
want: "decodeLogEntry handler err: invalid client proto: \"dog\"\n",
|
want: `level=DEBUG msg="decoding log entry; handler" err="invalid client proto: \"dog\""`,
|
||||||
}, {
|
}, {
|
||||||
name: "bad_answer",
|
name: "bad_answer",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":0.9,"Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":0.9,"Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
||||||
|
@ -157,7 +165,7 @@ func TestDecodeLogEntry(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "very_bad_answer",
|
name: "very_bad_answer",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3},"Elapsed":837429}`,
|
||||||
want: "decodeLogEntry handler err: illegal base64 data at input byte 61\n",
|
want: `level=DEBUG msg="decoding log entry; handler" err="illegal base64 data at input byte 61"`,
|
||||||
}, {
|
}, {
|
||||||
name: "bad_rule",
|
name: "bad_rule",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":false},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"Rule":false},"Elapsed":837429}`,
|
||||||
|
@ -169,22 +177,25 @@ func TestDecodeLogEntry(t *testing.T) {
|
||||||
}, {
|
}, {
|
||||||
name: "bad_reverse_hosts",
|
name: "bad_reverse_hosts",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"ReverseHosts":[{}]},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"ReverseHosts":[{}]},"Elapsed":837429}`,
|
||||||
want: "decodeResultReverseHosts: unexpected delim \"{\"\n",
|
want: `level=DEBUG msg="decoding result reverse hosts" err="unexpected delimiter: \"{\""`,
|
||||||
}, {
|
}, {
|
||||||
name: "bad_ip_list",
|
name: "bad_ip_list",
|
||||||
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"ReverseHosts":["example.net"],"IPList":[{}]},"Elapsed":837429}`,
|
log: `{"IP":"127.0.0.1","T":"2020-11-25T18:55:56.519796+03:00","QH":"an.yandex.ru","QT":"A","QC":"IN","CP":"","Answer":"Qz+BgAABAAEAAAAAAmFuBnlhbmRleAJydQAAAQABwAwAAQABAAAACgAEAAAAAA==","Result":{"IsFiltered":true,"Reason":3,"ReverseHosts":["example.net"],"IPList":[{}]},"Elapsed":837429}`,
|
||||||
want: "decodeResultIPList: unexpected delim \"{\"\n",
|
want: `level=DEBUG msg="decoding result ip list" err="unexpected delimiter: \"{\""`,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
decodeLogEntry(new(logEntry), tc.log)
|
l.decodeLogEntry(ctx, new(logEntry), tc.log)
|
||||||
|
got := logOutput.String()
|
||||||
s := logOutput.String()
|
|
||||||
if tc.want == "" {
|
if tc.want == "" {
|
||||||
assert.Empty(t, s)
|
assert.Empty(t, got)
|
||||||
} else {
|
} else {
|
||||||
assert.True(t, strings.HasSuffix(s, tc.want), "got %q", s)
|
require.NotEmpty(t, got)
|
||||||
|
|
||||||
|
// Remove newline.
|
||||||
|
got = got[:len(got)-1]
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
logOutput.Reset()
|
logOutput.Reset()
|
||||||
|
@ -200,6 +211,12 @@ func TestDecodeLogEntry_backwardCompatability(t *testing.T) {
|
||||||
aaaa2 = aaaa1.Next()
|
aaaa2 = aaaa1.Next()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
l := &queryLog{
|
||||||
|
logger: slogutil.NewDiscardLogger(),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
want *logEntry
|
want *logEntry
|
||||||
entry string
|
entry string
|
||||||
|
@ -249,7 +266,7 @@ func TestDecodeLogEntry_backwardCompatability(t *testing.T) {
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
e := &logEntry{}
|
e := &logEntry{}
|
||||||
decodeLogEntry(e, tc.entry)
|
l.decodeLogEntry(ctx, e, tc.entry)
|
||||||
|
|
||||||
assert.Equal(t, tc.want, e)
|
assert.Equal(t, tc.want, e)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,7 +54,7 @@ func (e *logEntry) shallowClone() (clone *logEntry) {
|
||||||
// addResponse adds data from resp to e.Answer if resp is not nil. If isOrig is
|
// addResponse adds data from resp to e.Answer if resp is not nil. If isOrig is
|
||||||
// true, addResponse sets the e.OrigAnswer field instead of e.Answer. Any
|
// true, addResponse sets the e.OrigAnswer field instead of e.Answer. Any
|
||||||
// errors are logged.
|
// errors are logged.
|
||||||
func (e *logEntry) addResponse(resp *dns.Msg, isOrig bool) {
|
func (e *logEntry) addResponse(ctx context.Context, l *slog.Logger, resp *dns.Msg, isOrig bool) {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -65,8 +67,9 @@ func (e *logEntry) addResponse(resp *dns.Msg, isOrig bool) {
|
||||||
e.Answer, err = resp.Pack()
|
e.Answer, err = resp.Pack()
|
||||||
err = errors.Annotate(err, "packing answer: %w")
|
err = errors.Annotate(err, "packing answer: %w")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("querylog: %s", err)
|
l.ErrorContext(ctx, "adding data from response", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
@ -15,7 +16,7 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
)
|
)
|
||||||
|
@ -74,7 +75,8 @@ func (l *queryLog) initWeb() {
|
||||||
|
|
||||||
// handleQueryLog is the handler for the GET /control/querylog HTTP API.
|
// handleQueryLog is the handler for the GET /control/querylog HTTP API.
|
||||||
func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
|
func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
|
||||||
params, err := parseSearchParams(r)
|
ctx := r.Context()
|
||||||
|
params, err := l.parseSearchParams(ctx, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "parsing params: %s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "parsing params: %s", err)
|
||||||
|
|
||||||
|
@ -87,18 +89,18 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
|
||||||
l.confMu.RLock()
|
l.confMu.RLock()
|
||||||
defer l.confMu.RUnlock()
|
defer l.confMu.RUnlock()
|
||||||
|
|
||||||
entries, oldest = l.search(params)
|
entries, oldest = l.search(ctx, params)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
resp := entriesToJSON(entries, oldest, l.anonymizer.Load())
|
resp := l.entriesToJSON(ctx, entries, oldest, l.anonymizer.Load())
|
||||||
|
|
||||||
aghhttp.WriteJSONResponseOK(w, r, resp)
|
aghhttp.WriteJSONResponseOK(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleQueryLogClear is the handler for the POST /control/querylog/clear HTTP
|
// handleQueryLogClear is the handler for the POST /control/querylog/clear HTTP
|
||||||
// API.
|
// API.
|
||||||
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, r *http.Request) {
|
||||||
l.clear()
|
l.clear(r.Context())
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleQueryLogInfo is the handler for the GET /control/querylog_info HTTP
|
// handleQueryLogInfo is the handler for the GET /control/querylog_info HTTP
|
||||||
|
@ -280,11 +282,12 @@ func getDoubleQuotesEnclosedValue(s *string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSearchCriterion parses a search criterion from the query parameter.
|
// parseSearchCriterion parses a search criterion from the query parameter.
|
||||||
func parseSearchCriterion(q url.Values, name string, ct criterionType) (
|
func (l *queryLog) parseSearchCriterion(
|
||||||
ok bool,
|
ctx context.Context,
|
||||||
sc searchCriterion,
|
q url.Values,
|
||||||
err error,
|
name string,
|
||||||
) {
|
ct criterionType,
|
||||||
|
) (ok bool, sc searchCriterion, err error) {
|
||||||
val := q.Get(name)
|
val := q.Get(name)
|
||||||
if val == "" {
|
if val == "" {
|
||||||
return false, sc, nil
|
return false, sc, nil
|
||||||
|
@ -301,7 +304,7 @@ func parseSearchCriterion(q url.Values, name string, ct criterionType) (
|
||||||
// TODO(e.burkov): Make it work with parts of IDNAs somehow.
|
// TODO(e.burkov): Make it work with parts of IDNAs somehow.
|
||||||
loweredVal := strings.ToLower(val)
|
loweredVal := strings.ToLower(val)
|
||||||
if asciiVal, err = idna.ToASCII(loweredVal); err != nil {
|
if asciiVal, err = idna.ToASCII(loweredVal); err != nil {
|
||||||
log.Debug("can't convert %q to ascii: %s", val, err)
|
l.logger.DebugContext(ctx, "converting to ascii", "value", val, slogutil.KeyError, err)
|
||||||
} else if asciiVal == loweredVal {
|
} else if asciiVal == loweredVal {
|
||||||
// Purge asciiVal to prevent checking the same value
|
// Purge asciiVal to prevent checking the same value
|
||||||
// twice.
|
// twice.
|
||||||
|
@ -331,7 +334,10 @@ func parseSearchCriterion(q url.Values, name string, ct criterionType) (
|
||||||
|
|
||||||
// parseSearchParams parses search parameters from the HTTP request's query
|
// parseSearchParams parses search parameters from the HTTP request's query
|
||||||
// string.
|
// string.
|
||||||
func parseSearchParams(r *http.Request) (p *searchParams, err error) {
|
func (l *queryLog) parseSearchParams(
|
||||||
|
ctx context.Context,
|
||||||
|
r *http.Request,
|
||||||
|
) (p *searchParams, err error) {
|
||||||
p = newSearchParams()
|
p = newSearchParams()
|
||||||
|
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
|
@ -369,7 +375,7 @@ func parseSearchParams(r *http.Request) (p *searchParams, err error) {
|
||||||
}} {
|
}} {
|
||||||
var ok bool
|
var ok bool
|
||||||
var c searchCriterion
|
var c searchCriterion
|
||||||
ok, c, err = parseSearchCriterion(q, v.urlField, v.ct)
|
ok, c, err = l.parseSearchCriterion(ctx, q, v.urlField, v.ct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -8,7 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
)
|
)
|
||||||
|
@ -19,7 +20,8 @@ import (
|
||||||
type jobject = map[string]any
|
type jobject = map[string]any
|
||||||
|
|
||||||
// entriesToJSON converts query log entries to JSON.
|
// entriesToJSON converts query log entries to JSON.
|
||||||
func entriesToJSON(
|
func (l *queryLog) entriesToJSON(
|
||||||
|
ctx context.Context,
|
||||||
entries []*logEntry,
|
entries []*logEntry,
|
||||||
oldest time.Time,
|
oldest time.Time,
|
||||||
anonFunc aghnet.IPMutFunc,
|
anonFunc aghnet.IPMutFunc,
|
||||||
|
@ -28,7 +30,7 @@ func entriesToJSON(
|
||||||
|
|
||||||
// The elements order is already reversed to be from newer to older.
|
// The elements order is already reversed to be from newer to older.
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
jsonEntry := entryToJSON(entry, anonFunc)
|
jsonEntry := l.entryToJSON(ctx, entry, anonFunc)
|
||||||
data = append(data, jsonEntry)
|
data = append(data, jsonEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +46,11 @@ func entriesToJSON(
|
||||||
}
|
}
|
||||||
|
|
||||||
// entryToJSON converts a log entry's data into an entry for the JSON API.
|
// entryToJSON converts a log entry's data into an entry for the JSON API.
|
||||||
func entryToJSON(entry *logEntry, anonFunc aghnet.IPMutFunc) (jsonEntry jobject) {
|
func (l *queryLog) entryToJSON(
|
||||||
|
ctx context.Context,
|
||||||
|
entry *logEntry,
|
||||||
|
anonFunc aghnet.IPMutFunc,
|
||||||
|
) (jsonEntry jobject) {
|
||||||
hostname := entry.QHost
|
hostname := entry.QHost
|
||||||
question := jobject{
|
question := jobject{
|
||||||
"type": entry.QType,
|
"type": entry.QType,
|
||||||
|
@ -53,7 +59,12 @@ func entryToJSON(entry *logEntry, anonFunc aghnet.IPMutFunc) (jsonEntry jobject)
|
||||||
}
|
}
|
||||||
|
|
||||||
if qhost, err := idna.ToUnicode(hostname); err != nil {
|
if qhost, err := idna.ToUnicode(hostname); err != nil {
|
||||||
log.Debug("querylog: translating %q into unicode: %s", hostname, err)
|
l.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
"translating into unicode",
|
||||||
|
"hostname", hostname,
|
||||||
|
slogutil.KeyError, err,
|
||||||
|
)
|
||||||
} else if qhost != hostname && qhost != "" {
|
} else if qhost != hostname && qhost != "" {
|
||||||
question["unicode_name"] = qhost
|
question["unicode_name"] = qhost
|
||||||
}
|
}
|
||||||
|
@ -96,21 +107,26 @@ func entryToJSON(entry *logEntry, anonFunc aghnet.IPMutFunc) (jsonEntry jobject)
|
||||||
jsonEntry["service_name"] = entry.Result.ServiceName
|
jsonEntry["service_name"] = entry.Result.ServiceName
|
||||||
}
|
}
|
||||||
|
|
||||||
setMsgData(entry, jsonEntry)
|
l.setMsgData(ctx, entry, jsonEntry)
|
||||||
setOrigAns(entry, jsonEntry)
|
l.setOrigAns(ctx, entry, jsonEntry)
|
||||||
|
|
||||||
return jsonEntry
|
return jsonEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
// setMsgData sets the message data in jsonEntry.
|
// setMsgData sets the message data in jsonEntry.
|
||||||
func setMsgData(entry *logEntry, jsonEntry jobject) {
|
func (l *queryLog) setMsgData(ctx context.Context, entry *logEntry, jsonEntry jobject) {
|
||||||
if len(entry.Answer) == 0 {
|
if len(entry.Answer) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := &dns.Msg{}
|
msg := &dns.Msg{}
|
||||||
if err := msg.Unpack(entry.Answer); err != nil {
|
if err := msg.Unpack(entry.Answer); err != nil {
|
||||||
log.Debug("querylog: failed to unpack dns msg answer: %v: %s", entry.Answer, err)
|
l.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
"unpacking dns message",
|
||||||
|
"answer", entry.Answer,
|
||||||
|
slogutil.KeyError, err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -126,7 +142,7 @@ func setMsgData(entry *logEntry, jsonEntry jobject) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// setOrigAns sets the original answer data in jsonEntry.
|
// setOrigAns sets the original answer data in jsonEntry.
|
||||||
func setOrigAns(entry *logEntry, jsonEntry jobject) {
|
func (l *queryLog) setOrigAns(ctx context.Context, entry *logEntry, jsonEntry jobject) {
|
||||||
if len(entry.OrigAnswer) == 0 {
|
if len(entry.OrigAnswer) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -134,7 +150,12 @@ func setOrigAns(entry *logEntry, jsonEntry jobject) {
|
||||||
orig := &dns.Msg{}
|
orig := &dns.Msg{}
|
||||||
err := orig.Unpack(entry.OrigAnswer)
|
err := orig.Unpack(entry.OrigAnswer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("querylog: orig.Unpack(entry.OrigAnswer): %v: %s", entry.OrigAnswer, err)
|
l.logger.DebugContext(
|
||||||
|
ctx,
|
||||||
|
"setting original answer",
|
||||||
|
"answer", entry.OrigAnswer,
|
||||||
|
slogutil.KeyError, err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -11,7 +13,7 @@ import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/golibs/container"
|
"github.com/AdguardTeam/golibs/container"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
@ -22,6 +24,10 @@ const queryLogFileName = "querylog.json"
|
||||||
|
|
||||||
// queryLog is a structure that writes and reads the DNS query log.
|
// queryLog is a structure that writes and reads the DNS query log.
|
||||||
type queryLog struct {
|
type queryLog struct {
|
||||||
|
// logger is used for logging the operation of the query log. It must not
|
||||||
|
// be nil.
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
// confMu protects conf.
|
// confMu protects conf.
|
||||||
confMu *sync.RWMutex
|
confMu *sync.RWMutex
|
||||||
|
|
||||||
|
@ -76,24 +82,34 @@ func NewClientProto(s string) (cp ClientProto, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *queryLog) Start() {
|
// type check
|
||||||
|
var _ QueryLog = (*queryLog)(nil)
|
||||||
|
|
||||||
|
// Start implements the [QueryLog] interface for *queryLog.
|
||||||
|
func (l *queryLog) Start(ctx context.Context) (err error) {
|
||||||
if l.conf.HTTPRegister != nil {
|
if l.conf.HTTPRegister != nil {
|
||||||
l.initWeb()
|
l.initWeb()
|
||||||
}
|
}
|
||||||
|
|
||||||
go l.periodicRotate()
|
go l.periodicRotate(ctx)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *queryLog) Close() {
|
// Shutdown implements the [QueryLog] interface for *queryLog.
|
||||||
|
func (l *queryLog) Shutdown(ctx context.Context) (err error) {
|
||||||
l.confMu.RLock()
|
l.confMu.RLock()
|
||||||
defer l.confMu.RUnlock()
|
defer l.confMu.RUnlock()
|
||||||
|
|
||||||
if l.conf.FileEnabled {
|
if l.conf.FileEnabled {
|
||||||
err := l.flushLogBuffer()
|
err = l.flushLogBuffer(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("querylog: closing: %s", err)
|
// Don't wrap the error because it's informative enough as is.
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkInterval(ivl time.Duration) (ok bool) {
|
func checkInterval(ivl time.Duration) (ok bool) {
|
||||||
|
@ -123,6 +139,7 @@ func validateIvl(ivl time.Duration) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteDiskConfig implements the [QueryLog] interface for *queryLog.
|
||||||
func (l *queryLog) WriteDiskConfig(c *Config) {
|
func (l *queryLog) WriteDiskConfig(c *Config) {
|
||||||
l.confMu.RLock()
|
l.confMu.RLock()
|
||||||
defer l.confMu.RUnlock()
|
defer l.confMu.RUnlock()
|
||||||
|
@ -131,7 +148,7 @@ func (l *queryLog) WriteDiskConfig(c *Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear memory buffer and remove log files
|
// Clear memory buffer and remove log files
|
||||||
func (l *queryLog) clear() {
|
func (l *queryLog) clear(ctx context.Context) {
|
||||||
l.fileFlushLock.Lock()
|
l.fileFlushLock.Lock()
|
||||||
defer l.fileFlushLock.Unlock()
|
defer l.fileFlushLock.Unlock()
|
||||||
|
|
||||||
|
@ -146,19 +163,24 @@ func (l *queryLog) clear() {
|
||||||
oldLogFile := l.logFile + ".1"
|
oldLogFile := l.logFile + ".1"
|
||||||
err := os.Remove(oldLogFile)
|
err := os.Remove(oldLogFile)
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
log.Error("removing old log file %q: %s", oldLogFile, err)
|
l.logger.ErrorContext(
|
||||||
|
ctx,
|
||||||
|
"removing old log file",
|
||||||
|
"file", oldLogFile,
|
||||||
|
slogutil.KeyError, err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Remove(l.logFile)
|
err = os.Remove(l.logFile)
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
log.Error("removing log file %q: %s", l.logFile, err)
|
l.logger.ErrorContext(ctx, "removing log file", "file", l.logFile, slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("querylog: cleared")
|
l.logger.DebugContext(ctx, "cleared")
|
||||||
}
|
}
|
||||||
|
|
||||||
// newLogEntry creates an instance of logEntry from parameters.
|
// newLogEntry creates an instance of logEntry from parameters.
|
||||||
func newLogEntry(params *AddParams) (entry *logEntry) {
|
func newLogEntry(ctx context.Context, logger *slog.Logger, params *AddParams) (entry *logEntry) {
|
||||||
q := params.Question.Question[0]
|
q := params.Question.Question[0]
|
||||||
qHost := aghnet.NormalizeDomain(q.Name)
|
qHost := aghnet.NormalizeDomain(q.Name)
|
||||||
|
|
||||||
|
@ -187,8 +209,8 @@ func newLogEntry(params *AddParams) (entry *logEntry) {
|
||||||
entry.ReqECS = params.ReqECS.String()
|
entry.ReqECS = params.ReqECS.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.addResponse(params.Answer, false)
|
entry.addResponse(ctx, logger, params.Answer, false)
|
||||||
entry.addResponse(params.OrigAnswer, true)
|
entry.addResponse(ctx, logger, params.OrigAnswer, true)
|
||||||
|
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
@ -209,9 +231,12 @@ func (l *queryLog) Add(params *AddParams) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(s.chzhen): Pass context.
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
err := params.validate()
|
err := params.validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("querylog: adding record: %s, skipping", err)
|
l.logger.ErrorContext(ctx, "adding record", slogutil.KeyError, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -220,7 +245,7 @@ func (l *queryLog) Add(params *AddParams) {
|
||||||
params.Result = &filtering.Result{}
|
params.Result = &filtering.Result{}
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := newLogEntry(params)
|
entry := newLogEntry(ctx, l.logger, params)
|
||||||
|
|
||||||
l.bufferLock.Lock()
|
l.bufferLock.Lock()
|
||||||
defer l.bufferLock.Unlock()
|
defer l.bufferLock.Unlock()
|
||||||
|
@ -232,9 +257,9 @@ func (l *queryLog) Add(params *AddParams) {
|
||||||
|
|
||||||
// TODO(s.chzhen): Fix occasional rewrite of entires.
|
// TODO(s.chzhen): Fix occasional rewrite of entires.
|
||||||
go func() {
|
go func() {
|
||||||
flushErr := l.flushLogBuffer()
|
flushErr := l.flushLogBuffer(ctx)
|
||||||
if flushErr != nil {
|
if flushErr != nil {
|
||||||
log.Error("querylog: flushing after adding: %s", flushErr)
|
l.logger.ErrorContext(ctx, "flushing after adding", slogutil.KeyError, flushErr)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
@ -247,7 +272,8 @@ func (l *queryLog) ShouldLog(host string, _, _ uint16, ids []string) bool {
|
||||||
|
|
||||||
c, err := l.findClient(ids)
|
c, err := l.findClient(ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("querylog: finding client: %s", err)
|
// TODO(s.chzhen): Pass context.
|
||||||
|
l.logger.ErrorContext(context.TODO(), "finding client", slogutil.KeyError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c != nil && c.IgnoreQueryLog {
|
if c != nil && c.IgnoreQueryLog {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/golibs/timeutil"
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
@ -14,14 +15,11 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
testutil.DiscardLogOutput(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestQueryLog tests adding and loading (with filtering) entries from disk and
|
// TestQueryLog tests adding and loading (with filtering) entries from disk and
|
||||||
// memory.
|
// memory.
|
||||||
func TestQueryLog(t *testing.T) {
|
func TestQueryLog(t *testing.T) {
|
||||||
l, err := newQueryLog(Config{
|
l, err := newQueryLog(Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
FileEnabled: true,
|
FileEnabled: true,
|
||||||
RotationIvl: timeutil.Day,
|
RotationIvl: timeutil.Day,
|
||||||
|
@ -30,16 +28,21 @@ func TestQueryLog(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
// Add disk entries.
|
// Add disk entries.
|
||||||
addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||||
// Write to disk (first file).
|
// Write to disk (first file).
|
||||||
require.NoError(t, l.flushLogBuffer())
|
require.NoError(t, l.flushLogBuffer(ctx))
|
||||||
|
|
||||||
// Start writing to the second file.
|
// Start writing to the second file.
|
||||||
require.NoError(t, l.rotate())
|
require.NoError(t, l.rotate(ctx))
|
||||||
|
|
||||||
// Add disk entries.
|
// Add disk entries.
|
||||||
addEntry(l, "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
|
addEntry(l, "example.org", net.IPv4(1, 1, 1, 2), net.IPv4(2, 2, 2, 2))
|
||||||
// Write to disk.
|
// Write to disk.
|
||||||
require.NoError(t, l.flushLogBuffer())
|
require.NoError(t, l.flushLogBuffer(ctx))
|
||||||
|
|
||||||
// Add memory entries.
|
// Add memory entries.
|
||||||
addEntry(l, "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
|
addEntry(l, "test.example.org", net.IPv4(1, 1, 1, 3), net.IPv4(2, 2, 2, 3))
|
||||||
addEntry(l, "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4))
|
addEntry(l, "example.com", net.IPv4(1, 1, 1, 4), net.IPv4(2, 2, 2, 4))
|
||||||
|
@ -119,8 +122,9 @@ func TestQueryLog(t *testing.T) {
|
||||||
params := newSearchParams()
|
params := newSearchParams()
|
||||||
params.searchCriteria = tc.sCr
|
params.searchCriteria = tc.sCr
|
||||||
|
|
||||||
entries, _ := l.search(params)
|
entries, _ := l.search(ctx, params)
|
||||||
require.Len(t, entries, len(tc.want))
|
require.Len(t, entries, len(tc.want))
|
||||||
|
|
||||||
for _, want := range tc.want {
|
for _, want := range tc.want {
|
||||||
assertLogEntry(t, entries[want.num], want.host, want.answer, want.client)
|
assertLogEntry(t, entries[want.num], want.host, want.answer, want.client)
|
||||||
}
|
}
|
||||||
|
@ -130,6 +134,7 @@ func TestQueryLog(t *testing.T) {
|
||||||
|
|
||||||
func TestQueryLogOffsetLimit(t *testing.T) {
|
func TestQueryLogOffsetLimit(t *testing.T) {
|
||||||
l, err := newQueryLog(Config{
|
l, err := newQueryLog(Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RotationIvl: timeutil.Day,
|
RotationIvl: timeutil.Day,
|
||||||
MemSize: 100,
|
MemSize: 100,
|
||||||
|
@ -142,12 +147,16 @@ func TestQueryLogOffsetLimit(t *testing.T) {
|
||||||
firstPageDomain = "first.example.org"
|
firstPageDomain = "first.example.org"
|
||||||
secondPageDomain = "second.example.org"
|
secondPageDomain = "second.example.org"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
// Add entries to the log.
|
// Add entries to the log.
|
||||||
for range entNum {
|
for range entNum {
|
||||||
addEntry(l, secondPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
addEntry(l, secondPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||||
}
|
}
|
||||||
// Write them to the first file.
|
// Write them to the first file.
|
||||||
require.NoError(t, l.flushLogBuffer())
|
require.NoError(t, l.flushLogBuffer(ctx))
|
||||||
|
|
||||||
// Add more to the in-memory part of log.
|
// Add more to the in-memory part of log.
|
||||||
for range entNum {
|
for range entNum {
|
||||||
addEntry(l, firstPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
addEntry(l, firstPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||||
|
@ -191,8 +200,7 @@ func TestQueryLogOffsetLimit(t *testing.T) {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
params.offset = tc.offset
|
params.offset = tc.offset
|
||||||
params.limit = tc.limit
|
params.limit = tc.limit
|
||||||
entries, _ := l.search(params)
|
entries, _ := l.search(ctx, params)
|
||||||
|
|
||||||
require.Len(t, entries, tc.wantLen)
|
require.Len(t, entries, tc.wantLen)
|
||||||
|
|
||||||
if tc.wantLen > 0 {
|
if tc.wantLen > 0 {
|
||||||
|
@ -205,6 +213,7 @@ func TestQueryLogOffsetLimit(t *testing.T) {
|
||||||
|
|
||||||
func TestQueryLogMaxFileScanEntries(t *testing.T) {
|
func TestQueryLogMaxFileScanEntries(t *testing.T) {
|
||||||
l, err := newQueryLog(Config{
|
l, err := newQueryLog(Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
FileEnabled: true,
|
FileEnabled: true,
|
||||||
RotationIvl: timeutil.Day,
|
RotationIvl: timeutil.Day,
|
||||||
|
@ -213,20 +222,21 @@ func TestQueryLogMaxFileScanEntries(t *testing.T) {
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
const entNum = 10
|
const entNum = 10
|
||||||
// Add entries to the log.
|
// Add entries to the log.
|
||||||
for range entNum {
|
for range entNum {
|
||||||
addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
addEntry(l, "example.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||||
}
|
}
|
||||||
// Write them to disk.
|
// Write them to disk.
|
||||||
require.NoError(t, l.flushLogBuffer())
|
require.NoError(t, l.flushLogBuffer(ctx))
|
||||||
|
|
||||||
params := newSearchParams()
|
params := newSearchParams()
|
||||||
|
|
||||||
for _, maxFileScanEntries := range []int{5, 0} {
|
for _, maxFileScanEntries := range []int{5, 0} {
|
||||||
t.Run(fmt.Sprintf("limit_%d", maxFileScanEntries), func(t *testing.T) {
|
t.Run(fmt.Sprintf("limit_%d", maxFileScanEntries), func(t *testing.T) {
|
||||||
params.maxFileScanEntries = maxFileScanEntries
|
params.maxFileScanEntries = maxFileScanEntries
|
||||||
entries, _ := l.search(params)
|
entries, _ := l.search(ctx, params)
|
||||||
assert.Len(t, entries, entNum-maxFileScanEntries)
|
assert.Len(t, entries, entNum-maxFileScanEntries)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -234,6 +244,7 @@ func TestQueryLogMaxFileScanEntries(t *testing.T) {
|
||||||
|
|
||||||
func TestQueryLogFileDisabled(t *testing.T) {
|
func TestQueryLogFileDisabled(t *testing.T) {
|
||||||
l, err := newQueryLog(Config{
|
l, err := newQueryLog(Config{
|
||||||
|
Logger: slogutil.NewDiscardLogger(),
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
FileEnabled: false,
|
FileEnabled: false,
|
||||||
RotationIvl: timeutil.Day,
|
RotationIvl: timeutil.Day,
|
||||||
|
@ -248,8 +259,10 @@ func TestQueryLogFileDisabled(t *testing.T) {
|
||||||
addEntry(l, "example3.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
addEntry(l, "example3.org", net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
|
||||||
|
|
||||||
params := newSearchParams()
|
params := newSearchParams()
|
||||||
ll, _ := l.search(params)
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
ll, _ := l.search(ctx, params)
|
||||||
require.Len(t, ll, 2)
|
require.Len(t, ll, 2)
|
||||||
|
|
||||||
assert.Equal(t, "example3.org", ll[0].QHost)
|
assert.Equal(t, "example3.org", ll[0].QHost)
|
||||||
assert.Equal(t, "example2.org", ll[1].QHost)
|
assert.Equal(t, "example2.org", ll[1].QHost)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -10,7 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -57,7 +59,6 @@ type qLogFile struct {
|
||||||
|
|
||||||
// newQLogFile initializes a new instance of the qLogFile.
|
// newQLogFile initializes a new instance of the qLogFile.
|
||||||
func newQLogFile(path string) (qf *qLogFile, err error) {
|
func newQLogFile(path string) (qf *qLogFile, err error) {
|
||||||
// Don't use [aghos.OpenFile] here, because the file is expected to exist.
|
|
||||||
f, err := os.OpenFile(path, os.O_RDONLY, aghos.DefaultPermFile)
|
f, err := os.OpenFile(path, os.O_RDONLY, aghos.DefaultPermFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -102,7 +103,11 @@ func (q *qLogFile) validateQLogLineIdx(lineIdx, lastProbeLineIdx, ts, fSize int6
|
||||||
// for so that when we call "ReadNext" this line was returned.
|
// for so that when we call "ReadNext" this line was returned.
|
||||||
// - Depth of the search (how many times we compared timestamps).
|
// - Depth of the search (how many times we compared timestamps).
|
||||||
// - If we could not find it, it returns one of the errors described above.
|
// - If we could not find it, it returns one of the errors described above.
|
||||||
func (q *qLogFile) seekTS(timestamp int64) (pos int64, depth int, err error) {
|
func (q *qLogFile) seekTS(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *slog.Logger,
|
||||||
|
timestamp int64,
|
||||||
|
) (pos int64, depth int, err error) {
|
||||||
q.lock.Lock()
|
q.lock.Lock()
|
||||||
defer q.lock.Unlock()
|
defer q.lock.Unlock()
|
||||||
|
|
||||||
|
@ -151,7 +156,7 @@ func (q *qLogFile) seekTS(timestamp int64) (pos int64, depth int, err error) {
|
||||||
lastProbeLineIdx = lineIdx
|
lastProbeLineIdx = lineIdx
|
||||||
|
|
||||||
// Get the timestamp from the query log record.
|
// Get the timestamp from the query log record.
|
||||||
ts := readQLogTimestamp(line)
|
ts := readQLogTimestamp(ctx, logger, line)
|
||||||
if ts == 0 {
|
if ts == 0 {
|
||||||
return 0, depth, fmt.Errorf(
|
return 0, depth, fmt.Errorf(
|
||||||
"looking up timestamp %d in %q: record %q has empty timestamp",
|
"looking up timestamp %d in %q: record %q has empty timestamp",
|
||||||
|
@ -385,20 +390,22 @@ func readJSONValue(s, prefix string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// readQLogTimestamp reads the timestamp field from the query log line.
|
// readQLogTimestamp reads the timestamp field from the query log line.
|
||||||
func readQLogTimestamp(str string) int64 {
|
func readQLogTimestamp(ctx context.Context, logger *slog.Logger, str string) int64 {
|
||||||
val := readJSONValue(str, `"T":"`)
|
val := readJSONValue(str, `"T":"`)
|
||||||
if len(val) == 0 {
|
if len(val) == 0 {
|
||||||
val = readJSONValue(str, `"Time":"`)
|
val = readJSONValue(str, `"Time":"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(val) == 0 {
|
if len(val) == 0 {
|
||||||
log.Error("Couldn't find timestamp: %s", str)
|
logger.ErrorContext(ctx, "couldn't find timestamp", "line", str)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
tm, err := time.Parse(time.RFC3339Nano, val)
|
tm, err := time.Parse(time.RFC3339Nano, val)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Couldn't parse timestamp: %s", val)
|
logger.ErrorContext(ctx, "couldn't parse timestamp", "value", val, slogutil.KeyError, err)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -24,6 +25,7 @@ func prepareTestFile(t *testing.T, dir string, linesNum int) (name string) {
|
||||||
|
|
||||||
f, err := os.CreateTemp(dir, "*.txt")
|
f, err := os.CreateTemp(dir, "*.txt")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Use defer and not t.Cleanup to make sure that the file is closed
|
// Use defer and not t.Cleanup to make sure that the file is closed
|
||||||
// after this function is done.
|
// after this function is done.
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -108,6 +110,7 @@ func TestQLogFile_ReadNext(t *testing.T) {
|
||||||
// Calculate the expected position.
|
// Calculate the expected position.
|
||||||
fileInfo, err := q.file.Stat()
|
fileInfo, err := q.file.Stat()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var expPos int64
|
var expPos int64
|
||||||
if expPos = fileInfo.Size(); expPos > 0 {
|
if expPos = fileInfo.Size(); expPos > 0 {
|
||||||
expPos--
|
expPos--
|
||||||
|
@ -129,6 +132,7 @@ func TestQLogFile_ReadNext(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
require.Equal(t, io.EOF, err)
|
require.Equal(t, io.EOF, err)
|
||||||
|
|
||||||
assert.Equal(t, tc.linesNum, read)
|
assert.Equal(t, tc.linesNum, read)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -146,6 +150,9 @@ func TestQLogFile_SeekTS_good(t *testing.T) {
|
||||||
num: 10,
|
num: 10,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
logger := slogutil.NewDiscardLogger()
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
for _, l := range linesCases {
|
for _, l := range linesCases {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -171,16 +178,19 @@ func TestQLogFile_SeekTS_good(t *testing.T) {
|
||||||
t.Run(l.name+"_"+tc.name, func(t *testing.T) {
|
t.Run(l.name+"_"+tc.name, func(t *testing.T) {
|
||||||
line, err := getQLogFileLine(q, tc.line)
|
line, err := getQLogFileLine(q, tc.line)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
ts := readQLogTimestamp(line)
|
|
||||||
|
ts := readQLogTimestamp(ctx, logger, line)
|
||||||
assert.NotEqualValues(t, 0, ts)
|
assert.NotEqualValues(t, 0, ts)
|
||||||
|
|
||||||
// Try seeking to that line now.
|
// Try seeking to that line now.
|
||||||
pos, _, err := q.seekTS(ts)
|
pos, _, err := q.seekTS(ctx, logger, ts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.NotEqualValues(t, 0, pos)
|
assert.NotEqualValues(t, 0, pos)
|
||||||
|
|
||||||
testLine, err := q.ReadNext()
|
testLine, err := q.ReadNext()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, line, testLine)
|
assert.Equal(t, line, testLine)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -199,6 +209,9 @@ func TestQLogFile_SeekTS_bad(t *testing.T) {
|
||||||
num: 10,
|
num: 10,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
logger := slogutil.NewDiscardLogger()
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
for _, l := range linesCases {
|
for _, l := range linesCases {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -221,14 +234,14 @@ func TestQLogFile_SeekTS_bad(t *testing.T) {
|
||||||
|
|
||||||
line, err := getQLogFileLine(q, l.num/2)
|
line, err := getQLogFileLine(q, l.num/2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testCases[2].ts = readQLogTimestamp(line) - 1
|
|
||||||
|
|
||||||
|
testCases[2].ts = readQLogTimestamp(ctx, logger, line) - 1
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
assert.NotEqualValues(t, 0, tc.ts)
|
assert.NotEqualValues(t, 0, tc.ts)
|
||||||
|
|
||||||
var depth int
|
var depth int
|
||||||
_, depth, err = q.seekTS(tc.ts)
|
_, depth, err = q.seekTS(ctx, logger, tc.ts)
|
||||||
assert.NotEmpty(t, l.num)
|
assert.NotEmpty(t, l.num)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
|
@ -262,11 +275,13 @@ func TestQLogFile(t *testing.T) {
|
||||||
// Seek to the start.
|
// Seek to the start.
|
||||||
pos, err := q.SeekStart()
|
pos, err := q.SeekStart()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Greater(t, pos, int64(0))
|
assert.Greater(t, pos, int64(0))
|
||||||
|
|
||||||
// Read first line.
|
// Read first line.
|
||||||
line, err := q.ReadNext()
|
line, err := q.ReadNext()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Contains(t, line, "0.0.0.2")
|
assert.Contains(t, line, "0.0.0.2")
|
||||||
assert.True(t, strings.HasPrefix(line, "{"), line)
|
assert.True(t, strings.HasPrefix(line, "{"), line)
|
||||||
assert.True(t, strings.HasSuffix(line, "}"), line)
|
assert.True(t, strings.HasSuffix(line, "}"), line)
|
||||||
|
@ -274,6 +289,7 @@ func TestQLogFile(t *testing.T) {
|
||||||
// Read second line.
|
// Read second line.
|
||||||
line, err = q.ReadNext()
|
line, err = q.ReadNext()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.EqualValues(t, 0, q.position)
|
assert.EqualValues(t, 0, q.position)
|
||||||
assert.Contains(t, line, "0.0.0.1")
|
assert.Contains(t, line, "0.0.0.1")
|
||||||
assert.True(t, strings.HasPrefix(line, "{"), line)
|
assert.True(t, strings.HasPrefix(line, "{"), line)
|
||||||
|
@ -282,12 +298,14 @@ func TestQLogFile(t *testing.T) {
|
||||||
// Try reading again (there's nothing to read anymore).
|
// Try reading again (there's nothing to read anymore).
|
||||||
line, err = q.ReadNext()
|
line, err = q.ReadNext()
|
||||||
require.Equal(t, io.EOF, err)
|
require.Equal(t, io.EOF, err)
|
||||||
|
|
||||||
assert.Empty(t, line)
|
assert.Empty(t, line)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestQLogFileData(t *testing.T, data string) (file *qLogFile) {
|
func newTestQLogFileData(t *testing.T, data string) (file *qLogFile) {
|
||||||
f, err := os.CreateTemp(t.TempDir(), "*.txt")
|
f, err := os.CreateTemp(t.TempDir(), "*.txt")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testutil.CleanupAndRequireSuccess(t, f.Close)
|
testutil.CleanupAndRequireSuccess(t, f.Close)
|
||||||
|
|
||||||
_, err = f.WriteString(data)
|
_, err = f.WriteString(data)
|
||||||
|
@ -295,6 +313,7 @@ func newTestQLogFileData(t *testing.T, data string) (file *qLogFile) {
|
||||||
|
|
||||||
file, err = newQLogFile(f.Name())
|
file, err = newQLogFile(f.Name())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testutil.CleanupAndRequireSuccess(t, file.Close)
|
testutil.CleanupAndRequireSuccess(t, file.Close)
|
||||||
|
|
||||||
return file
|
return file
|
||||||
|
@ -308,6 +327,9 @@ func TestQLog_Seek(t *testing.T) {
|
||||||
`{"T":"` + strV + `"}` + nl
|
`{"T":"` + strV + `"}` + nl
|
||||||
timestamp, _ := time.Parse(time.RFC3339Nano, "2020-08-31T18:44:25.376690873+03:00")
|
timestamp, _ := time.Parse(time.RFC3339Nano, "2020-08-31T18:44:25.376690873+03:00")
|
||||||
|
|
||||||
|
logger := slogutil.NewDiscardLogger()
|
||||||
|
ctx := testutil.ContextWithTimeout(t, testTimeout)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
wantErr error
|
wantErr error
|
||||||
name string
|
name string
|
||||||
|
@ -340,8 +362,10 @@ func TestQLog_Seek(t *testing.T) {
|
||||||
|
|
||||||
q := newTestQLogFileData(t, data)
|
q := newTestQLogFileData(t, data)
|
||||||
|
|
||||||
_, depth, err := q.seekTS(timestamp.Add(time.Second * time.Duration(tc.delta)).UnixNano())
|
ts := timestamp.Add(time.Second * time.Duration(tc.delta)).UnixNano()
|
||||||
|
_, depth, err := q.seekTS(ctx, logger, ts)
|
||||||
require.Truef(t, errors.Is(err, tc.wantErr), "%v", err)
|
require.Truef(t, errors.Is(err, tc.wantErr), "%v", err)
|
||||||
|
|
||||||
assert.Equal(t, tc.wantDepth, depth)
|
assert.Equal(t, tc.wantDepth, depth)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/logutil/slogutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// qLogReader allows reading from multiple query log files in the reverse
|
// qLogReader allows reading from multiple query log files in the reverse
|
||||||
|
@ -16,6 +18,10 @@ import (
|
||||||
// pointer to a particular query log file, and to a specific position in this
|
// pointer to a particular query log file, and to a specific position in this
|
||||||
// file, and it reads lines in reverse order starting from that position.
|
// file, and it reads lines in reverse order starting from that position.
|
||||||
type qLogReader struct {
|
type qLogReader struct {
|
||||||
|
// logger is used for logging the operation of the query log reader. It
|
||||||
|
// must not be nil.
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
// qFiles is an array with the query log files. The order is from oldest
|
// qFiles is an array with the query log files. The order is from oldest
|
||||||
// to newest.
|
// to newest.
|
||||||
qFiles []*qLogFile
|
qFiles []*qLogFile
|
||||||
|
@ -25,7 +31,7 @@ type qLogReader struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// newQLogReader initializes a qLogReader instance with the specified files.
|
// newQLogReader initializes a qLogReader instance with the specified files.
|
||||||
func newQLogReader(files []string) (*qLogReader, error) {
|
func newQLogReader(ctx context.Context, logger *slog.Logger, files []string) (*qLogReader, error) {
|
||||||
qFiles := make([]*qLogFile, 0)
|
qFiles := make([]*qLogFile, 0)
|
||||||
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
|
@ -38,7 +44,7 @@ func newQLogReader(files []string) (*qLogReader, error) {
|
||||||
// Close what we've already opened.
|
// Close what we've already opened.
|
||||||
cErr := closeQFiles(qFiles)
|
cErr := closeQFiles(qFiles)
|
||||||
if cErr != nil {
|
if cErr != nil {
|
||||||
log.Debug("querylog: closing files: %s", cErr)
|
logger.DebugContext(ctx, "closing files", slogutil.KeyError, cErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -47,16 +53,20 @@ func newQLogReader(files []string) (*qLogReader, error) {
|
||||||
qFiles = append(qFiles, q)
|
qFiles = append(qFiles, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &qLogReader{qFiles: qFiles, currentFile: len(qFiles) - 1}, nil
|
return &qLogReader{
|
||||||
|
logger: logger,
|
||||||
|
qFiles: qFiles,
|
||||||
|
currentFile: len(qFiles) - 1,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// seekTS performs binary search of a query log record with the specified
|
// seekTS performs binary search of a query log record with the specified
|
||||||
// timestamp. If the record is found, it sets qLogReader's position to point
|
// timestamp. If the record is found, it sets qLogReader's position to point
|
||||||
// to that line, so that the next ReadNext call returned this line.
|
// to that line, so that the next ReadNext call returned this line.
|
||||||
func (r *qLogReader) seekTS(timestamp int64) (err error) {
|
func (r *qLogReader) seekTS(ctx context.Context, timestamp int64) (err error) {
|
||||||
for i := len(r.qFiles) - 1; i >= 0; i-- {
|
for i := len(r.qFiles) - 1; i >= 0; i-- {
|
||||||
q := r.qFiles[i]
|
q := r.qFiles[i]
|
||||||
_, _, err = q.seekTS(timestamp)
|
_, _, err = q.seekTS(ctx, r.logger, timestamp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, errTSTooEarly) {
|
if errors.Is(err, errTSTooEarly) {
|
||||||
// Look at the next file, since we've reached the end of this
|
// Look at the next file, since we've reached the end of this
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue