Compare commits
30 Commits
v0.108.0-b
...
3389-query
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94458c5658 | ||
|
|
123ca87388 | ||
|
|
994906fbd4 | ||
|
|
06d465b0d1 | ||
|
|
ca313521dc | ||
|
|
2902f030be | ||
|
|
371261b2c6 | ||
|
|
d26c480d03 | ||
|
|
b6d00f774b | ||
|
|
6fea435d89 | ||
|
|
05706bd7ea | ||
|
|
d3ada9881a | ||
|
|
7309a53356 | ||
|
|
00327757e1 | ||
|
|
5f0e53ded7 | ||
|
|
5cd4ce766d | ||
|
|
e695fd9885 | ||
|
|
c43053e7d2 | ||
|
|
86e25944b3 | ||
|
|
fd7260f6de | ||
|
|
c591e46254 | ||
|
|
66d9ea7cca | ||
|
|
dafc785845 | ||
|
|
e9b17891bb | ||
|
|
0b27f048a7 | ||
|
|
649454e77b | ||
|
|
ca22d8524d | ||
|
|
07f4f0474c | ||
|
|
8813e135b6 | ||
|
|
f4f2c11eb9 |
130
.github/ISSUE_TEMPLATE/bug.yml
vendored
130
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -10,52 +10,58 @@
|
||||
- 'label': >
|
||||
I have checked the
|
||||
[Wiki](https://github.com/AdguardTeam/AdGuardHome/wiki) and
|
||||
[Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions)
|
||||
[Discussions](https://github.com/AdguardTeam/AdGuardHome/discussions/categories/q-a)
|
||||
and found no answer
|
||||
'required': true
|
||||
- 'label': >
|
||||
I have searched other issues and found no duplicates
|
||||
'required': true
|
||||
- 'label': >
|
||||
I want to report a bug and not ask a question
|
||||
I want to report a bug and not [ask a question or ask for
|
||||
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
|
||||
'id': 'prerequisites'
|
||||
'type': 'checkboxes'
|
||||
- 'attributes':
|
||||
'description': 'On which operating system type does the issue occur?'
|
||||
'label': 'Operating system type'
|
||||
'description': 'On which Platform does the issue occur?'
|
||||
'label': 'Platform (OS and CPU architecture)'
|
||||
'options':
|
||||
- 'FreeBSD'
|
||||
- 'Linux, OpenWrt'
|
||||
- 'Linux, Other (please mention the version in the description)'
|
||||
- 'macOS (aka Darwin)'
|
||||
- 'OpenBSD'
|
||||
- 'Windows'
|
||||
- 'Other (please mention in the description)'
|
||||
- 'Darwin (aka macOS)/AMD64 (aka x86_64)'
|
||||
- 'Darwin (aka macOS)/ARM64'
|
||||
- 'FreeBSD/386'
|
||||
- 'FreeBSD/AMD64 (aka x86_64)'
|
||||
- 'FreeBSD/ARM64'
|
||||
- 'FreeBSD/ARMv5'
|
||||
- 'FreeBSD/ARMv6'
|
||||
- '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'
|
||||
'type': 'dropdown'
|
||||
'validations':
|
||||
'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':
|
||||
'description': 'How did you install AdGuard Home?'
|
||||
'label': 'Installation'
|
||||
@@ -63,7 +69,7 @@
|
||||
- 'GitHub releases or script from README'
|
||||
- 'Docker'
|
||||
- 'Snapcraft'
|
||||
- 'Custom port'
|
||||
- 'Custom package (OpenWrt, HomeAssistant, etc; please mention in the description)'
|
||||
- 'Other (please mention in the description)'
|
||||
'id': 'install'
|
||||
'type': 'dropdown'
|
||||
@@ -89,21 +95,55 @@
|
||||
'validations':
|
||||
'required': true
|
||||
- 'attributes':
|
||||
'description': 'Please describe the bug'
|
||||
'label': 'Description'
|
||||
'description': >
|
||||
Please describe what you did. An `nslookup` or a `dig` command is
|
||||
the best way. For crashes, please provide a full failure log.
|
||||
'label': 'Action'
|
||||
'value': |
|
||||
#### What did you do?
|
||||
|
||||
#### Expected result
|
||||
|
||||
#### Actual result
|
||||
|
||||
#### Screenshots (if applicable)
|
||||
|
||||
#### Additional information
|
||||
'id': 'description'
|
||||
```sh
|
||||
nslookup -debug -type=a 'www.example.com' '$YOUR_AGH_ADDRESS'
|
||||
```
|
||||
'id': 'failing_action'
|
||||
'type': 'textarea'
|
||||
'validations':
|
||||
'required': true
|
||||
'description': 'File a bug report'
|
||||
- 'attributes':
|
||||
'description': >
|
||||
What did you expect to see? Please add a description and/or
|
||||
screenshots, if applicable.
|
||||
'label': 'Expected result'
|
||||
'placeholder': >
|
||||
What did you expect to see?
|
||||
'id': 'expected'
|
||||
'type': 'textarea'
|
||||
'validations':
|
||||
'required': true
|
||||
- 'attributes':
|
||||
'description': >
|
||||
What happened instead? Please add a description and/or screenshots,
|
||||
if applicable.
|
||||
'label': 'Actual result'
|
||||
'placeholder': >
|
||||
What did you see instead?
|
||||
'id': 'result'
|
||||
'type': 'textarea'
|
||||
'validations':
|
||||
'required': true
|
||||
- 'attributes':
|
||||
'description': >
|
||||
Please add additional information, such as non-standard OS or port,
|
||||
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'
|
||||
'validations':
|
||||
'required': false
|
||||
'description': >
|
||||
Open a bug report. Please do not open bug reports for questions or help
|
||||
with configuring clients. If you want to ask for help, use the Discussions
|
||||
section.
|
||||
'name': 'Bug'
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/feature.yml
vendored
35
.github/ISSUE_TEMPLATE/feature.yml
vendored
@@ -23,19 +23,32 @@
|
||||
'id': 'prerequisites'
|
||||
'type': 'checkboxes'
|
||||
- 'attributes':
|
||||
'description': 'Please describe the request'
|
||||
'label': 'Description'
|
||||
'value': |
|
||||
#### What problem are you trying to solve?
|
||||
|
||||
#### Proposed solution
|
||||
|
||||
#### Alternatives considered
|
||||
|
||||
#### Additional information
|
||||
'id': 'description'
|
||||
'description': 'Please describe the problem you are trying to solve'
|
||||
'label': 'The problem'
|
||||
'placeholder': >
|
||||
Please describe the problem you are trying to solve
|
||||
'id': 'problem'
|
||||
'type': 'textarea'
|
||||
'validations':
|
||||
'required': true
|
||||
- 'attributes':
|
||||
'description': 'What feature are you proposing to solve this problem?'
|
||||
'label': 'Proposed solution'
|
||||
'placeholder': >
|
||||
What feature are you proposing to solve this problem?
|
||||
'id': 'proposed_solution'
|
||||
'type': 'textarea'
|
||||
'validations':
|
||||
'required': true
|
||||
- 'attributes':
|
||||
'label': 'Alternatives considered and additional information'
|
||||
'placeholder': >
|
||||
Are there any other ways to solve the problem?
|
||||
'id': 'additional'
|
||||
'type': 'textarea'
|
||||
'validations':
|
||||
'required': false
|
||||
'description': 'Suggest a feature or an enhancement for AdGuard Home'
|
||||
'labels':
|
||||
- 'feature request'
|
||||
'name': 'Feature request or enhancement'
|
||||
|
||||
20
.github/PULL_REQUEST_TEMPLATE
vendored
Normal file
20
.github/PULL_REQUEST_TEMPLATE
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
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.
|
||||
18
.github/workflows/potential-duplicates.yml
vendored
Normal file
18
.github/workflows/potential-duplicates.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
'name': 'potential-duplicates'
|
||||
'on':
|
||||
'issues':
|
||||
'types':
|
||||
- 'opened'
|
||||
'jobs':
|
||||
'run':
|
||||
'runs-on': 'ubuntu-latest'
|
||||
'steps':
|
||||
- 'uses': 'wow-actions/potential-duplicates@v1'
|
||||
'with':
|
||||
'GITHUB_TOKEN': '${{ secrets.GITHUB_TOKEN }}'
|
||||
'state': 'all'
|
||||
'threshold': 0.6
|
||||
'comment': |
|
||||
Potential duplicates: {{#issues}}
|
||||
* [#{{ number }}] {{ title }} ({{ accuracy }}%)
|
||||
{{/issues}}
|
||||
76
CHANGELOG.md
76
CHANGELOG.md
@@ -14,21 +14,87 @@ and this project adheres to
|
||||
<!--
|
||||
## [v0.108.0] - TBA
|
||||
|
||||
## [v0.107.32] - 2023-06-28 (APPROX.)
|
||||
## [v0.107.33] - 2023-06-28 (APPROX.)
|
||||
|
||||
See also the [v0.107.32 GitHub milestone][ms-v0.107.32].
|
||||
See also the [v0.107.33 GitHub milestone][ms-v0.107.33].
|
||||
|
||||
[ms-v0.107.32]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
|
||||
[ms-v0.107.33]: https://github.com/AdguardTeam/AdGuardHome/milestone/68?closed=1
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
### 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
|
||||
([#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.
|
||||
@@ -40,8 +106,8 @@ NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
|
||||
### Fixed
|
||||
|
||||
- DNSCrypt upstream not resetting the client and resolver information on
|
||||
dialing errors ([#5872]).
|
||||
- DNSCrypt upstream not resetting the client and resolver information on
|
||||
dialing errors ([#5872]).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
---
|
||||
!include test.yaml
|
||||
|
||||
---
|
||||
!include release.yaml
|
||||
|
||||
---
|
||||
!include snapcraft.yaml
|
||||
|
||||
---
|
||||
!include test.yaml
|
||||
|
||||
@@ -1,348 +1,290 @@
|
||||
---
|
||||
'version': 2
|
||||
'plan':
|
||||
'project-key': 'AGH'
|
||||
'key': 'AGHBSNAPSPECS'
|
||||
'name': 'AdGuard Home - Build and publish release'
|
||||
'project-key': 'AGH'
|
||||
'key': 'AGHBSNAPSPECS'
|
||||
'name': 'AdGuard Home - Build and publish release'
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
|
||||
'stages':
|
||||
- 'Build frontend':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Build frontend'
|
||||
- 'Build frontend':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Build frontend'
|
||||
|
||||
- 'Make release':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Make release'
|
||||
- 'Make release':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Make release'
|
||||
|
||||
- 'Make and publish docker':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Make and publish docker'
|
||||
- 'Make and publish docker':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Make and publish docker'
|
||||
|
||||
- 'Publish to static storage':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Publish to static storage'
|
||||
- 'Publish to static storage':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Publish to static storage'
|
||||
|
||||
- 'Publish to Snapstore':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Publish to Snapstore'
|
||||
|
||||
- 'Publish to GitHub Releases':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Publish to GitHub Releases'
|
||||
- 'Publish to GitHub Releases':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Publish to GitHub Releases'
|
||||
|
||||
'Build frontend':
|
||||
'docker':
|
||||
'image': '${bamboo.dockerGo}'
|
||||
'volumes':
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'key': 'BF'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'force-clean-build': true
|
||||
- 'script':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
- |
|
||||
#!/bin/sh
|
||||
'docker':
|
||||
'image': '${bamboo.dockerGo}'
|
||||
'volumes':
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'key': 'BF'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'force-clean-build': true
|
||||
- 'script':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
- |
|
||||
#!/bin/sh
|
||||
|
||||
set -e -f -u -x
|
||||
set -e -f -u -x
|
||||
|
||||
# Explicitly checkout the revision that we need.
|
||||
git checkout "${bamboo.repository.revision.number}"
|
||||
# Explicitly checkout the revision that we need.
|
||||
git checkout "${bamboo.repository.revision.number}"
|
||||
|
||||
make js-deps js-build
|
||||
'artifacts':
|
||||
- 'name': 'AdGuardHome frontend'
|
||||
'pattern': 'build*/**'
|
||||
'shared': true
|
||||
'required': true
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
make js-deps js-build
|
||||
'artifacts':
|
||||
- 'name': 'AdGuardHome frontend'
|
||||
'pattern': 'build/**'
|
||||
'shared': true
|
||||
'required': true
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
'Make release':
|
||||
'docker':
|
||||
'image': '${bamboo.dockerGo}'
|
||||
'volumes':
|
||||
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
|
||||
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
|
||||
'key': 'MR'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'force-clean-build': true
|
||||
- 'script':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
- |
|
||||
#!/bin/sh
|
||||
'docker':
|
||||
'image': '${bamboo.dockerGo}'
|
||||
'volumes':
|
||||
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
|
||||
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
|
||||
'key': 'MR'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'force-clean-build': true
|
||||
- 'script':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
- |
|
||||
#!/bin/sh
|
||||
|
||||
set -e -f -u -x
|
||||
set -e -f -u -x
|
||||
|
||||
# Explicitly checkout the revision that we need.
|
||||
git checkout "${bamboo.repository.revision.number}"
|
||||
# Explicitly checkout the revision that we need.
|
||||
git checkout "${bamboo.repository.revision.number}"
|
||||
|
||||
# Run the build with the specified channel.
|
||||
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
|
||||
| awk '{ gsub(/\\n/, "\n"); print; }'\
|
||||
| gpg --import --batch --yes
|
||||
# Run the build with the specified channel.
|
||||
echo "${bamboo.gpgSecretKeyPart1}${bamboo.gpgSecretKeyPart2}"\
|
||||
| awk '{ gsub(/\\n/, "\n"); print; }'\
|
||||
| gpg --import --batch --yes
|
||||
|
||||
make\
|
||||
CHANNEL=${bamboo.channel}\
|
||||
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
|
||||
FRONTEND_PREBUILT=1\
|
||||
PARALLELISM=1\
|
||||
VERBOSE=2\
|
||||
build-release
|
||||
# TODO(a.garipov): Use more fine-grained artifact rules.
|
||||
'artifacts':
|
||||
- 'name': 'AdGuardHome dists'
|
||||
'pattern': 'dist/**'
|
||||
'shared': true
|
||||
'required': true
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
make\
|
||||
CHANNEL=${bamboo.channel}\
|
||||
GPG_KEY_PASSPHRASE=${bamboo.gpgPassword}\
|
||||
FRONTEND_PREBUILT=1\
|
||||
PARALLELISM=1\
|
||||
VERBOSE=2\
|
||||
build-release
|
||||
# TODO(a.garipov): Use more fine-grained artifact rules.
|
||||
'artifacts':
|
||||
- 'name': 'AdGuardHome dists'
|
||||
'pattern': 'dist/**'
|
||||
'shared': true
|
||||
'required': true
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
'Make and publish docker':
|
||||
'key': 'MPD'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'force-clean-build': true
|
||||
- 'script':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
- |
|
||||
#!/bin/sh
|
||||
'key': 'MPD'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'force-clean-build': true
|
||||
- 'script':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
- |
|
||||
#!/bin/sh
|
||||
|
||||
set -e -f -u -x
|
||||
set -e -f -u -x
|
||||
|
||||
COMMIT="${bamboo.repository.revision.number}"
|
||||
export COMMIT
|
||||
readonly COMMIT
|
||||
COMMIT="${bamboo.repository.revision.number}"
|
||||
export COMMIT
|
||||
readonly COMMIT
|
||||
|
||||
# Explicitly checkout the revision that we need.
|
||||
git checkout "$COMMIT"
|
||||
# Explicitly checkout the revision that we need.
|
||||
git checkout "$COMMIT"
|
||||
|
||||
# Install Qemu, create builder.
|
||||
docker version -f '{{ .Server.Experimental }}'
|
||||
docker buildx rm buildx-builder || :
|
||||
docker buildx create --name buildx-builder --driver docker-container\
|
||||
--use
|
||||
docker buildx inspect --bootstrap
|
||||
# Install Qemu, create builder.
|
||||
docker version -f '{{ .Server.Experimental }}'
|
||||
docker buildx rm buildx-builder || :
|
||||
docker buildx create --name buildx-builder --driver docker-container\
|
||||
--use
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
# Login to DockerHub.
|
||||
docker login -u="${bamboo.dockerHubUsername}"\
|
||||
-p="${bamboo.dockerHubPassword}"
|
||||
# Login to DockerHub.
|
||||
docker login -u="${bamboo.dockerHubUsername}"\
|
||||
-p="${bamboo.dockerHubPassword}"
|
||||
|
||||
# Boot the builder.
|
||||
docker buildx inspect --bootstrap
|
||||
# Boot the builder.
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
# Print Docker info.
|
||||
docker info
|
||||
# Print Docker info.
|
||||
docker info
|
||||
|
||||
# Prepare and push the build.
|
||||
env\
|
||||
CHANNEL="${bamboo.channel}"\
|
||||
DIST_DIR='dist'\
|
||||
DOCKER_IMAGE_NAME='adguard/adguardhome'\
|
||||
DOCKER_OUTPUT="type=image,name=adguard/adguardhome,push=true"\
|
||||
VERBOSE='1'\
|
||||
sh ./scripts/make/build-docker.sh
|
||||
'environment':
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
'final-tasks':
|
||||
- 'clean'
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
# Prepare and push the build.
|
||||
env\
|
||||
CHANNEL="${bamboo.channel}"\
|
||||
DIST_DIR='dist'\
|
||||
DOCKER_IMAGE_NAME='adguard/adguardhome'\
|
||||
DOCKER_OUTPUT="type=image,name=adguard/adguardhome,push=true"\
|
||||
VERBOSE='1'\
|
||||
sh ./scripts/make/build-docker.sh
|
||||
'environment':
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
'final-tasks':
|
||||
- 'clean'
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
'Publish to static storage':
|
||||
'key': 'PUB'
|
||||
'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
|
||||
'key': 'PUB'
|
||||
'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
|
||||
set -e -f -u -x
|
||||
|
||||
cd ./dist/
|
||||
cd ./dist/
|
||||
|
||||
CHANNEL="${bamboo.channel}"
|
||||
export CHANNEL
|
||||
CHANNEL="${bamboo.channel}"
|
||||
export CHANNEL
|
||||
|
||||
../bamboo-deploy-publisher/deploy.sh adguard-home-"$CHANNEL"
|
||||
'final-tasks':
|
||||
- 'clean'
|
||||
'requirements':
|
||||
- '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'
|
||||
../bamboo-deploy-publisher/deploy.sh adguard-home-"$CHANNEL"
|
||||
'final-tasks':
|
||||
- 'clean'
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
'Publish to GitHub Releases':
|
||||
'key': 'PTGR'
|
||||
'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
|
||||
'key': 'PTGR'
|
||||
'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
|
||||
set -e -f -u -x
|
||||
|
||||
channel="${bamboo.channel}"
|
||||
readonly channel
|
||||
channel="${bamboo.channel}"
|
||||
readonly channel
|
||||
|
||||
if [ "$channel" != 'release' ] && [ "${channel}" != 'beta' ]
|
||||
then
|
||||
echo "don't publish to GitHub Releases for this channel"
|
||||
if [ "$channel" != 'release' ] && [ "${channel}" != 'beta' ]
|
||||
then
|
||||
echo "don't publish to GitHub Releases for this channel"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd ./dist/
|
||||
cd ./dist/
|
||||
|
||||
env\
|
||||
GITHUB_TOKEN="${bamboo.githubPublicRepoPassword}"\
|
||||
../bamboo-deploy-publisher/deploy.sh adguard-home-github
|
||||
'final-tasks':
|
||||
- 'clean'
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
env\
|
||||
GITHUB_TOKEN="${bamboo.githubPublicRepoPassword}"\
|
||||
../bamboo-deploy-publisher/deploy.sh adguard-home-github
|
||||
'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.
|
||||
- 'cron': '0 42 13 ? * MON-FRI *'
|
||||
# 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.
|
||||
- 'cron': '0 42 13 ? * 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 release'
|
||||
'link-to-jira': true
|
||||
'create': 'manually'
|
||||
'delete':
|
||||
'after-deleted-days': 1
|
||||
'after-inactive-days': 30
|
||||
'integration':
|
||||
'push-on-success': false
|
||||
'merge-from': 'AdGuard Home - Build and publish 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'
|
||||
- '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'
|
||||
'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'
|
||||
# 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'
|
||||
# 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'
|
||||
# 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'
|
||||
|
||||
211
bamboo-specs/snapcraft.yaml
Normal file
211
bamboo-specs/snapcraft.yaml
Normal file
@@ -0,0 +1,211 @@
|
||||
---
|
||||
# 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'
|
||||
@@ -1,64 +1,64 @@
|
||||
---
|
||||
'version': 2
|
||||
'plan':
|
||||
'project-key': 'AGH'
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'project-key': 'AGH'
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.7'
|
||||
|
||||
'stages':
|
||||
- 'Tests':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Test'
|
||||
- 'Tests':
|
||||
'manual': false
|
||||
'final': false
|
||||
'jobs':
|
||||
- 'Test'
|
||||
|
||||
'Test':
|
||||
'docker':
|
||||
'image': '${bamboo.dockerGo}'
|
||||
'volumes':
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
|
||||
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
|
||||
'key': 'TEST'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'force-clean-build': true
|
||||
- 'script':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
- |
|
||||
#!/bin/sh
|
||||
'docker':
|
||||
'image': '${bamboo.dockerGo}'
|
||||
'volumes':
|
||||
'${system.YARN_DIR}': '${bamboo.cacheYarn}'
|
||||
'${system.GO_CACHE_DIR}': '${bamboo.cacheGo}'
|
||||
'${system.GO_PKG_CACHE_DIR}': '${bamboo.cacheGoPkg}'
|
||||
'key': 'TEST'
|
||||
'other':
|
||||
'clean-working-dir': true
|
||||
'tasks':
|
||||
- 'checkout':
|
||||
'force-clean-build': true
|
||||
- 'script':
|
||||
'interpreter': 'SHELL'
|
||||
'scripts':
|
||||
- |
|
||||
#!/bin/sh
|
||||
|
||||
set -e -f -u -x
|
||||
set -e -f -u -x
|
||||
|
||||
make VERBOSE=1 ci go-tools lint
|
||||
'final-tasks':
|
||||
- 'clean'
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
make VERBOSE=1 ci go-tools lint
|
||||
'final-tasks':
|
||||
- 'clean'
|
||||
'requirements':
|
||||
- 'adg-docker': 'true'
|
||||
|
||||
'branches':
|
||||
'create': 'for-pull-request'
|
||||
'delete':
|
||||
'after-deleted-days': 1
|
||||
'after-inactive-days': 5
|
||||
'integration':
|
||||
'push-on-success': false
|
||||
'merge-from': 'AdGuard Home - Build and run tests'
|
||||
'link-to-jira': true
|
||||
'create': 'for-pull-request'
|
||||
'delete':
|
||||
'after-deleted-days': 1
|
||||
'after-inactive-days': 5
|
||||
'integration':
|
||||
'push-on-success': false
|
||||
'merge-from': 'AdGuard Home - Build and run tests'
|
||||
'link-to-jira': true
|
||||
|
||||
'notifications':
|
||||
- 'events':
|
||||
- 'plan-status-changed'
|
||||
'recipients':
|
||||
- 'webhook':
|
||||
'name': 'Build webhook'
|
||||
'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo'
|
||||
- 'events':
|
||||
- 'plan-status-changed'
|
||||
'recipients':
|
||||
- 'webhook':
|
||||
'name': 'Build webhook'
|
||||
'url': 'http://prod.jirahub.service.eu.consul/v1/webhook/bamboo'
|
||||
|
||||
'labels': []
|
||||
'other':
|
||||
'concurrent-build-plugin': 'system-default'
|
||||
'concurrent-build-plugin': 'system-default'
|
||||
|
||||
14
go.mod
14
go.mod
@@ -4,10 +4,11 @@ go 1.19
|
||||
|
||||
require (
|
||||
github.com/AdguardTeam/dnsproxy v0.50.2
|
||||
github.com/AdguardTeam/golibs v0.13.2
|
||||
github.com/AdguardTeam/golibs v0.13.3
|
||||
github.com/AdguardTeam/urlfilter v0.16.1
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
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/dimfeld/httptreemux/v5 v5.5.0
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
@@ -27,13 +28,13 @@ require (
|
||||
github.com/mdlayher/raw v0.1.0
|
||||
github.com/miekg/dns v1.1.54
|
||||
github.com/quic-go/quic-go v0.35.1
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/ti-mo/netfilter v0.5.0
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
golang.org/x/crypto v0.9.0
|
||||
golang.org/x/crypto v0.10.0
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
golang.org/x/net v0.10.0
|
||||
golang.org/x/sys v0.8.0
|
||||
golang.org/x/net v0.11.0
|
||||
golang.org/x/sys v0.9.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
howett.net/plist v1.0.0
|
||||
@@ -44,7 +45,6 @@ require (
|
||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
||||
github.com/ameshkov/dnsstamps v1.0.3 // 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/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // 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
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/sync v0.2.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
golang.org/x/tools v0.9.3 // indirect
|
||||
)
|
||||
|
||||
28
go.sum
28
go.sum
@@ -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/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.13.2 h1:BPASsyQKmb+b8VnvsNOHp7bKfcZl9Z+Z2UhPjOiupSc=
|
||||
github.com/AdguardTeam/golibs v0.13.2/go.mod h1:7ylQLv2Lqsc3UW3jHoITynYk6Y1tYtgEMkR09ppfsN8=
|
||||
github.com/AdguardTeam/golibs v0.13.3 h1:RT3QbzThtaLiFLkIUDS6/hlGEXrh0zYvdf4bd7UWpGo=
|
||||
github.com/AdguardTeam/golibs v0.13.3/go.mod h1:wkJ6EUsN4np/9Gp7+9QeooY9E2U2WCLJYAioLCzkHsI=
|
||||
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/go.mod h1:46YZDOV1+qtdRDuhZKVPSSp7JWWes0KayqHrKAFBdEI=
|
||||
@@ -113,17 +113,13 @@ 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/go.mod h1:YWp/H8Qs5fVmf17v7JNZzA0mPJ+mS2e9JdiUF9LlKzQ=
|
||||
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/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
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.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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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/go.mod h1:nt+8B9hx/QpqHr7Hazq+2qMCCA8u2OTkyc/7+U9ARz8=
|
||||
@@ -138,8 +134,8 @@ go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
|
||||
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
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/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
@@ -156,8 +152,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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
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.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
@@ -181,16 +177,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-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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
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/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.3/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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
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-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
|
||||
@@ -53,14 +53,14 @@ func (s *Server) beforeRequestHandler(
|
||||
// getClientRequestFilteringSettings looks up client filtering settings using
|
||||
// the client's IP address and ID, if any, from dctx.
|
||||
func (s *Server) getClientRequestFilteringSettings(dctx *dnsContext) *filtering.Settings {
|
||||
setts := s.dnsFilter.GetConfig()
|
||||
setts := s.dnsFilter.Settings()
|
||||
setts.ProtectionEnabled = dctx.protectionEnabled
|
||||
if s.conf.FilterHandler != nil {
|
||||
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
|
||||
|
||||
@@ -57,16 +57,13 @@ func (s *Server) genDNSFilterMessage(
|
||||
return s.genBlockedHost(req, s.conf.SafeBrowsingBlockHost, dctx)
|
||||
case filtering.FilteredParental:
|
||||
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:
|
||||
// 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)
|
||||
return s.genForBlockingMode(req, ipsFromRules(res.Rules))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,10 @@ func (s *Server) updateStats(
|
||||
pctx := ctx.proxyCtx
|
||||
e := stats.Entry{}
|
||||
e.Domain = strings.ToLower(pctx.Req.Question[0].Name)
|
||||
e.Domain = e.Domain[:len(e.Domain)-1] // remove last "."
|
||||
if e.Domain != "." {
|
||||
// Remove last ".", but save the domain as is for "." queries.
|
||||
e.Domain = e.Domain[:len(e.Domain)-1]
|
||||
}
|
||||
|
||||
if clientID := ctx.clientID; clientID != "" {
|
||||
e.Client = clientID
|
||||
|
||||
@@ -46,6 +46,10 @@ type testStats struct {
|
||||
|
||||
// Update implements the [stats.Interface] interface for *testStats.
|
||||
func (l *testStats) Update(e stats.Entry) {
|
||||
if e.Domain == "" {
|
||||
return
|
||||
}
|
||||
|
||||
l.lastEntry = e
|
||||
}
|
||||
|
||||
@@ -54,9 +58,12 @@ func (l *testStats) ShouldCount(string, uint16, uint16, []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
func TestServer_ProcessQueryLogsAndStats(t *testing.T) {
|
||||
const domain = "example.com."
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
domain string
|
||||
proto proxy.Proto
|
||||
addr net.Addr
|
||||
clientID string
|
||||
@@ -67,6 +74,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult stats.Result
|
||||
}{{
|
||||
name: "success_udp",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoUDP,
|
||||
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "",
|
||||
@@ -77,6 +85,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult: stats.RNotFiltered,
|
||||
}, {
|
||||
name: "success_tls_clientid",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoTLS,
|
||||
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "cli42",
|
||||
@@ -87,6 +96,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult: stats.RNotFiltered,
|
||||
}, {
|
||||
name: "success_tls",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoTLS,
|
||||
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "",
|
||||
@@ -97,6 +107,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult: stats.RNotFiltered,
|
||||
}, {
|
||||
name: "success_quic",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoQUIC,
|
||||
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "",
|
||||
@@ -107,6 +118,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult: stats.RNotFiltered,
|
||||
}, {
|
||||
name: "success_https",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoHTTPS,
|
||||
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "",
|
||||
@@ -117,6 +129,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult: stats.RNotFiltered,
|
||||
}, {
|
||||
name: "success_dnscrypt",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoDNSCrypt,
|
||||
addr: &net.TCPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "",
|
||||
@@ -127,6 +140,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult: stats.RNotFiltered,
|
||||
}, {
|
||||
name: "success_udp_filtered",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoUDP,
|
||||
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "",
|
||||
@@ -137,6 +151,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult: stats.RFiltered,
|
||||
}, {
|
||||
name: "success_udp_sb",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoUDP,
|
||||
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "",
|
||||
@@ -147,6 +162,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult: stats.RSafeBrowsing,
|
||||
}, {
|
||||
name: "success_udp_ss",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoUDP,
|
||||
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "",
|
||||
@@ -157,6 +173,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantStatResult: stats.RSafeSearch,
|
||||
}, {
|
||||
name: "success_udp_pc",
|
||||
domain: domain,
|
||||
proto: proxy.ProtoUDP,
|
||||
addr: &net.UDPAddr{IP: net.IP{1, 2, 3, 4}, Port: 1234},
|
||||
clientID: "",
|
||||
@@ -165,6 +182,17 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
wantCode: resultCodeSuccess,
|
||||
reason: filtering.FilteredParental,
|
||||
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)
|
||||
@@ -181,7 +209,7 @@ func TestProcessQueryLogsAndStats(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := &dns.Msg{
|
||||
Question: []dns.Question{{
|
||||
Name: "example.com.",
|
||||
Name: tc.domain,
|
||||
}},
|
||||
}
|
||||
pctx := &proxy.DNSContext{
|
||||
|
||||
@@ -3,8 +3,10 @@ package filtering
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"golang.org/x/exp/slices"
|
||||
@@ -44,6 +46,15 @@ func initBlockedServices() {
|
||||
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.
|
||||
func BlockedSvcKnown(s string) (ok bool) {
|
||||
_, ok = serviceRules[s]
|
||||
@@ -52,15 +63,22 @@ func BlockedSvcKnown(s string) (ok bool) {
|
||||
}
|
||||
|
||||
// ApplyBlockedServices - set blocked services settings for this DNS request
|
||||
func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
|
||||
func (d *DNSFilter) ApplyBlockedServices(setts *Settings) {
|
||||
d.confLock.RLock()
|
||||
defer d.confLock.RUnlock()
|
||||
|
||||
setts.ServicesRules = []ServiceEntry{}
|
||||
if list == nil {
|
||||
d.confLock.RLock()
|
||||
defer d.confLock.RUnlock()
|
||||
|
||||
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 {
|
||||
rules, ok := serviceRules[name]
|
||||
if !ok {
|
||||
@@ -90,7 +108,7 @@ func (d *DNSFilter) handleBlockedServicesAll(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
|
||||
d.confLock.RLock()
|
||||
list := d.Config.BlockedServices
|
||||
list := d.Config.BlockedServices.IDs
|
||||
d.confLock.RUnlock()
|
||||
|
||||
_ = aghhttp.WriteJSONResponse(w, r, list)
|
||||
@@ -106,7 +124,7 @@ func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
d.confLock.Lock()
|
||||
d.Config.BlockedServices = list
|
||||
d.Config.BlockedServices.IDs = list
|
||||
d.confLock.Unlock()
|
||||
|
||||
log.Debug("Updated blocked services list: %d", len(list))
|
||||
|
||||
@@ -103,9 +103,9 @@ type Config struct {
|
||||
|
||||
Rewrites []*LegacyRewrite `yaml:"rewrites"`
|
||||
|
||||
// Names of services to block (globally).
|
||||
// BlockedServices is the configuration of blocked services.
|
||||
// Per-client settings can override this configuration.
|
||||
BlockedServices []string `yaml:"blocked_services"`
|
||||
BlockedServices *BlockedServices `yaml:"blocked_services"`
|
||||
|
||||
// EtcHosts is a container of IP-hostname pairs taken from the operating
|
||||
// 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))
|
||||
}
|
||||
|
||||
// GetConfig - get configuration
|
||||
func (d *DNSFilter) GetConfig() (s Settings) {
|
||||
// Settings returns filtering settings.
|
||||
func (d *DNSFilter) Settings() (s *Settings) {
|
||||
d.confLock.RLock()
|
||||
defer d.confLock.RUnlock()
|
||||
|
||||
return Settings{
|
||||
return &Settings{
|
||||
FilteringEnabled: atomic.LoadUint32(&d.Config.enabled) != 0,
|
||||
SafeSearchEnabled: d.Config.SafeSearchConf.Enabled,
|
||||
SafeBrowsingEnabled: d.Config.SafeBrowsingEnabled,
|
||||
@@ -987,16 +987,19 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
|
||||
return nil, fmt.Errorf("rewrites: preparing: %s", err)
|
||||
}
|
||||
|
||||
bsvcs := []string{}
|
||||
for _, s := range d.BlockedServices {
|
||||
if !BlockedSvcKnown(s) {
|
||||
log.Debug("skipping unknown blocked-service %q", s)
|
||||
if d.BlockedServices != nil {
|
||||
bsvcs := []string{}
|
||||
for _, s := range d.BlockedServices.IDs {
|
||||
if !BlockedSvcKnown(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 {
|
||||
err = d.initFiltering(nil, blockFilters)
|
||||
|
||||
@@ -416,12 +416,12 @@ type checkHostResp struct {
|
||||
func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||
host := r.URL.Query().Get("name")
|
||||
|
||||
setts := d.GetConfig()
|
||||
setts := d.Settings()
|
||||
setts.FilteringEnabled = true
|
||||
setts.ProtectionEnabled = true
|
||||
|
||||
d.ApplyBlockedServices(&setts, nil)
|
||||
result, err := d.CheckHost(host, dns.TypeA, &setts)
|
||||
d.ApplyBlockedServices(setts)
|
||||
result, err := d.CheckHost(host, dns.TypeA, setts)
|
||||
if err != nil {
|
||||
aghhttp.Error(
|
||||
r,
|
||||
|
||||
@@ -84,7 +84,7 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Check cnames for cycles on initialisation.
|
||||
// TODO(a.garipov): Check cnames for cycles on initialization.
|
||||
cnames := stringutil.NewSet()
|
||||
host := dReq.Hostname
|
||||
for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" {
|
||||
|
||||
@@ -161,12 +161,8 @@ func (ss *Default) resetEngine(
|
||||
// type check
|
||||
var _ filtering.SafeSearch = (*Default)(nil)
|
||||
|
||||
// CheckHost implements the [filtering.SafeSearch] interface for
|
||||
// *DefaultSafeSearch.
|
||||
func (ss *Default) CheckHost(
|
||||
host string,
|
||||
qtype rules.RRType,
|
||||
) (res filtering.Result, err error) {
|
||||
// CheckHost implements the [filtering.SafeSearch] interface for *Default.
|
||||
func (ss *Default) CheckHost(host string, qtype rules.RRType) (res filtering.Result, err error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
ss.log(log.DEBUG, "lookup for %q finished in %s", host, time.Since(start))
|
||||
@@ -196,14 +192,10 @@ func (ss *Default) CheckHost(
|
||||
return filtering.Result{}, err
|
||||
}
|
||||
|
||||
if fltRes != nil {
|
||||
res = *fltRes
|
||||
ss.setCacheResult(host, qtype, res)
|
||||
res = *fltRes
|
||||
ss.setCacheResult(host, qtype, res)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return filtering.Result{}, fmt.Errorf("no ipv4 addresses for %q", host)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// searchHost looks up DNS rewrites in the internal DNS filtering engine.
|
||||
@@ -229,7 +221,11 @@ func (ss *Default) searchHost(host string, qtype rules.RRType) (res *rules.DNSRe
|
||||
}
|
||||
|
||||
// newResult creates Result object from rewrite rule. qtype must be either
|
||||
// [dns.TypeA] or [dns.TypeAAAA].
|
||||
// [dns.TypeA] or [dns.TypeAAAA]. If err is nil, res is never nil, so that the
|
||||
// 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(
|
||||
rewrite *rules.DNSRewrite,
|
||||
qtype rules.RRType,
|
||||
@@ -243,9 +239,10 @@ func (ss *Default) newResult(
|
||||
}
|
||||
|
||||
if rewrite.RRType == qtype {
|
||||
ip, ok := rewrite.Value.(net.IP)
|
||||
v := rewrite.Value
|
||||
ip, ok := v.(net.IP)
|
||||
if !ok || ip == nil {
|
||||
return nil, nil
|
||||
return nil, fmt.Errorf("expected ip rewrite value, got %T(%[1]v)", v)
|
||||
}
|
||||
|
||||
res.Rules[0].IP = ip
|
||||
@@ -255,14 +252,14 @@ func (ss *Default) newResult(
|
||||
|
||||
host := rewrite.NewCNAME
|
||||
if host == "" {
|
||||
return nil, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
ss.log(log.DEBUG, "resolving %q", host)
|
||||
|
||||
ips, err := ss.resolver.LookupIP(context.Background(), qtypeToProto(qtype), host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("resolving cname: %w", err)
|
||||
}
|
||||
|
||||
ss.log(log.DEBUG, "resolved %s", ips)
|
||||
@@ -276,11 +273,9 @@ func (ss *Default) newResult(
|
||||
}
|
||||
|
||||
res.Rules[0].IP = ip
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// qtypeToProto returns "ip4" for [dns.TypeA] and "ip6" for [dns.TypeAAAA].
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package safesearch_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -71,6 +72,25 @@ 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) {
|
||||
resolver := &aghtest.TestResolver{}
|
||||
ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
|
||||
@@ -105,6 +125,56 @@ 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) {
|
||||
conf := testConf
|
||||
ss, err := safesearch.NewDefault(conf, "", testCacheSize, testCacheTTL)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering/safesearch"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
)
|
||||
@@ -127,14 +128,13 @@ func (cs clientSource) MarshalText() (text []byte, err error) {
|
||||
// RuntimeClient is a client information about which has been obtained using the
|
||||
// source described in the Source field.
|
||||
type RuntimeClient struct {
|
||||
WHOISInfo *RuntimeClientWHOISInfo
|
||||
Host string
|
||||
Source clientSource
|
||||
}
|
||||
// WHOIS is the filtered WHOIS data of a client.
|
||||
WHOIS *whois.Info
|
||||
|
||||
// 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"`
|
||||
// Host is the host name of a client.
|
||||
Host string
|
||||
|
||||
// Source is the source from which the information about the client has
|
||||
// been obtained.
|
||||
Source clientSource
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
@@ -307,18 +308,6 @@ func (clients *clientsContainer) clientSource(ip netip.Addr) (src clientSource)
|
||||
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
|
||||
// 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
|
||||
@@ -352,7 +341,7 @@ func (clients *clientsContainer) clientOrArtificial(
|
||||
defer func() {
|
||||
c.Disallowed, c.DisallowedRule = clients.dnsServer.IsBlockedClient(ip, id)
|
||||
if c.WHOIS == nil {
|
||||
c.WHOIS = &querylog.ClientWHOIS{}
|
||||
c.WHOIS = &whois.Info{}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -369,7 +358,7 @@ func (clients *clientsContainer) clientOrArtificial(
|
||||
if ok {
|
||||
return &querylog.Client{
|
||||
Name: rc.Host,
|
||||
WHOIS: toQueryLogWHOIS(rc.WHOISInfo),
|
||||
WHOIS: rc.WHOIS,
|
||||
}, false
|
||||
}
|
||||
|
||||
@@ -701,7 +690,7 @@ func (clients *clientsContainer) Update(prev, c *Client) (err error) {
|
||||
}
|
||||
|
||||
// setWHOISInfo sets the WHOIS information for a client.
|
||||
func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *RuntimeClientWHOISInfo) {
|
||||
func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *whois.Info) {
|
||||
clients.lock.Lock()
|
||||
defer clients.lock.Unlock()
|
||||
|
||||
@@ -713,7 +702,7 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *RuntimeClientWH
|
||||
|
||||
rc, ok := clients.ipToRC[ip]
|
||||
if ok {
|
||||
rc.WHOISInfo = wi
|
||||
rc.WHOIS = wi
|
||||
log.Debug("clients: set whois info for runtime client %s: %+v", rc.Host, wi)
|
||||
|
||||
return
|
||||
@@ -725,7 +714,7 @@ func (clients *clientsContainer) setWHOISInfo(ip netip.Addr, wi *RuntimeClientWH
|
||||
Source: ClientSourceWHOIS,
|
||||
}
|
||||
|
||||
rc.WHOISInfo = wi
|
||||
rc.WHOIS = wi
|
||||
|
||||
clients.ipToRC[ip] = rc
|
||||
|
||||
@@ -762,9 +751,9 @@ func (clients *clientsContainer) addHostLocked(
|
||||
rc.Source = src
|
||||
} else {
|
||||
rc = &RuntimeClient{
|
||||
Host: host,
|
||||
Source: src,
|
||||
WHOISInfo: &RuntimeClientWHOISInfo{},
|
||||
Host: host,
|
||||
Source: src,
|
||||
WHOIS: &whois.Info{},
|
||||
}
|
||||
|
||||
clients.ipToRC[ip] = rc
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -199,7 +199,7 @@ func TestClients(t *testing.T) {
|
||||
|
||||
func TestClientsWHOIS(t *testing.T) {
|
||||
clients := newClientsContainer()
|
||||
whois := &RuntimeClientWHOISInfo{
|
||||
whois := &whois.Info{
|
||||
Country: "AU",
|
||||
Orgname: "Example Org",
|
||||
}
|
||||
@@ -210,7 +210,7 @@ func TestClientsWHOIS(t *testing.T) {
|
||||
rc := clients.ipToRC[ip]
|
||||
require.NotNil(t, rc)
|
||||
|
||||
assert.Equal(t, rc.WHOISInfo, whois)
|
||||
assert.Equal(t, rc.WHOIS, whois)
|
||||
})
|
||||
|
||||
t.Run("existing_auto-client", func(t *testing.T) {
|
||||
@@ -222,7 +222,7 @@ func TestClientsWHOIS(t *testing.T) {
|
||||
rc := clients.ipToRC[ip]
|
||||
require.NotNil(t, rc)
|
||||
|
||||
assert.Equal(t, rc.WHOISInfo, whois)
|
||||
assert.Equal(t, rc.WHOIS, whois)
|
||||
})
|
||||
|
||||
t.Run("can't_set_manually-added", func(t *testing.T) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
)
|
||||
|
||||
// clientJSON is a common structure used by several handlers to deal with
|
||||
@@ -28,7 +29,8 @@ type clientJSON struct {
|
||||
// the allowlist.
|
||||
DisallowedRule *string `json:"disallowed_rule,omitempty"`
|
||||
|
||||
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info,omitempty"`
|
||||
// WHOIS is the filtered WHOIS data of a client.
|
||||
WHOIS *whois.Info `json:"whois_info,omitempty"`
|
||||
SafeSearchConf *filtering.SafeSearchConfig `json:"safe_search"`
|
||||
|
||||
Name string `json:"name"`
|
||||
@@ -51,7 +53,7 @@ type clientJSON struct {
|
||||
}
|
||||
|
||||
type runtimeClientJSON struct {
|
||||
WHOISInfo *RuntimeClientWHOISInfo `json:"whois_info"`
|
||||
WHOIS *whois.Info `json:"whois_info"`
|
||||
|
||||
IP netip.Addr `json:"ip"`
|
||||
Name string `json:"name"`
|
||||
@@ -78,7 +80,7 @@ func (clients *clientsContainer) handleGetClients(w http.ResponseWriter, r *http
|
||||
|
||||
for ip, rc := range clients.ipToRC {
|
||||
cj := runtimeClientJSON{
|
||||
WHOISInfo: rc.WHOISInfo,
|
||||
WHOIS: rc.WHOIS,
|
||||
|
||||
Name: rc.Host,
|
||||
Source: rc.Source,
|
||||
@@ -344,16 +346,16 @@ func (clients *clientsContainer) findRuntime(ip netip.Addr, idStr string) (cj *c
|
||||
IDs: []string{idStr},
|
||||
Disallowed: &disallowed,
|
||||
DisallowedRule: &rule,
|
||||
WHOISInfo: &RuntimeClientWHOISInfo{},
|
||||
WHOIS: &whois.Info{},
|
||||
}
|
||||
|
||||
return cj
|
||||
}
|
||||
|
||||
cj = &clientJSON{
|
||||
Name: rc.Host,
|
||||
IDs: []string{idStr},
|
||||
WHOISInfo: rc.WHOISInfo,
|
||||
Name: rc.Host,
|
||||
IDs: []string{idStr},
|
||||
WHOIS: rc.WHOIS,
|
||||
}
|
||||
|
||||
disallowed, rule := clients.dnsServer.IsBlockedClient(ip, idStr)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/schedule"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
||||
"github.com/AdguardTeam/dnsproxy/fastip"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
@@ -316,6 +317,11 @@ var config = &configuration{
|
||||
Yandex: true,
|
||||
YouTube: true,
|
||||
},
|
||||
|
||||
BlockedServices: &filtering.BlockedServices{
|
||||
Schedule: schedule.EmptyWeekly(),
|
||||
IDs: []string{},
|
||||
},
|
||||
},
|
||||
UpstreamTimeout: timeutil.Duration{Duration: dnsforward.DefaultTimeout},
|
||||
UsePrivateRDNS: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||
"github.com/AdguardTeam/golibs/errors"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
@@ -25,7 +27,7 @@ import (
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Default ports.
|
||||
// Default listening ports.
|
||||
const (
|
||||
defaultPortDNS = 53
|
||||
defaultPortHTTP = 80
|
||||
@@ -169,13 +171,72 @@ func initDNSServer(
|
||||
Context.rdns = NewRDNS(Context.dnsServer, &Context.clients, config.DNS.UsePrivateRDNS)
|
||||
}
|
||||
|
||||
if config.Clients.Sources.WHOIS {
|
||||
Context.whois = initWHOIS(&Context.clients)
|
||||
}
|
||||
initWHOIS()
|
||||
|
||||
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
|
||||
// a subnet set that matches all locally served networks, see
|
||||
// [netutil.IsLocallyServed].
|
||||
@@ -218,9 +279,7 @@ func onDNSRequest(pctx *proxy.DNSContext) {
|
||||
Context.rdns.Begin(ip)
|
||||
}
|
||||
|
||||
if srcs.WHOIS && !netutil.IsSpecialPurposeAddr(ip) {
|
||||
Context.whois.Begin(ip)
|
||||
}
|
||||
Context.whoisCh <- ip
|
||||
}
|
||||
|
||||
func ipsToTCPAddrs(ips []netip.Addr, port int) (tcpAddrs []*net.TCPAddr) {
|
||||
@@ -390,7 +449,7 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
|
||||
// pref is a prefix for logging messages around the scope.
|
||||
const pref = "applying filters"
|
||||
|
||||
Context.filters.ApplyBlockedServices(setts, nil)
|
||||
Context.filters.ApplyBlockedServices(setts)
|
||||
|
||||
log.Debug("%s: looking for client with ip %s and clientid %q", pref, clientIP, clientID)
|
||||
|
||||
@@ -418,7 +477,7 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
|
||||
if svcs == nil {
|
||||
svcs = []string{}
|
||||
}
|
||||
Context.filters.ApplyBlockedServices(setts, svcs)
|
||||
Context.filters.ApplyBlockedServicesList(setts, svcs)
|
||||
log.Debug("%s: services for client %q set: %s", pref, c.Name, svcs)
|
||||
}
|
||||
|
||||
@@ -463,9 +522,7 @@ func startDNSServer() error {
|
||||
Context.rdns.Begin(ip)
|
||||
}
|
||||
|
||||
if srcs.WHOIS && !netutil.IsSpecialPurposeAddr(ip) {
|
||||
Context.whois.Begin(ip)
|
||||
}
|
||||
Context.whoisCh <- ip
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -57,7 +57,6 @@ type homeContext struct {
|
||||
queryLog querylog.QueryLog // query log module
|
||||
dnsServer *dnsforward.Server // DNS module
|
||||
rdns *RDNS // rDNS module
|
||||
whois *WHOIS // WHOIS module
|
||||
dhcpServer dhcpd.Interface // DHCP module
|
||||
auth *Auth // HTTP authentication module
|
||||
filters *filtering.DNSFilter // DNS filtering module
|
||||
@@ -84,6 +83,9 @@ type homeContext struct {
|
||||
client *http.Client
|
||||
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 []uint16
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ package home
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghio"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// middlerware is a wrapper function signature.
|
||||
// middleware is a wrapper function signature.
|
||||
type middleware func(http.Handler) http.Handler
|
||||
|
||||
// withMiddlewares consequently wraps h with all the middlewares.
|
||||
@@ -75,3 +75,48 @@ func limitRequestBody(h http.Handler) (limited http.Handler) {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
// currentSchemaVersion is the current schema version.
|
||||
const currentSchemaVersion = 20
|
||||
const currentSchemaVersion = 21
|
||||
|
||||
// These aliases are provided for convenience.
|
||||
type (
|
||||
@@ -94,6 +94,7 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
|
||||
upgradeSchema17to18,
|
||||
upgradeSchema18to19,
|
||||
upgradeSchema19to20,
|
||||
upgradeSchema20to21,
|
||||
}
|
||||
|
||||
n := 0
|
||||
@@ -1128,6 +1129,56 @@ func upgradeSchema19to20(diskConf yobj) (err error) {
|
||||
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
|
||||
// package.
|
||||
func funcName() string {
|
||||
|
||||
@@ -1140,3 +1140,46 @@ func TestUpgradeSchema19to20(t *testing.T) {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,13 @@ const (
|
||||
// readTimeout is the maximum duration for reading the entire request,
|
||||
// including the body.
|
||||
readTimeout = 60 * time.Second
|
||||
|
||||
// readHdrTimeout is the amount of time allowed to read request headers.
|
||||
readHdrTimeout = 60 * time.Second
|
||||
|
||||
// writeTimeout is the maximum duration before timing out writes of the
|
||||
// response.
|
||||
writeTimeout = 60 * time.Second
|
||||
// response. This limit is overwritten by [addWriteTimeout] middleware.
|
||||
writeTimeout = 10 * time.Minute
|
||||
)
|
||||
|
||||
type webConfig struct {
|
||||
@@ -169,7 +171,7 @@ func (web *webAPI) start() {
|
||||
errs := make(chan error, 2)
|
||||
|
||||
// Use an h2c handler to support unencrypted HTTP/2, e.g. for proxies.
|
||||
hdlr := h2c.NewHandler(withMiddlewares(Context.mux, limitRequestBody), &http2.Server{})
|
||||
hdlr := h2c.NewHandler(withMiddlewares(Context.mux, limitHandler), &http2.Server{})
|
||||
|
||||
// Create a new instance, because the Web is not usable after Shutdown.
|
||||
hostStr := web.conf.BindHost.String()
|
||||
@@ -254,7 +256,7 @@ func (web *webAPI) tlsServerLoop() {
|
||||
CipherSuites: Context.tlsCipherIDs,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
Handler: withMiddlewares(Context.mux, limitRequestBody),
|
||||
Handler: withMiddlewares(Context.mux, limitHandler),
|
||||
ReadTimeout: web.conf.ReadTimeout,
|
||||
ReadHeaderTimeout: web.conf.ReadHeaderTimeout,
|
||||
WriteTimeout: web.conf.WriteTimeout,
|
||||
@@ -288,7 +290,7 @@ func (web *webAPI) mustStartHTTP3(address string) {
|
||||
CipherSuites: Context.tlsCipherIDs,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
},
|
||||
Handler: withMiddlewares(Context.mux, limitRequestBody),
|
||||
Handler: withMiddlewares(Context.mux, limitHandler),
|
||||
}
|
||||
|
||||
log.Debug("web: starting http/3 server")
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
39
internal/next/changelog.md
Normal file
39
internal/next/changelog.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# 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
|
||||
@@ -17,20 +17,31 @@ import (
|
||||
|
||||
// Main is the entry point of AdGuard Home.
|
||||
func Main(frontend fs.FS) {
|
||||
// Initial Configuration
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// TODO(a.garipov): Set up logging.
|
||||
// Initial Configuration
|
||||
|
||||
cmdName := os.Args[0]
|
||||
opts, err := parseOptions(cmdName, os.Args[1:])
|
||||
exitCode, needExit := processOptions(opts, cmdName, err)
|
||||
if needExit {
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
err = setLog(opts)
|
||||
check(err)
|
||||
|
||||
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
|
||||
|
||||
// TODO(a.garipov): Set up configuration file name.
|
||||
const confFile = "AdGuardHome.1.yaml"
|
||||
|
||||
confMgr, err := configmgr.New(confFile, frontend, start)
|
||||
confMgr, err := configmgr.New(opts.confFile, frontend, start)
|
||||
check(err)
|
||||
|
||||
web := confMgr.Web()
|
||||
@@ -42,7 +53,7 @@ func Main(frontend fs.FS) {
|
||||
check(err)
|
||||
|
||||
sigHdlr := newSignalHandler(
|
||||
confFile,
|
||||
opts.confFile,
|
||||
frontend,
|
||||
start,
|
||||
web,
|
||||
|
||||
39
internal/next/cmd/log.go
Normal file
39
internal/next/cmd/log.go
Normal file
@@ -0,0 +1,39 @@
|
||||
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
|
||||
}
|
||||
403
internal/next/cmd/opt.go
Normal file
403
internal/next/cmd/opt.go
Normal file
@@ -0,0 +1,403 @@
|
||||
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
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func (h *signalHandler) reconfigure() {
|
||||
|
||||
status := h.shutdown()
|
||||
if status != statusSuccess {
|
||||
log.Info("sighdlr: reconfiruging: exiting with status %d", status)
|
||||
log.Info("sighdlr: reconfiguring: exiting with status %d", status)
|
||||
|
||||
os.Exit(status)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func New(
|
||||
frontend fs.FS,
|
||||
start time.Time,
|
||||
) (m *Manager, err error) {
|
||||
defer func() { err = errors.Annotate(err, "reading config") }()
|
||||
defer func() { err = errors.Annotate(err, "reading config: %w") }()
|
||||
|
||||
conf := &config{}
|
||||
f, err := os.Open(fileName)
|
||||
|
||||
@@ -78,34 +78,41 @@ func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
// Launch the new HTTP service in a separate goroutine to let this handler
|
||||
// finish and thus, this server to shutdown.
|
||||
go func() {
|
||||
defer cancelUpd()
|
||||
go svc.relaunch(updCtx, cancelUpd, newConf)
|
||||
}
|
||||
|
||||
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
|
||||
if updErr != nil {
|
||||
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
|
||||
// relaunch updates the web service in the configuration manager and starts it.
|
||||
// It is intended to be used as a goroutine.
|
||||
func (svc *Service) relaunch(ctx context.Context, cancel context.CancelFunc, newConf *Config) {
|
||||
defer log.OnPanic("websvc: relaunching")
|
||||
|
||||
defer cancel()
|
||||
|
||||
err := svc.confMgr.UpdateWeb(ctx, newConf)
|
||||
if err != nil {
|
||||
log.Error("websvc: updating web: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Consider better ways to do this.
|
||||
const maxUpdDur = 5 * time.Second
|
||||
updStart := time.Now()
|
||||
var newSvc agh.ServiceWithConfig[*Config]
|
||||
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||
if time.Since(updStart) >= maxUpdDur {
|
||||
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Consider better ways to do this.
|
||||
const maxUpdDur = 10 * time.Second
|
||||
updStart := time.Now()
|
||||
var newSvc agh.ServiceWithConfig[*Config]
|
||||
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||
if time.Since(updStart) >= maxUpdDur {
|
||||
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
||||
log.Debug("websvc: waiting for new websvc to be configured")
|
||||
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
log.Debug("websvc: waiting for new websvc to be configured")
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
updErr = newSvc.Start()
|
||||
if updErr != nil {
|
||||
log.Error("websvc: new svc failed to start with error: %s", updErr)
|
||||
}
|
||||
}()
|
||||
err = newSvc.Start()
|
||||
if err != nil {
|
||||
log.Error("websvc: new svc failed to start with error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func New(c *Config) (svc *Service, err error) {
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// newMux returns a new HTTP request multiplexor for the AdGuard Home web
|
||||
// newMux returns a new HTTP request multiplexer for the AdGuard Home web
|
||||
// service.
|
||||
func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||
mux = httptreemux.NewContextMux()
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
package querylog
|
||||
|
||||
import "github.com/AdguardTeam/AdGuardHome/internal/whois"
|
||||
|
||||
// Client is the information required by the query log to match against clients
|
||||
// during searches.
|
||||
type Client struct {
|
||||
WHOIS *ClientWHOIS `json:"whois,omitempty"`
|
||||
Name string `json:"name"`
|
||||
DisallowedRule string `json:"disallowed_rule"`
|
||||
Disallowed bool `json:"disallowed"`
|
||||
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"`
|
||||
WHOIS *whois.Info `json:"whois,omitempty"`
|
||||
Name string `json:"name"`
|
||||
DisallowedRule string `json:"disallowed_rule"`
|
||||
Disallowed bool `json:"disallowed"`
|
||||
IgnoreQueryLog bool `json:"-"`
|
||||
}
|
||||
|
||||
// clientCacheKey is the key by which a cached client information is found.
|
||||
|
||||
108
internal/querylog/csv.go
Normal file
108
internal/querylog/csv.go
Normal file
@@ -0,0 +1,108 @@
|
||||
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
|
||||
}
|
||||
73
internal/querylog/csv_test.go
Normal file
73
internal/querylog/csv_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package querylog
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||
"github.com/AdguardTeam/golibs/httphdr"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/golibs/stringutil"
|
||||
"github.com/AdguardTeam/golibs/timeutil"
|
||||
@@ -62,6 +64,7 @@ func (l *queryLog) initWeb() {
|
||||
l.conf.HTTPRegister(http.MethodGet, "/control/querylog", l.handleQueryLog)
|
||||
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/export", l.handleQueryLogExport)
|
||||
l.conf.HTTPRegister(
|
||||
http.MethodPut,
|
||||
"/control/querylog/config/update",
|
||||
@@ -96,6 +99,73 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
|
||||
_ = 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
|
||||
// API.
|
||||
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
||||
@@ -360,6 +430,17 @@ func parseSearchParams(r *http.Request) (p *searchParams, err error) {
|
||||
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 {
|
||||
urlField string
|
||||
ct criterionType
|
||||
@@ -378,9 +459,9 @@ func parseSearchParams(r *http.Request) (p *searchParams, err error) {
|
||||
}
|
||||
|
||||
if ok {
|
||||
p.searchCriteria = append(p.searchCriteria, c)
|
||||
searchCriteria = append(searchCriteria, c)
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
return searchCriteria, nil
|
||||
}
|
||||
|
||||
@@ -161,12 +161,15 @@ func (l *queryLog) clear() {
|
||||
// newLogEntry creates an instance of logEntry from parameters.
|
||||
func newLogEntry(params *AddParams) (entry *logEntry) {
|
||||
q := params.Question.Question[0]
|
||||
qHost := q.Name
|
||||
if qHost != "." {
|
||||
qHost = strings.ToLower(q.Name[:len(q.Name)-1])
|
||||
}
|
||||
|
||||
entry = &logEntry{
|
||||
// TODO(d.kolyshev): Export this timestamp to func params.
|
||||
Time: time.Now(),
|
||||
|
||||
QHost: strings.ToLower(q.Name[:len(q.Name)-1]),
|
||||
Time: time.Now(),
|
||||
QHost: qHost,
|
||||
QType: dns.Type(q.Qtype).String(),
|
||||
QClass: dns.Class(q.Qclass).String(),
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ func TestQueryLog(t *testing.T) {
|
||||
// Add memory entries.
|
||||
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, "", net.IPv4(1, 1, 1, 5), net.IPv4(2, 2, 2, 5))
|
||||
|
||||
type tcAssertion struct {
|
||||
host string
|
||||
@@ -59,10 +60,11 @@ func TestQueryLog(t *testing.T) {
|
||||
name: "all",
|
||||
sCr: []searchCriterion{},
|
||||
want: []tcAssertion{
|
||||
{num: 0, 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: "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: 0, host: ".", answer: net.IPv4(1, 1, 1, 5), client: net.IPv4(2, 2, 2, 5)},
|
||||
{num: 1, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
|
||||
{num: 2, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
|
||||
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
|
||||
{num: 4, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
|
||||
},
|
||||
}, {
|
||||
name: "by_domain_strict",
|
||||
@@ -104,10 +106,11 @@ func TestQueryLog(t *testing.T) {
|
||||
value: "2.2.2",
|
||||
}},
|
||||
want: []tcAssertion{
|
||||
{num: 0, 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: "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: 0, host: ".", answer: net.IPv4(1, 1, 1, 5), client: net.IPv4(2, 2, 2, 5)},
|
||||
{num: 1, host: "example.com", answer: net.IPv4(1, 1, 1, 4), client: net.IPv4(2, 2, 2, 4)},
|
||||
{num: 2, host: "test.example.org", answer: net.IPv4(1, 1, 1, 3), client: net.IPv4(2, 2, 2, 3)},
|
||||
{num: 3, host: "example.org", answer: net.IPv4(1, 1, 1, 2), client: net.IPv4(2, 2, 2, 2)},
|
||||
{num: 4, host: "example.org", answer: net.IPv4(1, 1, 1, 1), client: net.IPv4(2, 2, 2, 1)},
|
||||
},
|
||||
}}
|
||||
|
||||
|
||||
@@ -93,3 +93,67 @@ func TestQueryLog_Search_findClient(t *testing.T) {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
220
internal/schedule/schedule.go
Normal file
220
internal/schedule/schedule.go
Normal file
@@ -0,0 +1,220 @@
|
||||
// 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
|
||||
}
|
||||
371
internal/schedule/schedule_internal_test.go
Normal file
371
internal/schedule/schedule_internal_test.go
Normal file
@@ -0,0 +1,371 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ require (
|
||||
github.com/kisielk/errcheck v1.6.3
|
||||
github.com/kyoh86/looppointer v0.2.1
|
||||
github.com/securego/gosec/v2 v2.16.0
|
||||
golang.org/x/tools v0.9.3
|
||||
github.com/uudashr/gocognit v1.0.6
|
||||
golang.org/x/tools v0.10.0
|
||||
golang.org/x/vuln v0.1.0
|
||||
honnef.co/go/tools v0.4.3
|
||||
mvdan.cc/gofumpt v0.5.0
|
||||
@@ -26,8 +27,8 @@ require (
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // 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/mod v0.10.0 // indirect
|
||||
golang.org/x/sync v0.2.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -40,6 +40,8 @@ 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/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -56,20 +58,21 @@ 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.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.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
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-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-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-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
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-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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -79,8 +82,9 @@ 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-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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -92,8 +96,9 @@ 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.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.9.3 h1:Gn1I8+64MsuTb/HpH+LmQtNas23LhUVr3rYZ0eKuaMM=
|
||||
golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
|
||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
|
||||
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/go.mod h1:/YuzZYjGbwB8y19CisAppfyw3uTZnuCz3r+qgx/QRzU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
_ "github.com/kisielk/errcheck"
|
||||
_ "github.com/kyoh86/looppointer"
|
||||
_ "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/shadow/cmd/shadow"
|
||||
_ "golang.org/x/vuln/cmd/govulncheck"
|
||||
|
||||
@@ -143,14 +143,7 @@ func Verbose() (v string) {
|
||||
runtime.Version(),
|
||||
)
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
writeCommitTime(b)
|
||||
|
||||
stringutil.WriteToBuilder(b, nl, vFmtGOOSHdr, nl, vFmtGOARCHHdr)
|
||||
if goarm != "" {
|
||||
@@ -179,3 +172,16 @@ func Verbose() (v 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())
|
||||
}
|
||||
}
|
||||
|
||||
376
internal/whois/whois.go
Normal file
376
internal/whois/whois.go
Normal file
@@ -0,0 +1,376 @@
|
||||
// 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
|
||||
}
|
||||
155
internal/whois/whois_test.go
Normal file
155
internal/whois/whois_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,17 @@
|
||||
|
||||
## 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
|
||||
|
||||
* The value of the `Content-Type` header in the `POST /control/version.json` and
|
||||
|
||||
@@ -313,6 +313,51 @@
|
||||
'responses':
|
||||
'200':
|
||||
'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':
|
||||
'get':
|
||||
'tags':
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## `hooks/`: Git Hooks
|
||||
|
||||
### Usage
|
||||
### Usage
|
||||
|
||||
Run `make init` from the project root.
|
||||
|
||||
@@ -10,7 +10,7 @@ Run `make init` from the project root.
|
||||
|
||||
## `querylog/`: Query Log Helpers
|
||||
|
||||
### Usage
|
||||
### Usage
|
||||
|
||||
* `npm install`: install dependencies. Run this first.
|
||||
* `npm run anonymize <source> <dst>`: read the query log from the `<source>`
|
||||
@@ -26,157 +26,215 @@ don't print anything, and `1`, be verbose.
|
||||
|
||||
|
||||
|
||||
### `build-docker.sh`: Build A Multi-Architecture Docker Image
|
||||
### `build-docker.sh`: Build A Multi-Architecture Docker Image
|
||||
|
||||
Required environment:
|
||||
|
||||
* `CHANNEL`: release channel, see above.
|
||||
|
||||
* `COMMIT`: current Git revision.
|
||||
|
||||
* `DIST_DIR`: the directory where a release has previously been built.
|
||||
|
||||
* `VERSION`: release version.
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `DOCKER_IMAGE_NAME`: the name of the resulting Docker container. By default
|
||||
it's `adguardhome-dev`.
|
||||
|
||||
* `DOCKER_OUTPUT`: the `--output` parameters. By default they are
|
||||
`type=image,name=${DOCKER_IMAGE_NAME},push=false`.
|
||||
|
||||
* `SUDO`: allow users to use `sudo` or `doas` with `docker`. By default none
|
||||
is used.
|
||||
|
||||
|
||||
|
||||
### `build-release.sh`: Build A Release For All Platforms
|
||||
### `build-release.sh`: Build A Release For All Platforms
|
||||
|
||||
Required environment:
|
||||
|
||||
* `CHANNEL`: release channel, see above.
|
||||
|
||||
* `GPG_KEY` and `GPG_KEY_PASSPHRASE`: data for `gpg`. Only required if `SIGN`
|
||||
is `1`.
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `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
|
||||
AMD on Linux and Darwin:
|
||||
|
||||
```sh
|
||||
make ARCH='amd64 arm64' OS='darwin linux' … build-release
|
||||
```
|
||||
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`.
|
||||
|
||||
* `GO`: set an alternative name for the Go compiler.
|
||||
|
||||
* `SIGN`: `0` to not sign the resulting packages, `1` to sign. The default
|
||||
value is `1`.
|
||||
|
||||
* `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
|
||||
verbosity level `2` in `go-build.sh`, set this to `3` when calling
|
||||
`build-release.sh`.
|
||||
|
||||
* `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`.
|
||||
|
||||
|
||||
|
||||
### `clean.sh`: Cleanup
|
||||
### `clean.sh`: Cleanup
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `GO`: set an alternative name for the Go compiler.
|
||||
|
||||
Required environment:
|
||||
|
||||
* `DIST_DIR`: the directory where a release has previously been built.
|
||||
|
||||
|
||||
|
||||
### `go-build.sh`: Build The Backend
|
||||
### `go-build.sh`: Build The Backend
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `GOARM`: ARM processor options for the Go compiler.
|
||||
|
||||
* `GOMIPS`: ARM processor options for the Go compiler.
|
||||
|
||||
* `GO`: set an alternative name for the Go compiler.
|
||||
|
||||
* `OUT`: output binary name.
|
||||
|
||||
* `PARALLELISM`: set the maximum number of concurrently run build commands
|
||||
(that is, compiler, linker, etc.).
|
||||
|
||||
* `SOURCE_DATE_EPOCH`: the [standardized][repr] environment variable for the
|
||||
Unix epoch time of the latest commit in the repository. If set, overrides
|
||||
the default obtained from Git. Useful for reproducible builds.
|
||||
|
||||
* `VERBOSE`: verbosity level. `1` shows every command that is run and every
|
||||
Go package that is processed. `2` also shows subcommands and environment.
|
||||
The default value is `0`, don't be verbose.
|
||||
|
||||
* `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`.
|
||||
|
||||
Required environment:
|
||||
|
||||
* `CHANNEL`: release channel, see above.
|
||||
|
||||
[repr]: https://reproducible-builds.org/docs/source-date-epoch/
|
||||
|
||||
|
||||
|
||||
### `go-deps.sh`: Install Backend Dependencies
|
||||
### `go-deps.sh`: Install Backend Dependencies
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `GO`: set an alternative name for the Go compiler.
|
||||
|
||||
* `VERBOSE`: verbosity level. `1` shows every command that is run and every
|
||||
Go package that is processed. `2` also shows subcommands and environment.
|
||||
The default value is `0`, don't be verbose.
|
||||
|
||||
|
||||
|
||||
### `go-lint.sh`: Run Backend Static Analyzers
|
||||
### `go-lint.sh`: Run Backend Static Analyzers
|
||||
|
||||
Don't forget to run `make go-tools` once first!
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `EXIT_ON_ERROR`: if set to `0`, don't exit the script after the first
|
||||
encountered error. The default value is `1`.
|
||||
|
||||
* `GO`: set an alternative name for the Go compiler.
|
||||
|
||||
* `VERBOSE`: verbosity level. `1` shows every command that is run. `2` also
|
||||
shows subcommands. The default value is `0`, don't be verbose.
|
||||
|
||||
|
||||
|
||||
### `go-test.sh`: Run Backend Tests
|
||||
### `go-test.sh`: Run Backend Tests
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `GO`: set an alternative name for the Go compiler.
|
||||
|
||||
* `RACE`: set to `0` to not use the Go race detector. The default value is
|
||||
`1`, use the race detector.
|
||||
|
||||
* `TIMEOUT_FLAGS`: set timeout flags for tests. The default value is
|
||||
`--timeout 30s`.
|
||||
|
||||
* `VERBOSE`: verbosity level. `1` shows every command that is run and every
|
||||
Go package that is processed. `2` also shows subcommands. The default
|
||||
value is `0`, don't be verbose.
|
||||
|
||||
|
||||
|
||||
### `go-tools.sh`: Install Backend Tooling
|
||||
### `go-tools.sh`: Install Backend Tooling
|
||||
|
||||
Installs the Go static analysis and other tools into `${PWD}/bin`. Either add
|
||||
`${PWD}/bin` to your `$PATH` before all other entries, or use the commands
|
||||
directly, or use the commands through `make` (for example, `make go-lint`).
|
||||
|
||||
Optional environment:
|
||||
|
||||
* `GO`: set an alternative name for the Go compiler.
|
||||
|
||||
|
||||
|
||||
### `version.sh`: Generate And Print The Current Version
|
||||
### `version.sh`: Generate And Print The Current Version
|
||||
|
||||
Required environment:
|
||||
|
||||
* `CHANNEL`: release channel, see above.
|
||||
|
||||
|
||||
|
||||
## `snap/`: Snap GUI Files
|
||||
## `snap/`: Snapcraft scripts
|
||||
|
||||
App icons (see https://github.com/AdguardTeam/AdGuardHome/pull/1836), Snap
|
||||
manifest file templates, and helper scripts.
|
||||
### `build.sh`
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
|
||||
## `translations/`: Twosky Integration Script
|
||||
|
||||
### Usage
|
||||
### Usage
|
||||
|
||||
* `go run main.go help`: print usage.
|
||||
|
||||
@@ -211,7 +269,7 @@ Optional environment:
|
||||
A simple script that downloads and updates the companies DB in the `client`
|
||||
code from [the repo][companiesrepo].
|
||||
|
||||
### Usage
|
||||
### Usage
|
||||
|
||||
```sh
|
||||
sh ./scripts/companiesdb/download.sh
|
||||
@@ -231,7 +289,7 @@ Optional environment:
|
||||
* `URL`: the URL of the index file. By default it's
|
||||
`https://adguardteam.github.io/HostlistsRegistry/assets/services.json`.
|
||||
|
||||
### Usage
|
||||
### Usage
|
||||
|
||||
```sh
|
||||
go run ./scripts/blocked-services/main.go
|
||||
@@ -251,7 +309,7 @@ Optional environment:
|
||||
* `URL`: the URL of the index file. By default it's
|
||||
`https://adguardteam.github.io/HostlistsRegistry/assets/filters.json`.
|
||||
|
||||
### Usage
|
||||
### Usage
|
||||
|
||||
```sh
|
||||
go run ./scripts/vetted-filters/main.go
|
||||
|
||||
@@ -5,11 +5,12 @@ verbose="${VERBOSE:-0}"
|
||||
if [ "$verbose" -gt '0' ]
|
||||
then
|
||||
set -x
|
||||
debug_flags='-D'
|
||||
debug_flags='--debug=1'
|
||||
else
|
||||
set +x
|
||||
debug_flags=''
|
||||
debug_flags='--debug=0'
|
||||
fi
|
||||
readonly debug_flags
|
||||
|
||||
set -e -f -u
|
||||
|
||||
@@ -61,21 +62,16 @@ readonly docker_output
|
||||
case "$channel"
|
||||
in
|
||||
('release')
|
||||
docker_image_full_name="${docker_image_name}:${version}"
|
||||
docker_tags="--tag ${docker_image_name}:latest"
|
||||
docker_tags="--tag=${docker_image_name}:${version},${docker_image_name}:latest"
|
||||
;;
|
||||
('beta')
|
||||
docker_image_full_name="${docker_image_name}:${version}"
|
||||
docker_tags="--tag ${docker_image_name}:beta"
|
||||
docker_tags="--tag=${docker_image_name}:${version},${docker_image_name}:beta"
|
||||
;;
|
||||
('edge')
|
||||
# Don't set the version tag when pushing to the edge channel.
|
||||
docker_image_full_name="${docker_image_name}:edge"
|
||||
docker_tags=''
|
||||
docker_tags="--tag=${docker_image_name}:edge"
|
||||
;;
|
||||
('development')
|
||||
docker_image_full_name="${docker_image_name}"
|
||||
docker_tags=''
|
||||
docker_tags="--tag=${docker_image_name}"
|
||||
;;
|
||||
(*)
|
||||
echo "invalid channel '$channel', supported values are\
|
||||
@@ -83,7 +79,7 @@ in
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
readonly docker_image_full_name docker_tags
|
||||
readonly docker_tags
|
||||
|
||||
# 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
|
||||
@@ -117,10 +113,8 @@ cp "./docker/web-bind.awk"\
|
||||
cp "./docker/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\
|
||||
$debug_flags\
|
||||
"$debug_flags"\
|
||||
buildx build\
|
||||
--build-arg BUILD_DATE="$build_date"\
|
||||
--build-arg DIST_DIR="$dist_dir"\
|
||||
@@ -128,7 +122,6 @@ $sudo_cmd docker\
|
||||
--build-arg VERSION="$version"\
|
||||
--output "$docker_output"\
|
||||
--platform "$docker_platforms"\
|
||||
$docker_tags\
|
||||
-t "$docker_image_full_name"\
|
||||
"$docker_tags"\
|
||||
-f ./docker/Dockerfile\
|
||||
.
|
||||
|
||||
@@ -78,14 +78,6 @@ else
|
||||
fi
|
||||
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.
|
||||
if [ "$sign" -eq '1' ]
|
||||
then
|
||||
@@ -106,7 +98,7 @@ log "checking tools"
|
||||
# Make sure we fail gracefully if one of the tools we need is missing. Use
|
||||
# alternatives when available.
|
||||
use_shasum='0'
|
||||
for tool in gpg gzip sed sha256sum snapcraft tar zip
|
||||
for tool in gpg gzip sed sha256sum tar zip
|
||||
do
|
||||
if ! command -v "$tool" > /dev/null
|
||||
then
|
||||
@@ -128,36 +120,36 @@ readonly use_shasum
|
||||
# Data section. Arrange data into space-separated tables for read -r to read.
|
||||
# Use a hyphen for missing values.
|
||||
|
||||
# os arch arm mips snap
|
||||
# os arch arm mips
|
||||
platforms="\
|
||||
darwin amd64 - - -
|
||||
darwin arm64 - - -
|
||||
freebsd 386 - - -
|
||||
freebsd amd64 - - -
|
||||
freebsd arm 5 - -
|
||||
freebsd arm 6 - -
|
||||
freebsd arm 7 - -
|
||||
freebsd arm64 - - -
|
||||
linux 386 - - i386
|
||||
linux amd64 - - amd64
|
||||
linux arm 5 - -
|
||||
linux arm 6 - -
|
||||
linux arm 7 - armhf
|
||||
linux arm64 - - arm64
|
||||
linux mips - softfloat -
|
||||
linux mips64 - softfloat -
|
||||
linux mips64le - softfloat -
|
||||
linux mipsle - softfloat -
|
||||
linux ppc64le - - -
|
||||
openbsd amd64 - - -
|
||||
openbsd arm64 - - -
|
||||
windows 386 - - -
|
||||
windows amd64 - - -
|
||||
windows arm64 - - -"
|
||||
darwin amd64 - -
|
||||
darwin arm64 - -
|
||||
freebsd 386 - -
|
||||
freebsd amd64 - -
|
||||
freebsd arm 5 -
|
||||
freebsd arm 6 -
|
||||
freebsd arm 7 -
|
||||
freebsd arm64 - -
|
||||
linux 386 - -
|
||||
linux amd64 - -
|
||||
linux arm 5 -
|
||||
linux arm 6 -
|
||||
linux arm 7 -
|
||||
linux arm64 - -
|
||||
linux mips - softfloat
|
||||
linux mips64 - softfloat
|
||||
linux mips64le - softfloat
|
||||
linux mipsle - softfloat
|
||||
linux ppc64le - -
|
||||
openbsd amd64 - -
|
||||
openbsd arm64 - -
|
||||
windows 386 - -
|
||||
windows amd64 - -
|
||||
windows arm64 - -"
|
||||
readonly platforms
|
||||
|
||||
# Function build builds the release for one platform. It builds a binary, an
|
||||
# archive and, if needed, a snap package.
|
||||
# Function build builds the release for one platform. It builds a binary and an
|
||||
# archive.
|
||||
build() {
|
||||
# Get the arguments. Here and below, use the "build_" prefix for all
|
||||
# variables local to function build.
|
||||
@@ -167,7 +159,6 @@ build() {
|
||||
build_arch="$4"\
|
||||
build_arm="$5"\
|
||||
build_mips="$6"\
|
||||
build_snap="$7"\
|
||||
;
|
||||
|
||||
# Use the ".exe" filename extension if we build a Windows release.
|
||||
@@ -229,52 +220,13 @@ build() {
|
||||
esac
|
||||
|
||||
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"
|
||||
|
||||
# Go over all platforms defined in the space-separated table above, tweak the
|
||||
# values where necessary, and feed to build.
|
||||
echo "$platforms" | while read -r os arch arm mips snap
|
||||
echo "$platforms" | while read -r os arch arm mips
|
||||
do
|
||||
# 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
|
||||
@@ -314,7 +266,7 @@ do
|
||||
;;
|
||||
esac
|
||||
|
||||
build "$dir" "$ar" "$os" "$arch" "$arm" "$mips" "$snap"
|
||||
build "$dir" "$ar" "$os" "$arch" "$arm" "$mips"
|
||||
done
|
||||
|
||||
log "packing frontend"
|
||||
@@ -413,14 +365,14 @@ do
|
||||
platform="$f"
|
||||
|
||||
# Remove the prefix.
|
||||
platform="${platform#./${dist}/AdGuardHome_}"
|
||||
platform="${platform#"./${dist}/AdGuardHome_"}"
|
||||
|
||||
# Remove the filename extensions.
|
||||
platform="${platform%.zip}"
|
||||
platform="${platform%.tar.gz}"
|
||||
|
||||
# Use the filename's base path.
|
||||
filename="${f#./${dist}/}"
|
||||
filename="${f#"./${dist}/"}"
|
||||
|
||||
if [ "$i" -eq "$ar_files_len" ]
|
||||
then
|
||||
|
||||
@@ -170,6 +170,17 @@ run_linter govulncheck ./...
|
||||
|
||||
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 unparam ./...
|
||||
|
||||
@@ -38,6 +38,7 @@ readonly go
|
||||
rm -f\
|
||||
bin/errcheck\
|
||||
bin/fieldalignment\
|
||||
bin/gocognit\
|
||||
bin/gocyclo\
|
||||
bin/gofumpt\
|
||||
bin/gosec\
|
||||
@@ -69,6 +70,7 @@ env\
|
||||
github.com/kisielk/errcheck\
|
||||
github.com/kyoh86/looppointer/cmd/looppointer\
|
||||
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/nilness/cmd/nilness\
|
||||
golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow\
|
||||
|
||||
77
scripts/snap/build.sh
Normal file
77
scripts/snap/build.sh
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/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
|
||||
29
scripts/snap/download.sh
Normal file
29
scripts/snap/download.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/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
|
||||
93
scripts/snap/upload.sh
Normal file
93
scripts/snap/upload.sh
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/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
|
||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -4,13 +4,20 @@ initialisms = [
|
||||
#
|
||||
# Do not add "PTR" since we use "Ptr" as a suffix.
|
||||
"inherit"
|
||||
, "ASN"
|
||||
, "DHCP"
|
||||
, "DNSSEC"
|
||||
# E.g. SentryDSN.
|
||||
, "DSN"
|
||||
, "ECS"
|
||||
, "EDNS"
|
||||
, "MX"
|
||||
, "QUIC"
|
||||
, "RA"
|
||||
, "RRSIG"
|
||||
, "SDNS"
|
||||
, "SLAAC"
|
||||
, "SOA"
|
||||
, "SVCB"
|
||||
, "TLD"
|
||||
, "WHOIS"
|
||||
|
||||
Reference in New Issue
Block a user