Compare commits

..

1 Commits

Author SHA1 Message Date
Andrey Meshkov
d49d284d4a all: github action for potential dups and improved issue forms 2023-06-12 13:53:26 +03:00
80 changed files with 1390 additions and 3822 deletions

View File

@@ -10,58 +10,52 @@
- 'label': > - 'label': >
I have checked the I have checked the
[Wiki](https://github.com/AdguardTeam/AdGuardHome/wiki) and [Wiki](https://github.com/AdguardTeam/AdGuardHome/wiki) and
[Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a) [Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions)
and found no answer and found no answer
'required': true 'required': true
- 'label': > - 'label': >
I have searched other issues and found no duplicates I have searched other issues and found no duplicates
'required': true 'required': true
- 'label': > - 'label': >
I want to report a bug and not [ask a question or ask for I want to report a bug and not ask a question
help](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a)
'required': true
- 'label': >
I have set up AdGuard Home correctly and [configured clients to
use it](https://github.com/AdguardTeam/AdGuardHome/wiki/Clients).
(Use the
[Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a)
for help with installing and configuring clients.)
'required': true 'required': true
'id': 'prerequisites' 'id': 'prerequisites'
'type': 'checkboxes' 'type': 'checkboxes'
- 'attributes': - 'attributes':
'description': 'On which Platform does the issue occur?' 'description': 'On which operating system type does the issue occur?'
'label': 'Platform (OS and CPU architecture)' 'label': 'Operating system type'
'options': 'options':
- 'Darwin (aka macOS)/AMD64 (aka x86_64)' - 'FreeBSD'
- 'Darwin (aka macOS)/ARM64' - 'Linux, OpenWrt'
- 'FreeBSD/386' - 'Linux, Other (please mention the version in the description)'
- 'FreeBSD/AMD64 (aka x86_64)' - 'macOS (aka Darwin)'
- 'FreeBSD/ARM64' - 'OpenBSD'
- 'FreeBSD/ARMv5' - 'Windows'
- 'FreeBSD/ARMv6' - 'Other (please mention in the description)'
- 'FreeBSD/ARMv7'
- 'Linux/386'
- 'Linux/AMD64 (aka x86_64)'
- 'Linux/ARM64'
- 'Linux/ARMv5'
- 'Linux/ARMv6'
- 'Linux/ARMv7'
- 'Linux/MIPS LE'
- 'Linux/MIPS'
- 'Linux/MIPS64 LE'
- 'Linux/MIPS64'
- 'Linux/PPC64 LE'
- 'OpenBSD/AMD64 (aka x86_64)'
- 'OpenBSD/ARM64'
- 'Windows/386'
- 'Windows/AMD64 (aka x86_64)'
- 'Windows/ARM64'
- 'Custom (please mention in the description)'
'id': 'os' 'id': 'os'
'type': 'dropdown' 'type': 'dropdown'
'validations': 'validations':
'required': true 'required': true
- 'attributes':
'description': 'On which CPU architecture does the issue occur?'
'label': 'CPU architecture'
'options':
- 'AMD64'
- 'x86'
- '64-bit ARM'
- 'ARMv5'
- 'ARMv6'
- 'ARMv7'
- '64-bit MIPS'
- '64-bit MIPS LE'
- '32-bit MIPS'
- '32-bit MIPS LE'
- '64-bit PowerPC LE'
- 'Other (please mention in the description)'
'id': 'arch'
'type': 'dropdown'
'validations':
'required': true
- 'attributes': - 'attributes':
'description': 'How did you install AdGuard Home?' 'description': 'How did you install AdGuard Home?'
'label': 'Installation' 'label': 'Installation'
@@ -69,7 +63,7 @@
- 'GitHub releases or script from README' - 'GitHub releases or script from README'
- 'Docker' - 'Docker'
- 'Snapcraft' - 'Snapcraft'
- 'Custom package (OpenWrt, HomeAssistant, etc; please mention in the description)' - 'Custom port'
- 'Other (please mention in the description)' - 'Other (please mention in the description)'
'id': 'install' 'id': 'install'
'type': 'dropdown' 'type': 'dropdown'
@@ -95,55 +89,54 @@
'validations': 'validations':
'required': true 'required': true
- 'attributes': - 'attributes':
'description': > 'description': 'Please provide a set of steps to reproduce the issue'
Please describe what you did. An `nslookup` or a `dig` command is 'label': 'Issue Details'
the best way. For crashes, please provide a full failure log. 'placeholder':
'label': 'Action'
'value': | 'value': |
```sh Steps to reproduce:
nslookup -debug -type=a 'www.example.com' '$YOUR_AGH_ADDRESS' 1.
``` 2.
'id': 'failing_action' 3.
'id': 'what-happened'
'type': 'textarea' 'type': 'textarea'
'validations': 'validations':
'required': true 'required': true
- 'attributes': - 'attributes':
'description': > 'label': 'Expected Behavior'
What did you expect to see? Please add a description and/or 'description': 'Describe the expected behavior'
screenshots, if applicable. 'placeholder': |
'label': 'Expected result' A clear and concise description of what you expected to happen.
'placeholder': > 'id': 'expected-behavior'
What did you expect to see? 'type': 'textarea'
'id': 'expected' 'validations':
'required': false
- 'attributes':
'label': 'Actual Behavior'
'description': 'Describe the actual behavior'
'placeholder': 'A clear description of what happened instead.'
'id': 'actual-behavior'
'type': 'textarea' 'type': 'textarea'
'validations': 'validations':
'required': true 'required': true
- 'attributes': - 'attributes':
'description': > 'label': 'Screenshots'
What happened instead? Please add a description and/or screenshots, 'description': |
if applicable. If applicable add screenshots explaining your problem.
'label': 'Actual result' You can drag and drop images or paste them from clipboard.
'placeholder': > Use `<details> </details>` tag to hide screenshots under the spoiler.
What did you see instead? 'placeholder': 'If applicable add screenshots explaining your problem.'
'id': 'result' 'value': |
'type': 'textarea' <details><summary>Screenshot 1:</summary>
'validations':
'required': true <!-- paste screenshot here -->
- 'attributes':
'description': > </details>
Please add additional information, such as non-standard OS or port, 'id': 'screenshots'
here. You can also put screenshots here, if applicable. For
example, it is better to copy and paste text from a terminal instead
of posting a screenshot of the terminal.
'label': 'Additional information and/or screenshots'
'placeholder': >
Additional OS information, screenshots of the UI, etc.
'id': 'additional'
'type': 'textarea' 'type': 'textarea'
'validations': 'validations':
'required': false 'required': false
'description': > 'description': >
Open a bug report. Please do not open bug reports for questions or help Open a bug report. Please do not open bug reports for questions. If you
with configuring clients. If you want to ask for help, use the Discussions want to ask a question, ask in in the Discussions section.
section. 'name': 'Bug report'
'name': 'Bug' 'labels': [ 'bug' ]

View File

@@ -23,32 +23,32 @@
'id': 'prerequisites' 'id': 'prerequisites'
'type': 'checkboxes' 'type': 'checkboxes'
- 'attributes': - 'attributes':
'description': 'Please describe the problem you are trying to solve' 'description': 'What happened?'
'label': 'The problem' 'placeholder': |
'placeholder': > Is your feature request related to a problem? Please add a clear and
Please describe the problem you are trying to solve concise description of what the problem is.
'id': 'problem' 'label': 'Issue Details'
'id': 'what-happened'
'type': 'textarea' 'type': 'textarea'
'validations': 'validations':
'required': true 'required': true
- 'attributes': - 'attributes':
'description': 'What feature are you proposing to solve this problem?' 'description': 'What do you propose to change in AdGuard Home?'
'placeholder': |
Describe the solution you'd like in a clear and concise manner.
'label': 'Proposed solution' 'label': 'Proposed solution'
'placeholder': > 'id': 'proposed-solution'
What feature are you proposing to solve this problem?
'id': 'proposed_solution'
'type': 'textarea' 'type': 'textarea'
'validations': 'validations':
'required': true 'required': true
- 'attributes': - 'attributes':
'label': 'Alternatives considered and additional information' 'description': 'Are there any other ways to solve the problem?'
'placeholder': > 'placeholder': |
Are there any other ways to solve the problem? A clear and concise description of any alternative solutions or features
'id': 'additional' you've considered.
'label': 'Alternative solution'
'id': 'alternative-solution'
'type': 'textarea' 'type': 'textarea'
'validations':
'required': false
'description': 'Suggest a feature or an enhancement for AdGuard Home' 'description': 'Suggest a feature or an enhancement for AdGuard Home'
'labels':
- 'feature request'
'name': 'Feature request or enhancement' 'name': 'Feature request or enhancement'
'labels': [ 'feature request' ]

View File

@@ -1,20 +0,0 @@
Before submitting a PR please make sure that:
1. You have discussed your solution in an issue and have got an
approval from a maintainer.
2. This isn't a localization fix; please send those to our
[CrowdIn](https://crowdin.com/project/adguard-applications/en#/adguard-home)
page.
3. Your code follows our
[code guidelines](https://github.com/AdguardTeam/CodeGuidelines/blob/master/Go/Go.md).
Add a short description here. The description should include:
1. Which issue this PR closes (`Closes #NNNN.`) or updates (`Updates
#NNNN.`).
2. A short description of how the change achieves that.
Do not forget to remove these instructions.

View File

@@ -1,18 +1,34 @@
'name': 'potential-duplicates' 'name': 'potential-duplicates'
'on': 'on':
'issues': 'issues':
'types': 'types': [ 'opened', 'edited' ]
- 'opened'
'jobs': 'jobs':
'run': 'run':
'runs-on': 'ubuntu-latest' 'runs-on': 'ubuntu-latest'
'steps': 'steps':
- 'uses': 'wow-actions/potential-duplicates@v1' - 'uses': 'wow-actions/potential-duplicates@v1'
'with': 'with':
'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}' 'GITHUB_TOKEN': ${{ secrets.GITHUB_TOKEN }}
# Issue title filter work with https://www.npmjs.com/package/anymatch.
# Any matched issue will stop detection immediately.
# You can specify multi filters in each line.
# 'filter': ''
# Exclude keywords in title before detecting.
# 'exclude': ''
# Label to set, when potential duplicates are detected.
'label': 'potential-duplicate'
# Get issues with state to compare. Supported state: 'all', 'closed',
# 'open'.
'state': 'all' 'state': 'all'
# If similarity is higher than this threshold([0,1]), issue will be
# marked as duplicate.
'threshold': 0.6 'threshold': 0.6
'comment': | # Reactions to be add to comment when potential duplicates are
# detected. Available reactions: "-1", "+1", "confused", "laugh",
# "heart", "hooray", "rocket", "eyes".
# 'reactions': 'eyes, confused'
# Comment to post when potential duplicates are detected.
'comment': >
Potential duplicates: {{#issues}} Potential duplicates: {{#issues}}
* [#{{ number }}] {{ title }} ({{ accuracy }}%) - [#{{ number }}] {{ title }} ({{ accuracy }}%)
{{/issues}} {{/issues}}

View File

@@ -14,101 +14,30 @@ and this project adheres to
<!-- <!--
## [v0.108.0] - TBA ## [v0.108.0] - TBA
## [v0.107.33] - 2023-06-28 (APPROX.) ## [v0.107.32] - 2023-06-28 (APPROX.)
See also the [v0.107.33 GitHub milestone][ms-v0.107.33]. See also the [v0.107.32 GitHub milestone][ms-v0.107.32].
[ms-v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1 [ms-v0.107.32]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
NOTE: Add new changes BELOW THIS COMMENT. NOTE: Add new changes BELOW THIS COMMENT.
--> -->
### Added ### Added
- The new HTTP API, `GET /control/querylog/export`, which can be used to
export query log items. See `openapi/openapi.yaml` for the full description
([#3389]).
- The ability to set inactivity periods for filtering blocked services in the
configuration file ([#951]). The UI changes are coming in the upcoming
releases.
- The ability to edit rewrite rules via `PUT /control/rewrite/update` HTTP API - The ability to edit rewrite rules via `PUT /control/rewrite/update` HTTP API
([#1577]). ([#1577]).
### Changed
#### Configuration Changes
In this release, the schema version has changed from 20 to 21.
- Property `dns.blocked_services`, which in schema versions 20 and earlier used
to be a list containing ids of blocked services, is now an object containing
ids and schedule for blocked services:
```yaml
# BEFORE:
'blocked_services':
- id_1
- id_2
# AFTER:
'blocked_services':
'ids':
- id_1
- id_2
'schedule':
'time_zone': 'Local'
'sun':
'start': '0s'
'end': '24h'
'mon':
'start': '10m'
'end': '23h30m'
'tue':
'start': '20m'
'end': '23h'
'wed':
'start': '30m'
'end': '22h30m'
'thu':
'start': '40m'
'end': '22h'
'fri':
'start': '50m'
'end': '21h30m'
'sat':
'start': '1h'
'end': '21h'
```
To rollback this change, replace `dns.blocked_services` object with the list
of ids of blocked services and change the `schema_version` back to `20`.
### Fixed
- Queries with the question-section target `.`, for example `NS .`, are now
counted in the statistics and correctly shown in the query log ([#5910]).
- Safe Search not working with `AAAA` queries for domains that don't have `AAAA`
records ([#5913]).
[#951]: https://github.com/AdguardTeam/AdGuardHome/issues/951
[#1577]: https://github.com/AdguardTeam/AdGuardHome/issues/1577
[#3389]: https://github.com/AdguardTeam/AdGuardHome/issues/3389
[#5910]: https://github.com/AdguardTeam/AdGuardHome/issues/5910
[#5913]: https://github.com/AdguardTeam/AdGuardHome/issues/5913
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
## [v0.107.32] - 2023-06-13
### Fixed ### Fixed
- DNSCrypt upstream not resetting the client and resolver information on - DNSCrypt upstream not resetting the client and resolver information on
dialing errors ([#5872]). dialing errors ([#5872]).
[#1577]: https://github.com/AdguardTeam/AdGuardHome/issues/1577
<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
@@ -2080,12 +2009,11 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
<!-- <!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.33...HEAD
[v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...v0.107.33
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...HEAD [Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.32...HEAD
[v0.107.32]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.31...v0.107.32 [v0.107.32]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.31...v0.107.32
-->
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.31...HEAD
[v0.107.31]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.30...v0.107.31 [v0.107.31]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.30...v0.107.31
[v0.107.30]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...v0.107.30 [v0.107.30]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.29...v0.107.30
[v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...v0.107.29 [v0.107.29]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.28...v0.107.29

View File

@@ -1,8 +1,5 @@
---
!include release.yaml
---
!include snapcraft.yaml
--- ---
!include test.yaml !include test.yaml
---
!include release.yaml

View File

@@ -34,6 +34,12 @@
'jobs': 'jobs':
- 'Publish to static storage' - 'Publish to static storage'
- 'Publish to Snapstore':
'manual': false
'final': false
'jobs':
- 'Publish to Snapstore'
- 'Publish to GitHub Releases': - 'Publish to GitHub Releases':
'manual': false 'manual': false
'final': false 'final': false
@@ -65,7 +71,7 @@
make js-deps js-build make js-deps js-build
'artifacts': 'artifacts':
- 'name': 'AdGuardHome frontend' - 'name': 'AdGuardHome frontend'
'pattern': 'build/**' 'pattern': 'build*/**'
'shared': true 'shared': true
'required': true 'required': true
'requirements': 'requirements':
@@ -198,6 +204,58 @@
'requirements': 'requirements':
- 'adg-docker': 'true' - 'adg-docker': 'true'
'Publish to Snapstore':
'docker':
'image': '${bamboo.dockerGo}'
'key': 'PTS'
'other':
'clean-working-dir': true
'tasks':
- 'clean'
- 'checkout':
'repository': 'bamboo-deploy-publisher'
'path': 'bamboo-deploy-publisher'
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
cd ./dist/
channel="${bamboo.channel}"
readonly channel
case "$channel"
in
('release')
snapchannel='candidate'
;;
('beta')
snapchannel='beta'
;;
('edge')
snapchannel='edge'
;;
(*)
echo "invalid channel '$channel'"
exit 1
;;
esac
env\
SNAPCRAFT_CHANNEL="$snapchannel"\
SNAPCRAFT_EMAIL="${bamboo.snapcraftEmail}"\
SNAPCRAFT_STORE_CREDENTIALS="${bamboo.snapcraftMacaroonPassword}"\
../bamboo-deploy-publisher/deploy.sh adguard-home-snap
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
'Publish to GitHub Releases': 'Publish to GitHub Releases':
'key': 'PTGR' 'key': 'PTGR'
'other': 'other':
@@ -237,8 +295,8 @@
- 'adg-docker': 'true' - 'adg-docker': 'true'
'triggers': 'triggers':
# Don't use minute values that end with a zero or a five as these are often # Don't use minute values that end with a zero or a five as these are often used
# used in CI and so resources during these minutes can be quite busy. # in CI and so resources during these minutes can be quite busy.
- 'cron': '0 42 13 ? * MON-FRI *' - 'cron': '0 42 13 ? * MON-FRI *'
'branches': 'branches':
'create': 'manually' 'create': 'manually'
@@ -263,8 +321,8 @@
'concurrent-build-plugin': 'system-default' 'concurrent-build-plugin': 'system-default'
'branch-overrides': 'branch-overrides':
# beta-vX.Y branches are the branches into which the commits that are needed # beta-vX.Y branches are the branches into which the commits that are needed to
# to release a new patch version are initially cherry-picked. # release a new patch version are initially cherry-picked.
- '^beta-v[0-9]+\.[0-9]+': - '^beta-v[0-9]+\.[0-9]+':
# Build betas on release branches manually. # Build betas on release branches manually.
'triggers': [] 'triggers': []
@@ -273,8 +331,8 @@
'variables': 'variables':
'channel': 'beta' 'channel': 'beta'
'dockerGo': 'adguard/golang-ubuntu:6.7' 'dockerGo': 'adguard/golang-ubuntu:6.7'
# 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
# release is built. # is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+': - '^release-v[0-9]+\.[0-9]+\.[0-9]+':
# Disable integration branches for release branches. # Disable integration branches for release branches.
'branch-config': 'branch-config':

View File

@@ -1,211 +0,0 @@
---
# This part of the release build is separate from the one described in
# release.yaml, because the Snapcraft infrastructure is brittle, and timeouts
# during logins and uploads often lead to release blocking.
'version': 2
'plan':
'project-key': 'AGH'
'key': 'AGHSNAP'
'name': 'AdGuard Home - Build and publish Snapcraft release'
# Make sure to sync any changes with the branch overrides below.
'variables':
'channel': 'edge'
'dockerGo': 'adguard/golang-ubuntu:6.7'
'snapcraftChannel': 'edge'
'stages':
- 'Download release':
'manual': false
'final': false
'jobs':
- 'Download release'
- 'Build packages':
'manual': false
'final': false
'jobs':
- 'Build packages'
- 'Publish to Snapstore':
'manual': false
'final': false
'jobs':
- 'Publish to Snapstore'
# TODO(a.garipov): Consider using the Artifact Downloader Task if it ever learns
# about plan branches.
'Download release':
'artifacts':
- 'name': 'i386_binary'
'pattern': 'AdGuardHome_i386'
'shared': true
'required': true
- 'name': 'amd64_binary'
'pattern': 'AdGuardHome_amd64'
'shared': true
'required': true
- 'name': 'armhf_binary'
'pattern': 'AdGuardHome_armhf'
'shared': true
'required': true
- 'name': 'arm64_binary'
'pattern': 'AdGuardHome_arm64'
'shared': true
'required': true
'docker':
'image': '${bamboo.dockerGo}'
'key': 'DR'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
env\
CHANNEL="${bamboo.channel}"\
VERBOSE='1'\
sh ./scripts/snap/download.sh
'requirements':
- 'adg-docker': 'true'
'Build packages':
'artifact-subscriptions':
- 'artifact': 'i386_binary'
- 'artifact': 'amd64_binary'
- 'artifact': 'armhf_binary'
- 'artifact': 'arm64_binary'
'artifacts':
- 'name': 'i386_snap'
'pattern': 'AdGuardHome_i386.snap'
'shared': true
'required': true
- 'name': 'amd64_snap'
'pattern': 'AdGuardHome_amd64.snap'
'shared': true
'required': true
- 'name': 'armhf_snap'
'pattern': 'AdGuardHome_armhf.snap'
'shared': true
'required': true
- 'name': 'arm64_snap'
'pattern': 'AdGuardHome_arm64.snap'
'shared': true
'required': true
'docker':
'image': '${bamboo.dockerGo}'
'key': 'BP'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
env\
VERBOSE='1'\
sh ./scripts/snap/build.sh
'requirements':
- 'adg-docker': 'true'
'Publish to Snapstore':
'artifact-subscriptions':
- 'artifact': 'i386_snap'
- 'artifact': 'amd64_snap'
- 'artifact': 'armhf_snap'
- 'artifact': 'arm64_snap'
'docker':
'image': '${bamboo.dockerGo}'
'key': 'PTS'
'other':
'clean-working-dir': true
'tasks':
- 'checkout':
'force-clean-build': true
- 'script':
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
env\
SNAPCRAFT_CHANNEL="${bamboo.snapcraftChannel}"\
SNAPCRAFT_STORE_CREDENTIALS="${bamboo.snapcraftMacaroonPassword}"\
VERBOSE='1'\
sh ./scripts/snap/upload.sh
'final-tasks':
- 'clean'
'requirements':
- 'adg-docker': 'true'
'triggers':
# Don't use minute values that end with a zero or a five as these are often
# used in CI and so resources during these minutes can be quite busy.
#
# NOTE: The time is chosen to be exactly one hour after the main release
# build as defined as in release.yaml.
- 'cron': '0 42 14 ? * MON-FRI *'
'branches':
'create': 'manually'
'delete':
'after-deleted-days': 1
'after-inactive-days': 30
'integration':
'push-on-success': false
'merge-from': 'AdGuard Home - Build and publish Snapcraft release'
'link-to-jira': true
'notifications':
- 'events':
- 'plan-completed'
'recipients':
- 'webhook':
'name': 'Build webhook'
'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo?channel=adguard-qa'
'labels': []
'other':
'concurrent-build-plugin': 'system-default'
'branch-overrides':
# beta-vX.Y branches are the branches into which the commits that are needed
# to release a new patch version are initially cherry-picked.
- '^beta-v[0-9]+\.[0-9]+':
# Build betas on release branches manually.
'triggers': []
# Set the default release channel on the release branch to beta, as we may
# need to build a few of these.
'variables':
'channel': 'beta'
'dockerGo': 'adguard/golang-ubuntu:6.7'
'snapcraftChannel': 'beta'
# release-vX.Y.Z branches are the branches from which the actual final
# release is built.
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
# Disable integration branches for release branches.
'branch-config':
'integration':
'push-on-success': false
'merge-from': 'beta-v0.107'
# Build final releases on release branches manually.
'triggers': []
# Set the default release channel on the final branch to release, as these
# are the ones that actually get released.
'variables':
'channel': 'release'
'dockerGo': 'adguard/golang-ubuntu:6.7'
'snapcraftChannel': 'candidate'

14
go.mod
View File

@@ -4,11 +4,10 @@ go 1.19
require ( require (
github.com/AdguardTeam/dnsproxy v0.50.2 github.com/AdguardTeam/dnsproxy v0.50.2
github.com/AdguardTeam/golibs v0.13.3 github.com/AdguardTeam/golibs v0.13.2
github.com/AdguardTeam/urlfilter v0.16.1 github.com/AdguardTeam/urlfilter v0.16.1
github.com/NYTimes/gziphandler v1.1.1 github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.2.7 github.com/ameshkov/dnscrypt/v2 v2.2.7
github.com/bluele/gcache v0.0.2
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/dimfeld/httptreemux/v5 v5.5.0
github.com/fsnotify/fsnotify v1.6.0 github.com/fsnotify/fsnotify v1.6.0
@@ -28,13 +27,13 @@ require (
github.com/mdlayher/raw v0.1.0 github.com/mdlayher/raw v0.1.0
github.com/miekg/dns v1.1.54 github.com/miekg/dns v1.1.54
github.com/quic-go/quic-go v0.35.1 github.com/quic-go/quic-go v0.35.1
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.2
github.com/ti-mo/netfilter v0.5.0 github.com/ti-mo/netfilter v0.5.0
go.etcd.io/bbolt v1.3.7 go.etcd.io/bbolt v1.3.7
golang.org/x/crypto v0.10.0 golang.org/x/crypto v0.9.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.11.0 golang.org/x/net v0.10.0
golang.org/x/sys v0.9.0 golang.org/x/sys v0.8.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.0 howett.net/plist v1.0.0
@@ -45,6 +44,7 @@ require (
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
github.com/ameshkov/dnsstamps v1.0.3 // indirect github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
github.com/bluele/gcache v0.0.2 // 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 v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
@@ -61,6 +61,6 @@ require (
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
golang.org/x/mod v0.10.0 // indirect golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.2.0 // indirect golang.org/x/sync v0.2.0 // indirect
golang.org/x/text v0.10.0 // indirect golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.9.3 // indirect golang.org/x/tools v0.9.3 // indirect
) )

28
go.sum
View File

@@ -2,8 +2,8 @@ github.com/AdguardTeam/dnsproxy v0.50.2 h1:p1471SsMZ6SMo7T51Olw4aNluahvMwSLMorwx
github.com/AdguardTeam/dnsproxy v0.50.2/go.mod h1:CQhZTkqC8X0ID6glrtyaxgqRRdiYfn1gJulC1cZ5Dn8= github.com/AdguardTeam/dnsproxy v0.50.2/go.mod h1:CQhZTkqC8X0ID6glrtyaxgqRRdiYfn1gJulC1cZ5Dn8=
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4= github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw= github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.13.3 h1:RT3QbzThtaLiFLkIUDS6/hlGEXrh0zYvdf4bd7UWpGo= github.com/AdguardTeam/golibs v0.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=
github.com/AdguardTeam/golibs v0.13.3/go.mod h1:wkJ6EUsN4np/9Gp7+9QeooY9E2U2WCLJYAioLCzkHsI= github.com/AdguardTeam/golibs v0.13.2/go.mod h1:7ylQLv2Lqsc3UW3jHoITynYk6Y1tYtgEMkR09ppfsN8=
github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU= github.com/AdguardTeam/gomitmproxy v0.2.0/go.mod h1:Qdv0Mktnzer5zpdpi5rAwixNJzW2FN91LjKJCkVbYGU=
github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw= github.com/AdguardTeam/urlfilter v0.16.1 h1:ZPi0rjqo8cQf2FVdzo6cqumNoHZx2KPXj2yZa1A5BBw=
github.com/AdguardTeam/urlfilter v0.16.1/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI= github.com/AdguardTeam/urlfilter v0.16.1/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
@@ -113,13 +113,17 @@ github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5
github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA= github.com/shirou/gopsutil/v3 v3.21.8 h1:nKct+uP0TV8DjjNiHanKf8SAuub+GNsbrOtM9Nl9biA=
github.com/shirou/gopsutil/v3 v3.21.8/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ= github.com/shirou/gopsutil/v3 v3.21.8/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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.0 h1:MZmsUw5bFRecOb0AeyjOPxTHg4UxYzyEs0Ek/6Lxoy8= github.com/ti-mo/netfilter v0.5.0 h1:MZmsUw5bFRecOb0AeyjOPxTHg4UxYzyEs0Ek/6Lxoy8=
github.com/ti-mo/netfilter v0.5.0/go.mod h1:nt+8B9hx/QpqHr7Hazq+2qMCCA8u2OTkyc/7+U9ARz8= github.com/ti-mo/netfilter v0.5.0/go.mod h1:nt+8B9hx/QpqHr7Hazq+2qMCCA8u2OTkyc/7+U9ARz8=
@@ -134,8 +138,8 @@ go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
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.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
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=
@@ -152,8 +156,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
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.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
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.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
@@ -177,16 +181,16 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/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.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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=

View File

@@ -0,0 +1,17 @@
//go:build windows
package aghnet
import (
"net"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
)
// listenPacketReusable announces on the local network address additionally
// configuring the socket to have a reusable binding.
func listenPacketReusable(_, _, _ string) (c net.PacketConn, err error) {
// TODO(e.burkov): Check if we are able to control sockets on Windows
// in the same way as on Unix.
return nil, aghos.Unsupported("listening packet reusable")
}

View File

@@ -0,0 +1,15 @@
//go:build windows
package dhcpd
import (
"net"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"golang.org/x/net/ipv4"
)
// Create a socket for receiving broadcast packets
func newBroadcastPacketConn(_ net.IP, _ int, _ string) (*ipv4.PacketConn, error) {
return nil, aghos.Unsupported("newBroadcastPacketConn")
}

View File

@@ -53,14 +53,14 @@ func (s *Server) beforeRequestHandler(
// getClientRequestFilteringSettings looks up client filtering settings using // getClientRequestFilteringSettings looks up client filtering settings using
// the client's IP address and ID, if any, from dctx. // the client's IP address and ID, if any, from dctx.
func (s *Server) getClientRequestFilteringSettings(dctx *dnsContext) *filtering.Settings { func (s *Server) getClientRequestFilteringSettings(dctx *dnsContext) *filtering.Settings {
setts := s.dnsFilter.Settings() setts := s.dnsFilter.GetConfig()
setts.ProtectionEnabled = dctx.protectionEnabled setts.ProtectionEnabled = dctx.protectionEnabled
if s.conf.FilterHandler != nil { if s.conf.FilterHandler != nil {
ip, _ := netutil.IPAndPortFromAddr(dctx.proxyCtx.Addr) ip, _ := netutil.IPAndPortFromAddr(dctx.proxyCtx.Addr)
s.conf.FilterHandler(ip, dctx.clientID, setts) s.conf.FilterHandler(ip, dctx.clientID, &setts)
} }
return setts return &setts
} }
// filterDNSRequest applies the dnsFilter and sets dctx.proxyCtx.Res if the // filterDNSRequest applies the dnsFilter and sets dctx.proxyCtx.Res if the

View File

@@ -57,13 +57,16 @@ func (s *Server) genDNSFilterMessage(
return s.genBlockedHost(req, s.conf.SafeBrowsingBlockHost, dctx) return s.genBlockedHost(req, s.conf.SafeBrowsingBlockHost, dctx)
case filtering.FilteredParental: case filtering.FilteredParental:
return s.genBlockedHost(req, s.conf.ParentalBlockHost, dctx) return s.genBlockedHost(req, s.conf.ParentalBlockHost, dctx)
case filtering.FilteredSafeSearch:
// If Safe Search generated the necessary IP addresses, use them.
// Otherwise, if there were no errors, there are no addresses for the
// requested IP version, so produce a NODATA response.
return s.genResponseWithIPs(req, ipsFromRules(res.Rules))
default: default:
return s.genForBlockingMode(req, ipsFromRules(res.Rules)) // If the query was filtered by Safe Search, filtering also must return
// the IP addresses that must be used in response. Return them
// regardless of the filtering method.
ips := ipsFromRules(res.Rules)
if res.Reason == filtering.FilteredSafeSearch && len(ips) > 0 {
return s.genResponseWithIPs(req, ips)
}
return s.genForBlockingMode(req, ips)
} }
} }

View File

@@ -123,10 +123,7 @@ func (s *Server) updateStats(
pctx := ctx.proxyCtx pctx := ctx.proxyCtx
e := stats.Entry{} e := stats.Entry{}
e.Domain = strings.ToLower(pctx.Req.Question[0].Name) e.Domain = strings.ToLower(pctx.Req.Question[0].Name)
if e.Domain != "." { e.Domain = e.Domain[:len(e.Domain)-1] // remove last "."
// Remove last ".", but save the domain as is for "." queries.
e.Domain = e.Domain[:len(e.Domain)-1]
}
if clientID := ctx.clientID; clientID != "" { if clientID := ctx.clientID; clientID != "" {
e.Client = clientID e.Client = clientID

View File

@@ -46,10 +46,6 @@ type testStats struct {
// Update implements the [stats.Interface] interface for *testStats. // Update implements the [stats.Interface] interface for *testStats.
func (l *testStats) Update(e stats.Entry) { func (l *testStats) Update(e stats.Entry) {
if e.Domain == "" {
return
}
l.lastEntry = e l.lastEntry = e
} }
@@ -58,12 +54,9 @@ func (l *testStats) ShouldCount(string, uint16, uint16, []string) bool {
return true return true
} }
func TestServer_ProcessQueryLogsAndStats(t *testing.T) { func TestProcessQueryLogsAndStats(t *testing.T) {
const domain = "example.com."
testCases := []struct { testCases := []struct {
name string name string
domain string
proto proxy.Proto proto proxy.Proto
addr net.Addr addr net.Addr
clientID string clientID string
@@ -74,7 +67,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult stats.Result wantStatResult stats.Result
}{{ }{{
name: "success_udp", name: "success_udp",
domain: domain,
proto: proxy.ProtoUDP, proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "", clientID: "",
@@ -85,7 +77,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered, wantStatResult: stats.RNotFiltered,
}, { }, {
name: "success_tls_clientid", name: "success_tls_clientid",
domain: domain,
proto: proxy.ProtoTLS, proto: proxy.ProtoTLS,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "cli42", clientID: "cli42",
@@ -96,7 +87,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered, wantStatResult: stats.RNotFiltered,
}, { }, {
name: "success_tls", name: "success_tls",
domain: domain,
proto: proxy.ProtoTLS, proto: proxy.ProtoTLS,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "", clientID: "",
@@ -107,7 +97,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered, wantStatResult: stats.RNotFiltered,
}, { }, {
name: "success_quic", name: "success_quic",
domain: domain,
proto: proxy.ProtoQUIC, proto: proxy.ProtoQUIC,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "", clientID: "",
@@ -118,7 +107,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered, wantStatResult: stats.RNotFiltered,
}, { }, {
name: "success_https", name: "success_https",
domain: domain,
proto: proxy.ProtoHTTPS, proto: proxy.ProtoHTTPS,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "", clientID: "",
@@ -129,7 +117,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered, wantStatResult: stats.RNotFiltered,
}, { }, {
name: "success_dnscrypt", name: "success_dnscrypt",
domain: domain,
proto: proxy.ProtoDNSCrypt, proto: proxy.ProtoDNSCrypt,
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "", clientID: "",
@@ -140,7 +127,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RNotFiltered, wantStatResult: stats.RNotFiltered,
}, { }, {
name: "success_udp_filtered", name: "success_udp_filtered",
domain: domain,
proto: proxy.ProtoUDP, proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "", clientID: "",
@@ -151,7 +137,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RFiltered, wantStatResult: stats.RFiltered,
}, { }, {
name: "success_udp_sb", name: "success_udp_sb",
domain: domain,
proto: proxy.ProtoUDP, proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "", clientID: "",
@@ -162,7 +147,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RSafeBrowsing, wantStatResult: stats.RSafeBrowsing,
}, { }, {
name: "success_udp_ss", name: "success_udp_ss",
domain: domain,
proto: proxy.ProtoUDP, proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "", clientID: "",
@@ -173,7 +157,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantStatResult: stats.RSafeSearch, wantStatResult: stats.RSafeSearch,
}, { }, {
name: "success_udp_pc", name: "success_udp_pc",
domain: domain,
proto: proxy.ProtoUDP, proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234}, addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
clientID: "", clientID: "",
@@ -182,17 +165,6 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
wantCode: resultCodeSuccess, wantCode: resultCodeSuccess,
reason: filtering.FilteredParental, reason: filtering.FilteredParental,
wantStatResult: stats.RParental, wantStatResult: stats.RParental,
}, {
name: "success_udp_pc_empty_fqdn",
domain: ".",
proto: proxy.ProtoUDP,
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 5}, Port: 1234},
clientID: "",
wantLogProto: "",
wantStatClient: "1.2.3.5",
wantCode: resultCodeSuccess,
reason: filtering.FilteredParental,
wantStatResult: stats.RParental,
}} }}
ups, err := upstream.AddressToUpstream("1.1.1.1", nil) ups, err := upstream.AddressToUpstream("1.1.1.1", nil)
@@ -209,7 +181,7 @@ func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
req := &dns.Msg{ req := &dns.Msg{
Question: []dns.Question{{ Question: []dns.Question{{
Name: tc.domain, Name: "example.com.",
}}, }},
} }
pctx := &proxy.DNSContext{ pctx := &proxy.DNSContext{

View File

@@ -3,10 +3,8 @@ package filtering
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/urlfilter/rules" "github.com/AdguardTeam/urlfilter/rules"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@@ -46,15 +44,6 @@ func initBlockedServices() {
log.Debug("filtering: initialized %d services", l) log.Debug("filtering: initialized %d services", l)
} }
// BlockedServices is the configuration of blocked services.
type BlockedServices struct {
// Schedule is blocked services schedule for every day of the week.
Schedule *schedule.Weekly `yaml:"schedule"`
// IDs is the names of blocked services.
IDs []string `yaml:"ids"`
}
// BlockedSvcKnown returns true if a blocked service ID is known. // BlockedSvcKnown returns true if a blocked service ID is known.
func BlockedSvcKnown(s string) (ok bool) { func BlockedSvcKnown(s string) (ok bool) {
_, ok = serviceRules[s] _, ok = serviceRules[s]
@@ -63,22 +52,15 @@ func BlockedSvcKnown(s string) (ok bool) {
} }
// ApplyBlockedServices - set blocked services settings for this DNS request // ApplyBlockedServices - set blocked services settings for this DNS request
func (d *DNSFilter) ApplyBlockedServices(setts *Settings) { func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
setts.ServicesRules = []ServiceEntry{}
if list == nil {
d.confLock.RLock() d.confLock.RLock()
defer d.confLock.RUnlock() defer d.confLock.RUnlock()
setts.ServicesRules = []ServiceEntry{} list = d.Config.BlockedServices
bsvc := d.BlockedServices
// TODO(s.chzhen): Use startTime from [dnsforward.dnsContext].
if !bsvc.Schedule.Contains(time.Now()) {
d.ApplyBlockedServicesList(setts, bsvc.IDs)
}
} }
// ApplyBlockedServicesList appends filtering rules to the settings.
func (d *DNSFilter) ApplyBlockedServicesList(setts *Settings, list []string) {
for _, name := range list { for _, name := range list {
rules, ok := serviceRules[name] rules, ok := serviceRules[name]
if !ok { if !ok {
@@ -108,7 +90,7 @@ func (d *DNSFilter) handleBlockedServicesAll(w http.ResponseWriter, r *http.Requ
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) { func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
d.confLock.RLock() d.confLock.RLock()
list := d.Config.BlockedServices.IDs list := d.Config.BlockedServices
d.confLock.RUnlock() d.confLock.RUnlock()
_ = aghhttp.WriteJSONResponse(w, r, list) _ = aghhttp.WriteJSONResponse(w, r, list)
@@ -124,7 +106,7 @@ func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ
} }
d.confLock.Lock() d.confLock.Lock()
d.Config.BlockedServices.IDs = list d.Config.BlockedServices = list
d.confLock.Unlock() d.confLock.Unlock()
log.Debug("Updated blocked services list: %d", len(list)) log.Debug("Updated blocked services list: %d", len(list))

View File

@@ -103,9 +103,9 @@ type Config struct {
Rewrites []*LegacyRewrite `yaml:"rewrites"` Rewrites []*LegacyRewrite `yaml:"rewrites"`
// BlockedServices is the configuration of blocked services. // Names of services to block (globally).
// Per-client settings can override this configuration. // Per-client settings can override this configuration.
BlockedServices *BlockedServices `yaml:"blocked_services"` BlockedServices []string `yaml:"blocked_services"`
// EtcHosts is a container of IP-hostname pairs taken from the operating // EtcHosts is a container of IP-hostname pairs taken from the operating
// system configuration files (e.g. /etc/hosts). // system configuration files (e.g. /etc/hosts).
@@ -298,12 +298,12 @@ func (d *DNSFilter) SetEnabled(enabled bool) {
atomic.StoreUint32(&d.enabled, mathutil.BoolToNumber[uint32](enabled)) atomic.StoreUint32(&d.enabled, mathutil.BoolToNumber[uint32](enabled))
} }
// Settings returns filtering settings. // GetConfig - get configuration
func (d *DNSFilter) Settings() (s *Settings) { func (d *DNSFilter) GetConfig() (s Settings) {
d.confLock.RLock() d.confLock.RLock()
defer d.confLock.RUnlock() defer d.confLock.RUnlock()
return &Settings{ return Settings{
FilteringEnabled: atomic.LoadUint32(&d.Config.enabled) != 0, FilteringEnabled: atomic.LoadUint32(&d.Config.enabled) != 0,
SafeSearchEnabled: d.Config.SafeSearchConf.Enabled, SafeSearchEnabled: d.Config.SafeSearchConf.Enabled,
SafeBrowsingEnabled: d.Config.SafeBrowsingEnabled, SafeBrowsingEnabled: d.Config.SafeBrowsingEnabled,
@@ -987,19 +987,16 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
return nil, fmt.Errorf("rewrites: preparing: %s", err) return nil, fmt.Errorf("rewrites: preparing: %s", err)
} }
if d.BlockedServices != nil {
bsvcs := []string{} bsvcs := []string{}
for _, s := range d.BlockedServices.IDs { for _, s := range d.BlockedServices {
if !BlockedSvcKnown(s) { if !BlockedSvcKnown(s) {
log.Debug("skipping unknown blocked-service %q", s) log.Debug("skipping unknown blocked-service %q", s)
continue continue
} }
bsvcs = append(bsvcs, s) bsvcs = append(bsvcs, s)
} }
d.BlockedServices.IDs = bsvcs d.BlockedServices = bsvcs
}
if blockFilters != nil { if blockFilters != nil {
err = d.initFiltering(nil, blockFilters) err = d.initFiltering(nil, blockFilters)

View File

@@ -416,12 +416,12 @@ type checkHostResp struct {
func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) { func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
host := r.URL.Query().Get("name") host := r.URL.Query().Get("name")
setts := d.Settings() setts := d.GetConfig()
setts.FilteringEnabled = true setts.FilteringEnabled = true
setts.ProtectionEnabled = true setts.ProtectionEnabled = true
d.ApplyBlockedServices(setts) d.ApplyBlockedServices(&setts, nil)
result, err := d.CheckHost(host, dns.TypeA, setts) result, err := d.CheckHost(host, dns.TypeA, &setts)
if err != nil { if err != nil {
aghhttp.Error( aghhttp.Error(
r, r,

View File

@@ -84,7 +84,7 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
return nil return nil
} }
// TODO(a.garipov): Check cnames for cycles on initialization. // TODO(a.garipov): Check cnames for cycles on initialisation.
cnames := stringutil.NewSet() cnames := stringutil.NewSet()
host := dReq.Hostname host := dReq.Hostname
for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" { for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" {

View File

@@ -161,8 +161,12 @@ func (ss *Default) resetEngine(
// type check // type check
var _ filtering.SafeSearch = (*Default)(nil) var _ filtering.SafeSearch = (*Default)(nil)
// CheckHost implements the [filtering.SafeSearch] interface for *Default. // CheckHost implements the [filtering.SafeSearch] interface for
func (ss *Default) CheckHost(host string, qtype rules.RRType) (res filtering.Result, err error) { // *DefaultSafeSearch.
func (ss *Default) CheckHost(
host string,
qtype rules.RRType,
) (res filtering.Result, err error) {
start := time.Now() start := time.Now()
defer func() { defer func() {
ss.log(log.DEBUG, "lookup for %q finished in %s", host, time.Since(start)) ss.log(log.DEBUG, "lookup for %q finished in %s", host, time.Since(start))
@@ -192,12 +196,16 @@ func (ss *Default) CheckHost(host string, qtype rules.RRType) (res filtering.Res
return filtering.Result{}, err return filtering.Result{}, err
} }
if fltRes != nil {
res = *fltRes res = *fltRes
ss.setCacheResult(host, qtype, res) ss.setCacheResult(host, qtype, res)
return res, nil return res, nil
} }
return filtering.Result{}, fmt.Errorf("no ipv4 addresses for %q", host)
}
// searchHost looks up DNS rewrites in the internal DNS filtering engine. // searchHost looks up DNS rewrites in the internal DNS filtering engine.
func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRewrite) { func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRewrite) {
ss.mu.RLock() ss.mu.RLock()
@@ -221,11 +229,7 @@ func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRe
} }
// newResult creates Result object from rewrite rule. qtype must be either // newResult creates Result object from rewrite rule. qtype must be either
// [dns.TypeA] or [dns.TypeAAAA]. If err is nil, res is never nil, so that the // [dns.TypeA] or [dns.TypeAAAA].
// empty result is converted into a NODATA response.
//
// TODO(a.garipov): Use the main rewrite result mechanism used in
// [dnsforward.Server.filterDNSRequest].
func (ss *Default) newResult( func (ss *Default) newResult(
rewrite *rules.DNSRewrite, rewrite *rules.DNSRewrite,
qtype rules.RRType, qtype rules.RRType,
@@ -239,10 +243,9 @@ func (ss *Default) newResult(
} }
if rewrite.RRType == qtype { if rewrite.RRType == qtype {
v := rewrite.Value ip, ok := rewrite.Value.(net.IP)
ip, ok := v.(net.IP)
if !ok || ip == nil { if !ok || ip == nil {
return nil, fmt.Errorf("expected ip rewrite value, got %T(%[1]v)", v) return nil, nil
} }
res.Rules[0].IP = ip res.Rules[0].IP = ip
@@ -252,14 +255,14 @@ func (ss *Default) newResult(
host := rewrite.NewCNAME host := rewrite.NewCNAME
if host == "" { if host == "" {
return res, nil return nil, nil
} }
ss.log(log.DEBUG, "resolving %q", host) ss.log(log.DEBUG, "resolving %q", host)
ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host) ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host)
if err != nil { if err != nil {
return nil, fmt.Errorf("resolving cname: %w", err) return nil, err
} }
ss.log(log.DEBUG, "resolved %s", ips) ss.log(log.DEBUG, "resolved %s", ips)
@@ -273,11 +276,13 @@ func (ss *Default) newResult(
} }
res.Rules[0].IP = ip res.Rules[0].IP = ip
}
return res, nil return res, nil
} }
return nil, nil
}
// qtypeToProto returns "ip4" for [dns.TypeA] and "ip6" for [dns.TypeAAAA]. // qtypeToProto returns "ip4" for [dns.TypeA] and "ip6" for [dns.TypeAAAA].
// It panics for other types. // It panics for other types.
func qtypeToProto(qtype rules.RRType) (proto string) { func qtypeToProto(qtype rules.RRType) (proto string) {

View File

@@ -1,7 +1,6 @@
package safesearch_test package safesearch_test
import ( import (
"context"
"net" "net"
"testing" "testing"
"time" "time"
@@ -72,25 +71,6 @@ func TestDefault_CheckHost_yandex(t *testing.T) {
} }
} }
func TestDefault_CheckHost_yandexAAAA(t *testing.T) {
conf := testConf
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
require.NoError(t, err)
res, err := ss.CheckHost("www.yandex.ru", dns.TypeAAAA)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
// TODO(a.garipov): Currently, the safe-search filter returns a single rule
// with a nil IP address. This isn't really necessary and should be changed
// once the TODO in [safesearch.Default.newResult] is resolved.
require.Len(t, res.Rules, 1)
assert.Nil(t, res.Rules[0].IP)
assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
}
func TestDefault_CheckHost_google(t *testing.T) { func TestDefault_CheckHost_google(t *testing.T) {
resolver := &aghtest.TestResolver{} resolver := &aghtest.TestResolver{}
ip, _ := resolver.HostToIPs("forcesafesearch.google.com") ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
@@ -125,56 +105,6 @@ func TestDefault_CheckHost_google(t *testing.T) {
} }
} }
// testResolver is a [filtering.Resolver] for tests.
//
// TODO(a.garipov): Move to aghtest and use everywhere.
type testResolver struct {
OnLookupIP func(ctx context.Context, network, host string) (ips []net.IP, err error)
}
// type check
var _ filtering.Resolver = (*testResolver)(nil)
// LookupIP implements the [filtering.Resolver] interface for *testResolver.
func (r *testResolver) LookupIP(
ctx context.Context,
network string,
host string,
) (ips []net.IP, err error) {
return r.OnLookupIP(ctx, network, host)
}
func TestDefault_CheckHost_duckduckgoAAAA(t *testing.T) {
conf := testConf
conf.CustomResolver = &testResolver{
OnLookupIP: func(_ context.Context, network, host string) (ips []net.IP, err error) {
assert.Equal(t, "ip6", network)
assert.Equal(t, "safe.duckduckgo.com", host)
return nil, nil
},
}
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
require.NoError(t, err)
// The DuckDuckGo safe-search addresses are resolved through CNAMEs, but
// DuckDuckGo doesn't have a safe-search IPv6 address. The result should be
// the same as the one for Yandex IPv6. That is, a NODATA response.
res, err := ss.CheckHost("www.duckduckgo.com", dns.TypeAAAA)
require.NoError(t, err)
assert.True(t, res.IsFiltered)
// TODO(a.garipov): Currently, the safe-search filter returns a single rule
// with a nil IP address. This isn't really necessary and should be changed
// once the TODO in [safesearch.Default.newResult] is resolved.
require.Len(t, res.Rules, 1)
assert.Nil(t, res.Rules[0].IP)
assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
}
func TestDefault_Update(t *testing.T) { func TestDefault_Update(t *testing.T) {
conf := testConf conf := testConf
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL) ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)

View File

@@ -7,7 +7,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch" "github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
) )
@@ -128,13 +127,14 @@ func (cs clientSource) MarshalText() (text []byte, err error) {
// RuntimeClient is a client information about which has been obtained using the // RuntimeClient is a client information about which has been obtained using the
// source described in the Source field. // source described in the Source field.
type RuntimeClient struct { type RuntimeClient struct {
// WHOIS is the filtered WHOIS data of a client. WHOISInfo *RuntimeClientWHOISInfo
WHOIS *whois.Info
// Host is the host name of a client.
Host string Host string
// Source is the source from which the information about the client has
// been obtained.
Source clientSource Source clientSource
} }
// RuntimeClientWHOISInfo is the filtered WHOIS data for a runtime client.
type RuntimeClientWHOISInfo struct {
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
Orgname string `json:"orgname,omitempty"`
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream" "github.com/AdguardTeam/dnsproxy/upstream"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
@@ -308,6 +307,18 @@ func (clients *clientsContainer) clientSource(ip netip.Addr) (src clientSource)
return rc.Source return rc.Source
} }
func toQueryLogWHOIS(wi *RuntimeClientWHOISInfo) (cw *querylog.ClientWHOIS) {
if wi == nil {
return &querylog.ClientWHOIS{}
}
return &querylog.ClientWHOIS{
City: wi.City,
Country: wi.Country,
Orgname: wi.Orgname,
}
}
// findMultiple is a wrapper around Find to make it a valid client finder for // findMultiple is a wrapper around Find to make it a valid client finder for
// the query log. c is never nil; if no information about the client is found, // the query log. c is never nil; if no information about the client is found,
// it returns an artificial client record by only setting the blocking-related // it returns an artificial client record by only setting the blocking-related
@@ -341,7 +352,7 @@ func (clients *clientsContainer) clientOrArtificial(
defer func() { defer func() {
c.Disallowed, c.DisallowedRule = clients.dnsServer.IsBlockedClient(ip, id) c.Disallowed, c.DisallowedRule = clients.dnsServer.IsBlockedClient(ip, id)
if c.WHOIS == nil { if c.WHOIS == nil {
c.WHOIS = &whois.Info{} c.WHOIS = &querylog.ClientWHOIS{}
} }
}() }()
@@ -358,7 +369,7 @@ func (clients *clientsContainer) clientOrArtificial(
if ok { if ok {
return &querylog.Client{ return &querylog.Client{
Name: rc.Host, Name: rc.Host,
WHOIS: rc.WHOIS, WHOIS: toQueryLogWHOIS(rc.WHOISInfo),
}, false }, false
} }
@@ -690,7 +701,7 @@ func (clients *clientsContainer) Update(prev, c *Client) (err error) {
} }
// setWHOISInfo sets the WHOIS information for a client. // setWHOISInfo sets the WHOIS information for a client.
func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) { func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *RuntimeClientWHOISInfo) {
clients.lock.Lock() clients.lock.Lock()
defer clients.lock.Unlock() defer clients.lock.Unlock()
@@ -702,7 +713,7 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
rc, ok := clients.ipToRC[ip] rc, ok := clients.ipToRC[ip]
if ok { if ok {
rc.WHOIS = wi rc.WHOISInfo = wi
log.Debug("clients: set whois info for runtime client %s: %+v", rc.Host, wi) log.Debug("clients: set whois info for runtime client %s: %+v", rc.Host, wi)
return return
@@ -714,7 +725,7 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
Source: ClientSourceWHOIS, Source: ClientSourceWHOIS,
} }
rc.WHOIS = wi rc.WHOISInfo = wi
clients.ipToRC[ip] = rc clients.ipToRC[ip] = rc
@@ -753,7 +764,7 @@ func (clients *clientsContainer) addHostLocked(
rc = &RuntimeClient{ rc = &RuntimeClient{
Host: host, Host: host,
Source: src, Source: src,
WHOIS: &whois.Info{}, WHOISInfo: &RuntimeClientWHOISInfo{},
} }
clients.ipToRC[ip] = rc clients.ipToRC[ip] = rc

View File

@@ -9,7 +9,7 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd" "github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -199,7 +199,7 @@ func TestClients(t *testing.T) {
func TestClientsWHOIS(t *testing.T) { func TestClientsWHOIS(t *testing.T) {
clients := newClientsContainer() clients := newClientsContainer()
whois := &whois.Info{ whois := &RuntimeClientWHOISInfo{
Country: "AU", Country: "AU",
Orgname: "Example Org", Orgname: "Example Org",
} }
@@ -210,7 +210,7 @@ func TestClientsWHOIS(t *testing.T) {
rc := clients.ipToRC[ip] rc := clients.ipToRC[ip]
require.NotNil(t, rc) require.NotNil(t, rc)
assert.Equal(t, rc.WHOIS, whois) assert.Equal(t, rc.WHOISInfo, whois)
}) })
t.Run("existing_auto-client", func(t *testing.T) { t.Run("existing_auto-client", func(t *testing.T) {
@@ -222,7 +222,7 @@ func TestClientsWHOIS(t *testing.T) {
rc := clients.ipToRC[ip] rc := clients.ipToRC[ip]
require.NotNil(t, rc) require.NotNil(t, rc)
assert.Equal(t, rc.WHOIS, whois) assert.Equal(t, rc.WHOISInfo, whois)
}) })
t.Run("can't_set_manually-added", func(t *testing.T) { t.Run("can't_set_manually-added", func(t *testing.T) {

View File

@@ -9,7 +9,6 @@ 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/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
) )
// clientJSON is a common structure used by several handlers to deal with // clientJSON is a common structure used by several handlers to deal with
@@ -29,8 +28,7 @@ type clientJSON struct {
// the allowlist. // the allowlist.
DisallowedRule *string `json:"disallowed_rule,omitempty"` DisallowedRule *string `json:"disallowed_rule,omitempty"`
// WHOIS is the filtered WHOIS data of a client. WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info,omitempty"`
WHOIS *whois.Info `json:"whois_info,omitempty"`
SafeSearchConf *filtering.SafeSearchConfig `json:"safe_search"` SafeSearchConf *filtering.SafeSearchConfig `json:"safe_search"`
Name string `json:"name"` Name string `json:"name"`
@@ -53,7 +51,7 @@ type clientJSON struct {
} }
type runtimeClientJSON struct { type runtimeClientJSON struct {
WHOIS *whois.Info `json:"whois_info"` WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"`
IP netip.Addr `json:"ip"` IP netip.Addr `json:"ip"`
Name string `json:"name"` Name string `json:"name"`
@@ -80,7 +78,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
for ip, rc := range clients.ipToRC { for ip, rc := range clients.ipToRC {
cj := runtimeClientJSON{ cj := runtimeClientJSON{
WHOIS: rc.WHOIS, WHOISInfo: rc.WHOISInfo,
Name: rc.Host, Name: rc.Host,
Source: rc.Source, Source: rc.Source,
@@ -346,7 +344,7 @@ func (clients *clientsContainer) findRuntime(ip netip.Addr, idStr string) (cj *c
IDs: []string{idStr}, IDs: []string{idStr},
Disallowed: &disallowed, Disallowed: &disallowed,
DisallowedRule: &rule, DisallowedRule: &rule,
WHOIS: &whois.Info{}, WHOISInfo: &RuntimeClientWHOISInfo{},
} }
return cj return cj
@@ -355,7 +353,7 @@ func (clients *clientsContainer) findRuntime(ip netip.Addr, idStr string) (cj *c
cj = &clientJSON{ cj = &clientJSON{
Name: rc.Host, Name: rc.Host,
IDs: []string{idStr}, IDs: []string{idStr},
WHOIS: rc.WHOIS, WHOISInfo: rc.WHOISInfo,
} }
disallowed, rule := clients.dnsServer.IsBlockedClient(ip, idStr) disallowed, rule := clients.dnsServer.IsBlockedClient(ip, idStr)

View File

@@ -14,7 +14,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward" "github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
"github.com/AdguardTeam/AdGuardHome/internal/stats" "github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/dnsproxy/fastip" "github.com/AdguardTeam/dnsproxy/fastip"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
@@ -317,11 +316,6 @@ var config = &configuration{
Yandex: true, Yandex: true,
YouTube: true, YouTube: true,
}, },
BlockedServices: &filtering.BlockedServices{
Schedule: schedule.EmptyWeekly(),
IDs: []string{},
},
}, },
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout}, UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
UsePrivateRDNS: true, UsePrivateRDNS: true,

View File

@@ -8,7 +8,6 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg" "github.com/AdguardTeam/AdGuardHome/internal/aghalg"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
@@ -18,7 +17,6 @@ import (
"github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/AdguardTeam/AdGuardHome/internal/querylog" "github.com/AdguardTeam/AdGuardHome/internal/querylog"
"github.com/AdguardTeam/AdGuardHome/internal/stats" "github.com/AdguardTeam/AdGuardHome/internal/stats"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
@@ -27,7 +25,7 @@ import (
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
// Default listening ports. // Default ports.
const ( const (
defaultPortDNS = 53 defaultPortDNS = 53
defaultPortHTTP = 80 defaultPortHTTP = 80
@@ -171,72 +169,13 @@ func initDNSServer(
Context.rdns = NewRDNS(Context.dnsServer, &Context.clients, config.DNS.UsePrivateRDNS) Context.rdns = NewRDNS(Context.dnsServer, &Context.clients, config.DNS.UsePrivateRDNS)
} }
initWHOIS() if config.Clients.Sources.WHOIS {
Context.whois = initWHOIS(&Context.clients)
}
return nil return nil
} }
// initWHOIS initializes the WHOIS.
//
// TODO(s.chzhen): Consider making configurable.
func initWHOIS() {
const (
// defaultQueueSize is the size of queue of IPs for WHOIS processing.
defaultQueueSize = 255
// defaultTimeout is the timeout for WHOIS requests.
defaultTimeout = 5 * time.Second
// defaultCacheSize is the maximum size of the cache. If it's zero,
// cache size is unlimited.
defaultCacheSize = 10_000
// defaultMaxConnReadSize is an upper limit in bytes for reading from
// net.Conn.
defaultMaxConnReadSize = 64 * 1024
// defaultMaxRedirects is the maximum redirects count.
defaultMaxRedirects = 5
// defaultMaxInfoLen is the maximum length of whois.Info fields.
defaultMaxInfoLen = 250
// defaultIPTTL is the Time to Live duration for cached IP addresses.
defaultIPTTL = 1 * time.Hour
)
Context.whoisCh = make(chan netip.Addr, defaultQueueSize)
var w whois.Interface
if config.Clients.Sources.WHOIS {
w = whois.New(&whois.Config{
DialContext: customDialContext,
ServerAddr: whois.DefaultServer,
Port: whois.DefaultPort,
Timeout: defaultTimeout,
CacheSize: defaultCacheSize,
MaxConnReadSize: defaultMaxConnReadSize,
MaxRedirects: defaultMaxRedirects,
MaxInfoLen: defaultMaxInfoLen,
CacheTTL: defaultIPTTL,
})
} else {
w = whois.Empty{}
}
go func() {
defer log.OnPanic("whois")
for ip := range Context.whoisCh {
info, changed := w.Process(context.Background(), ip)
if info != nil && changed {
Context.clients.setWHOISInfo(ip, info)
}
}
}()
}
// parseSubnetSet parses a slice of subnets. If the slice is empty, it returns // parseSubnetSet parses a slice of subnets. If the slice is empty, it returns
// a subnet set that matches all locally served networks, see // a subnet set that matches all locally served networks, see
// [netutil.IsLocallyServed]. // [netutil.IsLocallyServed].
@@ -279,7 +218,9 @@ func onDNSRequest(pctx *proxy.DNSContext) {
Context.rdns.Begin(ip) Context.rdns.Begin(ip)
} }
Context.whoisCh <- ip if srcs.WHOIS && !netutil.IsSpecialPurposeAddr(ip) {
Context.whois.Begin(ip)
}
} }
func ipsToTCPAddrs(ips []netip.Addr, port int) (tcpAddrs []*net.TCPAddr) { func ipsToTCPAddrs(ips []netip.Addr, port int) (tcpAddrs []*net.TCPAddr) {
@@ -449,7 +390,7 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
// pref is a prefix for logging messages around the scope. // pref is a prefix for logging messages around the scope.
const pref = "applying filters" const pref = "applying filters"
Context.filters.ApplyBlockedServices(setts) Context.filters.ApplyBlockedServices(setts, nil)
log.Debug("%s: looking for client with ip %s and clientid %q", pref, clientIP, clientID) log.Debug("%s: looking for client with ip %s and clientid %q", pref, clientIP, clientID)
@@ -477,7 +418,7 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
if svcs == nil { if svcs == nil {
svcs = []string{} svcs = []string{}
} }
Context.filters.ApplyBlockedServicesList(setts, svcs) Context.filters.ApplyBlockedServices(setts, svcs)
log.Debug("%s: services for client %q set: %s", pref, c.Name, svcs) log.Debug("%s: services for client %q set: %s", pref, c.Name, svcs)
} }
@@ -522,7 +463,9 @@ func startDNSServer() error {
Context.rdns.Begin(ip) Context.rdns.Begin(ip)
} }
Context.whoisCh <- ip if srcs.WHOIS && !netutil.IsSpecialPurposeAddr(ip) {
Context.whois.Begin(ip)
}
} }
return nil return nil

View File

@@ -57,6 +57,7 @@ type homeContext struct {
queryLog querylog.QueryLog // query log module queryLog querylog.QueryLog // query log module
dnsServer *dnsforward.Server // DNS module dnsServer *dnsforward.Server // DNS module
rdns *RDNS // rDNS module rdns *RDNS // rDNS module
whois *WHOIS // WHOIS module
dhcpServer dhcpd.Interface // DHCP module dhcpServer dhcpd.Interface // DHCP module
auth *Auth // HTTP authentication module auth *Auth // HTTP authentication module
filters *filtering.DNSFilter // DNS filtering module filters *filtering.DNSFilter // DNS filtering module
@@ -83,9 +84,6 @@ type homeContext struct {
client *http.Client client *http.Client
appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app appSignalChannel chan os.Signal // Channel for receiving OS signals by the console app
// whoisCh is the channel for receiving IPs for WHOIS processing.
whoisCh chan netip.Addr
// tlsCipherIDs are the ID of the cipher suites that AdGuard Home must use. // tlsCipherIDs are the ID of the cipher suites that AdGuard Home must use.
tlsCipherIDs []uint16 tlsCipherIDs []uint16

View File

@@ -3,13 +3,13 @@ package home
import ( import (
"io" "io"
"net/http" "net/http"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghio" "github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
// middleware is a wrapper function signature. // middlerware is a wrapper function signature.
type middleware func(http.Handler) http.Handler type middleware func(http.Handler) http.Handler
// withMiddlewares consequently wraps h with all the middlewares. // withMiddlewares consequently wraps h with all the middlewares.
@@ -75,48 +75,3 @@ func limitRequestBody(h http.Handler) (limited http.Handler) {
h.ServeHTTP(w, rr) h.ServeHTTP(w, rr)
}) })
} }
const (
// defaultWriteTimeout is the maximum duration before timing out writes of
// the response.
defaultWriteTimeout = 60 * time.Second
// longerWriteTimeout is the maximum duration before timing out for APIs
// expecting longer response requests.
longerWriteTimeout = 5 * time.Minute
)
// expectsLongTimeoutRequests shows if this request should use a bigger write
// timeout value. These are exceptions for poorly designed current APIs as
// well as APIs that are designed to expect large files and requests. Remove
// once the new, better APIs are up.
//
// TODO(d.kolyshev): This could be achieved with [http.NewResponseController]
// with go v1.20.
func expectsLongTimeoutRequests(r *http.Request) (ok bool) {
if r.Method != http.MethodGet {
return false
}
return r.URL.Path == "/control/querylog/export"
}
// addWriteTimeout wraps underlying handler h, adding a response write timeout.
func addWriteTimeout(h http.Handler) (limited http.Handler) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var handler http.Handler
if expectsLongTimeoutRequests(r) {
handler = http.TimeoutHandler(h, longerWriteTimeout, "write timeout exceeded")
} else {
handler = http.TimeoutHandler(h, defaultWriteTimeout, "write timeout exceeded")
}
handler.ServeHTTP(w, r)
})
}
// limitHandler wraps underlying handler h with default limits, such as request
// body limit and write timeout.
func limitHandler(h http.Handler) (limited http.Handler) {
return limitRequestBody(addWriteTimeout(h))
}

View File

@@ -22,7 +22,7 @@ import (
) )
// currentSchemaVersion is the current schema version. // currentSchemaVersion is the current schema version.
const currentSchemaVersion = 21 const currentSchemaVersion = 20
// These aliases are provided for convenience. // These aliases are provided for convenience.
type ( type (
@@ -94,7 +94,6 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
upgradeSchema17to18, upgradeSchema17to18,
upgradeSchema18to19, upgradeSchema18to19,
upgradeSchema19to20, upgradeSchema19to20,
upgradeSchema20to21,
} }
n := 0 n := 0
@@ -1129,56 +1128,6 @@ func upgradeSchema19to20(diskConf yobj) (err error) {
return nil return nil
} }
// upgradeSchema20to21 performs the following changes:
//
// # BEFORE:
// 'dns':
// 'blocked_services':
// - 'svc_name'
//
// # AFTER:
// 'dns':
// 'blocked_services':
// 'ids':
// - 'svc_name'
// 'schedule':
// 'time_zone': 'Local'
func upgradeSchema20to21(diskConf yobj) (err error) {
log.Printf("Upgrade yaml: 20 to 21")
diskConf["schema_version"] = 21
const field = "blocked_services"
dnsVal, ok := diskConf["dns"]
if !ok {
return nil
}
dns, ok := dnsVal.(yobj)
if !ok {
return fmt.Errorf("unexpected type of dns: %T", dnsVal)
}
blockedVal, ok := dns[field]
if !ok {
return nil
}
services, ok := blockedVal.(yarr)
if !ok {
return fmt.Errorf("unexpected type of blocked: %T", blockedVal)
}
dns[field] = yobj{
"ids": services,
"schedule": yobj{
"time_zone": "Local",
},
}
return nil
}
// TODO(a.garipov): Replace with log.Output when we port it to our logging // TODO(a.garipov): Replace with log.Output when we port it to our logging
// package. // package.
func funcName() string { func funcName() string {

View File

@@ -1140,46 +1140,3 @@ func TestUpgradeSchema19to20(t *testing.T) {
assert.Equal(t, 24*time.Hour, ivlVal.Duration) assert.Equal(t, 24*time.Hour, ivlVal.Duration)
}) })
} }
func TestUpgradeSchema20to21(t *testing.T) {
const newSchemaVer = 21
testCases := []struct {
in yobj
want yobj
name string
}{{
name: "nothing",
in: yobj{},
want: yobj{
"schema_version": newSchemaVer,
},
}, {
name: "no_clients",
in: yobj{
"dns": yobj{
"blocked_services": yarr{"ok"},
},
},
want: yobj{
"dns": yobj{
"blocked_services": yobj{
"ids": yarr{"ok"},
"schedule": yobj{
"time_zone": "Local",
},
},
},
"schema_version": newSchemaVer,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := upgradeSchema20to21(tc.in)
require.NoError(t, err)
assert.Equal(t, tc.want, tc.in)
})
}
}

View File

@@ -25,13 +25,11 @@ const (
// readTimeout is the maximum duration for reading the entire request, // readTimeout is the maximum duration for reading the entire request,
// including the body. // including the body.
readTimeout = 60 * time.Second readTimeout = 60 * time.Second
// readHdrTimeout is the amount of time allowed to read request headers. // readHdrTimeout is the amount of time allowed to read request headers.
readHdrTimeout = 60 * time.Second readHdrTimeout = 60 * time.Second
// writeTimeout is the maximum duration before timing out writes of the // writeTimeout is the maximum duration before timing out writes of the
// response. This limit is overwritten by [addWriteTimeout] middleware. // response.
writeTimeout = 10 * time.Minute writeTimeout = 60 * time.Second
) )
type webConfig struct { type webConfig struct {
@@ -171,7 +169,7 @@ func (web *webAPI) start() {
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.
hdlr := h2c.NewHandler(withMiddlewares(Context.mux, limitHandler), &http2.Server{}) hdlr := h2c.NewHandler(withMiddlewares(Context.mux, limitRequestBody), &http2.Server{})
// Create a new instance, because the Web is not usable after Shutdown. // Create a new instance, because the Web is not usable after Shutdown.
hostStr := web.conf.BindHost.String() hostStr := web.conf.BindHost.String()
@@ -256,7 +254,7 @@ func (web *webAPI) tlsServerLoop() {
CipherSuites: Context.tlsCipherIDs, CipherSuites: Context.tlsCipherIDs,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
}, },
Handler: withMiddlewares(Context.mux, limitHandler), Handler: withMiddlewares(Context.mux, limitRequestBody),
ReadTimeout: web.conf.ReadTimeout, ReadTimeout: web.conf.ReadTimeout,
ReadHeaderTimeout: web.conf.ReadHeaderTimeout, ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
WriteTimeout: web.conf.WriteTimeout, WriteTimeout: web.conf.WriteTimeout,
@@ -290,7 +288,7 @@ func (web *webAPI) mustStartHTTP3(address string) {
CipherSuites: Context.tlsCipherIDs, CipherSuites: Context.tlsCipherIDs,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
}, },
Handler: withMiddlewares(Context.mux, limitHandler), Handler: withMiddlewares(Context.mux, limitRequestBody),
} }
log.Debug("web: starting http/3 server") log.Debug("web: starting http/3 server")

259
internal/home/whois.go Normal file
View File

@@ -0,0 +1,259 @@
package home
import (
"context"
"encoding/binary"
"fmt"
"io"
"net"
"net/netip"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil"
)
const (
defaultServer = "whois.arin.net"
defaultPort = "43"
maxValueLength = 250
whoisTTL = 1 * 60 * 60 // 1 hour
)
// WHOIS - module context
type WHOIS struct {
clients *clientsContainer
ipChan chan netip.Addr
// dialContext specifies the dial function for creating unencrypted TCP
// connections.
dialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
// Contains IP addresses of clients
// An active IP address is resolved once again after it expires.
// If IP address couldn't be resolved, it stays here for some time to prevent further attempts to resolve the same IP.
ipAddrs cache.Cache
// TODO(a.garipov): Rewrite to use time.Duration. Like, seriously, why?
timeoutMsec uint
}
// initWHOIS creates the WHOIS module context.
func initWHOIS(clients *clientsContainer) *WHOIS {
w := WHOIS{
timeoutMsec: 5000,
clients: clients,
ipAddrs: cache.New(cache.Config{
EnableLRU: true,
MaxCount: 10000,
}),
dialContext: customDialContext,
ipChan: make(chan netip.Addr, 255),
}
go w.workerLoop()
return &w
}
// If the value is too large - cut it and append "..."
func trimValue(s string) string {
if len(s) <= maxValueLength {
return s
}
return s[:maxValueLength-3] + "..."
}
// isWHOISComment returns true if the string is empty or is a WHOIS comment.
func isWHOISComment(s string) (ok bool) {
return len(s) == 0 || s[0] == '#' || s[0] == '%'
}
// strmap is an alias for convenience.
type strmap = map[string]string
// whoisParse parses a subset of plain-text data from the WHOIS response into
// a string map.
func whoisParse(data string) (m strmap) {
m = strmap{}
var orgname string
lines := strings.Split(data, "\n")
for _, l := range lines {
if isWHOISComment(l) {
continue
}
kv := strings.SplitN(l, ":", 2)
if len(kv) != 2 {
continue
}
k := strings.ToLower(strings.TrimSpace(kv[0]))
v := strings.TrimSpace(kv[1])
if v == "" {
continue
}
switch k {
case "orgname", "org-name":
k = "orgname"
v = trimValue(v)
orgname = v
case "city", "country":
v = trimValue(v)
case "descr", "netname":
k = "orgname"
v = stringutil.Coalesce(orgname, v)
orgname = v
case "whois":
k = "whois"
case "referralserver":
k = "whois"
v = strings.TrimPrefix(v, "whois://")
default:
continue
}
m[k] = v
}
return m
}
// MaxConnReadSize is an upper limit in bytes for reading from net.Conn.
const MaxConnReadSize = 64 * 1024
// Send request to a server and receive the response
func (w *WHOIS) query(ctx context.Context, target, serverAddr string) (data string, err error) {
addr, _, _ := net.SplitHostPort(serverAddr)
if addr == "whois.arin.net" {
target = "n + " + target
}
conn, err := w.dialContext(ctx, "tcp", serverAddr)
if err != nil {
return "", err
}
defer func() { err = errors.WithDeferred(err, conn.Close()) }()
r, err := aghio.LimitReader(conn, MaxConnReadSize)
if err != nil {
return "", err
}
_ = conn.SetReadDeadline(time.Now().Add(time.Duration(w.timeoutMsec) * time.Millisecond))
_, err = conn.Write([]byte(target + "\r\n"))
if err != nil {
return "", err
}
// This use of ReadAll is now safe, because we limited the conn Reader.
var whoisData []byte
whoisData, err = io.ReadAll(r)
if err != nil {
return "", err
}
return string(whoisData), nil
}
// Query WHOIS servers (handle redirects)
func (w *WHOIS) queryAll(ctx context.Context, target string) (string, error) {
server := net.JoinHostPort(defaultServer, defaultPort)
const maxRedirects = 5
for i := 0; i != maxRedirects; i++ {
resp, err := w.query(ctx, target, server)
if err != nil {
return "", err
}
log.Debug("whois: received response (%d bytes) from %s IP:%s", len(resp), server, target)
m := whoisParse(resp)
redir, ok := m["whois"]
if !ok {
return resp, nil
}
redir = strings.ToLower(redir)
_, _, err = net.SplitHostPort(redir)
if err != nil {
server = net.JoinHostPort(redir, defaultPort)
} else {
server = redir
}
log.Debug("whois: redirected to %s IP:%s", redir, target)
}
return "", fmt.Errorf("whois: redirect loop")
}
// Request WHOIS information
func (w *WHOIS) process(ctx context.Context, ip netip.Addr) (wi *RuntimeClientWHOISInfo) {
resp, err := w.queryAll(ctx, ip.String())
if err != nil {
log.Debug("whois: error: %s IP:%s", err, ip)
return nil
}
log.Debug("whois: IP:%s response: %d bytes", ip, len(resp))
m := whoisParse(resp)
wi = &RuntimeClientWHOISInfo{
City: m["city"],
Country: m["country"],
Orgname: m["orgname"],
}
// Don't return an empty struct so that the frontend doesn't get
// confused.
if *wi == (RuntimeClientWHOISInfo{}) {
return nil
}
return wi
}
// Begin - begin requesting WHOIS info
func (w *WHOIS) Begin(ip netip.Addr) {
ipBytes := ip.AsSlice()
now := uint64(time.Now().Unix())
expire := w.ipAddrs.Get(ipBytes)
if len(expire) != 0 {
exp := binary.BigEndian.Uint64(expire)
if exp > now {
return
}
}
expire = make([]byte, 8)
binary.BigEndian.PutUint64(expire, now+whoisTTL)
_ = w.ipAddrs.Set(ipBytes, expire)
log.Debug("whois: adding %s", ip)
select {
case w.ipChan <- ip:
default:
log.Debug("whois: queue is full")
}
}
// workerLoop processes the IP addresses it got from the channel and associates
// the retrieving WHOIS info with a client.
func (w *WHOIS) workerLoop() {
for ip := range w.ipChan {
info := w.process(context.Background(), ip)
if info == nil {
continue
}
w.clients.setWHOISInfo(ip, info)
}
}

152
internal/home/whois_test.go Normal file
View File

@@ -0,0 +1,152 @@
package home
import (
"context"
"io"
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// fakeConn is a mock implementation of net.Conn to simplify testing.
//
// TODO(e.burkov): Search for other places in code where it may be used. Move
// into aghtest then.
type fakeConn struct {
// Conn is embedded here simply to make *fakeConn a net.Conn without
// actually implementing all methods.
net.Conn
data []byte
}
// Write implements net.Conn interface for *fakeConn. It always returns 0 and a
// nil error without mutating the slice.
func (c *fakeConn) Write(_ []byte) (n int, err error) {
return 0, nil
}
// Read implements net.Conn interface for *fakeConn. It puts the content of
// c.data field into b up to the b's capacity.
func (c *fakeConn) Read(b []byte) (n int, err error) {
return copy(b, c.data), io.EOF
}
// Close implements net.Conn interface for *fakeConn. It always returns nil.
func (c *fakeConn) Close() (err error) {
return nil
}
// SetReadDeadline implements net.Conn interface for *fakeConn. It always
// returns nil.
func (c *fakeConn) SetReadDeadline(_ time.Time) (err error) {
return nil
}
// fakeDial is a mock implementation of customDialContext to simplify testing.
func (c *fakeConn) fakeDial(ctx context.Context, network, addr string) (conn net.Conn, err error) {
return c, nil
}
func TestWHOIS(t *testing.T) {
const (
nl = "\n"
data = `OrgName: FakeOrg LLC` + nl +
`City: Nonreal` + nl +
`Country: Imagiland` + nl
)
fc := &fakeConn{
data: []byte(data),
}
w := WHOIS{
timeoutMsec: 5000,
dialContext: fc.fakeDial,
}
resp, err := w.queryAll(context.Background(), "1.2.3.4")
assert.NoError(t, err)
m := whoisParse(resp)
require.NotEmpty(t, m)
assert.Equal(t, "FakeOrg LLC", m["orgname"])
assert.Equal(t, "Imagiland", m["country"])
assert.Equal(t, "Nonreal", m["city"])
}
func TestWHOISParse(t *testing.T) {
const (
city = "Nonreal"
country = "Imagiland"
orgname = "FakeOrgLLC"
whois = "whois.example.net"
)
testCases := []struct {
want strmap
name string
in string
}{{
want: strmap{},
name: "empty",
in: ``,
}, {
want: strmap{},
name: "comments",
in: "%\n#",
}, {
want: strmap{},
name: "no_colon",
in: "city",
}, {
want: strmap{},
name: "no_value",
in: "city:",
}, {
want: strmap{"city": city},
name: "city",
in: `city: ` + city,
}, {
want: strmap{"country": country},
name: "country",
in: `country: ` + country,
}, {
want: strmap{"orgname": orgname},
name: "orgname",
in: `orgname: ` + orgname,
}, {
want: strmap{"orgname": orgname},
name: "orgname_hyphen",
in: `org-name: ` + orgname,
}, {
want: strmap{"orgname": orgname},
name: "orgname_descr",
in: `descr: ` + orgname,
}, {
want: strmap{"orgname": orgname},
name: "orgname_netname",
in: `netname: ` + orgname,
}, {
want: strmap{"whois": whois},
name: "whois",
in: `whois: ` + whois,
}, {
want: strmap{"whois": whois},
name: "referralserver",
in: `referralserver: whois://` + whois,
}, {
want: strmap{},
name: "other",
in: `other: value`,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := whoisParse(tc.in)
assert.Equal(t, tc.want, got)
})
}
}

View File

@@ -1,39 +0,0 @@
# AdGuard Home v0.108.0 Changelog DRAFT
This changelog should be merged into the main one once the next API matures
enough.
## [v0.108.0] - TODO
### Added
- The ability to log to stderr using `--logFile=stderr`.
- The new `--web-addr` flag to set the Web UI address in a `host:port` form.
- `SIGHUP` now reloads all configuration from the configuration file ([#5676]).
### Changed
#### New HTTP API
**TODO(a.garipov):** Describe the new API and add a link to the new OpenAPI doc.
#### Other changes
- `-h` is now an alias for `--help` instead of the removed `--host`, see below.
Use `--web-addr=host:port` to set an address on which to serve the Web UI.
### Fixed
- Inconsistent application of `--work-dir/-w` ([#2598], [#2902]).
- The order of `-v/--verbose` and `--version` being significant ([#2893]).
### Removed
- The deprecated `--no-mem-optimization` and `--no-etc-hosts` flags.
- `--host` and `-p/--port` flags. Use `--web-addr=host:port` to set an address
on which to serve the Web UI. `-h` is now an alias for `--help`, see above.
[#2598]: https://github.com/AdguardTeam/AdGuardHome/issues/2598
[#2893]: https://github.com/AdguardTeam/AdGuardHome/issues/2893
[#2902]: https://github.com/AdguardTeam/AdGuardHome/issues/2902
[#5676]: https://github.com/AdguardTeam/AdGuardHome/issues/5676

View File

@@ -1,5 +1,5 @@
// Package cmd is the AdGuard Home entry point. It assembles the configuration // Package cmd is the AdGuard Home entry point. It contains the on-disk
// file manager, sets up signal processing logic, and so on. // configuration file utilities, signal processing logic, and so on.
// //
// TODO(a.garipov): Move to the upper-level internal/. // TODO(a.garipov): Move to the upper-level internal/.
package cmd package cmd
@@ -7,6 +7,7 @@ package cmd
import ( import (
"context" "context"
"io/fs" "io/fs"
"math/rand"
"os" "os"
"time" "time"
@@ -15,57 +16,51 @@ import (
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
) )
// Main is the entry point of AdGuard Home. // Main is the entry point of application.
func Main(frontend fs.FS) { func Main(clientBuildFS fs.FS) {
start := time.Now()
// Initial Configuration // Initial Configuration
cmdName := os.Args[0] start := time.Now()
opts, err := parseOptions(cmdName, os.Args[1:]) rand.Seed(start.UnixNano())
exitCode, needExit := processOptions(opts, cmdName, err)
if needExit {
os.Exit(exitCode)
}
err = setLog(opts) // TODO(a.garipov): Set up logging.
check(err)
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid()) log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
if opts.workDir != "" {
log.Info("changing working directory to %q", opts.workDir)
err = os.Chdir(opts.workDir)
check(err)
}
// Web Service // Web Service
confMgr, err := configmgr.New(opts.confFile, frontend, start) // TODO(a.garipov): Use in the Web service.
check(err) _ = clientBuildFS
// TODO(a.garipov): Set up configuration file name.
const confFile = "AdGuardHome.1.yaml"
confMgr, err := configmgr.New(confFile, start)
fatalOnError(err)
web := confMgr.Web() web := confMgr.Web()
err = web.Start() err = web.Start()
check(err) fatalOnError(err)
dns := confMgr.DNS() dns := confMgr.DNS()
err = dns.Start() err = dns.Start()
check(err) fatalOnError(err)
sigHdlr := newSignalHandler( sigHdlr := newSignalHandler(
opts.confFile, confFile,
frontend,
start, start,
web, web,
dns, dns,
) )
sigHdlr.handle() go sigHdlr.handle()
select {}
} }
// defaultTimeout is the timeout used for some operations where another timeout // defaultTimeout is the timeout used for some operations where another timeout
// hasn't been defined yet. // hasn't been defined yet.
const defaultTimeout = 5 * time.Second const defaultTimeout = 15 * time.Second
// ctxWithDefaultTimeout is a helper function that returns a context with // ctxWithDefaultTimeout is a helper function that returns a context with
// timeout set to defaultTimeout. // timeout set to defaultTimeout.
@@ -73,9 +68,10 @@ func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) {
return context.WithTimeout(context.Background(), defaultTimeout) return context.WithTimeout(context.Background(), defaultTimeout)
} }
// check is a simple error-checking helper. It must only be used within Main. // fatalOnError is a helper that exits the program with an error code if err is
func check(err error) { // not nil. It must only be used within Main.
func fatalOnError(err error) {
if err != nil { if err != nil {
panic(err) log.Fatal(err)
} }
} }

View File

@@ -1,39 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/log"
)
// syslogServiceName is the name of the AdGuard Home service used for writing
// logs to the system log.
const syslogServiceName = "AdGuardHome"
// setLog sets up the text logging.
//
// TODO(a.garipov): Add parameters from configuration file.
func setLog(opts *options) (err error) {
switch opts.confFile {
case "stdout":
log.SetOutput(os.Stdout)
case "stderr":
log.SetOutput(os.Stderr)
case "syslog":
err = aghos.ConfigureSyslog(syslogServiceName)
if err != nil {
return fmt.Errorf("initializing syslog: %w", err)
}
default:
// TODO(a.garipov): Use the path.
}
if opts.verbose {
log.SetLevel(log.DEBUG)
log.Debug("verbose logging enabled")
}
return nil
}

View File

@@ -1,403 +0,0 @@
package cmd
import (
"flag"
"fmt"
"io"
"net/netip"
"os"
"strings"
"github.com/AdguardTeam/AdGuardHome/internal/version"
"golang.org/x/exp/slices"
)
// options contains all command-line options for the AdGuardHome(.exe) binary.
type options struct {
// confFile is the path to the configuration file.
confFile string
// logFile is the path to the log file. Special values:
//
// - "stdout": Write to stdout (the default).
// - "stderr": Write to stderr.
// - "syslog": Write to the system log.
logFile string
// pidFile is the path to the file where to store the PID.
//
// TODO(a.garipov): Use.
pidFile string
// serviceAction is the service control action to perform:
//
// - "install": Installs AdGuard Home as a system service.
// - "uninstall": Uninstalls it.
// - "status": Prints the service status.
// - "start": Starts the previously installed service.
// - "stop": Stops the previously installed service.
// - "restart": Restarts the previously installed service.
// - "reload": Reloads the configuration.
// - "run": This is a special command that is not supposed to be used
// directly it is specified when we register a service, and it indicates
// to the app that it is being run as a service.
//
// TODO(a.garipov): Use.
serviceAction string
// workDir is the path to the working directory. It is applied before all
// other configuration is read, so all relative paths are relative to it.
workDir string
// webAddrs contains the addresses on which to serve the web UI.
//
// TODO(a.garipov): Use.
webAddrs []netip.AddrPort
// checkConfig, if true, instructs AdGuard Home to check the configuration
// file and exit with a corresponding exit code.
//
// TODO(a.garipov): Use.
checkConfig bool
// disableUpdate, if true, prevents AdGuard Home from automatically checking
// for updates.
//
// TODO(a.garipov): Use.
disableUpdate bool
// glinetMode enables the GL-Inet compatibility mode.
//
// TODO(a.garipov): Use.
glinetMode bool
// help, if true, instructs AdGuard Home to print the command-line option
// help message and quit with a successful exit-code.
help bool
// localFrontend, if true, instructs AdGuard Home to use the local frontend
// directory instead of the files compiled into the binary.
//
// TODO(a.garipov): Use.
localFrontend bool
// performUpdate, if true, instructs AdGuard Home to update the current
// binary and restart the service in case it's installed.
//
// TODO(a.garipov): Use.
performUpdate bool
// verbose, if true, instructs AdGuard Home to enable verbose logging.
verbose bool
// version, if true, instructs AdGuard Home to print the version to stdout
// and quit with a successful exit-code. If verbose is also true, print a
// more detailed version description.
version bool
}
// Indexes to help with the [commandLineOptions] initialization.
const (
confFileIdx = iota
logFileIdx
pidFileIdx
serviceActionIdx
workDirIdx
webAddrsIdx
checkConfigIdx
disableUpdateIdx
glinetModeIdx
helpIdx
localFrontend
performUpdateIdx
verboseIdx
versionIdx
)
// commandLineOption contains information about a command-line option: its long
// and, if there is one, short forms, the value type, the description, and the
// default value.
type commandLineOption struct {
defaultValue any
description string
long string
short string
valueType string
}
// commandLineOptions are all command-line options currently supported by
// AdGuard Home.
var commandLineOptions = []*commandLineOption{
confFileIdx: {
// TODO(a.garipov): Remove the ".1" when the new code is ready.
defaultValue: "AdGuardHome.1.yaml",
description: "Path to the config file.",
long: "config",
short: "c",
valueType: "path",
},
logFileIdx: {
defaultValue: "stdout",
description: `Path to log file. Special values include "stdout", "stderr", and "syslog".`,
long: "logfile",
short: "l",
valueType: "path",
},
pidFileIdx: {
defaultValue: "",
description: "Path to the file where to store the PID.",
long: "pidfile",
short: "",
valueType: "path",
},
serviceActionIdx: {
defaultValue: "",
description: `Service control action: "status", "install" (as a service), ` +
`"uninstall" (as a service), "start", "stop", "restart", "reload" (configuration).`,
long: "service",
short: "s",
valueType: "action",
},
workDirIdx: {
defaultValue: "",
description: `Path to the working directory. ` +
`It is applied before all other configuration is read, ` +
`so all relative paths are relative to it.`,
long: "work-dir",
short: "w",
valueType: "path",
},
webAddrsIdx: {
defaultValue: []netip.AddrPort(nil),
description: `Address(es) to serve the web UI on, in the host:port format. ` +
`Can be used multiple times.`,
long: "web-addr",
short: "",
valueType: "host:port",
},
checkConfigIdx: {
defaultValue: false,
description: "Check configuration and quit.",
long: "check-config",
short: "",
valueType: "",
},
disableUpdateIdx: {
defaultValue: false,
description: "Disable automatic update checking.",
long: "no-check-update",
short: "",
valueType: "",
},
glinetModeIdx: {
defaultValue: false,
description: "Run in GL-Inet compatibility mode.",
long: "glinet",
short: "",
valueType: "",
},
helpIdx: {
defaultValue: false,
description: "Print this help message and quit.",
long: "help",
short: "h",
valueType: "",
},
localFrontend: {
defaultValue: false,
description: "Use local frontend directories.",
long: "local-frontend",
short: "",
valueType: "",
},
performUpdateIdx: {
defaultValue: false,
description: "Update the current binary and restart the service in case it's installed.",
long: "update",
short: "",
valueType: "",
},
verboseIdx: {
defaultValue: false,
description: "Enable verbose logging.",
long: "verbose",
short: "v",
valueType: "",
},
versionIdx: {
defaultValue: false,
description: `Print the version to stdout and quit. ` +
`Print a more detailed version description with -v.`,
long: "version",
short: "",
valueType: "",
},
}
// parseOptions parses the command-line options for AdGuardHome.
func parseOptions(cmdName string, args []string) (opts *options, err error) {
flags := flag.NewFlagSet(cmdName, flag.ContinueOnError)
opts = &options{}
for i, fieldPtr := range []any{
confFileIdx: &opts.confFile,
logFileIdx: &opts.logFile,
pidFileIdx: &opts.pidFile,
serviceActionIdx: &opts.serviceAction,
workDirIdx: &opts.workDir,
webAddrsIdx: &opts.webAddrs,
checkConfigIdx: &opts.checkConfig,
disableUpdateIdx: &opts.disableUpdate,
glinetModeIdx: &opts.glinetMode,
helpIdx: &opts.help,
localFrontend: &opts.localFrontend,
performUpdateIdx: &opts.performUpdate,
verboseIdx: &opts.verbose,
versionIdx: &opts.version,
} {
addOption(flags, fieldPtr, commandLineOptions[i])
}
flags.Usage = func() { usage(cmdName, os.Stderr) }
err = flags.Parse(args)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return opts, nil
}
// addOption adds the command-line option described by o to flags using fieldPtr
// as the pointer to the value.
func addOption(flags *flag.FlagSet, fieldPtr any, o *commandLineOption) {
switch fieldPtr := fieldPtr.(type) {
case *string:
flags.StringVar(fieldPtr, o.long, o.defaultValue.(string), o.description)
if o.short != "" {
flags.StringVar(fieldPtr, o.short, o.defaultValue.(string), o.description)
}
case *[]netip.AddrPort:
flags.Func(o.long, o.description, func(s string) (err error) {
addr, err := netip.ParseAddrPort(s)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return err
}
*fieldPtr = append(*fieldPtr, addr)
return nil
})
case *bool:
flags.BoolVar(fieldPtr, o.long, o.defaultValue.(bool), o.description)
if o.short != "" {
flags.BoolVar(fieldPtr, o.short, o.defaultValue.(bool), o.description)
}
default:
panic(fmt.Errorf("unexpected field pointer type %T", fieldPtr))
}
}
// usage prints a usage message similar to the one printed by package flag but
// taking long vs. short versions into account as well as using more informative
// value hints.
func usage(cmdName string, output io.Writer) {
options := slices.Clone(commandLineOptions)
slices.SortStableFunc(options, func(a, b *commandLineOption) (sortsBefore bool) {
return a.long < b.long
})
b := &strings.Builder{}
_, _ = fmt.Fprintf(b, "Usage of %s:\n", cmdName)
for _, o := range options {
writeUsageLine(b, o)
// Use four spaces before the tab to trigger good alignment for both 4-
// and 8-space tab stops.
if shouldIncludeDefault(o.defaultValue) {
_, _ = fmt.Fprintf(b, " \t%s (Default value: %q)\n", o.description, o.defaultValue)
} else {
_, _ = fmt.Fprintf(b, " \t%s\n", o.description)
}
}
_, _ = io.WriteString(output, b.String())
}
// shouldIncludeDefault returns true if this default value should be printed.
func shouldIncludeDefault(v any) (ok bool) {
switch v := v.(type) {
case bool:
return v
case string:
return v != ""
default:
return v == nil
}
}
// writeUsageLine writes the usage line for the provided command-line option.
func writeUsageLine(b *strings.Builder, o *commandLineOption) {
if o.short == "" {
if o.valueType == "" {
_, _ = fmt.Fprintf(b, " --%s\n", o.long)
} else {
_, _ = fmt.Fprintf(b, " --%s=%s\n", o.long, o.valueType)
}
return
}
if o.valueType == "" {
_, _ = fmt.Fprintf(b, " --%s/-%s\n", o.long, o.short)
} else {
_, _ = fmt.Fprintf(b, " --%[1]s=%[3]s/-%[2]s %[3]s\n", o.long, o.short, o.valueType)
}
}
// processOptions decides if AdGuard Home should exit depending on the results
// of command-line option parsing.
func processOptions(
opts *options,
cmdName string,
parseErr error,
) (exitCode int, needExit bool) {
if parseErr != nil {
// Assume that usage has already been printed.
return 2, true
}
if opts.help {
usage(cmdName, os.Stdout)
return 0, true
}
if opts.version {
if opts.verbose {
fmt.Println(version.Verbose())
} else {
fmt.Printf("AdGuard Home %s\n", version.Version())
}
return 0, true
}
return 0, false
}

View File

@@ -1,7 +1,6 @@
package cmd package cmd
import ( import (
"io/fs"
"os" "os"
"time" "time"
@@ -19,10 +18,6 @@ type signalHandler struct {
// confFile is the path to the configuration file. // confFile is the path to the configuration file.
confFile string confFile string
// frontend is the filesystem with the frontend and other statically
// compiled files.
frontend fs.FS
// start is the time at which AdGuard Home has been started. // start is the time at which AdGuard Home has been started.
start time.Time start time.Time
@@ -54,7 +49,7 @@ func (h *signalHandler) reconfigure() {
status := h.shutdown() status := h.shutdown()
if status != statusSuccess { if status != statusSuccess {
log.Info("sighdlr: reconfiguring: exiting with status %d", status) log.Info("sighdlr: reconfiruging: exiting with status %d", status)
os.Exit(status) os.Exit(status)
} }
@@ -63,16 +58,16 @@ func (h *signalHandler) reconfigure() {
// reconfigured without the full shutdown, and the error handling is // reconfigured without the full shutdown, and the error handling is
// currently not the best. // currently not the best.
confMgr, err := configmgr.New(h.confFile, h.frontend, h.start) confMgr, err := configmgr.New(h.confFile, h.start)
check(err) fatalOnError(err)
web := confMgr.Web() web := confMgr.Web()
err = web.Start() err = web.Start()
check(err) fatalOnError(err)
dns := confMgr.DNS() dns := confMgr.DNS()
err = dns.Start() err = dns.Start()
check(err) fatalOnError(err)
h.services = []agh.Service{ h.services = []agh.Service{
dns, dns,
@@ -108,16 +103,10 @@ func (h *signalHandler) shutdown() (status int) {
} }
// newSignalHandler returns a new signalHandler that shuts down svcs. // newSignalHandler returns a new signalHandler that shuts down svcs.
func newSignalHandler( func newSignalHandler(confFile string, start time.Time, svcs ...agh.Service) (h *signalHandler) {
confFile string,
frontend fs.FS,
start time.Time,
svcs ...agh.Service,
) (h *signalHandler) {
h = &signalHandler{ h = &signalHandler{
signal: make(chan os.Signal, 1), signal: make(chan os.Signal, 1),
confFile: confFile, confFile: confFile,
frontend: frontend,
start: start, start: start,
services: svcs, services: svcs,
} }

View File

@@ -5,7 +5,6 @@ package configmgr
import ( import (
"context" "context"
"fmt" "fmt"
"io/fs"
"os" "os"
"sync" "sync"
"time" "time"
@@ -43,12 +42,8 @@ type Manager struct {
// New creates a new *Manager that persists changes to the file pointed to by // New creates a new *Manager that persists changes to the file pointed to by
// fileName. It reads the configuration file and populates the service fields. // fileName. It reads the configuration file and populates the service fields.
// start is the startup time of AdGuard Home. // start is the startup time of AdGuard Home.
func New( func New(fileName string, start time.Time) (m *Manager, err error) {
fileName string, defer func() { err = errors.Annotate(err, "reading config") }()
frontend fs.FS,
start time.Time,
) (m *Manager, err error) {
defer func() { err = errors.Annotate(err, "reading config: %w") }()
conf := &config{} conf := &config{}
f, err := os.Open(fileName) f, err := os.Open(fileName)
@@ -84,7 +79,7 @@ func New(
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout) ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
defer cancel() defer cancel()
err = m.assemble(ctx, conf, frontend, start) err = m.assemble(ctx, conf, start)
if err != nil { if err != nil {
// Don't wrap the error, because it's informative enough as is. // Don't wrap the error, because it's informative enough as is.
return nil, err return nil, err
@@ -95,12 +90,7 @@ func New(
// assemble creates all services and puts them into the corresponding fields. // assemble creates all services and puts them into the corresponding fields.
// The fields of conf must not be modified after calling assemble. // The fields of conf must not be modified after calling assemble.
func (m *Manager) assemble( func (m *Manager) assemble(ctx context.Context, conf *config, start time.Time) (err error) {
ctx context.Context,
conf *config,
frontend fs.FS,
start time.Time,
) (err error) {
dnsConf := &dnssvc.Config{ dnsConf := &dnssvc.Config{
Addresses: conf.DNS.Addresses, Addresses: conf.DNS.Addresses,
BootstrapServers: conf.DNS.BootstrapDNS, BootstrapServers: conf.DNS.BootstrapDNS,
@@ -114,7 +104,6 @@ func (m *Manager) assemble(
webSvcConf := &websvc.Config{ webSvcConf := &websvc.Config{
ConfigManager: m, ConfigManager: m,
Frontend: frontend,
// TODO(a.garipov): Fill from config file. // TODO(a.garipov): Fill from config file.
TLS: nil, TLS: nil,
Start: start, Start: start,
@@ -210,10 +199,7 @@ func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
} }
} }
m.web, err = websvc.New(c) m.web = websvc.New(c)
if err != nil {
return fmt.Errorf("creating web svc: %w", err)
}
return nil return nil
} }

View File

@@ -53,7 +53,6 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
newConf := &Config{ newConf := &Config{
ConfigManager: svc.confMgr, ConfigManager: svc.confMgr,
Frontend: svc.frontend,
TLS: svc.tls, TLS: svc.tls,
Addresses: req.Addresses, Addresses: req.Addresses,
SecureAddresses: req.SecureAddresses, SecureAddresses: req.SecureAddresses,
@@ -78,25 +77,18 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
// Launch the new HTTP service in a separate goroutine to let this handler // Launch the new HTTP service in a separate goroutine to let this handler
// finish and thus, this server to shutdown. // finish and thus, this server to shutdown.
go svc.relaunch(updCtx, cancelUpd, newConf) go func() {
} defer cancelUpd()
// relaunch updates the web service in the configuration manager and starts it. updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
// It is intended to be used as a goroutine. if updErr != nil {
func (svc *Service) relaunch(ctx context.Context, cancel context.CancelFunc, newConf *Config) { writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
defer log.OnPanic("websvc: relaunching")
defer cancel()
err := svc.confMgr.UpdateWeb(ctx, newConf)
if err != nil {
log.Error("websvc: updating web: %s", err)
return return
} }
// TODO(a.garipov): Consider better ways to do this. // TODO(a.garipov): Consider better ways to do this.
const maxUpdDur = 5 * time.Second const maxUpdDur = 10 * time.Second
updStart := time.Now() updStart := time.Now()
var newSvc agh.ServiceWithConfig[*Config] var newSvc agh.ServiceWithConfig[*Config]
for newSvc = svc.confMgr.Web(); newSvc == svc; { for newSvc = svc.confMgr.Web(); newSvc == svc; {
@@ -107,12 +99,12 @@ func (svc *Service) relaunch(ctx context.Context, cancel context.CancelFunc, new
} }
log.Debug("websvc: waiting for new websvc to be configured") log.Debug("websvc: waiting for new websvc to be configured")
time.Sleep(1 * time.Second)
time.Sleep(100 * time.Millisecond)
} }
err = newSvc.Start() updErr = newSvc.Start()
if err != nil { if updErr != nil {
log.Error("websvc: new svc failed to start with error: %s", err) log.Error("websvc: new svc failed to start with error: %s", updErr)
} }
}()
} }

View File

@@ -24,7 +24,9 @@ func TestService_HandlePatchSettingsHTTP(t *testing.T) {
ForceHTTPS: false, ForceHTTPS: false,
} }
svc, err := websvc.New(&websvc.Config{ confMgr := newConfigManager()
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
return websvc.New(&websvc.Config{
TLS: &tls.Config{ TLS: &tls.Config{
Certificates: []tls.Certificate{{}}, Certificates: []tls.Certificate{{}},
}, },
@@ -33,11 +35,10 @@ func TestService_HandlePatchSettingsHTTP(t *testing.T) {
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
ForceHTTPS: true, ForceHTTPS: true,
}) })
require.NoError(t, err) }
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) {
confMgr := newConfigManager() return nil
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) { return svc } }
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) { return nil }
_, addr := newTestServer(t, confMgr) _, addr := newTestServer(t, confMgr)
u := &url.URL{ u := &url.URL{
@@ -55,7 +56,7 @@ func TestService_HandlePatchSettingsHTTP(t *testing.T) {
respBody := httpPatch(t, u, req, http.StatusOK) respBody := httpPatch(t, u, req, http.StatusOK)
resp := &websvc.HTTPAPIHTTPSettings{} resp := &websvc.HTTPAPIHTTPSettings{}
err = json.Unmarshal(respBody, resp) err := json.Unmarshal(respBody, resp)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, wantWeb, resp) assert.Equal(t, wantWeb, resp)

View File

@@ -2,11 +2,9 @@ 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 // Middlewares
@@ -21,18 +19,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)
}

View File

@@ -2,9 +2,6 @@ package websvc
// Path constants // Path constants
const ( const (
PathRoot = "/"
PathFrontend = "/*filepath"
PathHealthCheck = "/health-check" PathHealthCheck = "/health-check"
PathV1SettingsAll = "/api/v1/settings/all" PathV1SettingsAll = "/api/v1/settings/all"

View File

@@ -46,7 +46,8 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
return c return c
} }
svc, err := websvc.New(&websvc.Config{ confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
return websvc.New(&websvc.Config{
TLS: &tls.Config{ TLS: &tls.Config{
Certificates: []tls.Certificate{{}}, Certificates: []tls.Certificate{{}},
}, },
@@ -55,10 +56,6 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
Timeout: time.Duration(wantWeb.Timeout), Timeout: time.Duration(wantWeb.Timeout),
ForceHTTPS: true, ForceHTTPS: true,
}) })
require.NoError(t, err)
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
return svc
} }
_, addr := newTestServer(t, confMgr) _, addr := newTestServer(t, confMgr)
@@ -70,7 +67,7 @@ func TestService_HandleGetSettingsAll(t *testing.T) {
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)

View File

@@ -11,7 +11,6 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io" "io"
"io/fs"
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
@@ -40,10 +39,6 @@ type Config struct {
// dynamically reconfigure them. // dynamically reconfigure them.
ConfigManager ConfigManager ConfigManager ConfigManager
// Frontend is the filesystem with the frontend and other statically
// compiled files.
Frontend fs.FS
// TLS is the optional TLS configuration. If TLS is not nil, // TLS is the optional TLS configuration. If TLS is not nil,
// SecureAddresses must not be empty. // SecureAddresses must not be empty.
TLS *tls.Config TLS *tls.Config
@@ -72,7 +67,6 @@ type Config struct {
// [agh.Service] that does nothing. // [agh.Service] that does nothing.
type Service struct { type Service struct {
confMgr ConfigManager confMgr ConfigManager
frontend fs.FS
tls *tls.Config tls *tls.Config
start time.Time start time.Time
servers []*http.Server servers []*http.Server
@@ -83,22 +77,13 @@ type Service struct {
// New returns a new properly initialized *Service. If c is nil, svc is a nil // New returns a new properly initialized *Service. If c is nil, svc is a nil
// *Service that does nothing. The fields of c must not be modified after // *Service that does nothing. The fields of c must not be modified after
// calling New. // calling New.
// func New(c *Config) (svc *Service) {
// TODO(a.garipov): Get rid of this special handling of nil or explain it
// better.
func New(c *Config) (svc *Service, err error) {
if c == nil { if c == nil {
return nil, nil return nil
}
frontend, err := fs.Sub(c.Frontend, "build/static")
if err != nil {
return nil, fmt.Errorf("frontend fs: %w", err)
} }
svc = &Service{ svc = &Service{
confMgr: c.ConfigManager, confMgr: c.ConfigManager,
frontend: frontend,
tls: c.TLS, tls: c.TLS,
start: c.Start, start: c.Start,
timeout: c.Timeout, timeout: c.Timeout,
@@ -136,10 +121,10 @@ func New(c *Config) (svc *Service, err error) {
}) })
} }
return svc, nil return svc
} }
// newMux returns a new HTTP request multiplexer for the AdGuard Home web // newMux returns a new HTTP request multiplexor for the AdGuard Home web
// service. // service.
func newMux(svc *Service) (mux *httptreemux.ContextMux) { func newMux(svc *Service) (mux *httptreemux.ContextMux) {
mux = httptreemux.NewContextMux() mux = httptreemux.NewContextMux()
@@ -147,54 +132,41 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
routes := []struct { routes := []struct {
handler http.HandlerFunc handler http.HandlerFunc
method string method string
pattern string path string
isJSON bool isJSON bool
}{{ }{{
handler: svc.handleGetHealthCheck, handler: svc.handleGetHealthCheck,
method: http.MethodGet, method: http.MethodGet,
pattern: PathHealthCheck, path: 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, isJSON: false,
}, { }, {
handler: svc.handleGetSettingsAll, handler: svc.handleGetSettingsAll,
method: http.MethodGet, method: http.MethodGet,
pattern: PathV1SettingsAll, path: PathV1SettingsAll,
isJSON: true, isJSON: true,
}, { }, {
handler: svc.handlePatchSettingsDNS, handler: svc.handlePatchSettingsDNS,
method: http.MethodPatch, method: http.MethodPatch,
pattern: PathV1SettingsDNS, path: PathV1SettingsDNS,
isJSON: true, isJSON: true,
}, { }, {
handler: svc.handlePatchSettingsHTTP, handler: svc.handlePatchSettingsHTTP,
method: http.MethodPatch, method: http.MethodPatch,
pattern: PathV1SettingsHTTP, path: PathV1SettingsHTTP,
isJSON: true, isJSON: true,
}, { }, {
handler: svc.handleGetV1SystemInfo, handler: svc.handleGetV1SystemInfo,
method: http.MethodGet, method: http.MethodGet,
pattern: PathV1SystemInfo, path: PathV1SystemInfo,
isJSON: true, isJSON: true,
}} }}
for _, r := range routes { for _, r := range routes {
var hdlr http.Handler
if r.isJSON { if r.isJSON {
hdlr = jsonMw(r.handler) mux.Handle(r.method, r.path, jsonMw(r.handler))
} else { } else {
hdlr = r.handler mux.Handle(r.method, r.path, r.handler)
} }
mux.Handle(r.method, r.pattern, logMw(hdlr))
} }
return mux return mux

View File

@@ -5,14 +5,12 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"io" "io"
"io/fs"
"net/http" "net/http"
"net/netip" "net/netip"
"net/url" "net/url"
"testing" "testing"
"time" "time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"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"
@@ -90,9 +88,6 @@ func newTestServer(
c := &websvc.Config{ c := &websvc.Config{
ConfigManager: confMgr, ConfigManager: confMgr,
Frontend: &aghtest.FS{
OnOpen: func(_ string) (_ fs.File, _ error) { return nil, fs.ErrNotExist },
},
TLS: nil, TLS: nil,
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")}, Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
SecureAddresses: nil, SecureAddresses: nil,
@@ -101,10 +96,9 @@ func newTestServer(
ForceHTTPS: false, ForceHTTPS: false,
} }
svc, err := websvc.New(c) svc = websvc.New(c)
require.NoError(t, err)
err = svc.Start() err := svc.Start()
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() { t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), testTimeout) ctx, cancel := context.WithTimeout(context.Background(), testTimeout)

View File

@@ -1,17 +1,25 @@
package querylog package querylog
import "github.com/AdguardTeam/AdGuardHome/internal/whois"
// Client is the information required by the query log to match against clients // Client is the information required by the query log to match against clients
// during searches. // during searches.
type Client struct { type Client struct {
WHOIS *whois.Info `json:"whois,omitempty"` WHOIS *ClientWHOIS `json:"whois,omitempty"`
Name string `json:"name"` Name string `json:"name"`
DisallowedRule string `json:"disallowed_rule"` DisallowedRule string `json:"disallowed_rule"`
Disallowed bool `json:"disallowed"` Disallowed bool `json:"disallowed"`
IgnoreQueryLog bool `json:"-"` IgnoreQueryLog bool `json:"-"`
} }
// ClientWHOIS is the filtered WHOIS data for the client.
//
// TODO(a.garipov): Merge with home.RuntimeClientWHOISInfo after the
// refactoring is done.
type ClientWHOIS struct {
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
Orgname string `json:"orgname,omitempty"`
}
// clientCacheKey is the key by which a cached client information is found. // clientCacheKey is the key by which a cached client information is found.
type clientCacheKey struct { type clientCacheKey struct {
clientID string clientID string

View File

@@ -1,108 +0,0 @@
package querylog
import (
"strconv"
"strings"
"time"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)
// csvRow is an alias type for csv rows.
type csvRow = [18]string
// csvHeaderRow is a slice of strings with column names for CSV header row.
var csvHeaderRow = csvRow{
"ans_dnssec",
"ans_rcode",
"ans_type",
"ans_value",
"cached",
"client_ip",
"client_id",
"ecs",
"elapsed",
"filter_id",
"filter_rule",
"proto",
"qclass",
"qname",
"qtype",
"reason",
"time",
"upstream",
}
// toCSV returns a slice of strings with entry fields according to the
// csvHeaderRow slice.
func (e *logEntry) toCSV() (out *csvRow) {
var filterID, filterRule string
if e.Result.IsFiltered && len(e.Result.Rules) > 0 {
rule := e.Result.Rules[0]
filterID = strconv.FormatInt(rule.FilterListID, 10)
filterRule = rule.Text
}
aData := ansData(e)
return &csvRow{
strconv.FormatBool(e.AuthenticatedData),
aData.rCode,
aData.typ,
aData.value,
strconv.FormatBool(e.Cached),
e.IP.String(),
e.ClientID,
e.ReqECS,
strconv.FormatFloat(e.Elapsed.Seconds()*1000, 'f', -1, 64),
filterID,
filterRule,
string(e.ClientProto),
e.QClass,
e.QHost,
e.QType,
e.Result.Reason.String(),
e.Time.Format(time.RFC3339Nano),
e.Upstream,
}
}
// csvAnswer is a helper struct for csv row answer fields.
type csvAnswer struct {
rCode string
typ string
value string
}
// ansData returns a map with message answer data.
func ansData(entry *logEntry) (out csvAnswer) {
if len(entry.Answer) == 0 {
return out
}
msg := &dns.Msg{}
if err := msg.Unpack(entry.Answer); err != nil {
log.Debug("querylog: failed to unpack dns msg answer: %v: %s", entry.Answer, err)
return out
}
out.rCode = dns.RcodeToString[msg.Rcode]
if len(msg.Answer) == 0 {
return out
}
rr := msg.Answer[0]
header := rr.Header()
out.typ = dns.TypeToString[header.Rrtype]
// Remove the header string from the answer value since it's mostly
// unnecessary in the log.
out.value = strings.TrimPrefix(rr.String(), header.String())
return out
}

View File

@@ -1,73 +0,0 @@
package querylog
import (
"net"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var testDate = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
func TestLogEntry_toCSV(t *testing.T) {
ans, err := dns.NewRR("www.example.org. IN A 127.0.0.1")
require.NoError(t, err)
ansBytes, err := (&dns.Msg{Answer: []dns.RR{ans}}).Pack()
require.NoError(t, err)
testCases := []struct {
entry *logEntry
want *csvRow
name string
}{{
name: "simple",
entry: &logEntry{
Time: testDate,
QHost: "test.host",
QType: "A",
QClass: "IN",
ReqECS: "",
ClientID: "test-client-id",
ClientProto: ClientProtoDoH,
Upstream: "https://test.upstream:443/dns-query",
Answer: ansBytes,
OrigAnswer: nil,
IP: net.IP{1, 2, 3, 4},
Result: filtering.Result{},
Elapsed: 500 * time.Millisecond,
Cached: false,
AuthenticatedData: false,
},
want: &[18]string{
"false",
"NOERROR",
"A",
"127.0.0.1",
"false",
"1.2.3.4",
"test-client-id",
"",
"500",
"",
"",
"doh",
"IN",
"test.host",
"A",
"NotFilteredNotFound",
"2022-01-01T00:00:00Z",
"https://test.upstream:443/dns-query",
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, tc.entry.toCSV())
})
}
}

View File

@@ -1,7 +1,6 @@
package querylog package querylog
import ( import (
"encoding/csv"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math" "math"
@@ -15,7 +14,6 @@ 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/httphdr"
"github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/stringutil" "github.com/AdguardTeam/golibs/stringutil"
"github.com/AdguardTeam/golibs/timeutil" "github.com/AdguardTeam/golibs/timeutil"
@@ -64,7 +62,6 @@ func (l *queryLog) initWeb() {
l.conf.HTTPRegister(http.MethodGet, "/control/querylog", l.handleQueryLog) l.conf.HTTPRegister(http.MethodGet, "/control/querylog", l.handleQueryLog)
l.conf.HTTPRegister(http.MethodPost, "/control/querylog_clear", l.handleQueryLogClear) l.conf.HTTPRegister(http.MethodPost, "/control/querylog_clear", l.handleQueryLogClear)
l.conf.HTTPRegister(http.MethodGet, "/control/querylog/config", l.handleGetQueryLogConfig) l.conf.HTTPRegister(http.MethodGet, "/control/querylog/config", l.handleGetQueryLogConfig)
l.conf.HTTPRegister(http.MethodGet, "/control/querylog/export", l.handleQueryLogExport)
l.conf.HTTPRegister( l.conf.HTTPRegister(
http.MethodPut, http.MethodPut,
"/control/querylog/config/update", "/control/querylog/config/update",
@@ -99,73 +96,6 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
_ = aghhttp.WriteJSONResponse(w, r, resp) _ = aghhttp.WriteJSONResponse(w, r, resp)
} }
// exportChunkSize is a size of one search-flush iteration for query log export.
//
// TODO(a.meshkov): Consider making configurable.
const exportChunkSize = 500
// handleQueryLogExport is the handler for the GET /control/querylog/export
// HTTP API.
func (l *queryLog) handleQueryLogExport(w http.ResponseWriter, r *http.Request) {
searchCriteria, err := parseSearchCriteria(r.URL.Query())
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "parsing params: %s", err)
return
}
params := &searchParams{
limit: exportChunkSize,
searchCriteria: searchCriteria,
}
w.Header().Set(httphdr.ContentType, "text/csv; charset=UTF-8; header=present")
w.Header().Set(httphdr.ContentDisposition, "attachment;filename=data.csv")
csvWriter := csv.NewWriter(w)
// Write header.
if err = csvWriter.Write(csvHeaderRow[:]); err != nil {
http.Error(w, "writing csv header", http.StatusInternalServerError)
return
}
csvWriter.Flush()
var entries []*logEntry
for {
func() {
l.confMu.RLock()
defer l.confMu.RUnlock()
entries, _ = l.search(params)
}()
if len(entries) == 0 {
break
}
params.offset += params.limit
for _, entry := range entries {
row := entry.toCSV()
if err = csvWriter.Write(row[:]); err != nil {
// TODO(a.garipov): Set Trailer X-Error header.
log.Error("%s %s %s: %s: %s", r.Method, r.Host, r.URL, "writing csv record", err)
return
}
}
csvWriter.Flush()
}
if err = csvWriter.Error(); err != nil {
// TODO(a.garipov): Set Trailer X-Error header.
log.Error("%s %s %s: %s: %s", r.Method, r.Host, r.URL, "writing csv", err)
}
}
// 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, _ *http.Request) {
@@ -430,17 +360,6 @@ func parseSearchParams(r *http.Request) (p *searchParams, err error) {
p.maxFileScanEntries = 0 p.maxFileScanEntries = 0
} }
p.searchCriteria, err = parseSearchCriteria(q)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return p, nil
}
// parseSearchCriteria parses a list of search criteria from the query.
func parseSearchCriteria(q url.Values) (searchCriteria []searchCriterion, err error) {
for _, v := range []struct { for _, v := range []struct {
urlField string urlField string
ct criterionType ct criterionType
@@ -459,9 +378,9 @@ func parseSearchCriteria(q url.Values) (searchCriteria []searchCriterion, err er
} }
if ok { if ok {
searchCriteria = append(searchCriteria, c) p.searchCriteria = append(p.searchCriteria, c)
} }
} }
return searchCriteria, nil return p, nil
} }

View File

@@ -161,15 +161,12 @@ func (l *queryLog) clear() {
// newLogEntry creates an instance of logEntry from parameters. // newLogEntry creates an instance of logEntry from parameters.
func newLogEntry(params *AddParams) (entry *logEntry) { func newLogEntry(params *AddParams) (entry *logEntry) {
q := params.Question.Question[0] q := params.Question.Question[0]
qHost := q.Name
if qHost != "." {
qHost = strings.ToLower(q.Name[:len(q.Name)-1])
}
entry = &logEntry{ entry = &logEntry{
// TODO(d.kolyshev): Export this timestamp to func params. // TODO(d.kolyshev): Export this timestamp to func params.
Time: time.Now(), Time: time.Now(),
QHost: qHost,
QHost: strings.ToLower(q.Name[:len(q.Name)-1]),
QType: dns.Type(q.Qtype).String(), QType: dns.Type(q.Qtype).String(),
QClass: dns.Class(q.Qclass).String(), QClass: dns.Class(q.Qclass).String(),

View File

@@ -43,7 +43,6 @@ func TestQueryLog(t *testing.T) {
// 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))
addEntry(l, "", net.IPv4(1, 1, 1, 5), net.IPv4(2, 2, 2, 5))
type tcAssertion struct { type tcAssertion struct {
host string host string
@@ -60,11 +59,10 @@ func TestQueryLog(t *testing.T) {
name: "all", name: "all",
sCr: []searchCriterion{}, sCr: []searchCriterion{},
want: []tcAssertion{ want: []tcAssertion{
{num: 0, host: ".", answer: net.IPv4(1, 1, 1, 5), client: net.IPv4(2, 2, 2, 5)}, {num: 0, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
{num: 1, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)}, {num: 1, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
{num: 2, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)}, {num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)}, {num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
{num: 4, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
}, },
}, { }, {
name: "by_domain_strict", name: "by_domain_strict",
@@ -106,11 +104,10 @@ func TestQueryLog(t *testing.T) {
value: "2.2.2", value: "2.2.2",
}}, }},
want: []tcAssertion{ want: []tcAssertion{
{num: 0, host: ".", answer: net.IPv4(1, 1, 1, 5), client: net.IPv4(2, 2, 2, 5)}, {num: 0, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
{num: 1, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)}, {num: 1, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
{num: 2, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)}, {num: 2, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)}, {num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
{num: 4, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
}, },
}} }}

View File

@@ -93,67 +93,3 @@ func TestQueryLog_Search_findClient(t *testing.T) {
assert.Equal(t, knownClientName, gotClient.Name) assert.Equal(t, knownClientName, gotClient.Name)
} }
// BenchmarkQueryLog_Search compares the speed of search with limit-offset
// parameters and the one with oldenThan timestamp specified.
func BenchmarkQueryLog_Search(b *testing.B) {
l, err := newQueryLog(Config{
Enabled: true,
RotationIvl: timeutil.Day,
MemSize: 100,
BaseDir: b.TempDir(),
})
require.NoError(b, err)
const (
entNum = 100000
firstPageDomain = "first.example.org"
secondPageDomain = "second.example.org"
)
// Add entries to the log.
for i := 0; i < entNum; i++ {
addEntry(l, secondPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
}
// Write them to the first file.
require.NoError(b, l.flushLogBuffer())
// Add more to the in-memory part of log.
for i := 0; i < entNum; i++ {
addEntry(l, firstPageDomain, net.IPv4(1, 1, 1, 1), net.IPv4(2, 2, 2, 1))
}
b.Run("limit_offset", func(b *testing.B) {
params := newSearchParams()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
params.offset += params.limit
_, _ = l.search(params)
}
})
b.Run("timestamp", func(b *testing.B) {
params := newSearchParams()
params.olderThan = time.Now().Add(-1 * time.Hour)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
params.olderThan = params.olderThan.Add(1 * time.Minute)
_, _ = l.search(params)
}
})
// Most recent result, on a MBP15:
//
// goos: darwin
// goarch: amd64
// pkg: github.com/AdguardTeam/AdGuardHome/internal/querylog
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
// BenchmarkQueryLog_Search
// BenchmarkQueryLog_Search/limit_offset
// BenchmarkQueryLog_Search/limit_offset-12 547 2066079 ns/op 2325019 B/op 26633 allocs/op
// BenchmarkQueryLog_Search/timestamp
// BenchmarkQueryLog_Search/timestamp-12 1303 2028888 ns/op 2219337 B/op 25194 allocs/op
}

View File

@@ -1,220 +0,0 @@
// Package schedule provides types for scheduling.
package schedule
import (
"fmt"
"time"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/timeutil"
"gopkg.in/yaml.v3"
)
// Weekly is a schedule for one week. Each day of the week has one range with
// a beginning and an end.
type Weekly struct {
// location is used to calculate the offsets of the day ranges.
location *time.Location
// days are the day ranges of this schedule. The indexes of this array are
// the [time.Weekday] values.
days [7]dayRange
}
// EmptyWeekly creates empty weekly schedule with local time zone.
func EmptyWeekly() (w *Weekly) {
return &Weekly{
location: time.Local,
}
}
// Contains returns true if t is within the corresponding day range of the
// schedule in the schedule's time zone.
func (w *Weekly) Contains(t time.Time) (ok bool) {
t = t.In(w.location)
wd := t.Weekday()
dr := w.days[wd]
// Calculate the offset of the day range.
//
// NOTE: Do not use [time.Truncate] since it requires UTC time zone.
y, m, d := t.Date()
day := time.Date(y, m, d, 0, 0, 0, 0, w.location)
offset := t.Sub(day)
return dr.contains(offset)
}
// type check
var _ yaml.Unmarshaler = (*Weekly)(nil)
// UnmarshalYAML implements the [yaml.Unmarshaler] interface for *Weekly.
func (w *Weekly) UnmarshalYAML(value *yaml.Node) (err error) {
conf := &weeklyConfig{}
err = value.Decode(conf)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
weekly := Weekly{}
weekly.location, err = time.LoadLocation(conf.TimeZone)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
days := []dayConfig{
time.Sunday: conf.Sunday,
time.Monday: conf.Monday,
time.Tuesday: conf.Tuesday,
time.Wednesday: conf.Wednesday,
time.Thursday: conf.Thursday,
time.Friday: conf.Friday,
time.Saturday: conf.Saturday,
}
for i, d := range days {
r := dayRange{
start: d.Start.Duration,
end: d.End.Duration,
}
err = w.validate(r)
if err != nil {
return fmt.Errorf("weekday %s: %w", time.Weekday(i), err)
}
weekly.days[i] = r
}
*w = weekly
return nil
}
// weeklyConfig is the YAML configuration structure of Weekly.
type weeklyConfig struct {
// TimeZone is the local time zone.
TimeZone string `yaml:"time_zone"`
// Days of the week.
Sunday dayConfig `yaml:"sun,omitempty"`
Monday dayConfig `yaml:"mon,omitempty"`
Tuesday dayConfig `yaml:"tue,omitempty"`
Wednesday dayConfig `yaml:"wed,omitempty"`
Thursday dayConfig `yaml:"thu,omitempty"`
Friday dayConfig `yaml:"fri,omitempty"`
Saturday dayConfig `yaml:"sat,omitempty"`
}
// dayConfig is the YAML configuration structure of dayRange.
type dayConfig struct {
Start timeutil.Duration `yaml:"start"`
End timeutil.Duration `yaml:"end"`
}
// maxDayRange is the maximum value for day range end.
const maxDayRange = 24 * time.Hour
// validate returns the day range rounding errors, if any.
func (w *Weekly) validate(r dayRange) (err error) {
defer func() { err = errors.Annotate(err, "bad day range: %w") }()
err = r.validate()
if err != nil {
// Don't wrap the error since it's informative enough as is.
return err
}
start := r.start.Truncate(time.Minute)
end := r.end.Truncate(time.Minute)
switch {
case start != r.start:
return fmt.Errorf("start %s isn't rounded to minutes", r.start)
case end != r.end:
return fmt.Errorf("end %s isn't rounded to minutes", r.end)
default:
return nil
}
}
// type check
var _ yaml.Marshaler = (*Weekly)(nil)
// MarshalYAML implements the [yaml.Marshaler] interface for *Weekly.
func (w *Weekly) MarshalYAML() (v any, err error) {
return weeklyConfig{
TimeZone: w.location.String(),
Sunday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Sunday].start},
End: timeutil.Duration{Duration: w.days[time.Sunday].end},
},
Monday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Monday].start},
End: timeutil.Duration{Duration: w.days[time.Monday].end},
},
Tuesday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Tuesday].start},
End: timeutil.Duration{Duration: w.days[time.Tuesday].end},
},
Wednesday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Wednesday].start},
End: timeutil.Duration{Duration: w.days[time.Wednesday].end},
},
Thursday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Thursday].start},
End: timeutil.Duration{Duration: w.days[time.Thursday].end},
},
Friday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Friday].start},
End: timeutil.Duration{Duration: w.days[time.Friday].end},
},
Saturday: dayConfig{
Start: timeutil.Duration{Duration: w.days[time.Saturday].start},
End: timeutil.Duration{Duration: w.days[time.Saturday].end},
},
}, nil
}
// dayRange represents a single interval within a day. The interval begins at
// start and ends before end. That is, it contains a time point T if start <=
// T < end.
type dayRange struct {
// start is an offset from the beginning of the day. It must be greater
// than or equal to zero and less than 24h.
start time.Duration
// end is an offset from the beginning of the day. It must be greater than
// or equal to zero and less than or equal to 24h.
end time.Duration
}
// validate returns the day range validation errors, if any.
func (r dayRange) validate() (err error) {
switch {
case r == dayRange{}:
return nil
case r.start < 0:
return fmt.Errorf("start %s is negative", r.start)
case r.end < 0:
return fmt.Errorf("end %s is negative", r.end)
case r.start >= r.end:
return fmt.Errorf("start %s is greater or equal to end %s", r.start, r.end)
case r.start >= maxDayRange:
return fmt.Errorf("start %s is greater or equal to %s", r.start, maxDayRange)
case r.end > maxDayRange:
return fmt.Errorf("end %s is greater than %s", r.end, maxDayRange)
default:
return nil
}
}
// contains returns true if start <= offset < end, where offset is the time
// duration from the beginning of the day.
func (r *dayRange) contains(offset time.Duration) (ok bool) {
return r.start <= offset && offset < r.end
}

View File

@@ -1,371 +0,0 @@
package schedule
import (
"testing"
"time"
"github.com/AdguardTeam/golibs/testutil"
"github.com/AdguardTeam/golibs/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)
func TestWeekly_Contains(t *testing.T) {
baseTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
otherTime := baseTime.Add(1 * timeutil.Day)
// NOTE: In the Etc area the sign of the offsets is flipped. So, Etc/GMT-3
// is actually UTC+03:00.
otherTZ := time.FixedZone("Etc/GMT-3", 3*60*60)
// baseSchedule, 12:00 to 14:00.
baseSchedule := &Weekly{
days: [7]dayRange{
time.Friday: {start: 12 * time.Hour, end: 14 * time.Hour},
},
location: time.UTC,
}
// allDaySchedule, 00:00 to 24:00.
allDaySchedule := &Weekly{
days: [7]dayRange{
time.Friday: {start: 0, end: 24 * time.Hour},
},
location: time.UTC,
}
// oneMinSchedule, 00:00 to 00:01.
oneMinSchedule := &Weekly{
days: [7]dayRange{
time.Friday: {start: 0, end: 1 * time.Minute},
},
location: time.UTC,
}
testCases := []struct {
schedule *Weekly
assert assert.BoolAssertionFunc
t time.Time
name string
}{{
schedule: EmptyWeekly(),
assert: assert.False,
t: baseTime,
name: "empty",
}, {
schedule: allDaySchedule,
assert: assert.True,
t: baseTime,
name: "same_day_all_day",
}, {
schedule: baseSchedule,
assert: assert.True,
t: baseTime.Add(13 * time.Hour),
name: "same_day_inside",
}, {
schedule: baseSchedule,
assert: assert.False,
t: baseTime.Add(11 * time.Hour),
name: "same_day_outside",
}, {
schedule: allDaySchedule,
assert: assert.True,
t: baseTime.Add(24*time.Hour - time.Second),
name: "same_day_last_second",
}, {
schedule: allDaySchedule,
assert: assert.False,
t: otherTime,
name: "other_day_all_day",
}, {
schedule: baseSchedule,
assert: assert.False,
t: otherTime.Add(13 * time.Hour),
name: "other_day_inside",
}, {
schedule: baseSchedule,
assert: assert.False,
t: otherTime.Add(11 * time.Hour),
name: "other_day_outside",
}, {
schedule: baseSchedule,
assert: assert.True,
t: baseTime.Add(13 * time.Hour).In(otherTZ),
name: "same_day_inside_other_tz",
}, {
schedule: baseSchedule,
assert: assert.False,
t: baseTime.Add(11 * time.Hour).In(otherTZ),
name: "same_day_outside_other_tz",
}, {
schedule: oneMinSchedule,
assert: assert.True,
t: baseTime,
name: "one_minute_beginning",
}, {
schedule: oneMinSchedule,
assert: assert.True,
t: baseTime.Add(1*time.Minute - 1),
name: "one_minute_end",
}, {
schedule: oneMinSchedule,
assert: assert.False,
t: baseTime.Add(1 * time.Minute),
name: "one_minute_past_end",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.assert(t, tc.schedule.Contains(tc.t))
})
}
}
const brusselsSunday = `
sun:
start: 12h
end: 14h
time_zone: Europe/Brussels
`
func TestWeekly_UnmarshalYAML(t *testing.T) {
const (
sameTime = `
sun:
start: 9h
end: 9h
`
negativeStart = `
sun:
start: -1h
end: 1h
`
badTZ = `
time_zone: "bad_timezone"
`
badYAML = `
yaml: "bad"
yaml: "bad"
`
)
brusseltsTZ, err := time.LoadLocation("Europe/Brussels")
require.NoError(t, err)
brusselsWeekly := &Weekly{
days: [7]dayRange{{
start: time.Hour * 12,
end: time.Hour * 14,
}},
location: brusseltsTZ,
}
testCases := []struct {
name string
wantErrMsg string
data []byte
want *Weekly
}{{
name: "empty",
wantErrMsg: "",
data: []byte(""),
want: &Weekly{},
}, {
name: "null",
wantErrMsg: "",
data: []byte("null"),
want: &Weekly{},
}, {
name: "brussels_sunday",
wantErrMsg: "",
data: []byte(brusselsSunday),
want: brusselsWeekly,
}, {
name: "start_equal_end",
wantErrMsg: "weekday Sunday: bad day range: start 9h0m0s is greater or equal to end 9h0m0s",
data: []byte(sameTime),
want: &Weekly{},
}, {
name: "start_negative",
wantErrMsg: "weekday Sunday: bad day range: start -1h0m0s is negative",
data: []byte(negativeStart),
want: &Weekly{},
}, {
name: "bad_time_zone",
wantErrMsg: "unknown time zone bad_timezone",
data: []byte(badTZ),
want: &Weekly{},
}, {
name: "bad_yaml",
wantErrMsg: "yaml: unmarshal errors:\n line 3: mapping key \"yaml\" already defined at line 2",
data: []byte(badYAML),
want: &Weekly{},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w := &Weekly{}
err = yaml.Unmarshal(tc.data, w)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
assert.Equal(t, tc.want, w)
})
}
}
func TestWeekly_MarshalYAML(t *testing.T) {
brusselsTZ, err := time.LoadLocation("Europe/Brussels")
require.NoError(t, err)
brusselsWeekly := &Weekly{
days: [7]dayRange{time.Sunday: {
start: time.Hour * 12,
end: time.Hour * 14,
}},
location: brusselsTZ,
}
testCases := []struct {
name string
data []byte
want *Weekly
}{{
name: "empty",
data: []byte(""),
want: &Weekly{},
}, {
name: "null",
data: []byte("null"),
want: &Weekly{},
}, {
name: "brussels_sunday",
data: []byte(brusselsSunday),
want: brusselsWeekly,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var data []byte
data, err = yaml.Marshal(brusselsWeekly)
require.NoError(t, err)
w := &Weekly{}
err = yaml.Unmarshal(data, w)
require.NoError(t, err)
assert.Equal(t, brusselsWeekly, w)
})
}
}
func TestWeekly_Validate(t *testing.T) {
testCases := []struct {
name string
in dayRange
wantErrMsg string
}{{
name: "empty",
wantErrMsg: "",
in: dayRange{},
}, {
name: "start_seconds",
wantErrMsg: "bad day range: start 1s isn't rounded to minutes",
in: dayRange{
start: time.Second,
end: time.Hour,
},
}, {
name: "end_seconds",
wantErrMsg: "bad day range: end 1s isn't rounded to minutes",
in: dayRange{
start: 0,
end: time.Second,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
w := &Weekly{}
err := w.validate(tc.in)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}
func TestDayRange_Validate(t *testing.T) {
testCases := []struct {
name string
in dayRange
wantErrMsg string
}{{
name: "empty",
wantErrMsg: "",
in: dayRange{},
}, {
name: "valid",
wantErrMsg: "",
in: dayRange{
start: time.Hour,
end: time.Hour * 2,
},
}, {
name: "valid_end_max",
wantErrMsg: "",
in: dayRange{
start: 0,
end: time.Hour * 24,
},
}, {
name: "start_negative",
wantErrMsg: "start -1h0m0s is negative",
in: dayRange{
start: time.Hour * -1,
end: time.Hour * 2,
},
}, {
name: "end_negative",
wantErrMsg: "end -1h0m0s is negative",
in: dayRange{
start: 0,
end: time.Hour * -1,
},
}, {
name: "start_equal_end",
wantErrMsg: "start 1h0m0s is greater or equal to end 1h0m0s",
in: dayRange{
start: time.Hour,
end: time.Hour,
},
}, {
name: "start_greater_end",
wantErrMsg: "start 2h0m0s is greater or equal to end 1h0m0s",
in: dayRange{
start: time.Hour * 2,
end: time.Hour,
},
}, {
name: "start_equal_max",
wantErrMsg: "start 24h0m0s is greater or equal to 24h0m0s",
in: dayRange{
start: time.Hour * 24,
end: time.Hour * 48,
},
}, {
name: "end_greater_max",
wantErrMsg: "end 48h0m0s is greater than 24h0m0s",
in: dayRange{
start: 0,
end: time.Hour * 48,
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.in.validate()
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
})
}
}

View File

@@ -9,8 +9,7 @@ require (
github.com/kisielk/errcheck v1.6.3 github.com/kisielk/errcheck v1.6.3
github.com/kyoh86/looppointer v0.2.1 github.com/kyoh86/looppointer v0.2.1
github.com/securego/gosec/v2 v2.16.0 github.com/securego/gosec/v2 v2.16.0
github.com/uudashr/gocognit v1.0.6 golang.org/x/tools v0.9.3
golang.org/x/tools v0.10.0
golang.org/x/vuln v0.1.0 golang.org/x/vuln v0.1.0
honnef.co/go/tools v0.4.3 honnef.co/go/tools v0.4.3
mvdan.cc/gofumpt v0.5.0 mvdan.cc/gofumpt v0.5.0
@@ -27,8 +26,8 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/exp/typeparams v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/exp/typeparams v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.11.0 // indirect golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.3.0 // indirect golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.9.0 // indirect golang.org/x/sys v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -40,8 +40,6 @@ github.com/securego/gosec/v2 v2.16.0 h1:Pi0JKoasQQ3NnoRao/ww/N/XdynIB9NRYYZT5CyO
github.com/securego/gosec/v2 v2.16.0/go.mod h1:xvLcVZqUfo4aAQu56TNv7/Ltz6emAOQAEsrZrt7uGlI= github.com/securego/gosec/v2 v2.16.0/go.mod h1:xvLcVZqUfo4aAQu56TNv7/Ltz6emAOQAEsrZrt7uGlI=
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/uudashr/gocognit v1.0.6 h1:2Cgi6MweCsdB6kpcVQp7EW4U23iBFQWfTXiWlyp842Y=
github.com/uudashr/gocognit v1.0.6/go.mod h1:nAIUuVBnYU7pcninia3BHOvQkpQCeO76Uscky5BOwcY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -58,21 +56,20 @@ golang.org/x/exp/typeparams v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:AbB0pIl
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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-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-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
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-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -82,9 +79,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
@@ -96,9 +92,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201007032633-0806396f153e/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/vuln v0.1.0 h1:9GRdj6wAIkDrsMevuolY+SXERPjQPp2P1ysYA0jpZe0= golang.org/x/vuln v0.1.0 h1:9GRdj6wAIkDrsMevuolY+SXERPjQPp2P1ysYA0jpZe0=
golang.org/x/vuln v0.1.0/go.mod h1:/YuzZYjGbwB8y19CisAppfyw3uTZnuCz3r+qgx/QRzU= golang.org/x/vuln v0.1.0/go.mod h1:/YuzZYjGbwB8y19CisAppfyw3uTZnuCz3r+qgx/QRzU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -9,7 +9,6 @@ import (
_ "github.com/kisielk/errcheck" _ "github.com/kisielk/errcheck"
_ "github.com/kyoh86/looppointer" _ "github.com/kyoh86/looppointer"
_ "github.com/securego/gosec/v2/cmd/gosec" _ "github.com/securego/gosec/v2/cmd/gosec"
_ "github.com/uudashr/gocognit/cmd/gocognit"
_ "golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness" _ "golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness"
_ "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow" _ "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow"
_ "golang.org/x/vuln/cmd/govulncheck" _ "golang.org/x/vuln/cmd/govulncheck"

View File

@@ -143,7 +143,14 @@ func Verbose() (v string) {
runtime.Version(), runtime.Version(),
) )
writeCommitTime(b) if committime != "" {
commitTimeUnix, err := strconv.ParseInt(committime, 10, 64)
if err != nil {
stringutil.WriteToBuilder(b, nl, vFmtTimeHdr, fmt.Sprintf("parse error: %s", err))
} else {
stringutil.WriteToBuilder(b, nl, vFmtTimeHdr, time.Unix(commitTimeUnix, 0).String())
}
}
stringutil.WriteToBuilder(b, nl, vFmtGOOSHdr, nl, vFmtGOARCHHdr) stringutil.WriteToBuilder(b, nl, vFmtGOOSHdr, nl, vFmtGOARCHHdr)
if goarm != "" { if goarm != "" {
@@ -172,16 +179,3 @@ func Verbose() (v string) {
return b.String() return b.String()
} }
func writeCommitTime(b *strings.Builder) {
if committime == "" {
return
}
commitTimeUnix, err := strconv.ParseInt(committime, 10, 64)
if err != nil {
stringutil.WriteToBuilder(b, "\n", vFmtTimeHdr, fmt.Sprintf("parse error: %s", err))
} else {
stringutil.WriteToBuilder(b, "\n", vFmtTimeHdr, time.Unix(commitTimeUnix, 0).String())
}
}

View File

@@ -1,376 +0,0 @@
// Package whois provides WHOIS functionality.
package whois
import (
"bytes"
"context"
"fmt"
"io"
"net"
"net/netip"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/stringutil"
"github.com/bluele/gcache"
)
const (
// DefaultServer is the default WHOIS server.
DefaultServer = "whois.arin.net"
// DefaultPort is the default port for WHOIS requests.
DefaultPort = 43
)
// Interface provides WHOIS functionality.
type Interface interface {
// Process makes WHOIS request and returns WHOIS information or nil.
// changed indicates that Info was updated since last request.
Process(ctx context.Context, ip netip.Addr) (info *Info, changed bool)
}
// Empty is an empty [Interface] implementation which does nothing.
type Empty struct{}
// type check
var _ Interface = (*Empty)(nil)
// Process implements the [Interface] interface for Empty.
func (Empty) Process(_ context.Context, _ netip.Addr) (info *Info, changed bool) {
return nil, false
}
// Config is the configuration structure for Default.
type Config struct {
// DialContext specifies the dial function for creating unencrypted TCP
// connections.
DialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
// ServerAddr is the address of the WHOIS server.
ServerAddr string
// Timeout is the timeout for WHOIS requests.
Timeout time.Duration
// CacheTTL is the Time to Live duration for cached IP addresses.
CacheTTL time.Duration
// MaxConnReadSize is an upper limit in bytes for reading from net.Conn.
MaxConnReadSize int64
// MaxRedirects is the maximum redirects count.
MaxRedirects int
// MaxInfoLen is the maximum length of Info fields returned by Process.
MaxInfoLen int
// CacheSize is the maximum size of the cache. It must be greater than
// zero.
CacheSize int
// Port is the port for WHOIS requests.
Port uint16
}
// Default is the default WHOIS information processor.
type Default struct {
// cache is the cache containing IP addresses of clients. An active IP
// address is resolved once again after it expires. If IP address couldn't
// be resolved, it stays here for some time to prevent further attempts to
// resolve the same IP.
cache gcache.Cache
// dialContext connects to a remote server resolving hostname using our own
// DNS server and unecrypted TCP connection.
dialContext func(ctx context.Context, network, addr string) (conn net.Conn, err error)
// serverAddr is the address of the WHOIS server.
serverAddr string
// portStr is the port for WHOIS requests.
portStr string
// timeout is the timeout for WHOIS requests.
timeout time.Duration
// cacheTTL is the Time to Live duration for cached IP addresses.
cacheTTL time.Duration
// maxConnReadSize is an upper limit in bytes for reading from net.Conn.
maxConnReadSize int64
// maxRedirects is the maximum redirects count.
maxRedirects int
// maxInfoLen is the maximum length of Info fields returned by Process.
maxInfoLen int
}
// New returns a new default WHOIS information processor. conf must not be
// nil.
func New(conf *Config) (w *Default) {
return &Default{
serverAddr: conf.ServerAddr,
dialContext: conf.DialContext,
timeout: conf.Timeout,
cache: gcache.New(conf.CacheSize).LRU().Build(),
maxConnReadSize: conf.MaxConnReadSize,
maxRedirects: conf.MaxRedirects,
portStr: strconv.Itoa(int(conf.Port)),
maxInfoLen: conf.MaxInfoLen,
cacheTTL: conf.CacheTTL,
}
}
// trimValue trims s and replaces the last 3 characters of the cut with "..."
// to fit into max. max must be greater than 3.
func trimValue(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max-3] + "..."
}
// isWHOISComment returns true if the data is empty or is a WHOIS comment.
func isWHOISComment(data []byte) (ok bool) {
return len(data) == 0 || data[0] == '#' || data[0] == '%'
}
// whoisParse parses a subset of plain-text data from the WHOIS response into a
// string map. It trims values of the returned map to maxLen.
func whoisParse(data []byte, maxLen int) (info map[string]string) {
info = map[string]string{}
var orgname string
lines := bytes.Split(data, []byte("\n"))
for _, l := range lines {
if isWHOISComment(l) {
continue
}
before, after, found := bytes.Cut(l, []byte(":"))
if !found {
continue
}
key := strings.ToLower(string(before))
val := strings.TrimSpace(string(after))
if val == "" {
continue
}
switch key {
case "orgname", "org-name":
key = "orgname"
val = trimValue(val, maxLen)
orgname = val
case "city", "country":
val = trimValue(val, maxLen)
case "descr", "netname":
key = "orgname"
val = stringutil.Coalesce(orgname, val)
orgname = val
case "whois":
key = "whois"
case "referralserver":
key = "whois"
val = strings.TrimPrefix(val, "whois://")
default:
continue
}
info[key] = val
}
return info
}
// query sends request to a server and returns the response or error.
func (w *Default) query(ctx context.Context, target, serverAddr string) (data []byte, err error) {
addr, _, _ := net.SplitHostPort(serverAddr)
if addr == DefaultServer {
// Display type flags for query.
//
// See https://www.arin.net/resources/registry/whois/rws/api/#nicname-whois-queries.
target = "n + " + target
}
conn, err := w.dialContext(ctx, "tcp", serverAddr)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
defer func() { err = errors.WithDeferred(err, conn.Close()) }()
r, err := aghio.LimitReader(conn, w.maxConnReadSize)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
_ = conn.SetReadDeadline(time.Now().Add(w.timeout))
_, err = io.WriteString(conn, target+"\r\n")
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
// This use of ReadAll is now safe, because we limited the conn Reader.
data, err = io.ReadAll(r)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
return data, nil
}
// queryAll queries WHOIS server and handles redirects.
func (w *Default) queryAll(ctx context.Context, target string) (info map[string]string, err error) {
server := net.JoinHostPort(w.serverAddr, w.portStr)
var data []byte
for i := 0; i < w.maxRedirects; i++ {
data, err = w.query(ctx, target, server)
if err != nil {
// Don't wrap the error since it's informative enough as is.
return nil, err
}
log.Debug("whois: received response (%d bytes) from %q about %q", len(data), server, target)
info = whoisParse(data, w.maxInfoLen)
redir, ok := info["whois"]
if !ok {
return info, nil
}
redir = strings.ToLower(redir)
_, _, err = net.SplitHostPort(redir)
if err != nil {
server = net.JoinHostPort(redir, w.portStr)
} else {
server = redir
}
log.Debug("whois: redirected to %q about %q", redir, target)
}
return nil, fmt.Errorf("whois: redirect loop")
}
// type check
var _ Interface = (*Default)(nil)
// Process makes WHOIS request and returns WHOIS information or nil. changed
// indicates that Info was updated since last request.
func (w *Default) Process(ctx context.Context, ip netip.Addr) (wi *Info, changed bool) {
if netutil.IsSpecialPurposeAddr(ip) {
return nil, false
}
wi, expired := w.findInCache(ip)
if wi != nil && !expired {
// Don't return an empty struct so that the frontend doesn't get
// confused.
if (*wi == Info{}) {
return nil, false
}
return wi, false
}
var info Info
defer func() {
item := toCacheItem(info, w.cacheTTL)
err := w.cache.Set(ip, item)
if err != nil {
log.Debug("whois: cache: adding item %q: %s", ip, err)
}
}()
kv, err := w.queryAll(ctx, ip.String())
if err != nil {
log.Debug("whois: quering about %q: %s", ip, err)
return nil, true
}
info = Info{
City: kv["city"],
Country: kv["country"],
Orgname: kv["orgname"],
}
// Don't return an empty struct so that the frontend doesn't get confused.
if (info == Info{}) {
return nil, true
}
return &info, wi == nil || info != *wi
}
// findInCache finds Info in the cache. expired indicates that Info is valid.
func (w *Default) findInCache(ip netip.Addr) (wi *Info, expired bool) {
val, err := w.cache.Get(ip)
if err != nil {
if !errors.Is(err, gcache.KeyNotFoundError) {
log.Debug("whois: cache: retrieving info about %q: %s", ip, err)
}
return nil, false
}
item, ok := val.(*cacheItem)
if !ok {
log.Debug("whois: cache: %q bad type %T", ip, val)
return nil, false
}
return fromCacheItem(item)
}
// Info is the filtered WHOIS data for a runtime client.
type Info struct {
City string `json:"city,omitempty"`
Country string `json:"country,omitempty"`
Orgname string `json:"orgname,omitempty"`
}
// cacheItem represents an item that we will store in the cache.
type cacheItem struct {
// expiry is the time when cacheItem will expire.
expiry time.Time
// info is the WHOIS data for a runtime client.
info *Info
}
// toCacheItem creates a cached item from a WHOIS info and Time to Live
// duration.
func toCacheItem(info Info, ttl time.Duration) (item *cacheItem) {
return &cacheItem{
expiry: time.Now().Add(ttl),
info: &info,
}
}
// fromCacheItem creates a WHOIS info from the cached item. expired indicates
// that WHOIS info is valid. item must not be nil.
func fromCacheItem(item *cacheItem) (info *Info, expired bool) {
if time.Now().After(item.expiry) {
return item.info, true
}
return item.info, false
}

View File

@@ -1,155 +0,0 @@
package whois_test
import (
"context"
"io"
"net"
"net/netip"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/whois"
"github.com/AdguardTeam/golibs/testutil/fakenet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDefault_Process(t *testing.T) {
const (
nl = "\n"
city = "Nonreal"
country = "Imagiland"
orgname = "FakeOrgLLC"
referralserver = "whois.example.net"
)
ip := netip.MustParseAddr("1.2.3.4")
testCases := []struct {
want *whois.Info
name string
data string
}{{
want: nil,
name: "empty",
data: "",
}, {
want: nil,
name: "comments",
data: "%\n#",
}, {
want: nil,
name: "no_colon",
data: "city",
}, {
want: nil,
name: "no_value",
data: "city:",
}, {
want: &whois.Info{
City: city,
},
name: "city",
data: "city: " + city,
}, {
want: &whois.Info{
Country: country,
},
name: "country",
data: "country: " + country,
}, {
want: &whois.Info{
Orgname: orgname,
},
name: "orgname",
data: "orgname: " + orgname,
}, {
want: &whois.Info{
Orgname: orgname,
},
name: "orgname_hyphen",
data: "org-name: " + orgname,
}, {
want: &whois.Info{
Orgname: orgname,
},
name: "orgname_descr",
data: "descr: " + orgname,
}, {
want: &whois.Info{
Orgname: orgname,
},
name: "orgname_netname",
data: "netname: " + orgname,
}, {
want: &whois.Info{
City: city,
Country: country,
Orgname: orgname,
},
name: "full",
data: "OrgName: " + orgname + nl + "City: " + city + nl + "Country: " + country,
}, {
want: nil,
name: "whois",
data: "whois: " + referralserver,
}, {
want: nil,
name: "referralserver",
data: "referralserver: whois://" + referralserver,
}, {
want: nil,
name: "other",
data: "other: value",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
hit := 0
fakeConn := &fakenet.Conn{
OnRead: func(b []byte) (n int, err error) {
hit++
return copy(b, tc.data), io.EOF
},
OnWrite: func(b []byte) (n int, err error) {
return len(b), nil
},
OnClose: func() (err error) {
return nil
},
OnSetReadDeadline: func(t time.Time) (err error) {
return nil
},
}
w := whois.New(&whois.Config{
Timeout: 5 * time.Second,
DialContext: func(_ context.Context, _, addr string) (_ net.Conn, _ error) {
hit = 0
return fakeConn, nil
},
MaxConnReadSize: 1024,
MaxRedirects: 3,
MaxInfoLen: 250,
CacheSize: 100,
CacheTTL: time.Hour,
})
got, changed := w.Process(context.Background(), ip)
require.True(t, changed)
assert.Equal(t, tc.want, got)
assert.Equal(t, 1, hit)
// From cache.
got, changed = w.Process(context.Background(), ip)
require.False(t, changed)
assert.Equal(t, tc.want, got)
assert.Equal(t, 1, hit)
})
}
}

View File

@@ -13,8 +13,8 @@ import (
// outside of the same or underlying directory. // outside of the same or underlying directory.
//go:embed build //go:embed build
var frontend embed.FS var clientBuildFS embed.FS
func main() { func main() {
cmd.Main(frontend) cmd.Main(clientBuildFS)
} }

View File

@@ -6,17 +6,6 @@
## v0.107.30: API changes ## v0.107.30: API changes
### New HTTP API 'GET /control/querylog/export'
* The new `GET /control/querylog/export` HTTP API allows an export of query log
items in the CSV file. It returns a CSV object with the following format:
```csv
ans_dnssec,ans_rcode,ans_type,ans_value,cached,client_ip,client_id,ecs,elapsed,filter_id,filter_rule,proto,qclass,qname,qtype,reason,time,upstream
false,NOERROR,A,192.168.1.1,false,127.0.0.1,,,0.097409,,,,IN,example.com,A,Rewrite,2023-01-30T12:21:13.947563+07:00,
false,NOERROR,A,45.33.2.79,false,127.0.0.1,,,482.967871,,,,IN,test.com,A,NotFilteredNotFound,2022-12-13T12:18:04.964403+07:00,https://dns10.quad9.net:443/dns-query
```
### `POST /control/version.json` and `GET /control/dhcp/interfaces` content type ### `POST /control/version.json` and `GET /control/dhcp/interfaces` content type
* The value of the `Content-Type` header in the `POST /control/version.json` and * The value of the `Content-Type` header in the `POST /control/version.json` and

View File

@@ -313,51 +313,6 @@
'responses': 'responses':
'200': '200':
'description': 'OK.' 'description': 'OK.'
'/querylog/export':
'get':
'tags':
- 'log'
'description': >
Returns a CSV file stream with the following fields, sorted a-z:
ans_dnssec, ans_rcode, ans_type, ans_value, cached, client_ip,
clientid, ecs, elapsed, filter_id, filter_rule, proto, qclass, qname,
qtype, reason, time, upstream. The fields list is a subject to change.
The content is UTF-8 encoded with quotation marks.
'operationId': 'getQueryLogExport'
'summary': 'Get DNS server query log items in a CSV stream.'
'parameters':
- 'name': 'search'
'in': 'query'
'description': 'Filter by domain name or client IP'
'schema':
'type': 'string'
- 'name': 'response_status'
'in': 'query'
'description': 'Filter by response status'
'schema':
'type': 'string'
'enum':
- 'all'
- 'filtered'
- 'blocked'
- 'blocked_safebrowsing'
- 'blocked_parental'
- 'whitelisted'
- 'rewritten'
- 'safe_search'
- 'processed'
'responses':
'200':
'description': 'OK.'
'content':
'text/csv':
'schema':
'type': 'string'
'example': >
ans_dnssec,ans_rcode,ans_type,ans_value,cached,client_ip,client_id,ecs,elapsed,filter_id,filter_rule,proto,qclass,qname,qtype,reason,time,upstream
false,NOERROR,A,192.168.1.1,false,127.0.0.1,,,0.097409,,,,IN,example.com,A,Rewrite,2023-01-30T12:21:13.947563+07:00,
false,NOERROR,A,45.33.2.79,false,127.0.0.1,,,482.967871,,,,IN,test.com,A,NotFilteredNotFound,2022-12-13T12:18:04.964403+07:00,https://dns10.quad9.net:443/dns-query
'/stats': '/stats':
'get': 'get':
'tags': 'tags':

View File

@@ -31,21 +31,16 @@ don't print anything, and `1`, be verbose.
Required environment: Required environment:
* `CHANNEL`: release channel, see above. * `CHANNEL`: release channel, see above.
* `COMMIT`: current Git revision. * `COMMIT`: current Git revision.
* `DIST_DIR`: the directory where a release has previously been built. * `DIST_DIR`: the directory where a release has previously been built.
* `VERSION`: release version. * `VERSION`: release version.
Optional environment: Optional environment:
* `DOCKER_IMAGE_NAME`: the name of the resulting Docker container. By default * `DOCKER_IMAGE_NAME`: the name of the resulting Docker container. By default
it's `adguardhome-dev`. it's `adguardhome-dev`.
* `DOCKER_OUTPUT`: the `--output` parameters. By default they are * `DOCKER_OUTPUT`: the `--output` parameters. By default they are
`type=image,name=${DOCKER_IMAGE_NAME},push=false`. `type=image,name=${DOCKER_IMAGE_NAME},push=false`.
* `SUDO`: allow users to use `sudo` or `doas` with `docker`. By default none * `SUDO`: allow users to use `sudo` or `doas` with `docker`. By default none
is used. is used.
@@ -54,36 +49,29 @@ Optional environment:
### `build-release.sh`: Build A Release For All Platforms ### `build-release.sh`: Build A Release For All Platforms
Required environment: Required environment:
* `CHANNEL`: release channel, see above. * `CHANNEL`: release channel, see above.
* `GPG_KEY` and `GPG_KEY_PASSPHRASE`: data for `gpg`. Only required if `SIGN` * `GPG_KEY` and `GPG_KEY_PASSPHRASE`: data for `gpg`. Only required if `SIGN`
is `1`. is `1`.
Optional environment: Optional environment:
* `ARCH` and `OS`: space-separated list of architectures and operating systems * `ARCH` and `OS`: space-separated list of architectures and operating systems
for which to build a release. For example, to build only for 64-bit ARM and for which to build a release. For example, to build only for 64-bit ARM and
AMD on Linux and Darwin: AMD on Linux and Darwin:
```sh ```sh
make ARCH='amd64 arm64' OS='darwin linux' … build-release make ARCH='amd64 arm64' OS='darwin linux' … build-release
``` ```
The default value is `''`, which means build everything. The default value is `''`, which means build everything.
* `BUILD_SNAP`: `0` to not build Snapcraft packages, `1` to build. The
default value is `1`.
* `DIST_DIR`: the directory to build a release into. The default value is * `DIST_DIR`: the directory to build a release into. The default value is
`dist`. `dist`.
* `GO`: set an alternative name for the Go compiler. * `GO`: set an alternative name for the Go compiler.
* `SIGN`: `0` to not sign the resulting packages, `1` to sign. The default * `SIGN`: `0` to not sign the resulting packages, `1` to sign. The default
value is `1`. value is `1`.
* `VERBOSE`: `1` to be verbose, `2` to also print environment. This script * `VERBOSE`: `1` to be verbose, `2` to also print environment. This script
calls `go-build.sh` with the verbosity level one level lower, so to get calls `go-build.sh` with the verbosity level one level lower, so to get
verbosity level `2` in `go-build.sh`, set this to `3` when calling verbosity level `2` in `go-build.sh`, set this to `3` when calling
`build-release.sh`. `build-release.sh`.
* `VERSION`: release version. Will be set by `version.sh` if it is unset or * `VERSION`: release version. Will be set by `version.sh` if it is unset or
if it has the default `Makefile` value of `v0.0.0`. if it has the default `Makefile` value of `v0.0.0`.
@@ -92,11 +80,9 @@ Optional environment:
### `clean.sh`: Cleanup ### `clean.sh`: Cleanup
Optional environment: Optional environment:
* `GO`: set an alternative name for the Go compiler. * `GO`: set an alternative name for the Go compiler.
Required environment: Required environment:
* `DIST_DIR`: the directory where a release has previously been built. * `DIST_DIR`: the directory where a release has previously been built.
@@ -104,31 +90,22 @@ Required environment:
### `go-build.sh`: Build The Backend ### `go-build.sh`: Build The Backend
Optional environment: Optional environment:
* `GOARM`: ARM processor options for the Go compiler. * `GOARM`: ARM processor options for the Go compiler.
* `GOMIPS`: ARM processor options for the Go compiler. * `GOMIPS`: ARM processor options for the Go compiler.
* `GO`: set an alternative name for the Go compiler. * `GO`: set an alternative name for the Go compiler.
* `OUT`: output binary name. * `OUT`: output binary name.
* `PARALLELISM`: set the maximum number of concurrently run build commands * `PARALLELISM`: set the maximum number of concurrently run build commands
(that is, compiler, linker, etc.). (that is, compiler, linker, etc.).
* `SOURCE_DATE_EPOCH`: the [standardized][repr] environment variable for the * `SOURCE_DATE_EPOCH`: the [standardized][repr] environment variable for the
Unix epoch time of the latest commit in the repository. If set, overrides Unix epoch time of the latest commit in the repository. If set, overrides
the default obtained from Git. Useful for reproducible builds. the default obtained from Git. Useful for reproducible builds.
* `VERBOSE`: verbosity level. `1` shows every command that is run and every * `VERBOSE`: verbosity level. `1` shows every command that is run and every
Go package that is processed. `2` also shows subcommands and environment. Go package that is processed. `2` also shows subcommands and environment.
The default value is `0`, don't be verbose. The default value is `0`, don't be verbose.
* `VERSION`: release version. Will be set by `version.sh` if it is unset or * `VERSION`: release version. Will be set by `version.sh` if it is unset or
if it has the default `Makefile` value of `v0.0.0`. if it has the default `Makefile` value of `v0.0.0`.
Required environment: Required environment:
* `CHANNEL`: release channel, see above. * `CHANNEL`: release channel, see above.
[repr]: https://reproducible-builds.org/docs/source-date-epoch/ [repr]: https://reproducible-builds.org/docs/source-date-epoch/
@@ -138,9 +115,7 @@ Required environment:
### `go-deps.sh`: Install Backend Dependencies ### `go-deps.sh`: Install Backend Dependencies
Optional environment: Optional environment:
* `GO`: set an alternative name for the Go compiler. * `GO`: set an alternative name for the Go compiler.
* `VERBOSE`: verbosity level. `1` shows every command that is run and every * `VERBOSE`: verbosity level. `1` shows every command that is run and every
Go package that is processed. `2` also shows subcommands and environment. Go package that is processed. `2` also shows subcommands and environment.
The default value is `0`, don't be verbose. The default value is `0`, don't be verbose.
@@ -152,12 +127,9 @@ Optional environment:
Don't forget to run `make go-tools` once first! Don't forget to run `make go-tools` once first!
Optional environment: Optional environment:
* `EXIT_ON_ERROR`: if set to `0`, don't exit the script after the first * `EXIT_ON_ERROR`: if set to `0`, don't exit the script after the first
encountered error. The default value is `1`. encountered error. The default value is `1`.
* `GO`: set an alternative name for the Go compiler. * `GO`: set an alternative name for the Go compiler.
* `VERBOSE`: verbosity level. `1` shows every command that is run. `2` also * `VERBOSE`: verbosity level. `1` shows every command that is run. `2` also
shows subcommands. The default value is `0`, don't be verbose. shows subcommands. The default value is `0`, don't be verbose.
@@ -166,15 +138,11 @@ Optional environment:
### `go-test.sh`: Run Backend Tests ### `go-test.sh`: Run Backend Tests
Optional environment: Optional environment:
* `GO`: set an alternative name for the Go compiler. * `GO`: set an alternative name for the Go compiler.
* `RACE`: set to `0` to not use the Go race detector. The default value is * `RACE`: set to `0` to not use the Go race detector. The default value is
`1`, use the race detector. `1`, use the race detector.
* `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is * `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is
`--timeout 30s`. `--timeout 30s`.
* `VERBOSE`: verbosity level. `1` shows every command that is run and every * `VERBOSE`: verbosity level. `1` shows every command that is run and every
Go package that is processed. `2` also shows subcommands. The default Go package that is processed. `2` also shows subcommands. The default
value is `0`, don't be verbose. value is `0`, don't be verbose.
@@ -188,7 +156,6 @@ Installs the Go static analysis and other tools into `${PWD}/bin`. Either add
directly, or use the commands through `make` (for example, `make go-lint`). directly, or use the commands through `make` (for example, `make go-lint`).
Optional environment: Optional environment:
* `GO`: set an alternative name for the Go compiler. * `GO`: set an alternative name for the Go compiler.
@@ -196,39 +163,14 @@ Optional environment:
### `version.sh`: Generate And Print The Current Version ### `version.sh`: Generate And Print The Current Version
Required environment: Required environment:
* `CHANNEL`: release channel, see above. * `CHANNEL`: release channel, see above.
## `snap/`: Snapcraft scripts ## `snap/`: Snap GUI Files
### `build.sh` App icons (see https://github.com/AdguardTeam/AdGuardHome/pull/1836), Snap
manifest file templates, and helper scripts.
Builds the Snapcraft packages from the binaries created by `download.sh`.
### `download.sh`
Downloads the binaries to pack them into Snapcraft packages.
Required environment:
* `CHANNEL`: release channel, see above.
### `upload.sh`
Uploads the Snapcraft packages created by `build.sh`.
Required environment:
* `SNAPCRAFT_CHANNEL`: Snapcraft release channel: `edge`, `beta`, or
`candidate`.
* `SNAPCRAFT_STORE_CREDENTIALS`: Credentials for Snapcraft store.
Optional environment:
* `SNAPCRAFT_CMD`: Overrides the Snapcraft command. Default: `snapcraft`.

View File

@@ -5,12 +5,11 @@ verbose="${VERBOSE:-0}"
if [ "$verbose" -gt '0' ] if [ "$verbose" -gt '0' ]
then then
set -x set -x
debug_flags='--debug=1' debug_flags='-D'
else else
set +x set +x
debug_flags='--debug=0' debug_flags=''
fi fi
readonly debug_flags
set -e -f -u set -e -f -u
@@ -62,16 +61,21 @@ readonly docker_output
case "$channel" case "$channel"
in in
('release') ('release')
docker_tags="--tag=${docker_image_name}:${version},${docker_image_name}:latest" docker_image_full_name="${docker_image_name}:${version}"
docker_tags="--tag ${docker_image_name}:latest"
;; ;;
('beta') ('beta')
docker_tags="--tag=${docker_image_name}:${version},${docker_image_name}:beta" docker_image_full_name="${docker_image_name}:${version}"
docker_tags="--tag ${docker_image_name}:beta"
;; ;;
('edge') ('edge')
docker_tags="--tag=${docker_image_name}:edge" # Don't set the version tag when pushing to the edge channel.
docker_image_full_name="${docker_image_name}:edge"
docker_tags=''
;; ;;
('development') ('development')
docker_tags="--tag=${docker_image_name}" docker_image_full_name="${docker_image_name}"
docker_tags=''
;; ;;
(*) (*)
echo "invalid channel '$channel', supported values are\ echo "invalid channel '$channel', supported values are\
@@ -79,7 +83,7 @@ in
exit 1 exit 1
;; ;;
esac esac
readonly docker_tags readonly docker_image_full_name docker_tags
# Copy the binaries into a new directory under new names, so that it's easier to # Copy the binaries into a new directory under new names, so that it's easier to
# COPY them later. DO NOT remove the trailing underscores. See file # COPY them later. DO NOT remove the trailing underscores. See file
@@ -113,8 +117,10 @@ cp "./docker/web-bind.awk"\
cp "./docker/healthcheck.sh"\ cp "./docker/healthcheck.sh"\
"${dist_docker_scripts}/healthcheck.sh" "${dist_docker_scripts}/healthcheck.sh"
# Don't use quotes with $docker_tags and $debug_flags because we want word
# splitting and or an empty space if tags are empty.
$sudo_cmd docker\ $sudo_cmd docker\
"$debug_flags"\ $debug_flags\
buildx build\ buildx build\
--build-arg BUILD_DATE="$build_date"\ --build-arg BUILD_DATE="$build_date"\
--build-arg DIST_DIR="$dist_dir"\ --build-arg DIST_DIR="$dist_dir"\
@@ -122,6 +128,7 @@ $sudo_cmd docker\
--build-arg VERSION="$version"\ --build-arg VERSION="$version"\
--output "$docker_output"\ --output "$docker_output"\
--platform "$docker_platforms"\ --platform "$docker_platforms"\
"$docker_tags"\ $docker_tags\
-t "$docker_image_full_name"\
-f ./docker/Dockerfile\ -f ./docker/Dockerfile\
. .

View File

@@ -78,6 +78,14 @@ else
fi fi
readonly oses readonly oses
snap_enabled="${BUILD_SNAP:-1}"
readonly snap_enabled
if [ "$snap_enabled" -eq '0' ]
then
log 'snap: disabled'
fi
# Require the gpg key and passphrase to be set if the signing is required. # Require the gpg key and passphrase to be set if the signing is required.
if [ "$sign" -eq '1' ] if [ "$sign" -eq '1' ]
then then
@@ -98,7 +106,7 @@ log "checking tools"
# Make sure we fail gracefully if one of the tools we need is missing. Use # Make sure we fail gracefully if one of the tools we need is missing. Use
# alternatives when available. # alternatives when available.
use_shasum='0' use_shasum='0'
for tool in gpg gzip sed sha256sum tar zip for tool in gpg gzip sed sha256sum snapcraft tar zip
do do
if ! command -v "$tool" > /dev/null if ! command -v "$tool" > /dev/null
then then
@@ -120,36 +128,36 @@ readonly use_shasum
# Data section. Arrange data into space-separated tables for read -r to read. # Data section. Arrange data into space-separated tables for read -r to read.
# Use a hyphen for missing values. # Use a hyphen for missing values.
# os arch arm mips # os arch arm mips snap
platforms="\ platforms="\
darwin amd64 - - darwin amd64 - - -
darwin arm64 - - darwin arm64 - - -
freebsd 386 - - freebsd 386 - - -
freebsd amd64 - - freebsd amd64 - - -
freebsd arm 5 - freebsd arm 5 - -
freebsd arm 6 - freebsd arm 6 - -
freebsd arm 7 - freebsd arm 7 - -
freebsd arm64 - - freebsd arm64 - - -
linux 386 - - linux 386 - - i386
linux amd64 - - linux amd64 - - amd64
linux arm 5 - linux arm 5 - -
linux arm 6 - linux arm 6 - -
linux arm 7 - linux arm 7 - armhf
linux arm64 - - linux arm64 - - arm64
linux mips - softfloat linux mips - softfloat -
linux mips64 - softfloat linux mips64 - softfloat -
linux mips64le - softfloat linux mips64le - softfloat -
linux mipsle - softfloat linux mipsle - softfloat -
linux ppc64le - - linux ppc64le - - -
openbsd amd64 - - openbsd amd64 - - -
openbsd arm64 - - openbsd arm64 - - -
windows 386 - - windows 386 - - -
windows amd64 - - windows amd64 - - -
windows arm64 - -" windows arm64 - - -"
readonly platforms readonly platforms
# Function build builds the release for one platform. It builds a binary and an # Function build builds the release for one platform. It builds a binary, an
# archive. # archive and, if needed, a snap package.
build() { build() {
# Get the arguments. Here and below, use the "build_" prefix for all # Get the arguments. Here and below, use the "build_" prefix for all
# variables local to function build. # variables local to function build.
@@ -159,6 +167,7 @@ build() {
build_arch="$4"\ build_arch="$4"\
build_arm="$5"\ build_arm="$5"\
build_mips="$6"\ build_mips="$6"\
build_snap="$7"\
; ;
# Use the ".exe" filename extension if we build a Windows release. # Use the ".exe" filename extension if we build a Windows release.
@@ -220,13 +229,52 @@ build() {
esac esac
log "$build_archive" log "$build_archive"
# Exit if we don't need to build the Snap package.
if [ "$build_snap" = '-' ] || [ "$snap_enabled" -eq '0' ]
then
return
fi
# Prepare the Snap build.
build_snap_output="./${dist}/AdGuardHome_${build_snap}.snap"
build_snap_dir="${build_snap_output}.dir"
# Create the meta subdirectory and copy files there.
mkdir -p "${build_snap_dir}/meta"
cp "$build_output" './scripts/snap/local/adguard-home-web.sh' "$build_snap_dir"
cp -r './scripts/snap/gui' "${build_snap_dir}/meta/"
# Create a snap.yaml file, setting the values.
sed -e 's/%VERSION%/'"$version"'/'\
-e 's/%ARCH%/'"$build_snap"'/'\
./scripts/snap/snap.tmpl.yaml\
>"${build_snap_dir}/meta/snap.yaml"
# TODO(a.garipov): The snapcraft tool will *always* write everything,
# including errors, to stdout. And there doesn't seem to be a way to change
# that. So, save the combined output, but only show it when snapcraft
# actually fails.
set +e
build_snapcraft_output="$(
snapcraft pack "$build_snap_dir" --output "$build_snap_output" 2>&1
)"
build_snapcraft_exit_code="$?"
set -e
if [ "$build_snapcraft_exit_code" -ne '0' ]
then
log "$build_snapcraft_output"
exit "$build_snapcraft_exit_code"
fi
log "$build_snap_output"
} }
log "starting builds" log "starting builds"
# Go over all platforms defined in the space-separated table above, tweak the # Go over all platforms defined in the space-separated table above, tweak the
# values where necessary, and feed to build. # values where necessary, and feed to build.
echo "$platforms" | while read -r os arch arm mips echo "$platforms" | while read -r os arch arm mips snap
do do
# See if the architecture or the OS is in the allowlist. To do so, try # See if the architecture or the OS is in the allowlist. To do so, try
# removing everything that matches the pattern (well, a prefix, but that # removing everything that matches the pattern (well, a prefix, but that
@@ -266,7 +314,7 @@ do
;; ;;
esac esac
build "$dir" "$ar" "$os" "$arch" "$arm" "$mips" build "$dir" "$ar" "$os" "$arch" "$arm" "$mips" "$snap"
done done
log "packing frontend" log "packing frontend"
@@ -365,14 +413,14 @@ do
platform="$f" platform="$f"
# Remove the prefix. # Remove the prefix.
platform="${platform#"./${dist}/AdGuardHome_"}" platform="${platform#./${dist}/AdGuardHome_}"
# Remove the filename extensions. # Remove the filename extensions.
platform="${platform%.zip}" platform="${platform%.zip}"
platform="${platform%.tar.gz}" platform="${platform%.tar.gz}"
# Use the filename's base path. # Use the filename's base path.
filename="${f#"./${dist}/"}" filename="${f#./${dist}/}"
if [ "$i" -eq "$ar_files_len" ] if [ "$i" -eq "$ar_files_len" ]
then then

View File

@@ -3,7 +3,7 @@
# This comment is used to simplify checking local copies of the script. Bump # This comment is used to simplify checking local copies of the script. Bump
# this number every time a significant change is made to this script. # this number every time a significant change is made to this script.
# #
# AdGuard-Project-Version: 4 # AdGuard-Project-Version: 3
verbose="${VERBOSE:-0}" verbose="${VERBOSE:-0}"
readonly verbose readonly verbose
@@ -80,12 +80,6 @@ esac
# #
# * Package golang.org/x/net/context has been moved into stdlib. # * Package golang.org/x/net/context has been moved into stdlib.
# #
# Currently, the only standard exception are files generated from protobuf
# schemas, which use package reflect. If your project needs more exceptions,
# add and document them.
#
# TODO(a.garipov): Add deprecated packages golang.org/x/exp/maps and
# golang.org/x/exp/slices once all projects switch to Go 1.21.
blocklist_imports() { blocklist_imports() {
git grep\ git grep\
-e '[[:space:]]"errors"$'\ -e '[[:space:]]"errors"$'\
@@ -97,7 +91,6 @@ blocklist_imports() {
-e '[[:space:]]"golang.org/x/net/context"$'\ -e '[[:space:]]"golang.org/x/net/context"$'\
-n\ -n\
-- '*.go'\ -- '*.go'\
':!*.pb.go'\
| sed -e 's/^\([^[:space:]]\+\)\(.*\)$/\1 blocked import:\2/'\ | sed -e 's/^\([^[:space:]]\+\)\(.*\)$/\1 blocked import:\2/'\
|| exit 0 || exit 0
} }
@@ -108,7 +101,6 @@ method_const() {
git grep -F\ git grep -F\
-e '"DELETE"'\ -e '"DELETE"'\
-e '"GET"'\ -e '"GET"'\
-e '"PATCH"'\
-e '"POST"'\ -e '"POST"'\
-e '"PUT"'\ -e '"PUT"'\
-n\ -n\
@@ -170,24 +162,12 @@ run_linter govulncheck ./...
run_linter gocyclo --over 10 . run_linter gocyclo --over 10 .
# TODO(a.garipov): Enable for all.
run_linter gocognit --over 10\
./internal/aghalg/\
./internal/aghchan/\
./internal/aghhttp/\
./internal/aghio/\
./internal/tools/\
./internal/next/\
./internal/version/\
;
run_linter ineffassign ./... run_linter ineffassign ./...
run_linter unparam ./... run_linter unparam ./...
git ls-files -- 'Makefile' '*.conf' '*.go' '*.mod' '*.sh' '*.yaml' '*.yml'\ git ls-files -- 'Makefile' '*.go' '*.mod' '*.sh' '*.yaml' '*.yml'\
| xargs misspell --error\ | xargs misspell --error
| sed -e 's/^/misspell: /'
run_linter looppointer ./... run_linter looppointer ./...
@@ -203,13 +183,4 @@ run_linter -e shadow --strict ./...
# TODO(a.garipov): Enable --blank? # TODO(a.garipov): Enable --blank?
run_linter errcheck --asserts ./... run_linter errcheck --asserts ./...
staticcheck_matrix=' run_linter staticcheck ./...
darwin: GOOS=darwin
freebsd: GOOS=freebsd
linux: GOOS=linux
openbsd: GOOS=openbsd
windows: GOOS=windows
'
readonly staticcheck_matrix
echo "$staticcheck_matrix" | run_linter staticcheck --matrix ./...

View File

@@ -38,7 +38,6 @@ readonly go
rm -f\ rm -f\
bin/errcheck\ bin/errcheck\
bin/fieldalignment\ bin/fieldalignment\
bin/gocognit\
bin/gocyclo\ bin/gocyclo\
bin/gofumpt\ bin/gofumpt\
bin/gosec\ bin/gosec\
@@ -70,7 +69,6 @@ env\
github.com/kisielk/errcheck\ github.com/kisielk/errcheck\
github.com/kyoh86/looppointer/cmd/looppointer\ github.com/kyoh86/looppointer/cmd/looppointer\
github.com/securego/gosec/v2/cmd/gosec\ github.com/securego/gosec/v2/cmd/gosec\
github.com/uudashr/gocognit/cmd/gocognit\
golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment\ golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment\
golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness\ golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness\
golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow\ golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow\

View File

@@ -1,77 +0,0 @@
#!/bin/sh
verbose="${VERBOSE:-0}"
if [ "$verbose" -gt '0' ]
then
set -x
fi
set -e -f -u
# Function log is an echo wrapper that writes to stderr if the caller requested
# verbosity level greater than 0. Otherwise, it does nothing.
#
# TODO(a.garipov): Add to helpers.sh and use more actively in scripts.
log() {
if [ "$verbose" -gt '0' ]
then
# Don't use quotes to get word splitting.
echo "$1" 1>&2
fi
}
version="$( ./AdGuardHome_amd64 --version | cut -d ' ' -f 4 )"
if [ "$version" = '' ]
then
log 'empty version from ./AdGuardHome_amd64'
exit 1
fi
readonly version
log "version '$version'"
for arch in\
'i386'\
'amd64'\
'armhf'\
'arm64'
do
build_output="./AdGuardHome_${arch}"
snap_output="./AdGuardHome_${arch}.snap"
snap_dir="${snap_output}.dir"
# Create the meta subdirectory and copy files there.
mkdir -p "${snap_dir}/meta"
cp "$build_output" "${snap_dir}/AdGuardHome"
cp './snap/local/adguard-home-web.sh' "$snap_dir"
cp -r './snap/gui' "${snap_dir}/meta/"
# Create a snap.yaml file, setting the values.
sed\
-e 's/%VERSION%/'"$version"'/'\
-e 's/%ARCH%/'"$arch"'/'\
./snap/snap.tmpl.yaml\
> "${snap_dir}/meta/snap.yaml"
# TODO(a.garipov): The snapcraft tool will *always* write everything,
# including errors, to stdout. And there doesn't seem to be a way to change
# that. So, save the combined output, but only show it when snapcraft
# actually fails.
set +e
snapcraft_output="$(
snapcraft pack "$snap_dir" --output "$snap_output" 2>&1
)"
snapcraft_exit_code="$?"
set -e
if [ "$snapcraft_exit_code" -ne '0' ]
then
log "$snapcraft_output"
exit "$snapcraft_exit_code"
fi
log "$snap_output"
rm -f -r "$snap_dir"
done

View File

@@ -1,29 +0,0 @@
#!/bin/sh
verbose="${VERBOSE:-0}"
if [ "$verbose" -gt '0' ]
then
set -x
fi
set -e -f -u
channel="${CHANNEL:?please set CHANNEL}"
readonly channel
printf '%s %s\n'\
'386' 'i386'\
'amd64' 'amd64'\
'armv7' 'armhf'\
'arm64' 'arm64' \
| while read -r arch snap_arch
do
release_url="https://static.adtidy.org/adguardhome/${channel}/AdGuardHome_linux_${arch}.tar.gz"
output="./AdGuardHome_linux_${arch}.tar.gz"
curl -o "$output" -v "$release_url"
tar -f "$output" -v -x -z
cp ./AdGuardHome/AdGuardHome "./AdGuardHome_${snap_arch}"
rm -f -r "$output" ./AdGuardHome
done

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,93 +0,0 @@
#!/bin/sh
verbose="${VERBOSE:-0}"
if [ "$verbose" -gt '0' ]
then
set -x
fi
set -e -f -u
# Function log is an echo wrapper that writes to stderr if the caller requested
# verbosity level greater than 0. Otherwise, it does nothing.
log() {
if [ "$verbose" -gt '0' ]
then
# Don't use quotes to get word splitting.
echo "$1" 1>&2
fi
}
# Do not set a new lowercase variable, because the snapcraft tool expects the
# uppercase form.
if [ "${SNAPCRAFT_STORE_CREDENTIALS:-}" = '' ]
then
log 'please set SNAPCRAFT_STORE_CREDENTIALS'
exit 1
fi
export SNAPCRAFT_STORE_CREDENTIALS
snapcraft_channel="${SNAPCRAFT_CHANNEL:?please set SNAPCRAFT_CHANNEL}"
readonly snapcraft_channel
# Allow developers to overwrite the command, e.g. for testing.
snapcraft_cmd="${SNAPCRAFT_CMD:-snapcraft}"
readonly snapcraft_cmd
default_timeout='90s'
kill_timeout='120s'
readonly default_timeout kill_timeout
for arch in\
'i386'\
'amd64'\
'armhf'\
'arm64'
do
snap_file="./AdGuardHome_${arch}.snap"
# Catch the exit code and the combined output to later inspect it.
set +e
snapcraft_output="$(
# Use timeout(1) to force snapcraft to quit after a certain time. There
# seems to be no environment variable or flag to force this behavior.
timeout\
--preserve-status\
-k "$kill_timeout"\
-v "$default_timeout"\
"$snapcraft_cmd" upload\
--release="${snapcraft_channel}"\
--quiet\
"${snap_file}"\
2>&1
)"
snapcraft_exit_code="$?"
set -e
if [ "$snapcraft_exit_code" -eq '0' ]
then
log "successful upload: ${snapcraft_output}"
continue
fi
# Skip the ones that were failed by a duplicate upload error.
case "$snapcraft_output"
in
(*'A file with this exact same content has already been uploaded'|\
'Error checking upload uniqueness'*)
log "warning: duplicate upload, skipping"
log "snapcraft upload error: ${snapcraft_output}"
continue
;;
(*)
echo "unexpected snapcraft upload error: ${snapcraft_output}"
return "$snapcraft_exit_code"
;;
esac
done

View File

@@ -4,20 +4,13 @@ initialisms = [
# #
# Do not add "PTR" since we use "Ptr" as a suffix. # Do not add "PTR" since we use "Ptr" as a suffix.
"inherit" "inherit"
, "ASN"
, "DHCP" , "DHCP"
, "DNSSEC"
# E.g. SentryDSN.
, "DSN"
, "ECS"
, "EDNS" , "EDNS"
, "MX" , "MX"
, "QUIC" , "QUIC"
, "RA" , "RA"
, "RRSIG"
, "SDNS" , "SDNS"
, "SLAAC" , "SLAAC"
, "SOA"
, "SVCB" , "SVCB"
, "TLD" , "TLD"
, "WHOIS" , "WHOIS"